Files
smart_grid_iot/.trae/specs/customer-management-core/spec.md
T
andyesfly 1c80a24d1d feat: 新增客户管理全模块功能
实现客户信息、客户部门、客户账号三个子模块的完整CRUD,包含:
1. 数据库实体、DTO、控制器、服务层代码
2. RBAC权限映射与菜单种子数据扩展
3. 前端路由与API封装
4. 数据权限隔离与鉴权逻辑
5. 配套工具脚本与测试验证清单
2026-08-23 19:15:54 +08:00

16 KiB
Raw Blame History

鑫潼智电系统 · 客户管理核心模块 (V1.0) - Product Requirement Document

Overview

  • Summary: 基于数据库设计文档 D2 客户域,实现「客户信息 + 客户部门 + 客户访问账号」三个核心子模块的完整前后端功能,覆盖 CRUD、分页搜索、数据权限隔离、基础安全防护,并兼容业务员移动端 APP(复用同一套 REST API 和 sys_user 账号体系)。
  • Purpose: 为业务员和运营管理员提供结构化的客户档案管理能力,是后续项目管理、派单管理、设备管理、用电统计等所有下游业务模块的数据基础。
  • Target Users: 运营管理员(ADMIN 角色,全量数据)、业务员(SALESMAN 角色,仅本人客户数据)、业务员 APP(同 SALESMAN 权限,移动端使用)。

Goals

  • 实现客户信息(customer)完整 CRUD,含业务编号自动生成、省市区地址、状态流转、负责业务员分配
  • 实现客户部门(customer_department)完整 CRUD,按客户归属过滤
  • 实现客户访问账号(customer_account)完整 CRUD,按客户归属过滤,默认密码自动生成与 bcrypt 加密
  • 接入现有 RBAC 鉴权体系:菜单/按钮权限 + 行级数据隔离(ADMIN 全量、SALESMAN 按 salesman_id 过滤)
  • 前端 UI 与现有用户管理页保持一致风格(搜索栏 + 表格 + 弹窗),省市区级联、表格内部滚动
  • 接口防 SQL 注入(参数化查询),前端防 XSS(输入校验 + 避免 v-html),敏感字段脱敏
  • 同一套 API 无差别兼容 Web 管理后台和业务员 APP

Non-Goals (Out of Scope)

  • 客户拜访(customer_visit)、客户建筑(customer_building)、客户房间(customer_room)、客户设备(customer_equipment)四个子模块(后续迭代)
  • 共享电工 APP(WORKER 角色)和客户微信小程序(CUSTOMER 角色)的专属接口(文档确认本期不含)
  • 业务员 APP 的移动端 UI 页面(本期仅保证 API 可用,UI 由独立 uni-app 项目负责)
  • 客户状态驱动的自动流转、客户画像/报表分析(后续统计报表模块)
  • 文件上传(设备照片、合同文件)、短信通知初始密码、首次登录强制改密码

Background & Context

  • 现有架构: 后端 Midway.js (Koa) + Sequelize-typescript + PostgreSQL;前端 Vue3 + Ant Design Vue 4 + Vite;鉴权 JWT + RBAC 中间件(rbac.middleware.ts 基于路径前缀匹配权限码)
  • 已有数据库种子(database/db_seed.sql):4 个角色(ADMIN/SALESMAN/WORKER/CUSTOMER)、6 条顶级菜单(含「客户管理」parent_id=NULL id=1 permission=customer:list)、SALESMAN 角色已绑定客户/项目/统计报表 3 个顶级菜单
  • 已有客户管理路由占位(client/src/router/index.ts):/customer 路径指向 Placeholder.vue,permission=customer:list
  • RBAC 中间件映射表(rbac.middleware.ts PERMISSION_MAP):当前仅含 /api/users、/api/roles 等系统管理路由,需新增客户域 3 条映射
  • 数据隔离要求:SALESMAN 角色只能看到自己负责的客户;customer、customer_department、customer_account 三张表均需支持 salesman_id 行级过滤(customer_account 通过 customer_id → customer.salesman_id 关联)

Functional Requirements

FR-1:客户信息管理(customer)

