feat: 新增客户管理全模块功能
实现客户信息、客户部门、客户账号三个子模块的完整CRUD,包含: 1. 数据库实体、DTO、控制器、服务层代码 2. RBAC权限映射与菜单种子数据扩展 3. 前端路由与API封装 4. 数据权限隔离与鉴权逻辑 5. 配套工具脚本与测试验证清单
This commit is contained in:
1 parent
026ec34471
commit
1c80a24d1d
31 files changed
+26321
-653
No files matched your search
@@ -0,0 +1,108 @@
|
||||
# Checklist - 客户管理核心模块(客户信息 + 客户部门 + 客户账号)
|
||||
|
||||
## 数据库 Entity 与表结构检查
|
||||
- [ ] `server/src/entity/customer.entity.ts` 存在,字段齐全:id/customer_no(UNIQUE)/customer_name/province/city/district/address/contact_person/contact_phone/postal_code/fax/customer_level/status/salesman_id(FK)/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 关联
|
||||
- [ ] 3 张 Entity 均定义正确索引:customer(customer_name, salesman_id, status, customer_no unique) / department(customer_id) / account(customer_id, phone unique)
|
||||
- [ ] Midway 启动时 3 张 Entity 正确加载,PostgreSQL 中 `\d customer / \d customer_department / \d customer_account` 建表成功,外键约束存在,字段类型/长度/默认值与设计一致
|
||||
- [ ] Sequelize Entity 字段均加了中文 `comment`,snake_case 列名通过 `field` 映射,无驼峰列误建
|
||||
|
||||
## db_seed.sql 菜单与权限种子数据检查
|
||||
- [ ] 脚本幂等:连续执行 2 次无报错,序列 id 不漂移
|
||||
- [ ] id=1「客户管理」menu_type=1(目录),parent_id=NULL
|
||||
- [ ] 新增 3 条子菜单:id=12 客户信息 /customer/info (permission=customer:list) / id=13 客户部门 /customer/department (customer:department) / id=14 客户账号 /customer/account (customer:account),parent_id=1,component 分别为 customer/info/index 等
|
||||
- [ ] 新增 9 条按钮权限(menu_type=3)id=15~23:customer:create/update/delete × 1、department: × 1、account: × 1,permission 标识命名正确
|
||||
- [ ] `sys_role_menu` 中 role_id=1(ADMIN)绑定 id 1~23 全部;role_id=2(SALESMAN)绑定 id 1, 12~23(客户域全部)
|
||||
- [ ] SELECT count(*) FROM sys_menu = 23(原 11 条 + 新增 12 条);SELECT count(*) FROM sys_role_menu 数量正确
|
||||
|
||||
## 鉴权中间件与 RBAC 检查
|
||||
- [ ] `rbac.middleware.ts` 的 `PERMISSION_MAP` 新增 3 条:`/api/customers→customer:list`、`/api/customer-departments→customer:department`、`/api/customer-accounts→customer:account`
|
||||
- [ ] 未登录访问 3 组接口 → 返回 401 code=401 message=未登录
|
||||
- [ ] ADMIN token 访问所有接口 → 200 放行
|
||||
- [ ] SALESMAN token 访问列表接口 → 返回 salesman_id 过滤后的行级数据;访问他人客户的 PUT/DELETE → 403 无权限
|
||||
- [ ] SALESMAN 创建客户时,传入 salesman_id=3 被覆盖为自己的 id,保存后数据库 salesman_id 正确
|
||||
- [ ] WORKER/CUSTOMER 角色 token 访问客户域接口 → 403 无权限
|
||||
|
||||
## 客户信息 CRUD 接口检查(/api/customers)
|
||||
- [ ] GET / 分页搜索:page=1&pageSize=10&keyword=xx&status=1&salesmanId=1 → 返回 `{code:200,data:{list,total,page,pageSize}}` 结构正确
|
||||
- [ ] customer_no 生成规则:同日连建 3 条 → 编号 KH+YYYYMMDD+001/002/003;UNIQUE 冲突时无崩溃
|
||||
- [ ] POST 创建:必填项缺失 → 400 参数错误(DTO 校验工作);contact_phone 非手机号 regex 拒绝
|
||||
- [ ] GET /:id 不存在 → 404;存在但 SALESMAN 无权限 → 403
|
||||
- [ ] PUT /:id 更新:customer_no 不可改;非 ADMIN 无法修改 salesman_id
|
||||
- [ ] DELETE /:id:存在 customer_department 或 customer_account 关联时 → 返回 400 禁止删除;无关联删除成功
|
||||
- [ ] PUT /:id/status 状态切换(1→2→3→4)成功
|
||||
- [ ] 列表和详情返回字段中 **绝不** 出现 `password_hash` / `wx_openid` / `password` 任何敏感列
|
||||
|
||||
## 客户部门 CRUD 接口检查(/api/customer-departments)
|
||||
- [ ] 按 customer_id 过滤:GET /?customerId=N → 返回只属于该客户的部门
|
||||
- [ ] SALESMAN 越权创建:传 customer_id=(别人客户)→ 403;传自己客户 → 创建成功
|
||||
- [ ] 更新/删除归属校验:SALESMAN 不能改别人客户下的部门
|
||||
- [ ] DTO 校验:dept_name 空值 → 400;字段长度超限 → 400
|
||||
|
||||
## 客户账号 CRUD 接口检查(/api/customer-accounts)
|
||||
- [ ] 创建账号:phone=13800001234 → 默认密码 hash 通过 bcrypt.compare('001234', hash) === true;phone 不足 6 位时默认密码为 123456
|
||||
- [ ] 创建重复 phone → 返回 400 '手机号已存在'
|
||||
- [ ] 列表 / 详情接口响应 JSON 中字段白名单:id/customerId/phone/status/createdAt/lastLoginAt(不含 password_hash / wx_openid)
|
||||
- [ ] 更新状态成功;resetPassword=true 触发重新哈希默认密码
|
||||
- [ ] SALESMAN 归属校验:只允许操作自己客户下的账号,否则 403
|
||||
- [ ] customer_account.status = 0 禁用账号后,若未来有登录接口需拒绝(本期无登录实现,但字段必须正确写入)
|
||||
|
||||
## DTO 参数校验与类型检查
|
||||
- [ ] 非法 pageSize(>200、非数字)→ 返回 400 校验错误
|
||||
- [ ] 注入型 keyword(`<script>alert(1)</script>` / `' OR 1=1 --`)→ DTO 长度/regex 校验或正常参数化查询,接口无 500
|
||||
- [ ] 所有接口均不使用 `any` 类型(Controller @Body/@Query 绑定具体 DTO 类,Service 参数具名 interface 类型)
|
||||
- [ ] server package.json 安装的 class-validator / class-transformer 已正确应用装饰器(@IsString/@IsInt/@Max/@Min/@Matches 等)
|
||||
|
||||
## 前端路由、菜单、API 封装检查
|
||||
- [ ] router/index.ts 中 /customer 拆成 3 条子路由:/info /department /account,component 指向各自 vue,meta.permission 与菜单对应
|
||||
- [ ] 登录后侧边栏「客户管理」目录可展开为 3 个子菜单,点击分别跳转;面包屑正确
|
||||
- [ ] 3 个 api/*.ts 文件存在:customer.ts / customerDepartment.ts / customerAccount.ts;全部走 @/utils/request(Axios)、携带 Authorization 拦截器、错误统一提示
|
||||
- [ ] TypeScript 类型定义完整:CustomerInfo / CustomerQuery / DepartmentInfo 等接口导出,页面使用时无隐式 any
|
||||
|
||||
## 客户信息前端页面(customer/info/index.vue)
|
||||
- [ ] UI 风格与 system/user/index.vue 完全一致:a-card 标题 / 顶部新增批量删除按钮 / 搜索栏 / 表格 / 弹窗
|
||||
- [ ] 搜索:关键词输入、状态下拉、业务员下拉(ADMIN 显示、SALESMAN 隐藏/禁用);查询/重置按钮
|
||||
- [ ] 表格列:客户编号/名称/省市区(拼接)/地址/联系人/电话/客户级别/状态(a-switch)/创建时间/操作;列宽合理、ellipsis 生效
|
||||
- [ ] 新增/编辑弹窗:a-cascader 省市区级联可用、数据来源于 regionOptions(region.ts);表单必填校验 + 手机号 regex + 长度限制;保存成功 → 弹窗关闭 → page=1 → 刷新列表(保留筛选)
|
||||
- [ ] 编辑时 customer_no 字段只读(禁用或隐藏)
|
||||
- [ ] 删除确认弹框(a-popconfirm)生效;批量删除按钮选中时启用
|
||||
- [ ] useTableScroll 正确应用:wrapperRef + tableHeight,表格 body 内部滚动,整页无垂直滚动条溢出
|
||||
- [ ] 状态 switch 点开关即时调用 updateStatus 接口,成功后 message.success
|
||||
|
||||
## 客户部门/账号前端页面检查
|
||||
- [ ] 部门页:客户下拉可用(ADMIN 全量/SALESMAN 仅自己客户);新增/编辑表单客户字段必填校验
|
||||
- [ ] 账号页:顶部展示「默认密码=手机号后 6 位」文字提示;重置密码按钮/复选框存在并生效
|
||||
- [ ] 三页面列表刷新链路一致:新增/编辑/删除成功后统一 reset page to 1 → refetch,筛选条件保留
|
||||
- [ ] SALESMAN 登录时,页面 UI 与权限一致:客户下拉过滤、越权操作按钮隐藏(若通过接口已 403,则按钮隐藏更友好)
|
||||
|
||||
## 安全防护检查
|
||||
- [ ] 前端防 XSS:所有表格列使用 AntDV 默认文本渲染,无一处使用 v-html;表单 maxlength 限制 + 正则过滤 script 标签;注入输入保存后列表纯文本显示、浏览器无 alert 弹窗
|
||||
- [ ] 后端防 SQL 注入:grep server/src 无任何 `sequelize.query()` 字符串拼接 SQL(若有必须用 replacements 参数化);所有查询走 Sequelize Model 方法 + where 对象;用 Postman 发 `' OR 1=1 -- ` keyword 不返回全表数据
|
||||
- [ ] 接口鉴权覆盖:3 个模块 × 6 方法共 18 个接口,均经过 auth + rbac 中间件,无匿名可写接口
|
||||
- [ ] 密码加密:customer_account 创建和重置密码时使用 hashPassword 生成 bcrypt(saltRounds≥10),数据库仅存哈希,无明文登录密码任何位置落盘/日志
|
||||
- [ ] 敏感字段脱敏:customer_account 列表/详情响应不返回 password_hash 与 wx_openid(浏览器 DevTools Network 面板确认)
|
||||
|
||||
## 业务员 APP 接口兼容性检查
|
||||
- [ ] 用 Postman/curl 带相同 JWT 头(Authorization: Bearer xxx)调用 3 个模块的 GET/POST/PUT/DELETE 接口,返回结构与 Web 端完全相同(code/message/data 字段一致)
|
||||
- [ ] 分页参数 page/pageSize、筛选 keyword/status 行为完全相同;分页上限保护 APP 侧调超大 pageSize 时 400 正确拒绝
|
||||
- [ ] 错误码映射:401/403/400/404/500 都有明确 message,APP 侧可据此弹窗提示
|
||||
- [ ] 同一 SALESMAN token,Web 和 APP 端同时调用 list,返回数据集完全一致(同一行级隔离)
|
||||
|
||||
## 集成与端到端流程检查
|
||||
- [ ] ADMIN 完整走通:登录 → 客户管理展开 → 客户信息新增 2 条 → 给每条新增 1 个部门 + 1 个账号 → 列表分页翻到第 2 页 → 搜索关键词命中 → 编辑修改 → 删除 1 条客户(先删部门和账号再删客户成功,未删时报 400)→ 全程无 401/403/500
|
||||
- [ ] SALESMAN 完整走通:登录 → 客户列表仅见归属自己的 → 新增客户(salesman_id 自动写自己)→ 新增自己客户的部门/账号成功 → 尝试在 URL 手改别人客户 ID PUT 接口 → 返回 403 且数据未变
|
||||
- [ ] db_seed.sql 重新执行不影响已录入的客户域业务数据(仅系统表按 id 幂等更新,customer 等业务表无 TRUNCATE)
|
||||
|
||||
## 代码质量与构建检查
|
||||
- [ ] Server:`yarn lint` 无 ESLint 错误;`yarn build`(midway-bin build -c)构建成功,tsc 无类型错误
|
||||
- [ ] Client:`yarn build`(vite build)无 TS 错误、无警告;ESLint 无告警
|
||||
- [ ] 无硬编码敏感信息(JWT secret、db password)仍留在代码中的问题;统一从 config 读取
|
||||
- [ ] 新增代码文件命名/目录与现有约定一致:entity/*.entity.ts、dto/*.dto.ts、service/*.service.ts、controller/*.controller.ts、views/customer/*/index.vue
|
||||
- [ ] 前后端字段名映射正确:后端 snake_case(DB)→ camelCase(Entity + JSON 响应)→ 前端 camelCase 使用,无错配(例:password_hash → passwordHash → 返回 JSON 无此字段即白名单过滤生效)
|
||||
|
||||
## 业务一致性 / 边界用例检查
|
||||
- [ ] 删除客户:先建客户→建部门→直接删客户→接口返回400禁止→先删部门/账号→再删客户成功(外键保护链路)
|
||||
- [ ] customer_no 跨天:模拟 23:59 和 00:01 分别创建,序号从 001 重新起
|
||||
- [ ] 空列表:新建干净数据库客户域无数据,三页面均展示 a-empty 无报错
|
||||
- [ ] 超长字段:客户地址传 256+ 字符 → DTO 校验截断或报错,数据库不静默截断导致数据丢失
|
||||
@@ -0,0 +1,185 @@
|
||||
# 鑫潼智电系统 · 客户管理核心模块 (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 省市区 + 弹窗模式 + 表格内滚动
|
||||
@@ -0,0 +1,212 @@
|
||||
# 鑫潼智电系统 · 客户管理核心模块 - 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) 新增绑定 12~23 全部;SALESMAN(role_id=2) 新增绑定 12~23 全部(删除原只绑定 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 分别为 KH<today>001 / 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 ──┘
|
||||
```
|
||||
Reference in new issue
Block a user