Files
bid-doctor/docs/architecture/m6-plan.md
YANG JIANKUAN dd8c04001d
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(m6): 额度流水、套餐开通、数据删除、运营后台(M6 剩下的 P0)
决策 M6-1–M6-10(docs/architecture/m6-plan.md、docs/decisions.md)。

用户端:
- 额度与套餐页:本期额度、按功能分布、扣点规则、流水(逐条余额、关联项目和任务、导出 CSV)
- 开通 / 续费申请(对公转账,运营确认到账后开通),80% / 100% 提醒
- 专业版到期:整个账号只读、额度清零,到期前 30 / 7 / 0 天提醒(worker 每小时 billing_tick)
- 删除项目(彻底删除、销毁数据密钥),注销账号(企业名称 + 短信验证码)
- 通知铃铛、账号页

运营后台:
- 独立进程 bid-doctor ops(/ops/v1)和 apps/admin;用户名 + 密码 + TOTP + IP 白名单
- 开通申请(到账 / 开通 / 退回 / 发票)、企业查询和调额度、误报 / 漏报、失败任务、审计;不看业务内容
- 左侧菜单(标准 logo、可展开收起的分组、图标、待办数、环境标签)
- 运营人员角色只留结构:ops_admin.role、权限点、每个接口的检查点、菜单按权限过滤

数据库:迁移 0010(排队的订阅、申请单号、通知类型、billing_due)、0011(bd_ops 权限)、0012(运营角色)。
另修:取消解析 / 检查接口报 500;uuid7 进程内单调;data_key 销毁后不再新建;审计 None 存 SQL NULL。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:06:06 +08:00

13 KiB
Raw Blame History

M6 剩下的 P0:额度流水、套餐开通、数据删除、运营后台(实现计划 v0.1)

依据:FR-M6-03 / 05 / 06 / 07 / 09(docs/analysis/03-functional-requirements.md),J4、J5(02-user-journeys.md),G6、G8、G10,决策 Q9、Q13、P1、DM4;表结构已在基线迁移里(data-model.md §4、§6),用户端接口已在 openapi.yaml 里定义;运营后台的接口和选型「做到时再定」(架构 §3.11、§4.3)。

0. 现状

方面 已有 还没有
额度 注册发放、预扣 / 结算 / 退回 / 免扣、GET /quota、顶栏额度(≥ 80% 赭色) 流水接口和额度页、80% / 100% 提醒、人工调整、过期
套餐 免费版注册即开通、GET /plans、专业版套餐(解析 500,D2 待定) 开通 / 续费申请、运营开通、到期处理
删除 资质删除(QL6) 删除项目、注销账号
通知 解析、检查、报告完成写通知;事件流 前端通知入口、标为已读
运营后台 表:ops_admin、ops_session;角色 bd_ops(文档里有,部署脚本里还没建);反馈写入 进程、登录、接口、前端,全部没有

另外发现一处冲突:原型 project.html 的删除写「30 天内可以在回收站恢复」,而 G10、DM4(已确认)是删除时销毁项目数据密钥、彻底删除。按决策优先,见 M6-1。

1. 用户端

1.1 额度页(FR-M6-03,原型 quota.html)

  • 侧栏账户区(头像、套餐标签)点开进「额度与套餐」页;顶栏额度改成链接,也进这个页。
  • 页面:本期额度卡(剩余 / 已用 / 退回,期间起止)→ 扣点规则表(原型原样)→ 按功能分布(读标招标文件、补遗和澄清、查标、免扣次数)→ 本期流水。
  • 流水:GET /quota/entries,按单位筛选、按时间倒序分页;每行写时间、类型(发放 / 预扣 / 结算 / 退回 / 免扣 / 过期 / 调整)、数量(+ / −,墨色,Q1)、余额、关联项目 · 任务(「招标文件」「第 2 次检查」;项目删了写「已删除的项目」和任务编号,G10)、说明(调整原因、失败原因、「距上次扣额度的检查不到 24 小时」)。导出 CSV 在前端生成。
  • 提醒:预扣后余额跨过 80%、100%(按本期发放算)时写站内通知,quota_alert 保证每期每档只提醒一次。

1.2 开通与续费(FR-M6-05,G8)

  • 免费版:额度页和顶栏「额度用完」提示里有「开通专业版」;专业版:到期前 30 天起显示「续费」。
  • 申请对话框(原型):企业名称(注册时的,只读)、发票类型(普票 / 专票)、纳税人识别号、联系人、手机号;专票再加地址电话、开户行账号。POST /activation-requests,不扣额度、不用先付款。
  • 提交后页面顶部显示申请单号、状态(等待对公转账 / 已到账 / 已开通 / 已退回)、收款信息(收款单位、开户行、账号,从配置读 BD_PAYEE_*)和「转账备注写申请单号」。
  • 运营确认到账并开通:建专业版订阅、发放额度(流水「发放」,说明写申请单号),发站内通知;之后填发票号。
  • 续费:当前专业版还没到期时,新一期从旧一期结束时开始;旧一期剩下的额度到期清零(「本期没用完的不结转」,原型)。

