跳转到主要内容

FSD m10 · 系统管理与审计(user + audit + infra/notify)

本册覆盖代码域 internal/domain/user、横切包 internal/audit 与 internal/infra/notify。全局规则见 00-总则 §2(双日志轨 §2.6)。

m10 功能节目录

ID名称路由/入口
F10.01用户管理(CRUD/禁用/改角色/重密)GET/POST /api/users、`POST /api/users/:id/disable
F10.02TOTP 2FA 管理(三态机+重置)`POST /api/auth/totp/setup
F10.03生命周期时间线查询GET /api/lifecycle/:type/:id
F10.04操作日志审计页GET /api/operation-logs
F10.05INBOX 待办与邮件对账重发GET /api/inbox、POST /api/inbox/:id/read、GET /api/notifications/failed、POST /api/notifications/:id/retry
F10.06outbox 投递与重试(dispatcher 定时任务)

F10.01 用户管理(CRUD/禁用/改角色/重密)

对应 SRS:F-ADMIN-002 | 实现落点:internal/domain/user/service.go:110(Create)/:159(SetStatus)/:176(SetRole)/:196(ResetPassword)/user/in_tx.go:19(SetUserStatusInTx)/:53(SetUserRoleInTx)/main.go:248-254(路由) | 操作入口:web /admin/users 用户管理页

1 功能定义

ADMIN 管理 B 端账号(B 端三角色+患者查看):建号(bcrypt 真算+邮箱唯一)、列表/详情(敏感列零透出——password_hash/totp_secret 不在 SELECT 列集)、禁用/启用(ACTIVE⇄DISABLED 双向白名单机)、改角色(「权限设置」=改 role,RBAC 仍 role 级不建每用户覆盖表)、重置密码(reason 必填——敏感操作审计纪律)。

2 触发条件与前置状态

user:manage(仅 ADMIN);目标账号存在(404)。

3 输入与校验
端点字段校验
POST /api/usersrole/name/email/password/phone?/hospital_id?/region?ValidateCreateInput 纯函数(role 白名单/名字非空/邮箱格式/密码强度);邮箱唯一(409 ErrEmailTaken)
GET /api/usersrole/status/分页过滤可选
POST /api/users/:id/disablereason 必填status 谓词 CAS+version 双谓词(SetUserStatusInTx)
POST /api/users/:id/enablereason 必填同上
POST /api/users/:id/rolerole+reason 必填role 白名单;version CAS(SetUserRoleInTx)
POST /api/users/:id/passwordnew_password+reason 必填密码强度校验;version 服务端内部查询 CAS
4 处理流程
5 输出与结果状态

201 {id} / 列表/详情 JSON(敏感列零透出) / 204;被禁用账号下次登录 401(F1.01 三条件之一)。

6 状态流转

user_account.status: ACTIVE⇄DISABLED(SetUserStatusInTx AST 白名单函数);role 变更经 SetUserRoleInTx(lifecycle event=ROLE_CHANGE,from/to 填旧/新角色);SetStatus 拆 Disable/Enable 双 handler(去 c.Path() 路由字面量耦合——M8 核查 B6)。

7 边界与异常
场景行为
邮箱已存在409 ErrEmailTaken
非法角色/弱密码400
并发管理操作409(status+version 双谓词)
重置密码无 reason400(M9 ⑤ A1 收口——敏感操作审计纪律对齐 disable)
> ⚠ DIF-F10-1(观察项)自禁/自改角色现状未拦(无自我保护谓词,user/service.go:159/176)——登记 99-附录 B 观察项
8 权限与数据规则

user:manage(ADMIN);全端点 op_log 带 actor/reason;密码 bcrypt;GDPR 抹除走 F4.05(患者数据链与员工停用链分流——ErrNotPatient 409)。


F10.02 TOTP 2FA 管理(三态机+重置)

对应 SRS:§4.2(后台 2FA 机制细化) | 实现落点:internal/domain/auth/service.go(TOTPSetup/TOTPEnable/TOTPDisable)/internal/domain/user/service.go:242(ResetTOTP)/main.go:24-26(自助三端点)/main.go:254(ADMIN 重置) | 操作入口:web /admin/settings 安全设置(自助绑定/解绑);用户管理页「重置 2FA」

1 功能定义

B 端员工自助管理 TOTP:setup 生成密钥(落 PENDING,返 secret+otpauth_url 供 Google Authenticator 扫码)→ enable 验首码转正(ENABLED)→ disable 验现码解绑(回 NONE)。ADMIN 可重置他人(清 secret+NONE+op_log——无法验码正是重置语义)。

