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

18 KiB
Raw Blame History

鑫潼智电系统 · 客户管理核心模块 - The Implementation Plan (Decomposed and Prioritized Task List)

[x] Task 1: 新增 3 个 Sequelize Entity 模型文件

  • Priority: high
  • Depends On: None
  • Description:
    • 在 server/src/entity/ 下新增 customer.entity.ts:字段严格对齐 spec.md + DB 设计文档,新增 customer_no (VARCHAR(64) UNIQUE) 和 salesman_id (BIGINT FK→sys_user.id),province/city/district/address/contact_person/contact_phone/postal_code/fax/customer_level/status(SMALLINT default 1)/created_at/updated_at
    • 新增 customer_department.entity.ts:id/customer_id(FK)/dept_name/contact_person/contact_phone/function_desc/created_at,BelongsTo Customer 关联
    • 新增 customer_account.entity.ts:id/customer_id(FK)/phone(UNIQUE)/password_hash/wx_openid/status/last_login_at/created_at,BelongsTo Customer 关联
    • 每个 Entity 定义必要索引(customer: customer_name, salesman_id, status, customer_no unique; department: customer_id; account: customer_id, phone unique)
    • 字段使用 field 映射 snake_case 列名,comment 加中文注释,@CreatedAt @UpdatedAt 装饰器
  • Acceptance Criteria Addressed: FR-1, FR-2, FR-3, NFR-1
  • Test Requirements:
    • programmatic TR-1.1: Sequelize sync 后 PostgreSQL 中三张表成功建立,字段类型/长度/默认值/索引与定义一致,外键约束存在
    • programmatic TR-1.2: customer 表插入一条含 phone 的 customer_account,尝试重号 phone 抛唯一约束错误
    • human-judgement TR-1.3: 代码 review 确认字段未遗漏 customer_no / salesman_id / password_hash / status 等关键列
  • Notes: 沿用现有 sysUser.entity.ts 风格,DataType + 注释 + timestamps 配置一致

[x] Task 2: 扩展 db_seed.sql 客户域菜单与权限

  • Priority: high
  • Depends On: None
  • Description:
    • 把现有 id=1「客户管理」顶级菜单 menu_type 从 2 改为 1(目录),parent_id=NULL 不变
    • 新增 3 条子菜单(id=12/13/14),parent_id=1:12=客户信息(/customer/info component=customer/info/index permission=customer:list)、13=客户部门(/customer/department component=customer/department/index permission=customer:department)、14=客户账号(/customer/account component=customer/account/index permission=customer:account)
    • 新增 9 条按钮权限菜单(menu_type=3)id=15~23:customer:create / customer:update / customer:delete / department:create / department:update / department:delete / account:create / account:update / account:delete
    • UPDATE sys_role_menu:ADMIN(role_id=1) 新增绑定 1223 全部;SALESMAN(role_id=2) 新增绑定 1223 全部(删除原只绑定 id=1 的记录,或新增并保留 id=1)
    • 保持 ON CONFLICT DO UPDATE / DO NOTHING 幂等性
  • Acceptance Criteria Addressed: FR-5, AC-4
  • Test Requirements:
    • programmatic TR-2.1: 执行 psql -f db_seed.sql 后 sys_menu 表 id 1~23 全部存在,id=1 menu_type=1,id=12/13/14 parent_id=1
    • programmatic TR-2.2: sys_role_menu 中 role_id=1 包含 menu_id 1~23 全部;role_id=2 至少包含 1,12,13,14,15..23
  • Notes: 先 TRUNCATE 或逐条 DELETE ON CONFLICT 前需注意不要误删系统管理菜单,建议所有 INSERT 使用显式 id + ON CONFLICT (id) DO UPDATE

