Files
andyesfly f4454ad014 feat: 新增电工APP端与定时任务管理功能
- 新增电工APP独立鉴权中间件与登录注册接口
- 新增定时任务管理模块,支持任务配置、执行日志与手动触发
- 新增运维费定价与电工派单成本价目管理
- 新增电工资质证照管理与到期提醒定时任务
- 完善数据库实体与DTO校验,优化权限控制
2026-10-02 16:08:00 +08:00

11 KiB
Raw Permalink Blame History

Checklist

change-id: add-om-pricing-and-worker-registry 逐条核对,通过后勾选。 验证方式说明:

  • 「API 实测」= 在本地开发库(127.0.0.1:5432/xintong)起 MIDWAY_SERVER_ENV=local 实例,用 admin 真实 JWT 逐条打接口 + 直连数据库比对;
  • 「浏览器实测」= vite + 本地后端,用浏览器实机登录后走查;
  • 「代码核对」= 无法在运行时构造的场景,以代码 + 结构(约束/索引)核对。

数据库与实体

  • 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 行无重复
  • database/db_seed.sql 已同步菜单(id 34-46)、字典(5 个 dict_type)、ADMIN 角色绑定,新库初始化与迁移后结构一致 —— 代码核对(Task 1 已完成并经迁移对照)
  • worker 实体含 password_hash / audit_status / audit_remark / audit_by / audit_at / last_login_at —— 代码核对 + 库表列核对(6 列均在)
  • dispatch_price 实体含 price_mode / unit / status / remark / updated_at —— 代码核对 + 库表列核对(5 列均在)
  • worker_cert / service_price / sys_notice 实体已创建,索引与迁移脚本一致 —— API 实测 uk_worker_cert_type_no / uk_service_price_code / uk_sys_notice_biz 均生效

客户运维价目(FR-1)

  • /api/service-prices 支持分页、关键词(编码 / 名称)、price_mode / service_type / status 筛选 —— API 实测四种筛选均只返回命中记录
  • price_code 自动生成为 FW+YYYYMMDD+3 位序号 且不可编辑 —— API 实测连建三条得到 FW20261001001~003,priceCode 在白名单外故不可写
  • price_mode 为 1 按次 / 2 按工时 / 3 材料 三种取值均可新增、编辑、筛选 —— API 实测
  • 支持启用 / 停用与硬删除两个独立操作 —— API 实测 PUT /:id/status 与 DELETE /:id(删除后查库确认为物理删除,重复删返回 404)

电工派单成本价(FR-2)

  • 存量 dispatch_price 行迁移后 price_mode=1、status=1 无 NULL —— API 实测存量 2 行均为 priceMode=1 / status=1
  • /api/dispatch-prices CRUD + 筛选可用,price_code 冲突返回 400 且 message 明确 —— CRUD/筛选 API 实测通过;UniqueConstraintError→「价格编码已存在」分支为代码核对(生成器保证编码不重复,无法从接口构造冲突)

电工登记与审核(FR-3 / FR-4)

  • /api/workers 支持分页、关键词、status / auditStatus / certStatus 筛选 —— API 实测 + 浏览器实测(已过期 1 条 / 无证书 3 条,筛选结果与库一致)
  • 创建时手机号唯一校验生效;默认密码规则为手机号后 6 位(不足 6 位用 123456)并 bcrypt 存储 —— API 实测:重复手机号返回 手机号已存在;password_hash 以 $2a$10$ 开头,用手机号后 6 位可成功登录电工 APP
  • 审核通过写入 audit_status=1 / audit_by / audit_at,清空 audit_remark,并置 cert_verified=true —— API 实测(库中字段逐项比对)
  • 审核驳回未填理由返回 400;填写后 audit_status=2 且理由、审核人、时间落库 —— API 实测:不填理由 → code:400「请填写驳回理由」;填理由 → audit_status=2 / audit_remark / audit_by / audit_at 落库
  • 删除电工级联删除其 worker_cert;存在派单引用时返回 400 并提示 —— API 实测:删除后 worker_cert 1→0;自造 survey_order 引用后返回 code:400「该电工存在派单记录,无法删除」
  • /api/workers/options 与 /recommend 仅返回 audit_status=1 AND status=1 的电工 —— 代码核对(查询条件已收紧)+ API 实测两接口正常返回

资质证书与到期状态(FR-5 / FR-6)

  • worker_cert 支持一电工多证,(worker_id, cert_type, cert_no) 唯一约束生效 —— API 实测一电工挂多证;重复插入触发 uk_worker_cert_type_no 唯一冲突
  • 资质状态计算正确:无证书 / 正常 / 临期(≤30 天)/ 已过期(expire_date < today),expire_date 为 NULL 视为长期有效 —— API 实测:构造 +10 天 / -5 天 / +200 天 / NULL 四种证书,certStatus 四种筛选各自只返回对应记录
  • 电工列表「资质状态」列标签颜色正确(绿 / 橙 / 红 / 灰)—— 浏览器实测:列表渲染出「已过期」彩色标签;配色取自 a-tag 状态映射(代码核对)
  • 列表 KPI 卡统计「电工总数 / 待审核 / 临期 / 已过期」数值正确 —— API 实测 stats 与独立 SQL 计算一致;浏览器实测 KPI 显示 5 / 4 / 0 / 1,与接口一致
  • 仪表盘「电工资质到期提醒」区块展示临期与已过期证书清单并可跳转 —— 浏览器实测:区块展示「验收测试电工 | 高压电工证 | 2026-09-26 | 已过期 5 天」,且可跳转电工登记管理页