1.3 到期(FR-M6-05)

  • worker 每天跑一次定时任务(Procrastinate 周期任务):到期的订阅改「已过期」、记「过期」流水把余额清零;排队的下一期到点发放。
  • 提前 30 天、7 天、到期当天各发一次站内通知。
  • 过期后:数据保留,只读(M6-5)。

1.4 删除项目(J4、G10、data-model §6.1)

  • 入口:项目头「更多」菜单(和归档放一起),已递交、已归档的也能删。
  • 确认框:写明删掉什么、保留什么(额度流水、不含业务内容的审计),输入项目名称确认(原型)。
  • DELETE /projects/{id}:同一事务里写审计、抹掉这个项目审计的内容、删项目行(级联)、销毁数据密钥;提交后后台任务删对象存储前缀。正在解析 / 检查的项目先取消任务再删(预扣退回)。

1.5 账号页与注销(J4、data-model §6.2)

  • 「额度与套餐」页旁边一个「账号」页:企业名称、手机号(脱敏)、显示名、套餐;最下面「注销账号」。
  • 注销:确认框写明后果(全部项目、资质库删除;开通申请和发票记录按财务要求保留;剩余额度作废、不退款),输入企业名称 + 短信验证码。POST /tenant/close:立即吊销会话、企业标「已注销」,项目按 §1.4 由后台任务逐个删。

1.6 通知入口

  • 顶栏加铃铛(原型有),有未读时带圆点;点开是浮层列表(最近 30 条:解析 / 检查 / 报告完成、额度提醒、开通结果、到期提醒),点一条跳到相关页面并标为已读,「全部标为已读」。POST /notifications/read。

2. 运营后台(FR-M6-09)

2.1 形态

  • 后端:同一个 Python 包的第三种进程 bid-doctor ops(和 api、worker 一样的镜像),单独端口、路径前缀 /ops/v1,用 bd_ops 数据库角色(绕过行级安全策略);用户端 api 进程里不挂任何运营接口。部署时运营端口只对内网 / 白名单开放。
  • 登录:用户名 + 密码 + TOTP 二次验证;IP 白名单(BD_OPS_ALLOWED_IPS,空着时拒绝启动于生产环境);会话 8 小时;账号用命令行建(bid-doctor ops-admin create),没有自助注册。
  • 前端:apps/admin,Vue 3 + Vite,复用 tokens.css 和组件样式,不引入带样式的框架(M6-3);单独构建、单独部署,私有化不交付。
  • 审计:每个运营操作(包括查询企业详情)都写审计事件,操作人记运营账号。

2.2 页面和接口(最小版)

页面 做什么 接口(/ops/v1)
企业 按企业名称、手机号(用 HMAC 查)、申请单号搜;详情:成员(手机号脱敏)、套餐和到期日、各单位余额、项目数、最近任务;不显示项目名称和任何业务内容 GET /tenants、GET /tenants/{id}
开通申请 待处理在前;标记「已到账」→「开通」(选套餐和周期,默认专业版 1 年),或「退回」写原因;开通后填发票号 GET /activation-requests、POST .../{id}/activate、/reject、/invoice
额度调整 在企业详情里:单位、±数量、原因(必填) POST /tenants/{id}/quota-adjustments
反馈 误报 / 漏报列表:类型、用户写的理由、问题标题和级别、规则编号、提示词版本、出处页码,不显示招标 / 投标文件原文(M6-7) GET /feedback
失败任务 解析、检查、报告失败的:错误类别、错误码、阶段、耗时、模型调用次数,不含文档内容 GET /failed-jobs
审计 按企业 / 对象 / 时间查审计事件(项目删除后只剩类型和时间) GET /audit-events

3. 步骤

  1. 用户端后端:流水、提醒、开通申请、到期任务、删除项目、注销、通知已读,各配集成测试(额度对账:任何失败任务都没有净扣点;删除后对象不可解密)。
  2. 用户端前端:额度与套餐页、账号页、通知入口、删除项目。
  3. 运营后台后端:ops 进程、bd_ops 角色(部署脚本)、登录和 TOTP、上表接口、审计。
  4. 运营后台前端:apps/admin 六个页面。
  5. 浏览器走查:注册 → 用完免费额度 → 申请开通 → 运营开通 → 流水核对 → 到期(改时钟)→ 删除项目 → 注销。

4. 待确认(建议见每条)

