# Checklist > change-id: `add-om-pricing-and-worker-registry` > 逐条核对,通过后勾选。 > 验证方式说明: > - 「API 实测」= 在本地开发库(127.0.0.1:5432/xintong)起 `MIDWAY_SERVER_ENV=local` 实例,用 admin 真实 JWT 逐条打接口 + 直连数据库比对; > - 「浏览器实测」= `vite` + 本地后端,用浏览器实机登录后走查; > - 「代码核对」= 无法在运行时构造的场景,以代码 + 结构(约束/索引)核对。 ## 数据库与实体 - [x] `database/migrations/20261001_om_worker_registry.sql` 已创建,覆盖 `worker` / `worker_cert` / `service_price` / `dispatch_price` / `sys_notice` 五处变更,且可重复执行无报错 —— API 实测:本地库重跑迁移无报错,`worker` 18 列、`dispatch_price` 11 列不变,`sys_menu` 34-46 仍为 13 行无重复 - [x] `database/db_seed.sql` 已同步菜单(id 34-46)、字典(5 个 dict_type)、ADMIN 角色绑定,新库初始化与迁移后结构一致 —— 代码核对(Task 1 已完成并经迁移对照) - [x] `worker` 实体含 `password_hash` / `audit_status` / `audit_remark` / `audit_by` / `audit_at` / `last_login_at` —— 代码核对 + 库表列核对(6 列均在) - [x] `dispatch_price` 实体含 `price_mode` / `unit` / `status` / `remark` / `updated_at` —— 代码核对 + 库表列核对(5 列均在) - [x] `worker_cert` / `service_price` / `sys_notice` 实体已创建,索引与迁移脚本一致 —— API 实测 `uk_worker_cert_type_no` / `uk_service_price_code` / `uk_sys_notice_biz` 均生效 ## 客户运维价目(FR-1) - [x] `/api/service-prices` 支持分页、关键词(编码 / 名称)、`price_mode` / `service_type` / `status` 筛选 —— API 实测四种筛选均只返回命中记录 - [x] `price_code` 自动生成为 `FW+YYYYMMDD+3 位序号` 且不可编辑 —— API 实测连建三条得到 `FW20261001001`~`003`,`priceCode` 在白名单外故不可写 - [x] `price_mode` 为 1 按次 / 2 按工时 / 3 材料 三种取值均可新增、编辑、筛选 —— API 实测 - [x] 支持启用 / 停用与硬删除两个独立操作 —— API 实测 `PUT /:id/status` 与 `DELETE /:id`(删除后查库确认为物理删除,重复删返回 404) ## 电工派单成本价(FR-2) - [x] 存量 `dispatch_price` 行迁移后 `price_mode=1`、`status=1` 无 NULL —— API 实测存量 2 行均为 `priceMode=1 / status=1` - [x] `/api/dispatch-prices` CRUD + 筛选可用,`price_code` 冲突返回 400 且 message 明确 —— CRUD/筛选 API 实测通过;`UniqueConstraintError`→「价格编码已存在」分支为代码核对(生成器保证编码不重复,无法从接口构造冲突) ## 电工登记与审核(FR-3 / FR-4) - [x] `/api/workers` 支持分页、关键词、`status` / `auditStatus` / `certStatus` 筛选 —— API 实测 + 浏览器实测(已过期 1 条 / 无证书 3 条,筛选结果与库一致) - [x] 创建时手机号唯一校验生效;默认密码规则为手机号后 6 位(不足 6 位用 `123456`)并 bcrypt 存储 —— API 实测:重复手机号返回 `手机号已存在`;`password_hash` 以 `$2a$10$` 开头,用手机号后 6 位可成功登录电工 APP - [x] 审核通过写入 `audit_status=1` / `audit_by` / `audit_at`,清空 `audit_remark`,并置 `cert_verified=true` —— API 实测(库中字段逐项比对) - [x] 审核驳回未填理由返回 400;填写后 `audit_status=2` 且理由、审核人、时间落库 —— API 实测:不填理由 → `code:400「请填写驳回理由」`;填理由 → `audit_status=2 / audit_remark / audit_by / audit_at` 落库 - [x] 删除电工级联删除其 `worker_cert`;存在派单引用时返回 400 并提示 —— API 实测:删除后 `worker_cert` 1→0;自造 `survey_order` 引用后返回 `code:400「该电工存在派单记录,无法删除」` - [x] `/api/workers/options` 与 `/recommend` 仅返回 `audit_status=1 AND status=1` 的电工 —— 代码核对(查询条件已收紧)+ API 实测两接口正常返回 ## 资质证书与到期状态(FR-5 / FR-6) - [x] `worker_cert` 支持一电工多证,`(worker_id, cert_type, cert_no)` 唯一约束生效 —— API 实测一电工挂多证;重复插入触发 `uk_worker_cert_type_no` 唯一冲突 - [x] 资质状态计算正确:无证书 / 正常 / 临期(≤30 天)/ 已过期(`expire_date < today`),`expire_date` 为 NULL 视为长期有效 —— API 实测:构造 +10 天 / -5 天 / +200 天 / NULL 四种证书,`certStatus` 四种筛选各自只返回对应记录 - [x] 电工列表「资质状态」列标签颜色正确(绿 / 橙 / 红 / 灰)—— 浏览器实测:列表渲染出「已过期」彩色标签;配色取自 `a-tag` 状态映射(代码核对) - [x] 列表 KPI 卡统计「电工总数 / 待审核 / 临期 / 已过期」数值正确 —— API 实测 `stats` 与独立 SQL 计算一致;浏览器实测 KPI 显示 5 / 4 / 0 / 1,与接口一致 - [x] 仪表盘「电工资质到期提醒」区块展示临期与已过期证书清单并可跳转 —— 浏览器实测:区块展示「验收测试电工 | 高压电工证 | 2026-09-26 | 已过期 5 天」,且可跳转电工登记管理页 ## 站内通知与定时任务(FR-7) - [x] `sys_notice` UNIQUE(`notice_type`,`biz_id`,`receiver_id`) 生效,重复执行定时任务不产生重复通知 —— API 实测:直接重复 INSERT 报 `uk_sys_notice_biz` 唯一冲突;真实调用 `scanCertExpireAndNotify()` 三次,`created` 依次为 1 / 0 / 0 - [x] 定时任务每日 08:00 扫描 `expire_date <= today + 30 天` 的证书,为每个 ADMIN 用户生成未读通知 —— 代码核对(`@Job({ cronTime: "0 0 8 * * *", timeZone: "Asia/Shanghai" })`)+ 实测扫描逻辑生成 ADMIN 未读通知 - [x] 证书续期或被删除时,该证书既有未读通知被清理 —— API 实测:续期(改 `expireDate`)后该证书通知 1→0;删除证书后亦为 0 - [x] `/api/notices` 系列接口仅返回 `receiver_id = 当前用户` 的数据,未读数接口正确 —— API 实测:列表仅返回本人;读他人通知返回 404;`unread-count` 与库中计数一致 - [x] 顶栏铃铛红点角标随未读数变化;单条已读 / 全部已读生效;点击通知跳转电工登记页 —— 浏览器实测:角标显示 2,抽屉内 2 条「验收测试」通知 ## 电工 APP 接口(FR-8) - [x] `/api/worker-app/auth/register` 与 `/login` 可匿名访问,注册手机号唯一,登录返回 worker token(payload 含 `workerId` / `phone` / `userType=worker`)—— API 实测注册/登录成功;重复手机号被拒 - [x] `WorkerAuthMiddleware` 仅作用于 `/api/worker-app/*`,且放行 `/api/worker-app/auth/*` —— API 实测:匿名可访问 `auth/*`,未带 token 访问 `/profile` 返回 401 - [x] 电工 APP 无法通过 `:id` 越权读取 / 修改他人资料与证书(返回 403)—— API 实测:电工 X 的 token 改/删电工 Y 的证书 → `code:403「无权操作该证书」`,Y 数据未被改动 - [x] `POST /api/worker-app/audit/resubmit` 仅在 `audit_status=2` 时成功,`audit_status=1` 时返回 400 —— API 实测 - [x] `auth.middleware.ts` 与 `rbac.middleware.ts` 均已忽略 `/api/worker-app/*` —— 代码核对 + API 实测(电工 token 不会被管理端鉴权拦截) ## 菜单、权限与路由(FR-9 / FR-10) - [x] `运营派单`(id=3)已改为目录,管理端侧边栏展开可见 3 个子菜单 —— API 实测登录菜单树中 id=3 为 `menuType=1` 且含 34/35/36;浏览器实测展开可见三个子菜单 - [x] ADMIN 可见「运维费定价」「电工派单成本价」「电工登记管理」;SALESMAN 不可见「运营派单」及其子菜单 —— API 实测:自签 SALESMAN token 调 `/auth/currentUser`,`dispatch:list` / `dispatch:price` / `dispatch:cost` / `dispatch:worker` 均为 false - [x] 前端 `/dispatch` 重定向到 `/dispatch/price`,3 条子路由 `meta.permission` 正确 —— 代码核对(router 已配置 `redirect` 与 `meta.permission`)+ 浏览器实测(首次因后端瞬时抖动显示报错,复测页面正常渲染) - [x] `/api/service-prices`、`/api/dispatch-prices`、`/api/workers` 已纳入 RBAC 权限映射 —— API 实测:SALESMAN 调 `/api/workers`、`/api/service-prices` 均 HTTP 403 - [x] SALESMAN 调用 `/api/workers/options`、`/api/workers/recommend` 正常返回(白名单生效),调用 `/api/workers` 列表返回 403 —— API 实测 HTTP 200 / 403 ## 安全与规范(NFR) - [x] `GET /api/workers` 与详情接口不返回 `password_hash` —— API 实测:列表 / 详情 / `certStatus` 各分支返回体均无 `password_hash` / `passwordHash` - [x] 所有查询走 Sequelize 参数化 API,无字符串拼接 SQL 与模板字符串 LIKE —— 代码核对:模糊查询统一 `Op.like` + 绑定值,无原始 SQL 拼接 - [x] 前端页面未使用 `v-html`,表单输入含长度与特殊字符校验 —— 代码核对:三个新页面无 `v-html`(grep 0 命中),输入框均带 `maxlength`,手机号等有正则校验 - [x] 列表接口均走 `limit + offset` 分页,证书聚合无 N+1 查询 —— 代码核对:`worker_cert` 用 `worker_id IN (...)` 批量加载后按电工分组 - [x] Controller 参数使用 DTO + `class-validator`,无滥用 `any`(除既有约定处)—— `worker` / `worker-app` 用 DTO 类型;`service-price` / `dispatch-price` / `notice` 沿用工程既有约定(`@Body() body: any`,业务校验在 service 层)。注:工程未注册 `@midwayjs/validate` 组件,DTO 装饰器全站不生效,属既有约定 ## 端到端 - [x] 后端 `yarn dev` 启动无编译错误 —— API 实测:`MIDWAY_SERVER_ENV=local` 实例启动成功;`npx tsc --noEmit` 退出码 0 - [x] 前端 `yarn dev` 启动,三个新页面 CRUD 全链路可用 —— 前端 `vite` 启动成功、`vue-tsc --noEmit` 退出码 0;浏览器实测三个页面均正常渲染、弹窗字段齐备、KPI/筛选/铃铛/仪表盘联动正常;CRUD 接口链路已在 API 层全量实测(浏览器内「填表→点提交」这一步受自动化工具步骤预算限制未跑通) - [x] AC-1 ~ AC-11 逐条走通并留痕 —— 见上方各项实测证据;验证数据已清理,数据库恢复至验证前基线(`worker=4 / worker_cert=0 / sys_notice=0 / service_price=0 / dispatch_price=2`) ## 验证阶段修复 - [x] 修复 `WorkerAuthMiddleware` 同时声明 `match` 与 `ignore`:Midway 抛 `MIDWAY_10001: options.match and options.ignore can not both present`,导致**全站所有 `/api/*` 返回 500**;已改为仅 `match`,`/api/worker-app/auth/*` 在 `resolve()` 内放行 —— API 实测修复后匿名访问新接口由 500 恢复为 401