系统 SHALL 提供客户信息的完整 CRUD 和查询能力:

  • 列表:分页(page/pageSize)、关键词搜索(客户名称/联系人/电话)、状态筛选(1意向/2签约/3运维中/4终止)、负责业务员筛选
  • 字段:id、customer_no(自动生成,KH+YYYYMMDD+3位序号,UNIQUE)、customer_name(必填)、province/city/district(省市区级联)、address、contact_person、contact_phone、postal_code、fax、customer_level(学校/医院/企业等)、status(默认1意向)、salesman_id(关联 sys_user.id,允许 NULL 由 ADMIN 分配)、created_at、updated_at
  • 创建:必填校验、customer_no 自动生成、写入 salesman_id(若当前角色为 SALESMAN 则强制=当前用户ID)
  • 更新:字段白名单更新、customer_no 只读不可改
  • 删除:硬删除,若存在关联 customer_department / customer_account 则提示禁止删除或级联删除(参照外键约束)
  • 列表返回不包含敏感字段(无密码类字段)

FR-2:客户部门管理(customer_department)

系统 SHALL 提供客户部门的完整 CRUD,数据按所属客户过滤:

  • 列表:分页、关键词搜索(部门名称/联系人/电话)、按 customer_id 过滤(必选或必带权限过滤)
  • 字段:id、customer_id(FK customer,必填)、dept_name(必填)、contact_person、contact_phone、function_desc、created_at
  • 行级权限:SALESMAN 仅可操作自己客户下的部门(通过 customer.salesman_id 关联校验)
  • 删除:若存在关联 customer_room(虽本期不实现,但需保留外键约束友好提示)

FR-3:客户访问账号管理(customer_account)

系统 SHALL 提供客户侧访问账号的完整 CRUD,数据按所属客户过滤:

  • 列表:分页、关键词搜索(电话)、按 customer_id 过滤、状态筛选(1启用/0禁用)
  • 字段:id、customer_id(FK customer,必填)、phone(UNIQUE,必填,登录账号)、password_hash(bcrypt)、wx_openid(本期可NULL)、status(默认1启用)、last_login_at、created_at
  • 创建:默认密码 = 手机号后6位(不足6位用 123456),自动 bcrypt 后存 password_hash
  • 更新:可改状态;修改密码需单独字段触发并重新 bcrypt
  • 行级权限:SALESMAN 仅可操作自己客户下的账号
  • 列表返回不得携带 password_hash / wx_openid 等敏感字段

FR-4:接口鉴权与数据权限(复用现有体系扩展)

系统 SHALL 将客户域接口接入现有认证与权限体系:

  • 所有客户域接口要求登录(走 auth.middleware → JWT 校验)
  • RBAC 中间件新增 3 条路径映射:/api/customers→customer:list、/api/customer-departments→customer:department、/api/customer-accounts→customer:account
  • 超级管理员(ADMIN)直接放行
  • 行级数据隔离:SALESMAN 角色的 customer 列表自动加 salesman_id = ctx.state.user.userId 条件;customer_department / customer_account 通过 customer_id JOIN customer.salesman_id 过滤
  • 越权访问控制:SALESMAN 更新/删除时校验目标记录是否归属自己,否则返回 403

FR-5:菜单与按钮权限(db_seed.sql 扩展)

系统 SHALL 在种子数据中补全客户域菜单与按钮权限:

  • 将「客户管理」顶级菜单(id=1)改为目录类型 menu_type=1,作为父节点
  • 新增 3 条子菜单:客户信息(/customer/info / customer/info/index / permission=customer:list)、客户部门(/customer/department / customer/department/index / permission=customer:department)、客户账号(/customer/account / customer/account/index / permission=customer:account)
  • 新增若干按钮权限(menu_type=3):各模块的 create / update / delete 按钮权限(如 customer:create、customer:update、customer:delete)
  • 角色-菜单绑定:ADMIN 全部绑定,SALESMAN 绑定客户域全部菜单和按钮

FR-6:前端页面(Web 端 3 个页面)

系统 SHALL 提供 3 个 Vue 页面,风格与现有用户管理完全一致:

  • 客户信息页(customer/info/index.vue):搜索栏(关键词、状态、业务员下拉)+ 表格(列含客户编号、名称、省市区、联系人、电话、级别、状态、创建时间、操作)+ 新增/编辑弹窗(含省市区 a-cascader + 业务员下拉 + 表单校验)
  • 客户部门页(customer/department/index.vue):搜索栏(关键词、客户下拉)+ 表格 + 弹窗(部门名称、联系人、电话、职能描述)
  • 客户账号页(customer/account/index.vue):搜索栏(关键词、客户下拉、状态)+ 表格(列含客户名称、电话、状态、创建时间、操作;提示默认密码=手机后6位)+ 弹窗(客户下拉、电话、状态;编辑时提供"重置密码为手机后6位"快捷操作)
  • 通用:表格内部滚动(复用 useTableScroll composable)、新增/编辑成功后页码重置到第一页并重新拉取列表、删除二次确认、状态 switch 一键切换