2 触发条件与前置状态
  • 自助三端点:bo JWT 登录即可(无权限码)。
  • setup 前置:totp_status='NONE'(已 ENABLED 拒绝 409);enable 前置:PENDING。
  • 三态机:NONE → PENDING → ENABLED;PENDING 态不触发两段式登录(NeedsMFA 唯一判据 totp_status=='ENABLED'——否则 setup 与登录互相锁死,⚠ DIF-M8 ①)。
3 输入与校验
端点字段校验
POST /api/auth/totp/setup无状态谓词(NONE 才可)
POST /api/auth/totp/enablecode(6 位)长度 400;TOTPValidate ±1 窗;PENDING 谓词
POST /api/auth/totp/disablecode(6 位)ENABLED 谓词;验现码
POST /api/users/:id/totp/resetreason 必填user:manage;version 服务端内部查询 CAS
4 处理流程

setup 生成 20 字节随机密钥 → base32 无 padding 且长度 %8==0(双端约束,⚠ DIF-M5 ⑦)→ AES-256-GCM 加密落库(master_key)→ 返回明文 secret+otpauth URL(仅此一次可见)。enable 校验通过后状态谓词 CAS 转正。

5 输出与结果状态

setup 200 {secret, otpauth_url};enable/disable 204;reset 204;ENABLED 后该账号 F1.01 登录走两段式。

6 状态流转

见 §4 三态图;totp_status 列非 AST 守卫词边界命中(\bstatus\b 不匹配 totp_status——⚠ DIF-M8 ⑥ 扩面注记);RESET 同时清 totp_secret(NULL=未启用哨兵,not_null_guard 白名单第 1 条)。

7 边界与异常
场景行为
已 ENABLED 再 setup409
PENDING 直接 enable 未 setup409(状态谓词)
码错误401/400(按端点语义)
master_key 未配置503(2FA 不可用降级可见)
ADMIN 重置后该账号回到 NONE,需重新 setup(原密钥作废不可恢复)
8 权限与数据规则

自助=登录即可(本人 uid);重置=user:manage;密文存储 AES-GCM(master_key 生产必换);op_log user.totp_reset 留痕。


F10.03 生命周期时间线查询

对应 SRS:F-TICK-003(后台详细日志) | 实现落点:internal/audit/lifecycle_query.go:11(ListByEntity)/audit/handler.go:20/main.go:210(路由挂 ticket:read) | 操作入口:web 工单详情时间线 tab;web 审计页

1 功能定义

统一生命周期端点:按 (entity_type, entity_id) 查 entity_lifecycle_event 全时间线(ORDER BY id)。entity_type 值域=ticket / payment_order / trip / hospital / hospital_dept / hospital_expert / user_account(lifecycle 入口覆盖率守卫:对象类型 ⊆ 已挂时间线入口的类型 ∪ 白名单)。

2 触发条件与前置状态

ticket:read(CONSULTANT/DOCTOR/ADMIN 均可——统一端点设计);A/C 两轴事件同表靠值区分(stage 值与状态枚举天然不重叠——DIF-M7 ①)。

3 输入与校验
参数校验
:type实体类型(白名单外空结果)
:id正整数
4 处理流程
5 输出与结果状态

事件数组(创建/流转/分配/阶段/文档上传替换全轨迹);工单详情内嵌同数据源单点(Detail.Timeline 与本端点同调 ListByEntity)。

6 状态流转

只读;写入路径全部在 transition 封装内(迁移-日志同路径——无旁路日志)。

7 边界与异常
场景行为
类型/id 无事件空数组
跨类型查询按 type 隔离(不跨实体聚合)
挂点覆盖web vitest lifecycle 入口覆盖守卫(对象类型 ⊆ 挂线类型 ∪ 白名单)
8 权限与数据规则

ticket:read;actor_id 语义(0=系统)+ GDPR 匿名自动传播(F4.05)。


F10.04 操作日志审计页

对应 SRS:§4.2(审计日志) | 实现落点:internal/audit/operation_log.go:29(Append)/operation_log_query.go:21(QueryOperationLogs)/audit/handler.go:35/main.go:212(ops:read) | 操作入口:web /admin 审计页(op_log 倒序)

1 功能定义

操作日志查询(审计页数据源):/api 全部写方法经中间件统一记录(actor_id/actor_role/action/entity/reason/trace_id/changes JSON/at,标准列)+ 关键操作 service 内显式补记;查询端点参数化(entity/actor/action)倒序。

2 触发条件与前置状态

ops:read(CONSULTANT+ADMIN);中间件豁免注册表(读与心跳豁免——AST 守卫检查全部写 handler 命中审计路由组)。

3 输入与校验
参数说明
entity_type/entity_id过滤
actor_id / action过滤
分页缺省倒序
4 处理流程
5 输出与结果状态

日志数组;changes JSON 载字段级 diff(Update 带 audit.Diff——如分配 id 变化/scrubbed 条数)。

6 状态流转