# 问题 建议
M6-1 删除项目要不要「30 天回收站」(原型这么写) 不要,立即彻底删除:按已确认的 G10、DM4 销毁数据密钥;回收站要软删除,和「彻底删除」冲突(同 QL6)。确认框要求输入项目名称,写明不能恢复。原型文案改掉
M6-2 运营接口放在哪 单独的第三种进程 bid-doctor ops(同一镜像、单独端口、bd_ops 角色);用户端 api 进程里没有运营接口,绕过行级安全策略的代码不会被公网请求触到
M6-3 运营后台前端选型 Vue 3 + 复用 tokens.css / 组件样式,不引入带样式框架:页面只有六个、以表格和表单为主;和用户端同一套组件,维护成本低。另一种做法是引入 Element Plus 之类加快开发,但多一套视觉和依赖
M6-4 运营登录 用户名 + 密码 + TOTP + IP 白名单,账号命令行创建(架构 §3.11 已写独立登录、白名单、二次验证)
M6-5 套餐到期后怎么办 不自动降为免费版;项目只读(能看、能下载已导出的报告、能删除;不能新建项目、解析、检查、改清单);额度清零;提前 30 天、7 天、当天提醒。续费到账后恢复
M6-6 开通申请页写不写价格、收款信息从哪来 页面不写价格(D2 额度数字和定价都还没定),写「运营会联系你确认金额」;收款单位 / 开户行 / 账号从配置读,没配时显示「(待定)」,生产环境没配拒绝启动
M6-7 运营看反馈时能不能看原文 不看原文:只看标题、理由、规则编号、提示词版本、页码(红线 10:数据敏感、按租户隔离)。要复盘原文时请用户导出报告发过来,或以后做「用户授权查看」
M6-8 注销账号的确认方式 输入企业名称 + 短信验证码;有没到期的专业版时写明剩余额度作废、不退款

5. 落地记录(2026-11-05)

M6-1–M6-8 全部按建议确认后实现。和上文不一样、或计划里没写到的:

  • 迁移 0010:订阅加「排队中」(scheduled),续费到账时当前一期还没到期就排队,到点由定时任务生效发放;开通申请加申请单号 request_no(KT- 加 6 位);通知加「已到期」「申请被退回」;billing_due(now) 是 bd_auth 所有的 SECURITY DEFINER 函数,定时任务(worker 每小时 billing_tick)用它跨企业找到期、该生效、该提醒的订阅,只拿编号,处理时回到各企业自己的租户事务。到期时还有没了结的预扣就等下一次再清零,避免退回的 1 次漏在余额里。
  • 迁移 0011:bd_ops 的权限(跨企业只读、运营账号和会话表可写、审计可写),bd_app 收回运营账号表的权限。角色本身由部署脚本建(init-roles.sh,CI 同一个脚本)。
  • 只读的落点:到期后写操作在鉴权依赖里统一拦(identity/deps.py 的 READ_ONLY_ALLOWED),退出、续费申请、删除项目、注销、通知已读、反馈、管理登录设备照常放行;不用逐个接口去改。
  • 额度单位:只发放现在用得上的「解析」;专业版套餐里的 generate(M4 标书生成)等功能上线再发,免得顶栏出现用不了的额度。
  • 删除项目:正在解析、检查、生成报告时返回 409 job_already_running,先取消(预扣退回)再删;data_key 销毁以后不再建新密钥,还在跑的任务写不回文件。审计的 before / after 改为 Python 的 None 存成 SQL NULL(以前是 JSON 的 null)。
  • 注销账号:验证码改为登录后 POST /me/sms-codes 发到自己的号码(前端不知道完整号码);同步部分完成后手机号就能重新注册,删项目、资质库、抹审计内容、销毁密钥、清对象存储由后台任务 close_tenant 做,可以重复执行。
  • uuid7 单调:同一进程内严格递增(RFC 9562 方法 2),额度流水按编号排序算逐条余额。
  • 顺手修的:取消解析、取消检查的接口在刷新以前转 API 会触发懒加载报错(MissingGreenlet),取消按钮会 500。
  • 运营后台:接口说明见 ops-api.md;前端 apps/admin(pnpm dev:admin,:5174)六个页面:登录、开通申请、企业(含调额度)、误报 / 漏报、失败任务、审计。

测试:后端 156 个(新增 test_billing_flow.py 4 个、test_ops_console.py 3 个,覆盖流水对账、提醒只发一次、开通 / 续费 / 到期只读、删除后不可解密、注销后可重新注册、运营登录和 IP 白名单、bd_app 读不到运营表、bd_ops 改不了业务表)。浏览器走查:注册 → 申请开通 → 运营标记到账、开通、登记发票 → 流水核对 → 调额度 → 到期只读 → 删除项目 → 注销账号。

没做的:运营后台没有「撤回申请」「延长订阅」这类操作(需要时用退回 + 重新申请、调额度代替);收款信息和发票是线下流程,系统只登记发票号。

运营后台第二版(2026-11-05,决策 M6-9、M6-10):菜单改到左侧(墨色渐变底,标准横版 logo 反白,非生产环境挂赭色环境标签),分组可以展开收起、每项带图标和待办数(GET /summary),底部是当前运营人员(姓名、角色)和退出;登录页加 logo。运营人员的角色只留结构:迁移 0012 加 ops_admin.role,权限点和「角色 → 权限点」对应表、每个接口的检查点、按权限点过滤的菜单都已就位,现在只有管理员。见 ops-api.md。