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,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 省市区 + 弹窗模式 + 表格内滚动
|
||||
Reference in new issue
Block a user