FR-7:前后端接口契约一致性(兼容 APP)

系统 SHALL 提供统一的 REST 接口,Web 后台和业务员 APP 共用:

  • 路径前缀:/api/customers、/api/customer-departments、/api/customer-accounts(与现有 /api/users 风格一致)
  • 通用返回结构:{ code: 200, message: '...', data: { list, total, page, pageSize } }(列表)或 { code: 200, message, data: {...} | null }(单条/写操作)
  • 鉴权方式:统一 Authorization: Bearer <access_token>(JWT),APP 端和 Web 端完全相同
  • 错误码:401 未登录 / 403 无权限 / 400 参数错误 / 404 不存在 / 500 服务异常
  • 分页参数:page、pageSize;搜索 keyword;筛选字段均为 Query 参数,后端统一做数字类型校验和上限保护(pageSize ≤ 200)
  • 接口幂等:写操作均通过 id 区分,UNIQUE 字段(customer_no、customer_account.phone)冲突返回 400 + 明确 message

Non-Functional Requirements

NFR-1:防 SQL 注入

所有数据库查询必须通过 Sequelize 参数化 API(findAndCountAll、create、update、destroy + where 对象),禁止任何字符串拼接 SQL;LIKE 模糊查询使用 { [Op.like]: \%${keyword}%` }` 而非字符串模板。

NFR-2:前端防 XSS

  • 前端不使用 v-html 渲染任何用户输入字段,全部使用 v-text 或 AntDV 默认文本渲染(AntDV 默认转义)
  • 表单输入添加长度限制(VARCHAR 字段按数据库长度上限 + regex 过滤 < > " ' <script> 等关键字)
  • 富文本/HTML 内容本期不存在,无需 DOMPurify

NFR-3:接口鉴权覆盖

客户域 3 个 Controller 的每个请求方法均需经过 auth.middleware 和 rbac.middleware;白名单仅 ignore 指定的公开路由(客户域无公开接口)。

NFR-4:性能

  • 列表接口均走 Sequelize limit + offset 分页,避免一次性加载全表
  • customer 表需在 customer_name、salesman_id、status 字段建索引;customer_department / customer_account 在 customer_id 建索引(Sequelize Entity 定义 indexes)
  • 单接口响应时间目标:< 500ms(开发环境、本地数据库)

NFR-5:字段脱敏与敏感信息

  • 列表/详情接口永远不返回 password_hash、wx_openid(customer_account)等敏感字段
  • contact_phone 列表展示可保留完整值(内部系统),但需确保不写入前端错误日志中

NFR-6:TypeScript 类型安全

  • Entity / DTO / 前端 API 类型均用显式 interface 定义,禁止滥用 any(Controller 层 @Body / @Query 参数尽量使用 DTO class + class-validator 装饰器校验)

Constraints

  • Technical: Midway.js + Sequelize-typescript + PostgreSQL 15+(不能换 TypeORM,沿用现有 Sequelize 体系);Vue3 + Ant Design Vue 4 + Vite;JWT 鉴权 + 现有 RBAC 中间件体系
  • Business: 鑫潼单一运营主体,customer_id 逻辑隔离(非严格多租户);SALESMAN 角色行级可见范围 = salesman_id = 自身用户ID
  • Dependencies: 依赖现有表 sys_user(salesman_id 外键)、sys_role、sys_menu、sys_role_menu、sys_user_role、sys_dict(customer_status 字典已存在 db_seed.sql 中)

Assumptions

  • 省市区级联数据来源:引入开源轻量省市区 JSON(china-division 或等效),无需对接外部 API
  • 客户编号 customer_no 每日序号的并发安全:通过 PostgreSQL UNIQUE 约束 + 捕获冲突重试或日期序号表管理(本期简化为取当日最大序号+1,极端并发下重试)
  • 业务员 APP 项目为独立 uni-app 工程(不在当前仓库 client 目录),本期仅保证后端 API 契约稳定可被 HTTP 调用
  • customer_account 表的 wx_openid 字段本期不做微信登录实现,仅保留字段允许 NULL
  • 数据库建表方式:Sequelize Entity sync 自动建表(与现有 sys_user 方式一致),不手写 SQL 到 db_init.sql

Acceptance Criteria

AC-1:客户信息 CRUD 完整可用

  • Given: 使用 ADMIN 账号登录后台,进入客户信息页
  • When: 填写完整的新增客户表单(必填项:名称、省市区、联系人、电话、级别、状态、业务员)并提交
  • Then: 返回 code=200,列表刷新显示新增客户,customer_no 格式符合 KH+YYYYMMDD+序号;编辑修改联系人后列表刷新;删除未关联部门/账号的客户后列表移除;尝试删除已关联数据的客户返回 400 提示
  • Verification: programmatic

AC-2:SALESMAN 行级数据隔离生效

  • Given: 使用 SALESMAN 角色的 salesman01(id=2)登录;ADMIN 创建了客户 A(salesman_id=2)和客户 B(salesman_id=NULL 或其他)
  • When: salesman01 调用 GET /api/customers
  • Then: 返回列表仅包含客户 A;尝试 PUT/DELETE 客户 B 返回 403;创建新客户时 salesman_id 自动写入=2(即使请求体传了其他值也被忽略)
  • Verification: programmatic

AC-3:客户部门/账号的归属校验

  • Given: 客户 C 归 salesman01 所有,客户 D 归其他人所有
  • When: salesman01 POST /api/customer-departments 传 customer_id=D
  • Then: 返回 403 禁止越权操作;传 customer_id=C 则创建成功并归属正确
  • Verification: programmatic

AC-4:RBAC 菜单与按钮权限生效

  • Given: ADMIN 和 SALESMAN 登录后获取菜单树
  • Then: ADMIN 看到客户管理目录下 3 个子菜单;SALESMAN 也看到相同 3 个子菜单(本期种子已绑定);无权限角色(WORKER/CUSTOMER)登录访问 /customer 路由跳 403
  • Verification: human-judgment(UI 菜单可见性 + programmatic 接口权限码校验)

AC-5:客户账号默认密码规则正确

  • Given: 新建客户访问账号,电话 13800001234
  • When: 保存后数据库 password_hash 字段存在 bcrypt 哈希值
  • Then: 该哈希值与 bcrypt('001234') 对比为 true(用工具脚本或单元测试验证);列表接口不返回 password_hash
  • Verification: programmatic

AC-6:防 SQL 注入

  • Given: 列表接口 keyword 参数传入 ' OR 1=1 --
  • When: 后端执行查询
  • Then: 参数被安全转义,查询结果只返回正常的模糊匹配(无匹配则返回空列表),不会返回全表数据;SQL 日志不出现原始注入字符串拼接
  • Verification: programmatic

AC-7:前端防 XSS

  • Given: 新增客户表单客户名称输入 <script>alert('xss')</script>
  • When: 保存后列表页渲染该客户名称
  • Then: 名称以纯文本展示(脚本不执行);浏览器 Console 无异常报错
  • Verification: human-judgment(页面观察)

AC-8:业务员 APP 接口兼容

  • Given: 使用任意 HTTP 客户端(curl/Postman)模拟 APP 请求,Authorization 头携带 SALESMAN 的有效 JWT
  • When: 调用客户域 GET/POST/PUT/DELETE 接口
  • Then: 返回结构与 Web 端调用完全一致(同 code/message/data 结构);分页/搜索/鉴权行为无差别
  • Verification: programmatic

AC-9:前端列表刷新链路

  • Given: 当前在客户列表第 3 页
  • When: 新增/编辑/删除一条记录成功
  • Then: 分页页码重置为 1,列表重新拉取显示最新数据;搜索筛选条件保留(不清空 keyword/status)
  • Verification: human-judgment + programmatic(网络面板确认请求 page=1)

Open Questions

  • (已确认)功能范围:仅客户信息 + 客户部门 + 客户账号 3 个模块
  • (已确认)customer.salesman_id 行级隔离 + ADMIN 全量
  • (已确认)APP 兼容 = 业务员 APP 与后台共用 API,复用 sys_user 登录
  • (已确认)customer.status 枚举 1意向/2签约/3运维中/4终止;customer_no 自动生成
  • (已确认)customer_account 默认密码=手机后6位,bcrypt 加密
  • (已确认)前端 UI:a-cascader 省市区 + 弹窗模式 + 表格内滚动