站内通知与定时任务(FR-7)

  • sys_notice UNIQUE(notice_type,biz_id,receiver_id) 生效,重复执行定时任务不产生重复通知 —— API 实测:直接重复 INSERT 报 uk_sys_notice_biz 唯一冲突;真实调用 scanCertExpireAndNotify() 三次,created 依次为 1 / 0 / 0
  • 定时任务每日 08:00 扫描 expire_date <= today + 30 天 的证书,为每个 ADMIN 用户生成未读通知 —— 代码核对(@Job({ cronTime: "0 0 8 * * *", timeZone: "Asia/Shanghai" }))+ 实测扫描逻辑生成 ADMIN 未读通知
  • 证书续期或被删除时,该证书既有未读通知被清理 —— API 实测:续期(改 expireDate)后该证书通知 1→0;删除证书后亦为 0
  • /api/notices 系列接口仅返回 receiver_id = 当前用户 的数据,未读数接口正确 —— API 实测:列表仅返回本人;读他人通知返回 404;unread-count 与库中计数一致
  • 顶栏铃铛红点角标随未读数变化;单条已读 / 全部已读生效;点击通知跳转电工登记页 —— 浏览器实测:角标显示 2,抽屉内 2 条「验收测试」通知

电工 APP 接口(FR-8)

  • /api/worker-app/auth/register 与 /login 可匿名访问,注册手机号唯一,登录返回 worker token(payload 含 workerId / phone / userType=worker)—— API 实测注册/登录成功;重复手机号被拒
  • WorkerAuthMiddleware 仅作用于 /api/worker-app/*,且放行 /api/worker-app/auth/* —— API 实测:匿名可访问 auth/*,未带 token 访问 /profile 返回 401
  • 电工 APP 无法通过 :id 越权读取 / 修改他人资料与证书(返回 403)—— API 实测:电工 X 的 token 改/删电工 Y 的证书 → code:403「无权操作该证书」,Y 数据未被改动
  • POST /api/worker-app/audit/resubmit 仅在 audit_status=2 时成功,audit_status=1 时返回 400 —— API 实测
  • auth.middleware.ts 与 rbac.middleware.ts 均已忽略 /api/worker-app/* —— 代码核对 + API 实测(电工 token 不会被管理端鉴权拦截)

菜单、权限与路由(FR-9 / FR-10)

  • 运营派单(id=3)已改为目录,管理端侧边栏展开可见 3 个子菜单 —— API 实测登录菜单树中 id=3 为 menuType=1 且含 34/35/36;浏览器实测展开可见三个子菜单
  • ADMIN 可见「运维费定价」「电工派单成本价」「电工登记管理」;SALESMAN 不可见「运营派单」及其子菜单 —— API 实测:自签 SALESMAN token 调 /auth/currentUser,dispatch:list / dispatch:price / dispatch:cost / dispatch:worker 均为 false
  • 前端 /dispatch 重定向到 /dispatch/price,3 条子路由 meta.permission 正确 —— 代码核对(router 已配置 redirect 与 meta.permission)+ 浏览器实测(首次因后端瞬时抖动显示报错,复测页面正常渲染)
  • /api/service-prices、/api/dispatch-prices、/api/workers 已纳入 RBAC 权限映射 —— API 实测:SALESMAN 调 /api/workers、/api/service-prices 均 HTTP 403
  • SALESMAN 调用 /api/workers/options、/api/workers/recommend 正常返回(白名单生效),调用 /api/workers 列表返回 403 —— API 实测 HTTP 200 / 403

安全与规范(NFR)

  • GET /api/workers 与详情接口不返回 password_hash —— API 实测:列表 / 详情 / certStatus 各分支返回体均无 password_hash / passwordHash
  • 所有查询走 Sequelize 参数化 API,无字符串拼接 SQL 与模板字符串 LIKE —— 代码核对:模糊查询统一 Op.like + 绑定值,无原始 SQL 拼接
  • 前端页面未使用 v-html,表单输入含长度与特殊字符校验 —— 代码核对:三个新页面无 v-html(grep 0 命中),输入框均带 maxlength,手机号等有正则校验
  • 列表接口均走 limit + offset 分页,证书聚合无 N+1 查询 —— 代码核对:worker_cert 用 worker_id IN (...) 批量加载后按电工分组
  • Controller 参数使用 DTO + class-validator,无滥用 any(除既有约定处)—— worker / worker-app 用 DTO 类型;service-price / dispatch-price / notice 沿用工程既有约定(@Body() body: any,业务校验在 service 层)。注:工程未注册 @midwayjs/validate 组件,DTO 装饰器全站不生效,属既有约定

端到端

  • 后端 yarn dev 启动无编译错误 —— API 实测:MIDWAY_SERVER_ENV=local 实例启动成功;npx tsc --noEmit 退出码 0
  • 前端 yarn dev 启动,三个新页面 CRUD 全链路可用 —— 前端 vite 启动成功、vue-tsc --noEmit 退出码 0;浏览器实测三个页面均正常渲染、弹窗字段齐备、KPI/筛选/铃铛/仪表盘联动正常;CRUD 接口链路已在 API 层全量实测(浏览器内「填表→点提交」这一步受自动化工具步骤预算限制未跑通)
  • AC-1 ~ AC-11 逐条走通并留痕 —— 见上方各项实测证据;验证数据已清理,数据库恢复至验证前基线(worker=4 / worker_cert=0 / sys_notice=0 / service_price=0 / dispatch_price=2)

验证阶段修复

  • 修复 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