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

115 lines
13 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.

# 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`。