Files
bid-doctor/docs/architecture/ops-api.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

66 lines
5.1 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.

# 运营后台接口(/ops/v1)
运营后台的接口不在用户端契约 `openapi.yaml` 里:它们只挂在 `bid-doctor ops` 进程上(决策 M6-2),前端 `apps/admin` 是唯一的调用方,类型在 `apps/admin/src/api.ts` 里手写。实现见 `server/src/bid_doctor/ops/`(`console_api.py` 路由、`console.py` 查询、`auth.py` 登录)。
## 安全
| 方面 | 做法 |
|---|---|
| 进程 | 和 api、worker 同一镜像的第三种进程,单独端口(开发 8019);部署时只对内网开放 |
| 来源地址 | `BD_OPS_ALLOWED_IPS`(CIDR 列表)以外的请求一律 403 `ops_ip_denied`;生产环境不配或放开全部地址拒绝启动 |
| 登录 | 用户名 + 密码(scrypt)+ TOTP 6 位动态码(身份验证器 App);同一用户名 15 分钟最多试 5 次;账号只能用 `bid-doctor ops-admin create` 建 |
| 会话 | cookie `bd_ops_session`(HttpOnly、SameSite=Strict,8 小时)+ 写操作带 `X-CSRF-Token`(等于 cookie `bd_ops_csrf`) |
| 数据库 | 跨企业查询用 `bd_ops`(绕过行级安全策略,业务表只读,运营账号和会话表可写,审计可写);对某个企业的修改走 `bd_app` + 那个企业的租户事务。`bd_app` 读不到运营账号表 |
| 留痕 | 每个操作都写审计(`actor_kind = ops`,记运营账号),查看企业详情也记一条 `ops.tenant.view` |
| 内容 | 不显示项目名称、招标 / 投标原文、审计的修改前后(决策 M6-7、红线 10) |
## 角色和权限点(只留结构,决策 M6-10)
以后会有管理员、运营、销售、运维等不同权限的运营人员。现在只建结构:
- `ops_admin.role`(迁移 0012,不加 CHECK);
- 权限点 `OpsPermission`(`ops/auth.py`):`tenant.view`、`quota.adjust`、`activation.manage`、`invoice.manage`、`feedback.view`、`jobs.view`、`audit.view`;
- 「角色 → 权限点」对应表 `ROLE_PERMISSIONS`:现在只有 `admin`(全部权限)。表里有、对应表里没有的角色什么权限都没有;
- 每个接口开头都调 `auth.require(op, 权限点)`,没有权限返回 403 `forbidden`;`/me` 返回角色和权限点,前端菜单按权限点过滤(`apps/admin/src/nav.ts`),一组都看不到时整组不显示。
加角色时只改 `ROLE_PERMISSIONS`、`ROLE_LABEL` 和 `ops-admin create --role` 的说明;要不要有「运营账号管理」页面、谁能建账号,到时再定。
## 菜单(决策 M6-9)
左侧菜单由 `apps/admin/src/nav.ts` 一处配置:分组(客户、质量、安全)→ 菜单项(图标、路由、要的权限点、待办数)。分组可以展开收起,记在本机;跳到收起分组里的页面时自动展开。待办数来自 `GET /summary`(待处理的开通申请、近 7 天失败任务、近 7 天反馈),做完操作和每分钟刷新一次。
## 接口
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/auth/login` | `{username, password, totp}`;失败 401 `ops_login_failed` |
| POST | `/auth/logout` | |
| GET | `/me` | 当前运营账号:角色、权限点、运行环境(非生产环境时菜单顶部提示) |
| GET | `/summary` | 菜单上的待办数;没有权限的那一项给 0 |
| GET | `/tenants?q=` | 企业名称(模糊)、手机号(精确,按 HMAC 查)、申请单号;不带条件列最近注册的 50 个 |
| GET | `/tenants/{id}` | 成员(手机号脱敏)、套餐历史、最近 30 条额度流水(不带项目名称)、任务按状态计数 |
| POST | `/tenants/{id}/quota-adjustments` | `{unit: parse, amount, reason}`;原因必填,不能扣成负数(G6) |
| GET | `/activation-requests?status=` | 待处理(等待转账、已到账)在前 |
| POST | `/activation-requests/{id}/paid` | 标记到账(只限等待转账) |
| POST | `/activation-requests/{id}/activate` | `{months = 12}`;免费版 / 已过期马上开始新一期,专业版没到期时新一期排队(续费) |
| POST | `/activation-requests/{id}/reject` | `{note}`,用户在额度页看得到原因 |
| POST | `/activation-requests/{id}/invoice` | `{invoice_no}`,开通以后才能登记 |
| GET | `/feedback?kind=` | 误报 / 漏报:标题、级别、理由、规则编号、规则集和提示词版本、页码 |
| GET | `/failed-jobs` | 解析、检查、报告失败的任务:错误类别、错误码、阶段、耗时、模型调用次数 |
| GET | `/audit-events?tenant_id=&action=` | 动作、对象、操作人、时间;`action` 按前缀筛选 |
## 本地跑起来
```
cd server && ../.venv/bin/bid-doctor ops # :8019
../.venv/bin/bid-doctor ops-admin create --username dev --display-name 开发运营 # 打印 otpauth:// 地址,用身份验证器 App 添加
pnpm dev:admin # :5174,/ops 代理到 :8019
```
已有的数据卷是在加 `bd_ops` 之前初始化的,要手工建一次角色(新数据卷由 `deploy/compose/postgres/init-roles.sh` 建):
```
docker exec -e PGPASSWORD=dev-owner bid-doctor-postgres-1 psql -U bd_owner -d bid_doctor \
-c "CREATE ROLE bd_ops LOGIN BYPASSRLS PASSWORD 'dev-ops'" -c "GRANT CONNECT ON DATABASE bid_doctor TO bd_ops"
```