[x] Task 3: 扩展 RBAC 中间件 + 注册 Entity 到配置

  • Priority: high
  • Depends On: [Task 1, Task 2]
  • Description:
    • 在 server/src/middleware/rbac.middleware.ts 的 PERMISSION_MAP 中新增 3 条:
      • /api/customers → customer:list
      • /api/customer-departments → customer:department
      • /api/customer-accounts → customer:account
    • 在 server/src/configuration.ts 或 Sequelize 配置 config.default.ts 的 models 数组中,显式 import 并加入 Customer / CustomerDepartment / CustomerAccount 三个 Entity(确保 sync 时加载)
  • Acceptance Criteria Addressed: FR-4, AC-2, AC-4
  • Test Requirements:
    • programmatic TR-3.1: 用无权限 token 访问 GET /api/customers 返回 403(rbac 中间件工作);ADMIN token 返回 200
    • programmatic TR-3.2: 服务启动无 Entity 加载报错,sync 后 3 张表存在
  • Notes: 若 models 采用目录扫描自动加载,Task 1 把 Entity 放对目录即可;如显式 import 列表则需手动追加

[x] Task 4: 新增客户域 DTO 参数校验类

  • Priority: high
  • Depends On: [Task 1]
  • Description:
    • 新建 server/src/dto/customer.dto.ts:
      • CustomerQueryDto:page/ pageSize(number, min 1, max 200)/ keyword(string, max 64)/ status(SMALLINT 1-4)/ salesmanId(optional)
      • CustomerCreateDto:customerName(required, max 128, regex 禁 script tag)/ province/city/district/address(max 256)/ contactPerson/ contactPhone(regex 手机号)/ postalCode?/ fax?/ customerLevel?/ status(default 1)/ salesmanId?
      • CustomerUpdateDto:字段可选,与 create 共用正则
    • 新建 server/src/dto/customer.department.dto.ts:CustomerDepartmentQueryDto / Create / Update(customerId required 数字)
    • 新建 server/src/dto/customer.account.dto.ts:CustomerAccountQueryDto / Create / Update;CreateDto 无 password 字段(由 service 自动生成),UpdateDto 含可选 resetPassword 标志位 + status
  • Acceptance Criteria Addressed: FR-1, FR-2, FR-3, NFR-6
  • Test Requirements:
    • programmatic TR-4.1: 传空 customerName 创建 → 返回 400 参数错误
    • programmatic TR-4.2: 传 pageSize=500 → 校验失败,返回 max 200 提示
    • programmatic TR-4.3: contactPhone 传 'abc' → regex 校验失败
  • Notes: 使用 @midwayjs/validate + class-validator 装饰器,与现有 dto/*.ts 风格一致;禁止使用 any

[x] Task 5: 实现 CustomerService(含 customer_no 生成 + 行级过滤)

  • Priority: high
  • Depends On: [Task 1, Task 4]
  • Description:
    • 新建 server/src/service/customer.service.ts,@Provide() 类
    • list(query, ctx):分页 + 关键词模糊查询 Op.or [customer_name, contact_person, contact_phone] + status 筛选;SALESMAN 角色强制追加 where salesman_id = ctx.state.user.userId;ADMIN 不加
    • detail(id, ctx):按 id 查询,SALESMAN 校验 salesman_id 归属,否则 403
    • generateCustomerNo():查询当日 customer_no 最大序号,规则 KH + YYYYMMDD + 3位序号(001 起始);捕获 UNIQUE 冲突重试 1 次
    • create(data, ctx):校验必要字段;若 SALESMAN 创建则 data.salesmanId 强制覆盖为 ctx.state.user.userId;调用 generateCustomerNo();写入后返回完整创建对象
    • update(id, data, ctx):归属校验;字段白名单更新(customer_no / salesman_id(非ADMIN)不可改)
    • delete(id, ctx):归属校验;先查是否存在 customer_department 或 customer_account 关联,有则返回 {code:400, message:'该客户存在关联部门/账号,无法删除'};否则删除
    • updateStatus(id, status, ctx):快捷更新状态
  • Acceptance Criteria Addressed: FR-1, FR-4, AC-1, AC-2
  • Test Requirements:
    • programmatic TR-5.1: ADMIN 创建 2 个客户后,SALESMAN(id=2) list 返回 total ≤ ADMIN 看到的 total,且返回每条 salesman_id=2
    • programmatic TR-5.2: 同一天连续 create 3 条客户,customer_no 分别为 KH001 / 002 / 003,格式正确
    • programmatic TR-5.3: SALESMAN 尝试传 salesman_id=3 创建,保存后实际 salesman_id=2(被覆盖)
    • programmatic TR-5.4: 删除有关联部门的客户 → 返回 400 禁止
  • Notes: 所有查询用 Sequelize 参数化 where 对象,禁止拼接;关联 existence 检查用 findOne({ where: { customerId: id }, limit: 1 }) 而非 countAll,提高性能

[x] Task 6: 实现 CustomerDepartmentService + CustomerAccountService

  • Priority: high
  • Depends On: [Task 1, Task 4, Task 5]
  • Description:
    • customer.department.service.ts:list(关键词搜索 + 按 customer_id 过滤;SALESMAN 通过 customer_id JOIN customer.salesman_id 过滤);create(校验 customer_id 归属:SALESMAN 需确保目标 customer.salesman_id = 自己);update / delete(归属校验 + 外键关联提示)
    • customer.account.service.ts:list(按 customer_id / status / keyword phone 模糊;SALESMAN JOIN 过滤;返回 exclude password_hash, wx_openid);create(默认密码=phone 后 6 位 → bcrypt 加密;phone UNIQUE 校验);update(字段白名单;若 resetPassword=true 则重新计算密码 hash;返回值永远不带 password_hash);delete(归属校验);updateStatus(快捷切换)
    • 复用 hashPassword 工具:server/src/util/crypto.ts
  • Acceptance Criteria Addressed: FR-2, FR-3, AC-3, AC-5
  • Test Requirements:
    • programmatic TR-6.1: SALESMAN 对自己客户创建部门 → 成功;对别人客户创建部门 → 403
    • programmatic TR-6.2: 创建 account phone=13800001234,数据库 password_hash 经过 bcrypt 验证等于 '001234'(可用单元测试对比)
    • programmatic TR-6.3: 同 phone 创建第二个 account → 返回 400 '手机号已存在'
    • programmatic TR-6.4: list 接口响应 JSON 中不含 password_hash / wx_openid 字段
  • Notes: bcrypt 比较使用 bcryptjs.compare;密码生成函数单独抽出便于单元测试

[x] Task 7: 实现客户域 3 个 Controller

  • Priority: high
  • Depends On: [Task 5, Task 6]
  • Description:
    • 新建 server/src/controller/customer.controller.ts:
      • @Controller('/api/customers');@Inject ctx + customerService
      • GET '/' list(@Query() query) → service.list(query, ctx)
      • GET '/:id' detail(@Param('id') id) → service.detail(id, ctx)
      • POST '/' create(@Body() body) → service.create(body, ctx)
      • PUT '/:id' update(@Param('id') id, @Body() body) → service.update(id, body, ctx)
      • DELETE '/:id' delete(@Param('id') id) → service.delete(id, ctx)
      • PUT '/:id/status' updateStatus → service.updateStatus
    • 新建 customer.department.controller.ts:@Controller('/api/customer-departments'),同上 6 个方法
    • 新建 customer.account.controller.ts:@Controller('/api/customer-accounts'),同上 6 个方法 + 可选 POST '/:id/reset-password'
    • 所有写操作:@UseMiddleware(AuthMiddleware, RbacMiddleware) 或沿用全局注册方式
    • 统一返回结构:{ code, message, data },与现有 user.service 对齐
  • Acceptance Criteria Addressed: FR-7, AC-8
  • Test Requirements:
    • programmatic TR-7.1: 用 curl 调 ADMIN token GET /api/customers?page=1&pageSize=10 → 返回结构 {code:200,message,data:{list,total,page,pageSize}}
    • programmatic TR-7.2: 无 Authorization 头 → 401
    • programmatic TR-7.3: PUT /api/customers/99999(不存在) → 404
  • Notes: 检查全局中间件是否已经自动 apply(auth + rbac),如全局已注册则 Controller 不再重复装饰;沿用现有 user.controller.ts 风格

[x] Task 8: 前端 API 封装 + 路由改造

  • Priority: high
  • Depends On: [Task 7]
  • Description:
    • 新建 client/src/api/customer.ts:getCustomerList / getCustomerDetail / createCustomer / updateCustomer / deleteCustomer / updateCustomerStatus;类型接口 CustomerInfo / CustomerQuery / CustomerCreate 等
    • 新建 client/src/api/customerDepartment.ts:对应 CRUD + 类型
    • 新建 client/src/api/customerAccount.ts:CRUD + resetAccountPassword + 类型
    • 更新 client/src/router/index.ts:将原 /customer Placeholder 路由拆为 3 条嵌套子路由:/customer/info → views/customer/info/index.vue,/customer/department → views/customer/department/index.vue,/customer/account → views/customer/account/index.vue;父级 component 仍使用 Layout,权限 customer:list/department/account 分别设置
  • Acceptance Criteria Addressed: FR-6, FR-7
  • Test Requirements:
    • programmatic TR-8.1: 登录后访问浏览器 /customer/info → 路由正确命中(非 404/403,权限验证通过时)
    • programmatic TR-8.2: 触发 createCustomer 请求 → Network 面板 url/method/payload 正确,拦截器注入 Authorization
  • Notes: 所有 API 方法复用 @/utils/request 实例(Axios 封装);类型定义 export 供页面使用

[x] Task 9: 前端省市区数据 + 客户信息页面实现

  • Priority: high
  • Depends On: [Task 8]
  • Description:
    • 新建 client/src/assets/data/region.ts:轻量省→市→区 3 级 JSON 树(可引入开源数据或手写示例子集至少 10 省+常用市+区),导出 regionOptions: CascaderOption[]
    • 新建 client/src/views/customer/info/index.vue:
      • 完全对齐现有 system/user/index.vue 布局风格
      • 搜索栏:关键词输入框 + 状态 a-select(字典 customer_status 1~4)+ 业务员 a-select(拉 sys_user 简化列表)+ 查询/重置按钮
      • 表格列:客户编号(ellipsis)、客户名称、省市区(拼接显示)、详细地址、联系人、电话、客户级别、状态(a-switch)、创建时间(formatDateTime)、操作(编辑/删除)
      • 弹窗:客户基本信息表单 + 省市区 a-cascader(options=regionOptions, 双向绑定数组 [province,city,district])+ 业务员下拉(仅 ADMIN 可见可改,SALESMAN 登录时隐藏/只读)+ AntD form 校验规则(名称必填、电话 regex、长度限制、XSS 关键字过滤)
      • 表格内部滚动:复用 useTableScroll composable(wrapperRef, tableHeight)
      • 增删成功后:关闭弹窗 → searchForm.page = 1 → fetchList()(保留筛选条件)
  • Acceptance Criteria Addressed: FR-6, AC-7, AC-9
  • Test Requirements:
    • human-judgement TR-9.1: 页面 UI 与用户管理页风格一致,表格滚动正常,弹窗表单校验生效
    • programmatic TR-9.2: 新增客户后 Network 返回 code=200,列表自动刷新并回到 page=1,搜索栏关键词未清空
    • human-judgement TR-9.3: 输入客户名称 <script>alert(1)</script> 保存后列表以纯文本显示,脚本不执行

[x] Task 10: 客户部门 + 客户账号前端页面实现

  • Priority: high
  • Depends On: [Task 9]
  • Description:
    • customer/department/index.vue:
      • 搜索栏:关键词 + 客户下拉(调用 customerApi.getCustomerList({pageSize: 999}) 取简表,ADMIN 显示全部,SALESMAN 只显示自己客户)+ 查询/重置
      • 表格列:所属客户、部门名称、联系人、联系电话、职能描述、创建时间、操作
      • 弹窗:客户下拉(必填)+ 部门名称(必填)+ 联系人 + 电话(regex)+ 职能描述 a-textarea
      • 行级归属:SALESMAN 新增/编辑时客户下拉只显示自己客户选项
    • customer/account/index.vue:
      • 搜索栏:关键词(电话)+ 客户下拉 + 状态 + 查询/重置
      • 表格列:所属客户、登录电话、状态(a-switch)、创建时间、操作(编辑/删除/重置密码按钮)
      • 弹窗:客户下拉(必填)+ 登录电话(必填 regex)+ 状态;编辑表单提示「默认密码=手机号后6位」;编辑页底部提供「重置为默认密码」a-checkbox 勾选,勾选后请求体传 resetPassword:true
      • 列表顶部提示:默认密码规则说明
  • Acceptance Criteria Addressed: FR-6, AC-3, AC-5
  • Test Requirements:
    • human-judgement TR-10.1: SALESMAN 登录新增部门,客户下拉只显示归属自己的客户
    • programmatic TR-10.2: 重置默认密码 → 后端更新 hash,用 bcrypt 验证对应密码正确(可后端打临时日志验证或通过 customer_account 登录接口间接测)
    • human-judgement TR-10.3: 删除确认二次弹框正常工作

[x] Task 11: 集成联调 + 权限/隔离验证

  • Priority: high
  • Depends On: [Task 7, Task 8, Task 9, Task 10]
  • Description:
    • 启动 server(midway dev)+ client(vite dev)
    • 场景 1 ADMIN:客户信息/部门/账号 三模块完整 CRUD 走通一遍(新增2条 → 编辑 → 列表搜索/分页/筛选 → 删除1条 → 状态切换)
    • 场景 2 SALESMAN:登录 salesman01 → list 仅见自己客户;尝试通过 Postman 越权 PUT /api/customers/<他人客户ID> → 403
    • 场景 3 数据一致性:customer_id 外键删除保护、customer_no UNIQUE、customer_account phone UNIQUE 三项约束均触发成功
    • 场景 4 APP 兼容:用 Postman 模拟 APP 请求(同 JWT token 同 header)→ 所有列表/写操作返回结构与浏览器一致
    • 场景 5 前端 XSS:在 3 个模块的文本输入字段分别注入 <img src=x onerror=alert(1)> + <script>...</script> → 保存后列表纯文本展示不执行
  • Acceptance Criteria Addressed: AC-1, AC-2, AC-3, AC-7, AC-8
  • Test Requirements:
    • programmatic TR-11.1: 5 个场景全部通过,接口无 500
    • human-judgement TR-11.2: 浏览器 Console 无 JS 报错,XSS 注入脚本不执行

[x] Task 12: SQL 注入安全验证 + 构建/Lint 检查

  • Priority: medium
  • Depends On: [Task 11]
  • Description:
    • 用 Postman 对 3 个列表接口发送 keyword = ' OR 1=1 -- 以及 ' OR '1'='1 → 返回结果数量与正常模糊匹配一致(可能=0),不出现全表数据泄露
    • 验证后端所有 Sequelize 代码均未使用 sequelize.query() 拼接字符串 SQL(grep 检查)
    • 运行 cd server && yarn lint + cd client && yarn build 确保无 TS 类型错误和 Lint 告警
    • 运行 db_seed.sql 脚本幂等:连续执行 2 次无异常,菜单/角色数据无重复漂移
  • Acceptance Criteria Addressed: AC-6, NFR-1, NFR-2, NFR-6
  • Test Requirements:
    • programmatic TR-12.1: SQL 注入测试返回 total 符合预期(无全表)
    • programmatic TR-12.2: yarn build 两端均通过,0 Error
    • programmatic TR-12.3: db_seed.sql 第二次执行仍成功,SELECT count(*) FROM sys_menu = 23
  • Notes: 若有 sequelize.query() 用于 customer_no 生成等场景,强制使用 replacements 参数化,禁止 \${var}``` 字符串拼接

Task Dependencies (DAG)

Task1 ─┐
Task2 ─┼─→ Task3 ─┐
Task4 ─┼─→ Task5 ─→ Task7 ─→ Task8 ─→ Task9 ─→ Task10 ─→ Task11 ─→ Task12
       └─→ Task6 ──┘