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

293 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运维费定价与电工登记管理 Spec
> change-id: `add-om-pricing-and-worker-registry`
> 范围:`smart_grid_iot`(后端 server + 管理端 client);电工 APP 仅交付后端接口,不改 `smart_grid_iot_app` 页面。
## Why
系统当前缺少两块运营基础能力:
1. **运维费定价缺失**:既没有面向客户的运维收费价目表,也没有电工派单成本价目的维护界面。数据库设计 D10 已有 `dispatch_price`(共享电工派单价格)实体,但只有 entity、无接口、无页面,无法维护。
2. **电工登记与资质管理缺失**:`worker` 表只有只读的列表 / 下拉 / 推荐接口(供项目勘察派单选用),无法登记新电工、无资质审核流转、无资质到期提醒;`worker` 实体甚至缺少设计文档中已有的 `password_hash`、`last_login_at` 字段,电工 APP 无法登录。
这两块是后续派单、维保、结算业务的前置主数据,需先补齐。
## What Changes
- 新增客户运维服务价目表 `service_price`:全局统一价目,用 `price_mode` 单表承载 **按次 / 按工时 / 材料** 三种计价方式,提供 CRUD + 启用停用。
- 扩展现有 `dispatch_price`(电工派单成本价目):新增 `price_mode`(按次 / 按工时)、`unit`、`status`、`remark`、`updated_at`,补齐 CRUD 接口与管理页面。
- 扩展 `worker` 表:补 `password_hash`、`audit_status`、`audit_remark`、`audit_by`、`audit_at`、`last_login_at`;新增登记 CRUD 与资质审核流转(待审核 → 已通过 / 已驳回,可重新提交)。
- 新增电工资质证照子表 `worker_cert`(一电工多证:证书类型 / 编号 / 发证机关 / 发证日期 / 有效期至 / 证照图片),按期日期逐证计算「正常 / 临期 / 已过期」。
- 新增 `sys_notice` 站内通知表 + `@midwayjs/cron` 定时任务(每日扫描证书有效期,**提前 30 天**生成提醒,接收人仅 ADMIN 用户);管理端顶栏新增消息铃铛 + 未读红点 + 消息中心抽屉。
- 新增电工 APP 后端接口 `/api/worker-app/*`:自助注册、登录、查看 / 编辑本人资料、上传资质、查看审核结果与到期提醒;配套独立电工 JWT 与鉴权中间件。**不修改 `smart_grid_iot_app` 工程页面**。
- 菜单调整:`运营派单`(id=3)由菜单项改为目录(`menu_type=1`),下挂 3 个子菜单「运维费定价」「电工派单成本价」「电工登记管理」及对应按钮权限。
- **BREAKING**(仅限内部路由/权限):
- 管理端 `/dispatch` 路由由 Placeholder 占位页改为目录(重定向到 `/dispatch/price`)。
- `/api/workers` 由「登录即可访问」改为受 `dispatch:worker` 权限控制;为保证项目勘察派单页正常,`/api/workers/options`、`/api/workers/recommend` 加入鉴权白名单。
- `dispatch_price` 表新增 NOT NULL 默认值字段(`price_mode` 默认 1、`status` 默认 1),存量数据自动补默认值。
## Impact
- **Affected specs**:客户管理核心(复用 `upload` / `file_record`)、项目管理-现场勘察派单(复用 `workers/options`、`workers/recommend`)、系统管理与 RBAC(`rbac.middleware.ts` + `db_seed.sql` 菜单/字典)
- **Affected code**:
- 后端实体:新增 `service_price.entity.ts`、`worker_cert.entity.ts`、`sys_notice.entity.ts`;修改 `worker.entity.ts`、`dispatch_price.entity.ts`
- 后端接口:新增 `service_price.*`、`dispatch_price.*`、`notice.*`、`worker_app.*`;扩展 `worker.*`
- 后端横切:`middleware/workerAuth.middleware.ts`(新增)、`middleware/rbac.middleware.ts`(改)、`util/token.ts`(改)、`task/cert_expire.task.ts`(新增)、`package.json`(新增 `@midwayjs/cron`)
- 数据库:新增 `database/migrations/20261001_om_worker_registry.sql`;同步修订 `database/db_seed.sql`
- 前端:新增 `api/servicePrice.ts`、`api/dispatchPrice.ts`、`api/workerRegistry.ts`、`api/notice.ts`;新增 `views/dispatch/price/index.vue`、`views/dispatch/cost/index.vue`、`views/dispatch/worker/index.vue`;修改 `router/index.ts`、`layouts/index.vue`
---
## ADDED Requirements
### FR-1:客户运维服务价目管理(`service_price`)
系统 SHALL 提供面向客户的运维收费价目 CRUD 与启停用能力。
- 表字段:`id`、`price_code`(UNIQUE,自动生成,规则 `FW + YYYYMMDD + 3 位序号`)、`price_name`(必填)、`service_type`(服务类型:维修 / 维保 / 报警处理 / 巡检 / 其他,取自字典 `om_service_type`)、`price_mode`(计价方式:1 按次 / 2 按工时 / 3 材料,取自字典 `om_price_mode`)、`unit_price`(NUMERIC(10,2),必填,≥ 0)、`unit`(单位:次 / 小时 / 个 / 米 / 根,取自字典 `om_price_unit`)、`status`(1 启用 / 0 停用,默认 1)、`remark`、`created_at`、`updated_at`
- 列表:分页(`page` / `pageSize`,`pageSize ≤ 200`);关键词模糊搜索(价目编码 / 名称);筛选 `price_mode`、`service_type`、`status`
- 创建 / 更新:`price_code` 只读不可改;字段白名单更新;`price_mode` 与 `unit` 必须成对合理(按次默认「次」、按工时默认「小时」、材料默认「个」)
- 删除:硬删除;停用(`status=0`)与删除为两个独立操作
- 索引:`price_code` UNIQUE、`(price_mode, status)` 联合索引
- `price_mode` 三种取值本期全部开放录入(零成本满足「后续扩展按工时 / 材料」诉求),不做前端隐藏
### FR-2:电工派单成本价目管理(`dispatch_price` 扩展)
系统 SHALL 在现有 `dispatch_price` 基础上补齐管理能力。
- 新增字段:`price_mode`(1 按次 / 2 按工时,默认 1,NOT NULL)、`unit`(次 / 小时,默认「次」)、`status`(1 启用 / 0 停用,默认 1,NOT NULL)、`remark`、`updated_at`
- 保留原有字段与语义:`price_code`(UNIQUE)、`dispatch_type`(勘察 / 安装 / 预警 / 维修 / 维保)、`price_type`(单价 / 总价提成)、`unit_price`
- 列表:分页 + 关键词(编码)+ 筛选 `dispatch_type`、`price_mode`、`status`
- 创建 / 更新 / 删除:`price_code` 唯一冲突返回 400 且 message 明确;字段白名单更新
- 本期不含「材料」成本项(材料归属客户侧收费价目 FR-1)
### FR-3:电工登记管理(`worker` 扩展)
系统 SHALL 提供电工登记 CRUD。
- 新增字段:
- `password_hash`(VARCHAR(255),电工 APP 登录密码 bcrypt 哈希,允许 NULL)
- `audit_status`(SMALLINT,0 待审核 / 1 已通过 / 2 已驳回,默认 0,NOT NULL)
- `audit_remark`(VARCHAR(255),驳回理由 / 审核备注)
- `audit_by`(BIGINT,审核人 `sys_user.id`)、`audit_at`(TIMESTAMPTZ)
- `last_login_at`(TIMESTAMPTZ)
- 保留字段:`worker_name`、`phone`(UNIQUE,登录账号)、`id_card`、`worker_title`、`rating`、`cert_info`、`cert_verified`、`completed_count`、`status`(1 在岗 / 0 离职)
- `status`(在岗 / 离职)与 `audit_status`(审核)为两个正交维度:未审核通过的电工不允许在派单下拉 / 推荐中出现
- 列表:分页 + 关键词(姓名 / 手机号)+ 筛选 `status`、`audit_status`、`certStatus`(资质状态:正常 / 临期 / 已过期 / 无证书)
- 创建:手机号唯一校验;后台创建时可选「同时创建 APP 登录密码」(默认密码 = 手机号后 6 位,不足 6 位用 `123456`,与 `customer_account` 规则一致)
- 更新:字段白名单;手机号变更需重新校验唯一
- 删除:硬删除,级联删除其 `worker_cert`;若存在派单引用(`survey_order` / `install_order` 的 `worker_id`)则返回 400 并提示
- 权限:仅 `dispatch:worker` 权限角色(本期仅 ADMIN)可访问
### FR-4:电工资质审核流转
系统 SHALL 支持电工登记的资质审核状态机。
- 状态流转:`0 待审核` → `1 已通过` 或 `2 已驳回`;`2 已驳回` 可重新提交回 `0 待审核`(由后台或电工 APP 触发)
- 后台「审核通过」:写入 `audit_status=1`、`audit_by=当前用户`、`audit_at=now`、清空 `audit_remark`
- 后台「审核驳回」:必须填写 `audit_remark`(驳回理由),写入 `audit_status=2`、审核人 / 时间
- 电工 APP「重新提交」:仅 `audit_status=2` 时可提交,提交后回到 `0 待审核` 并清空审核人 / 时间
- `worker.cert_verified` 由审核结果同步:`audit_status=1` 时置 `true`(保持与既有推荐算法字段兼容)
### FR-5:电工资质证照子表(`worker_cert`)
系统 SHALL 以子表管理电工的多本资质证照。
- 表字段:`id`、`worker_id`(FK → `worker`,NOT NULL,IDX)、`cert_type`(证书类型:低压电工证 / 高压电工证 / 特种作业证 / 登高证 / 其他,取自字典 `worker_cert_type`)、`cert_no`(证书编号)、`issue_authority`(发证机关)、`issue_date`(DATE,发证日期)、`expire_date`(DATE,有效期至,**到期提醒依据**,IDX)、`cert_image_url`(证照图片对象 key,复用 `upload` + `file_record`)、`created_at`、`updated_at`
- 一个电工可挂多本证书;同一电工下 `(cert_type, cert_no)` 组合不可重复(UNIQUE 组合索引)
- 证书到期状态为**计算值**(不落库),由 `expire_date` 与当前日期推导:
- `无证书`:该电工 `worker_cert` 为空
- `已过期`:存在任一本 `expire_date < today`
- `临期`:无已过期,且存在任一本 `today ≤ expire_date ≤ today + 30 天`
- `正常`:其余(含 `expire_date` 为 NULL 视为长期有效)
- 后台支持在电工编辑弹窗中增删改证书行
- 电工 APP 支持新增 / 编辑本人证书(新增 / 修改后不改变 `audit_status`,但会触发该证书已生成通知的清理,见 FR-7)
### FR-6:资质到期提醒(列表 / 仪表盘标签)
系统 SHALL 在管理端提供可见的资质到期提醒。
- 电工登记列表新增「资质状态」列,按 FR-5 计算值渲染标签(正常 = 绿色 / 临期 = 橙色 / 已过期 = 红色 / 无证书 = 灰色)
- 列表顶部提供 KPI 统计卡:电工总数、待审核数、临期数、已过期数
- 提供「资质临期 / 已过期」快捷筛选(复用 `certStatus` 查询参数)
- 仪表盘新增「电工资质到期提醒」区块,展示临期与已过期证书清单(电工姓名 + 证书类型 + 有效期至 + 剩余天数),点击跳转电工登记页
### FR-7:站内通知与定时任务
系统 SHALL 通过站内通知触达资质到期提醒。
- 新增表 `sys_notice`:`id`、`notice_type`(`cert_expire`)、`title`、`content`、`biz_type`(`worker_cert`)、`biz_id`(`worker_cert.id`)、`receiver_id`(`sys_user.id`,IDX)、`is_read`(0 未读 / 1 已读,默认 0)、`created_at`
- 幂等约束:UNIQUE(`notice_type`, `biz_id`, `receiver_id`);定时任务插入使用 `ON CONFLICT DO NOTHING`
- 定时任务:`@midwayjs/cron` 每日 08:00 执行,扫描 `expire_date <= today + 30 天` 的 `worker_cert`,为每个 ADMIN 用户(`sys_role.role_code = 'ADMIN'` 关联的 `sys_user`)生成一条未读通知;标题形如「电工【张三】的高压电工证将于 2026-10-20 到期(剩余 19 天)」
- 证书续期(`expire_date` 被更新)或证书被删除时,服务层同步清理该证书既有未读通知,保证续期后可重新提醒
- 通知接口:`GET /api/notices`(本人,分页)、`GET /api/notices/unread-count`、`PUT /api/notices/:id/read`、`PUT /api/notices/read-all`;仅返回 `receiver_id = 当前用户` 的数据
- 前端:`layouts/index.vue` 顶栏用户信息左侧新增消息铃铛 + 未读红点角标(轮询或进入页面时拉取未读数),点击打开消息中心抽屉(列表 + 标记已读 + 全部已读 + 点击跳转电工登记页)
### FR-8:电工 APP 后端接口(`/api/worker-app/*`)
系统 SHALL 为电工 APP 提供独立的后端接口(本期不改 APP 页面)。
- 鉴权方式:独立电工 JWT,payload 含 `workerId`、`phone`、`userType: "worker"`;`TokenUtil` 新增 `generateWorkerToken` / `verifyWorkerToken`
- 新增 `WorkerAuthMiddleware`,`match = ['/api/worker-app/*']`、`ignore = ['/api/worker-app/auth/*']`;校验通过后写入 `ctx.state.worker = { workerId, phone }`
- `rbac.middleware.ts` 与 `auth.middleware.ts` 均忽略 `/api/worker-app/*`(不走管理端 sys_user 鉴权)
- 接口清单:
- `POST /api/worker-app/auth/register`:自助注册(姓名、手机号、身份证号、职衔、密码);手机号唯一;默认 `audit_status=0`、`status=0`(未在岗)
- `POST /api/worker-app/auth/login`:手机号 + 密码登录,返回 worker token;未审核通过允许登录但仅能查看审核状态与资质
- `GET /api/worker-app/profile`:本人资料 + `audit_status` / `audit_remark` + 资质状态汇总
- `PUT /api/worker-app/profile`:修改本人可编辑字段(姓名 / 身份证号 / 职衔 / 密码)
- `GET /api/worker-app/certs`、`POST /api/worker-app/certs`、`PUT /api/worker-app/certs/:id`、`DELETE /api/worker-app/certs/:id`:本人证书增删改查(证书图片经 `POST /api/upload/image` 上传后回填 key)
- `POST /api/worker-app/audit/resubmit`:`audit_status=2` 时重新提交审核
- `GET /api/worker-app/notices`、`PUT /api/worker-app/notices/:id/read`:本人证书到期提醒(`receiver` 维度改为该 worker 的证书)
- 越权控制:所有 `:id` / 证书操作必须校验归属当前 `workerId`,否则 403
### FR-9:菜单 / 权限 / 字典
系统 SHALL 在种子数据与 RBAC 中补齐新模块。
- `运营派单`(id=3)改为目录:`menu_type=1`,`menu_path=/dispatch`,`permission=dispatch:list`
- 新增子菜单(`parent_id=3`,`menu_type=2`):
- `34`「运维费定价」`/dispatch/price` `dispatch/price/index` `dispatch:price`
- `35`「电工派单成本价」`/dispatch/cost` `dispatch/cost/index` `dispatch:cost`
- `36`「电工登记管理」`/dispatch/worker` `dispatch/worker/index` `dispatch:worker`
- 新增按钮权限(`menu_type=3`):`price:create` / `price:update` / `price:delete`、`cost:create` / `cost:update` / `cost:delete`、`worker:create` / `worker:update` / `worker:delete` / `worker:audit`
- 角色绑定:全部绑定给 ADMIN(role_id=1);SALESMAN 不绑定(保持无「运营派单」可见性)
- 新增字典:`om_service_type`、`om_price_mode`、`om_price_unit`、`worker_cert_type`、`worker_audit_status`
- `sys_menu` 序列重置为 36(含按钮权限则为最后一条 id)
- `rbac.middleware.ts` 新增映射:`/api/service-prices → dispatch:price`、`/api/dispatch-prices → dispatch:cost`、`/api/workers → dispatch:worker`;`ignore` 增加 `/api/worker-app/*`、`/api/workers/options`、`/api/workers/recommend`
- `/api/notices` 不做权限码映射(登录即可访问本人通知)
### FR-10:前端页面
系统 SHALL 提供 3 个管理端页面,风格与现有「客户档案管理」页完全一致(搜索栏 + 表格内滚动 + 新增/编辑弹窗 + 删除二次确认 + 状态开关)。
- `views/dispatch/price/index.vue`(运维费定价):搜索栏(关键词 / 计价方式 / 服务类型 / 状态)+ 表格(价目编码 / 名称 / 服务类型 / 计价方式 / 单价 / 单位 / 状态 / 操作)+ 弹窗(表单校验、计价方式联动单位)
- `views/dispatch/cost/index.vue`(电工派单成本价):搜索栏(关键词 / 派单类型 / 计价方式 / 状态)+ 表格(编码 / 派单类型 / 价格类型 / 计价方式 / 单价 / 单位 / 状态 / 操作)+ 弹窗
- `views/dispatch/worker/index.vue`(电工登记管理):KPI 统计卡 + 搜索栏(关键词 / 在岗状态 / 审核状态 / 资质状态)+ 表格(姓名 / 手机号 / 职衔 / 资质状态标签 / 审核状态标签 / 在岗状态 / 评分 / 完成数 / 操作)+ 编辑弹窗(基础信息 + 证书子表行编辑 + 证照图片上传)+ 审核弹窗(通过 / 驳回 + 驳回理由)
- 路由:`/dispatch` 改为重定向 `/dispatch/price`;新增 3 条子路由,`meta.permission` 对应 `dispatch:price` / `dispatch:cost` / `dispatch:worker`
- 页面使用现有 `useTableScroll` composable、`request` 封装与统一返回结构
---
## Non-Functional Requirements
### NFR-1:安全
- 所有查询走 Sequelize 参数化 API(`findAndCountAll` / `create` / `update` / `destroy` + `where` 对象),禁止字符串拼接 SQL;LIKE 使用 `{ [Op.like]: \`%${keyword}%\` }` 参数化
- 前端不使用 `v-html`;表单输入加长度上限与正则过滤 `<` `>` `"` `'`
- `worker` 列表 / 详情接口永不返回 `password_hash`;`sys_notice` 接口严格按 `receiver_id` 过滤
### NFR-2:权限与数据隔离
- 管理端:`/api/workers`、`/api/service-prices`、`/api/dispatch-prices` 受 `dispatch:*` 权限码控制,ADMIN 直通
- 电工 APP:所有接口以 `ctx.state.worker.workerId` 为唯一数据边界,禁止通过请求参数越权读取他人数据
- ADMIN 判定复用现有 `sys_role.role_code = 'ADMIN'` 逻辑
### NFR-3:性能
- 新增表均建必要索引:`worker_cert.worker_id`、`worker_cert.expire_date`、`service_price.price_code`(UNIQUE)、`sys_notice.receiver_id`、`sys_notice.(notice_type,biz_id,receiver_id)`(UNIQUE)、`worker.audit_status`
- 列表接口统一 `limit + offset` 分页;证书聚合按 `worker_id IN (...)` 批量查询,禁止 N+1
- 单接口响应目标 < 500ms(本地开发环境)
### NFR-4:类型与一致性
- Entity / DTO / 前端 API 类型均用显式 interface;Controller 参数使用 DTO class + `class-validator` 装饰器
- 统一返回结构:列表 `{ code: 200, message, data: { list, total, page, pageSize } }`;单条 / 写操作 `{ code: 200, message, data }`;错误码 401 / 403 / 400 / 404 / 500
### NFR-5:数据库变更
- 新增 `database/migrations/20261001_om_worker_registry.sql`(ALTER / CREATE TABLE / INSERT 菜单字典 / 序列重置),沿用现有 migration 命名与写法
- 同步修订 `database/db_seed.sql`,保证全新初始化数据库与迁移后结构一致
- 迁移脚本可重复执行(`IF NOT EXISTS` / `ON CONFLICT` / 有默认值的 NOT NULL 列)
---
## Constraints
- 技术栈不可更换:Midway.js (Koa) + Sequelize-typescript + PostgreSQL;Vue3 + Ant Design Vue 4 + Vite;JWT + 现有 RBAC 中间件
- `sequelize.sync = false`(见 `config.default.ts`),建表必须通过 SQL 迁移完成
- 定时任务需新增依赖 `@midwayjs/cron`(属官方组件,与现有 Midway 3.x 体系一致)
- 本期不做:派单 / 收入结算联动(不写 `worker_income`)、电工评分计算、客户侧查询运维价目的接口、`smart_grid_iot_app` 页面改造
## Assumptions(需你复核)
1. **`price_mode` 三种模式全部开放**:表结构、接口、页面下拉均支持「按次 / 按工时 / 材料」,而不是本期只开放「按次」。理由:这是满足「后续扩展」诉求的最低成本做法。若你希望界面暂只显示「按次」,请告知。
2. **材料归客户侧**:材料费用只作为客户运维价目的 `price_mode=3`,电工派单成本价目暂不含材料项。
3. **电工 APP 登录账号 = `worker` 表手机号 + 密码**,与 `sys_user` 完全独立,不新建 `sys_user` 记录;电工 APP 权限不走 RBAC 菜单,接口即权限。
4. **审核人 = ADMIN**:`audit_by` 记录 `sys_user.id`;本期不区分多级审核。
5. **通知接收人仅 ADMIN 角色用户**;提前 30 天标「临期」,`expire_date` 为 NULL 视为长期有效不提醒。
6. **定时任务依赖新增 `@midwayjs/cron`**;若你不希望引入新依赖,可改为 `@Configuration` 生命周期 + `setInterval` 每日检查(功能等价,代码稍脏)。
7. **电工下拉 / 推荐口径变更**:`/api/workers/options`、`/api/workers/recommend` 增加「`audit_status=1` 且 `status=1`」过滤条件——即只有审核通过且在岗的电工才能被派单。这会改变现有勘察派单页可选电工范围(当前是只要有 `status=1`)。**这是有意的业务收紧,请确认**。
8. **`dispatch_price` 无存量数据假设**(或存量极少),迁移直接补默认值即可,不需要数据回填脚本。
9. **菜单 id 从 34 起分配**,`sys_menu` 序列重置到最大值;`db_seed.sql` 原有注释中的菜单计数同步更新。
10. **管理端列表「资质状态」为实时计算**,不落库;因此 `certStatus` 筛选在 SQL 层用子查询 / `EXISTS` 实现(Sequelize `where` 内联 `fn`/`literal` 参数化写法),不使用字符串拼接。
## Acceptance Criteria
### AC-1:客户运维价目 CRUD 可用
- **Given**:ADMIN 登录,进入「运营派单 → 运维费定价」
- **When**:新增一条 `price_mode=1 按次`、`service_type=维修`、单价 200、单位「次」的价目并保存
- **Then**:返回 code=200,`price_code` 形如 `FW20261001001`;列表刷新可见该记录;编辑改单价后列表更新;删除后列表移除;`price_mode=2/3` 也能正常新增
- **Verification**:`programmatic`
### AC-2:电工派单成本价目可用且兼容存量
- **Given**:`dispatch_price` 表执行迁移后
- **When**:查询既有数据的 `price_mode` / `status`
- **Then**:存量行 `price_mode=1`、`status=1`(默认值生效,无 NULL);新增 / 编辑 / 删除 / 筛选均正常
- **Verification**:`programmatic`
### AC-3:电工资质审核流转正确
- **Given**:后台新增一名电工并录入 1 本证书(`audit_status=0`)
- **Then**:列表显示「待审核」;执行「审核驳回」未填理由时返回 400;填写理由后 `audit_status=2` 且 `audit_remark` 有值、`audit_by`/`audit_at` 有值;执行「审核通过」后 `audit_status=1`、`cert_verified=true`、`audit_remark` 清空
- **Verification**:`programmatic`
### AC-4:资质状态计算与筛选正确
- **Given**:电工 A 证书 `expire_date = today + 10 天`;电工 B 证书 `expire_date = today - 1 天`;电工 C 无证书;电工 D 证书 `expire_date = today + 100 天`
- **When**:访问电工列表
- **Then**:A 显示「临期」、B 显示「已过期」、C 显示「无证书」、D 显示「正常」;按 `certStatus=临期` 筛选只返回 A
- **Verification**:`programmatic`
### AC-5:到期提醒标签与仪表盘区块
- **Given**:存在 AC-4 场景数据,且 A 为 `audit_status=1`、`status=1`
- **Then**:电工登记页 KPI 显示待审核数 / 临期数 / 已过期数正确;仪表盘「电工资质到期提醒」区块列出 A、B 的证书与剩余天数,点击可跳转电工登记页
- **Verification**:`human-judgment`
### AC-6:定时任务生成站内通知且幂等
- **Given**:存在 `expire_date = today + 19 天` 的证书,且系统有 1 个 ADMIN 用户
- **When**:手动触发定时任务一次,再触发第二次
- **Then**:第一次生成 1 条 `sys_notice`(`receiver_id` = ADMIN,`is_read=0`);第二次不新增(UNIQUE + ON CONFLICT DO NOTHING)
- **Verification**:`programmatic`
### AC-7:消息中心可用
- **Given**:ADMIN 登录且存在 2 条未读通知
- **Then**:顶栏铃铛显示红点角标「2」;打开抽屉可见 2 条通知;单条标记已读后角标变「1」;「全部已读」后角标消失;点击通知跳转到电工登记页
- **Verification**:`human-judgment` + `programmatic`(`GET /api/notices/unread-count`)
### AC-8:电工 APP 接口可用且数据隔离
- **Given**:电工 E1、E2 各注册成功并登录
- **When**:E1 携带自己的 token 调用 `GET /api/worker-app/certs` 与 `PUT /api/worker-app/certs/:id`(传 E2 的证书 id)
- **Then**:只能看到 E1 自己的证书;操作 E2 的证书返回 403;`POST /api/worker-app/audit/resubmit` 在 `audit_status=2` 时可将状态改回 0,`audit_status=1` 时返回 400
- **Verification**:`programmatic`
### AC-9:权限与既有页面不受影响
- **Given**:SALESMAN 账号登录
- **When**:访问 `/api/workers/options`、`/api/workers/recommend`(项目勘察派单页使用)
- **Then**:正常返回;访问 `/api/workers/list` 或 `/api/service-prices` 返回 403;SALESMAN 侧边栏不出现「运营派单」及其子菜单
- **Verification**:`programmatic` + `human-judgment`
### AC-10:敏感字段不外泄
- **Given**:调用 `GET /api/workers` 与 `GET /api/workers/:id`
- **Then**:返回体不含 `password_hash`
- **Verification**:`programmatic`
### AC-11:迁移脚本可重复执行
- **Given**:在已执行过迁移的库上再执行一次 `20261001_om_worker_registry.sql`
- **Then**:无报错,菜单 / 字典数据收敛不重复(`ON CONFLICT` 生效)
- **Verification**:`programmatic`
## Open Questions
- [x] (已确认)运维费定价对象:客户收费价目 + 电工派单成本价目,**分开维护**
- [x] (已确认)交付范围:后端 API + 管理端 Web + 电工 APP 后端接口;不含 `smart_grid_iot_app` 页面改造
- [x] (已确认)资质建模:`worker_cert` 子表 + `worker.audit_status` 审核流转
- [x] (已确认)到期提醒:列表 / 仪表盘标签 **与** 定时任务站内通知并存
- [x] (已确认)客户价目建模:单表 `price_mode` 区分
- [x] (已确认)定价维度:全局统一价目,不按客户 / 等级区分
- [x] (已确认)菜单位置:「运营派单」下两个独立子菜单(+ 电工登记管理)
- [x] (已确认)电工 APP:自助注册 + 上传资质 + 查审核结果
- [ ] 待复核:Assumptions 第 7 条「派单可选电工收紧为审核通过 + 在岗」是否接受
- [ ] 待复核:Assumptions 第 1 条「三种 `price_mode` 是否本期全部开放录入」