只读;operation_log append-only 永不 UPDATE(GDPR 匿名传播设计依赖 id 引用而非名字)。

7 边界与异常
场景行为
op_log 写失败(非事务路径)仅 Warn 不回滚业务(尽力而为——transition 路径例外同事务强一致)
trace_id中间件注入(链路追踪衔接)
回调报文payment 路径 reason 含报文截断 512(F7.02)
8 权限与数据规则

ops:read;日志含敏感操作全留痕(gdpr.erase/user.totp_reset/ticket.assign/payment.transition…)。


F10.05 INBOX 待办与邮件对账重发

对应 SRS:F-REG-003(自动通知顾问,扩展) | 实现落点:internal/infra/notify/inbox.go:20(ListInbox)/:40(MarkInboxRead)/outbox.go:48(RetryFailed)/:60(ListFailedEmails)/notify/handler.go/main.go:238-243(路由) | 操作入口:web 待办铃铛(INBOX 列表/标读);看板对账 tab(FAILED 邮件重发)

1 功能定义

INBOX 站内待办:按 recipient='role:<ROLE>' 角色谓词隔离列表(register_notify/doctor_rejected 等),已读=回写 read_at(CAS 谓词防重,status 恒 PENDING——投递语义与已读语义分离,⚠ DIF-M5 ⑥)。邮件对账:FAILED 列表查询(ops:read)+ 人工重发(ops:manage:重置 PENDING/retry_count=0,dispatcher 30s 内再投)。

2 触发条件与前置状态
  • INBOX:登录即可(角色谓词隔离,无权限码——通知消费不需要权限码)。
  • 对账/重发:ops:read / ops:manage 双码分层。
3 输入与校验
端点输入校验
GET /api/inboxunread_only/limitrecipient=claims role 谓词
POST /api/inbox/:id/read:idread_at IS NULL CAS(幂等)
GET /api/notifications/failed无channel=EMAIL+status=FAILED
POST /api/notifications/:id/retry:id须 EMAIL+FAILED(qRetryFailed 谓词,否则空操作)
4 处理流程
5 输出与结果状态

待办数组/标读条数/FAILED 列表/重发 204;重发后 dispatcher 投递 SENT(dev/e2e log 档日志即送达)。

6 状态流转

INBOX:status 恒 PENDING(dispatcher 只扫 EMAIL 零改动——退避/终态语义不被 INBOX 借用);EMAIL:FAILED→PENDING→SENT。

7 边界与异常
场景行为
重复标读幂等(CAS 命中 0 行)
重发非 FAILED 单空操作(谓词不命中)
重发再失败退避重试 3 败再 FAILED(可再重发)
8 权限与数据规则

角色谓词=天然行级隔离;ops 双码与 F7.05 同纪律(读写分离)。


F10.06 outbox 投递与重试

对应 SRS:—(基础设施) | 实现落点:internal/infra/notify/dispatcher.go:45-123(Dispatcher/Tick/dispatchOne)/outbox.go:26(EnqueueInTx)/pure.go(NextRetryAt/Exhausted)/sendgrid.go | 操作入口:—(30s 定时任务,main.go 装配 Start/Stop)

1 功能定义

EMAIL 通道投递器:dispatcher 每 30s 扫描到期 PENDING 单 → Mailer 投递(provider 分流:log=日志即送达 dev/e2e 恒 log;sendgrid=HTTP API,缺 api_key/from 装配期显式失败)→ 发送后回写 CAS(WHERE status='PENDING'——防回写互相覆盖,不防双实例重复投递,单实例边界注释申报——⚠ DIF-M4 ⑭③);失败指数退避(NextRetryAt 纯函数:30s 起翻倍)3 败 FAILED。

2 触发条件与前置状态

服务启动 disp.Start();优雅停机排空(停机序:HTTP 停 → worker 排空 → dispatcher → hub → redis)。

3 输入与校验

扫描谓词:channel='EMAIL' AND status='PENDING' AND next_retry_at<=now;Exhausted(retry_count, max_retries) 纯函数判定终态。

4 处理流程
5 输出与结果状态

outbox 行终态 SENT/FAILED;last_error 截断留痕。

6 状态流转

PENDING → SENT / FAILED(retry_count 随退避递增);INBOX 行不进此路径(channel 过滤)。

7 边界与异常
场景行为
SendGrid 未配 api_key/from装配期 fatal(配置错误启动即暴露——沿 payment.provider 先例)
缺省(log 档)日志即送达(dev/e2e)
双实例部署不防重复投递(单实例边界申报)——回写 CAS 只防互相覆盖
发送后回写前崩溃PENDING 残留下轮重投(at-least-once;收件方幂等由业务语义保证)
8 权限与数据规则

系统任务(无 actor);按患者语言选模板 i18n(tech-design §10);payload JSON 透传(各业务节点构造)。