feat(m6): 额度流水、套餐开通、数据删除、运营后台(M6 剩下的 P0)
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled

决策 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>
This commit is contained in:
YANG JIANKUAN
2026-11-05 15:06:06 +08:00
parent 54cbb556d0
commit dd8c04001d
78 changed files with 5064 additions and 104 deletions

View File

@@ -203,12 +203,40 @@ paths:
'204': { description: 已吊销 }
'404': { $ref: '#/components/responses/NotFound' }
/me/sms-codes:
post:
tags: [me]
operationId: sendMySmsCode
summary: 登录后发验证码到自己的手机号(注销账号的二次确认,前端不需要完整号码)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [purpose]
properties:
purpose: { type: string, enum: [close_tenant] }
responses:
'202':
description: 已发送
content:
application/json:
schema:
type: object
required: [resend_after_s]
properties:
resend_after_s: { type: integer }
'429': { $ref: '#/components/responses/RateLimited' }
/tenant/close:
post:
tags: [me]
operationId: closeTenant
summary: 注销账号(删除企业下全部业务数据,不可恢复)
description: data-model §6.2。需要短信验证码和企业名称两次确认。成功后当前会话失效。
description: |
data-model §6.2,决策 M6-8。需要短信验证码(`POST /me/sms-codes`)和企业名称两次确认;
企业名称不一致返回 422 `tenant_name_mismatch`。成功后所有会话失效,手机号可以重新注册;项目、资质库等由后台删除。
requestBody:
required: true
content:
@@ -2168,7 +2196,7 @@ components:
description: 给用户看的检查计费说明,如「检查从解析额度扣 1 次;同一项目 24 小时内复查不扣」
QuotaOverview:
type: object
required: [plan, accounts]
required: [plan, accounts, usage]
properties:
plan: { $ref: '#/components/schemas/PlanSummary' }
accounts:
@@ -2180,26 +2208,44 @@ components:
unit: { $ref: '#/components/schemas/QuotaUnit' }
balance: { type: integer, description: 剩余,含正在预扣的 }
held: { type: integer, description: 正在预扣、还没结算的 }
period_granted: { type: integer }
period_used: { type: integer }
period_granted: { type: integer, description: 本期发放(按当前这一期订阅) }
period_used: { type: integer, description: 本期净用量(预扣 − 退回) }
period_released: { type: integer, description: 本期退回的次数(失败、超时、取消) }
used_ratio: { type: number, description: '0~1;≥ 0.8 时界面变赭色(Q9)' }
by_feature:
type: array
description: 按功能分布,如解析、检查
items:
type: object
required: [feature, used]
properties:
feature: { type: string, enum: [parse, check] }
used: { type: integer }
usage:
type: object
description: 本期按功能分布(没被退回的预扣);deleted 是项目已删除、看不出用途的;waived 是 24 小时内复查的免扣次数
required: [tender, addendum, check, deleted, waived]
properties:
tender: { type: integer }
addendum: { type: integer, description: 补遗和澄清 }
check: { type: integer }
deleted: { type: integer }
waived: { type: integer }
renewal:
description: 已续费、排队等当前一期结束后生效的那一期
oneOf:
- type: 'null'
- type: object
required: [starts_at, ends_at]
properties:
starts_at: { $ref: '#/components/schemas/Timestamp' }
ends_at: { $ref: '#/components/schemas/Timestamp' }
pending_request:
description: 还在等转账或开通的申请
oneOf:
- type: 'null'
- $ref: '#/components/schemas/ActivationRequest'
QuotaEntry:
type: object
required: [id, unit, kind, amount, created_at]
required: [id, unit, kind, amount, balance_after, created_at]
properties:
id: { $ref: '#/components/schemas/Id' }
unit: { $ref: '#/components/schemas/QuotaUnit' }
kind: { $ref: '#/components/schemas/QuotaEntryKind' }
amount: { type: integer, description: 对余额的影响;结算、免扣为 0 }
balance_after: { type: integer, description: 记完这一条以后的账户余额 }
hold_entry_id: { oneOf: [{ $ref: '#/components/schemas/Id' }, { type: 'null' }], description: 结算、退回对应的预扣 }
reason: { type: string, description: '调整原因;免扣时为规则说明,如「24 小时内复查」' }
project:
type: [object, 'null']
@@ -2222,7 +2268,8 @@ components:
invoice_info: { $ref: '#/components/schemas/InvoiceInfo' }
InvoiceInfo:
type: object
required: [title, tax_no]
required: [title, tax_no, contact_name, contact_phone]
description: 专票(invoice_type = special)还要地址电话和开户行账号
properties:
title: { type: string, description: 发票抬头 }
tax_no: { type: string, description: 纳税人识别号 }
@@ -2233,14 +2280,27 @@ components:
contact_phone: { type: string }
ActivationRequest:
type: object
required: [id, plan_code, status, created_at]
required: [id, request_no, plan_code, status, payment, created_at]
properties:
id: { $ref: '#/components/schemas/Id' }
request_no: { type: string, examples: [KT-7Q3M2X], description: 申请单号,对公转账时写在备注里 }
plan_code: { $ref: '#/components/schemas/PlanCode' }
status: { type: string, enum: [submitted, paid, activated, rejected, cancelled] }
status:
type: string
enum: [submitted, paid, activated, rejected, cancelled]
description: 等待转账 / 已到账 / 已开通 / 已退回 / 已撤回
invoice_info: { $ref: '#/components/schemas/InvoiceInfo' }
payment_instructions: { type: string, description: 对公转账账户和备注要求 }
ops_note: { type: string }
payment:
type: object
description: 收款信息(配置项;没配时为「(待定)」,决策 M6-6)。页面不写价格,运营联系确认金额
required: [payee_name, payee_bank, payee_account]
properties:
payee_name: { type: string }
payee_bank: { type: string }
payee_account: { type: string }
ops_note: { type: [string, 'null'], description: 退回原因 }
invoice_no: { type: [string, 'null'] }
handled_at: { oneOf: [{ $ref: '#/components/schemas/Timestamp' }, { type: 'null' }] }
created_at: { $ref: '#/components/schemas/Timestamp' }
# ---------- projects
@@ -3180,13 +3240,14 @@ components:
kind:
type: string
enum: [parse_succeeded, parse_failed, check_succeeded, check_failed, report_succeeded, report_failed, quota_80, quota_100,
subscription_activated, subscription_expiring]
subscription_activated, subscription_expiring, subscription_expired, activation_rejected]
title: { type: string }
project_id:
oneOf:
- $ref: '#/components/schemas/Id'
- type: 'null'
link: { type: [string, 'null'], description: '前端路由,如 /projects/<id>/results' }
project_name: { type: [string, 'null'] }
link: { type: [string, 'null'], description: '前端路由,如 /projects/<id>/results、/quota' }
read_at:
oneOf:
- $ref: '#/components/schemas/Timestamp'