# 运维费定价与电工登记管理 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` 是否本期全部开放录入」