feat: integrate cultivate admin internal APIs

This commit is contained in:
richjohnrambo
2026-09-24 00:02:27 +08:00
parent e002e45f82
commit 369898bd7c
39 changed files with 1426 additions and 2382 deletions
@@ -0,0 +1,696 @@
# Cultivate Server 管理后台内部接口开发对接文档
## 1. 文档目的
本文用于指导 `cultivate-server` 开发管理后台内部接口,使 IM Manager 可以通过服务间 HTTP API 管理养成业务,不再直接连接 Cultivate 数据库。
改造完成后必须满足:
1. IM Manager 不配置 `CULTIVATE_MYSQL_DSN`,也不持有 Cultivate 数据库账号。
2. Cultivate 数据只能由 `cultivate-server` 读写。
3. Manager 只负责管理员登录态、RBAC、页面协议转换和调用内部接口。
4. Cultivate 服务不可用时,只影响养成管理菜单,不得影响 Manager 启动及其他菜单。
5. 所有查询和写入必须按商户隔离,禁止信任前端传入的 `tenant_id`、`operator_id`。
## 2. 当前问题
当前 IM Manager 在配置中注册了独立数据库组:
```yaml
database:
cultivate:
link: "$CULTIVATE_MYSQL_DSN"
```
养成管理逻辑通过 `g.DB("cultivate")` 及 group 为 `cultivate` 的 DAO 直接访问以下表:
- `cv_flower_catalog`
- `cv_flower_stage_rule`
- `cv_user_flower`
- `cv_garden_active_day`
- `cv_feed_post`
- `cv_feed_comment`
- `cv_feed_media`
- `cv_feed_report`
- `cv_content_audit_log`
- `cv_admin_operation_log`
该实现造成:
- Manager 启动依赖 Cultivate 数据库配置。
- Cultivate 数据库账号扩散到 Manager。
- Cultivate 表结构变化会直接影响 Manager。
- 内容审核、通知、文件校验等事务边界跨服务泄漏。
本次应将上述数据库逻辑迁移到 `cultivate-server`,不能以给 Manager 增加数据库 Secret 的方式规避。
## 3. 服务职责
### 3.1 Cultivate Server
负责:
- 查询养成概览。
- 花种及成长阶段配置的增删改查和状态变更。
- 动态、评论审核列表、详情和人工审核。
- 举报列表、详情和处理。
- 管理操作日志查询和写入。
- 调用 IM 内部接口查询用户摘要、校验文件、发送系统通知及执行用户治理。
- 数据事务、幂等、租户隔离和审计。
### 3.2 IM Manager
负责:
- 校验后台管理员登录态和菜单权限。
- 从登录态解析商户、操作人和角色。
- 对 Cultivate 内部请求签名。
- 将 Cultivate 返回结果转换为现有后台页面响应。
- 将下游不可用错误转换成清晰的中文业务错误。
Manager 不再保存 Cultivate DAO、实体模型或数据库连接。
## 4. 接口基础约定
### 4.1 路径
内部管理接口统一使用:
```text
/internal/admin/cultivate/tenants/{tenant_code}/...
```
示例:
```text
GET /internal/admin/cultivate/tenants/default/dashboard
```
这些接口只能在集群内访问,不挂到公网 Ingress。
### 4.2 数据格式
- 请求和响应均使用 UTF-8 JSON。
- JSON 字段统一使用 `snake_case`。
- ID 统一使用十进制字符串返回,避免 JavaScript 超出安全整数范围。
- 时间统一使用 UTC RFC3339,例如 `2026-09-23T08:30:00Z`。
- 分页参数为 `page`、`per_page`,默认分别为 `1`、`20`。
- `per_page` 最大为 `100`。
统一成功响应:
```json
{
"code": 0,
"data": {},
"server_time": "2026-09-23T08:30:00Z",
"request_id": "req_xxx"
}
```
统一失败响应:
```json
{
"code": 40001,
"reason": "INVALID_ARGUMENT",
"message": "内容类型不正确",
"details": null,
"server_time": "2026-09-23T08:30:00Z",
"request_id": "req_xxx"
}
```
分页数据统一放在 `data` 中:
```json
{
"items": [],
"total": 0,
"page": 1,
"per_page": 20
}
```
## 5. 内部鉴权
### 5.1 请求头
| 请求头 | 必填 | 说明 |
| --- | --- | --- |
| `X-Service-Name` | 是 | 调用方服务名,Manager 固定为 `im-manager` |
| `X-Timestamp` | 是 | UTC Unix 秒 |
| `X-Nonce` | 是 | 每次请求随机值,服务端防重放 |
| `X-Signature` | 是 | HMAC-SHA256 十六进制小写字符串 |
| `X-Tenant-ID` | 是 | Manager 登录态解析出的商户 ID |
| `X-Operator-ID` | 是 | Manager 登录态解析出的管理员 ID |
| `X-Role-Key` | 否 | 管理员角色标识,用于审计,不作为唯一授权依据 |
| `X-Request-ID` | 是 | 全链路请求 ID |
### 5.2 签名串
双方复用现有内部服务密钥,不新增数据库相关 Secret。签名原文为:
```text
METHOD\n
REQUEST_URI\n
SHA256_HEX(BODY)\n
TIMESTAMP\n
NONCE\n
TENANT_ID\n
OPERATOR_ID\n
ROLE_KEY\n
REQUEST_ID
```
说明:
- `REQUEST_URI` 包含路径和排序后的 Query String。
- GET 请求的 Body 为空字节数组。
- 使用共享密钥计算 `HMAC-SHA256`。
- 时间窗口建议为正负 300 秒。
- `X-Service-Name + X-Nonce` 在时间窗口内只能使用一次。
- 必须使用常量时间比较签名。
- 商户、操作人及角色头已经纳入签名,业务层不得接受 Body 中同名字段覆盖。
### 5.3 商户校验
1. `{tenant_code}` 必须与签名后的 `X-Tenant-ID` 对应。
2. 所有 SQL 必须带 `tenant_id = X-Tenant-ID`。
3. 查不到商户或编码与 ID 不匹配时返回 `TENANT_NOT_FOUND` 或 `TENANT_MISMATCH`。
4. 禁止使用默认商户兜底处理错误的商户参数。
## 6. 接口清单
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/dashboard` | 养成管理概览 |
| GET | `/flowers` | 花种列表 |
| GET | `/flowers/{id}` | 花种详情 |
| POST | `/flowers` | 新建花种 |
| PUT | `/flowers/{id}` | 编辑花种 |
| POST | `/flowers/{id}/status` | 花种状态变更 |
| GET | `/content-reviews` | 内容审核列表 |
| GET | `/content-reviews/{object_type}/{id}` | 内容审核详情 |
| POST | `/content-reviews/{object_type}/{id}/actions` | 单条审核操作 |
| POST | `/content-reviews/batch-actions` | 批量审核操作 |
| GET | `/reports` | 举报列表 |
| GET | `/reports/{id}` | 举报详情 |
| POST | `/reports/{id}/actions` | 处理举报 |
| GET | `/operation-logs` | 操作日志列表 |
以下章节中的路径均省略前缀:
```text
/internal/admin/cultivate/tenants/{tenant_code}
```
## 7. 数据概览
### 7.1 `GET /dashboard`
响应 `data`:
```json
{
"plant_count": 0,
"growing_count": 0,
"bloom_count": 0,
"harvested_count": 0,
"removed_count": 0,
"active_7d": 0,
"active_30d": 0,
"post_count": 0,
"public_post_count": 0,
"friends_post_count": 0,
"private_post_count": 0,
"comment_count": 0,
"like_count": 0,
"taken_down_count": 0,
"pending_audit_count": 0,
"approved_audit_count": 0,
"rejected_audit_count": 0,
"pending_report_count": 0,
"processing_report_count": 0,
"processed_report_count": 0
}
```
统计口径必须沿用当前业务状态,且全部按商户过滤。没有数据时返回 `0`,不能返回 `null`。
## 8. 花种管理
### 8.1 `GET /flowers`
Query 参数:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `flower_code` | string | 编码模糊查询 |
| `flower_name` | string | 名称模糊查询 |
| `status` | string | 状态精确查询 |
| `config_version` | string | 配置版本 |
| `created_from` | RFC3339 | 创建开始时间 |
| `created_to` | RFC3339 | 创建结束时间 |
| `page` | int | 页码 |
| `per_page` | int | 每页数量 |
列表项:
```json
{
"id": "10001",
"flower_code": "rose",
"flower_name": "玫瑰",
"flower_desc": "",
"icon_file_id": "file_xxx",
"status": "online",
"config_version": "cat_20260923_163000",
"daily_water_limit": 3,
"daily_fertilize_limit": 1,
"sort_no": 10,
"stage_count": 4,
"created_at": "2026-09-23T08:30:00Z",
"updated_at": "2026-09-23T08:30:00Z"
}
```
排序规则:`sort_no ASC, id DESC`。
### 8.2 `GET /flowers/{id}`
在列表字段基础上增加:
```json
{
"share_config": {},
"stage_rules": [
{
"id": "10002",
"stage": "seed",
"next_stage": "sprout",
"required_points": 10,
"water_points": 2,
"fertilize_points": 3,
"allowed_actions": ["water", "fertilize"],
"image_file_id": "file_xxx",
"animation_file_id": "file_xxx",
"progress_text_template": "成长中",
"completion_text": "已进入发芽阶段",
"sort_no": 10
}
]
}
```
### 8.3 `POST /flowers` 与 `PUT /flowers/{id}`
请求体:
```json
{
"flower_code": "rose",
"flower_name": "玫瑰",
"flower_desc": "",
"icon_file_id": "file_xxx",
"status": "offline",
"daily_water_limit": 3,
"daily_fertilize_limit": 1,
"share_config": {},
"sort_no": 10,
"stage_rules": []
}
```
业务规则:
- `flower_code`、`flower_name` 必填。
- `flower_code` 创建后不可修改。
- 状态枚举:`online`、`offline`、`activity_only`、`maintenance`。
- 阶段枚举:`seed`、`sprout`、`bud`、`bloom`。
- 阶段链必须从 `seed` 连续配置并以 `bloom` 结束,不能重复或成环。
- 非 `bloom` 阶段 `required_points > 0`,且至少允许一个动作。
- 动作枚举:`water`、`fertilize`。
- 文件 ID 必须通过 IM 内部文件校验,风险或待审核素材不能保存。
- 花种和阶段规则必须在一个数据库事务内保存。
- 每次新增或编辑生成新的 `config_version` 并写管理操作日志。
- 新建返回 `data.id`,ID 使用字符串。
### 8.4 `POST /flowers/{id}/status`
```json
{
"status": "online",
"reason": "审核通过后上架"
}
```
更新上下架时间、配置版本并写操作日志。
## 9. 内容审核
### 9.1 `GET /content-reviews`
Query 参数:
- `object_type`:必填,`feed_post` 或 `feed_comment`。
- `author_id`:作者 ID。
- `status`:业务状态。
- `keyword`:正文关键字。
- `created_from`、`created_to`。
- `page`、`per_page`。
列表项:
```json
{
"id": "20001",
"object_type": "feed_post",
"post_id": "",
"author_id": "30001",
"content_text": "正文",
"flower_code": "rose",
"visibility": "public",
"status": "reviewing",
"audit_status": "pending",
"reject_reason": "",
"like_count": 0,
"comment_count": 0,
"created_at": "2026-09-23T08:30:00Z",
"updated_at": "2026-09-23T08:30:00Z"
}
```
按 `id DESC` 排序。
### 9.2 `GET /content-reviews/{object_type}/{id}`
详情在列表项基础上增加:
- `author_nickname`
- `author_avatar_file_id`
- `media[]`:`id`、`file_id`、`thumbnail_file_id`、`media_type`、`audit_status`、`sort_no`
- `audits[]`:`id`、`audit_source`、`status_before`、`status_after`、`result_code`、`risk_labels`、`operator_id`、`occurred_at`
- `related_reports[]`:关联举报摘要
用户摘要通过 IM 内部 API 查询。用户服务临时不可用时,允许昵称和头像为空,但内容主体仍应返回。
### 9.3 `POST /content-reviews/{object_type}/{id}/actions`
```json
{
"action": "approve",
"reason": ""
}
```
动作枚举:
- `approve`:审核通过。
- `reject`:审核拒绝,必须填写原因。
- `take_down`:下架,必须填写原因。
- `restore`:恢复内容。
要求:
- 使用行锁或等价并发控制。
- 校验合法的前置状态。
- 更新内容状态和审核字段。
- 评论首次通过时只能增加一次动态评论数。
- 下架动态时应按现有业务规则同步处理其可见性或关联评论。
- 每次操作写 `cv_content_audit_log` 和 `cv_admin_operation_log`。
- 事务提交后向作者发送系统通知。
- 重复请求不能重复计数或重复产生治理副作用。
### 9.4 `POST /content-reviews/batch-actions`
```json
{
"object_type": "feed_post",
"ids": ["20001", "20002"],
"action": "approve",
"reason": ""
}
```
- 单次 1 至 100 条。
- 每条独立处理,单条失败不回滚其他成功项。
- 返回:
```json
{
"success_count": 1,
"failed_count": 1,
"failures": {
"20002": "当前状态不允许审核通过"
}
}
```
## 10. 举报管理
### 10.1 `GET /reports`
Query 参数:
- `object_type`
- `status`
- `reporter_id`
- `created_from`、`created_to`
- `page`、`per_page`
列表项:
```json
{
"id": "40001",
"reporter_id": "30001",
"reported_user_id": "30002",
"object_type": "feed_post",
"object_id": "20001",
"reason_code": "spam",
"description": "疑似广告",
"evidence_file_ids": ["file_xxx"],
"source_page": "feed_detail",
"status": "submitted",
"handler_id": "",
"handle_result": "",
"handled_at": null,
"created_at": "2026-09-23T08:30:00Z"
}
```
### 10.2 `GET /reports/{id}`
详情增加:
- `reporter_nickname`
- `reported_user_nickname`
- `target_content`
- `history[]`:同一对象的历史举报
不得向被举报方的通知或普通业务接口泄露举报人身份和证据。
### 10.3 `POST /reports/{id}/actions`
```json
{
"action": "resolve",
"handle_result": "举报成立,内容已下架",
"content_action": "take_down",
"user_action": "none",
"reason": "违规广告"
}
```
枚举:
- `action`:`processing`、`resolve`、`reject`。
- `content_action`:`none`、`take_down`。
- `user_action`:`none`、`freeze`、`ban`。
校验规则:
- `processing` 不要求处理结果。
- `resolve`、`reject` 必须填写 `handle_result`。
- 只有 `resolve` 可以联动内容和用户处置。
- 发生联动处置时必须填写 `reason`。
- 报告状态更新、内容处置和操作日志应处于同一事务。
- 用户冻结或封禁通过 IM 内部治理 API 执行;失败时不得将举报错误标记为已完成。
- 完成后分别通知举报方和被举报方,被举报方通知不得携带举报人信息。
## 11. 操作日志
### 11.1 `GET /operation-logs`
Query 参数:
- `operator_id`
- `operation_type`
- `target_type`
- `target_id`
- `request_id`
- `created_from`、`created_to`
- `page`、`per_page`
列表项:
```json
{
"id": "50001",
"operator_id": "14",
"operation_type": "content.approve",
"target_type": "feed_post",
"target_id": "20001",
"request_id": "req_xxx",
"before_snapshot": {},
"after_snapshot": {},
"reason": "",
"created_at": "2026-09-23T08:30:00Z"
}
```
日志只读,不提供删除接口。快照不得保存 Token、手机号、数据库连接、完整举报证据或私有文件 URL。
## 12. 状态和枚举
| 类型 | 枚举 |
| --- | --- |
| 内容类型 | `feed_post`、`feed_comment` |
| 花种状态 | `online`、`offline`、`activity_only`、`maintenance` |
| 成长阶段 | `seed`、`sprout`、`bud`、`bloom` |
| 成长动作 | `water`、`fertilize` |
| 可见范围 | `public`、`friends`、`private` |
| 内容状态 | `reviewing`、`published`、`rejected`、`taken_down` |
| 审核动作 | `approve`、`reject`、`take_down`、`restore` |
| 举报状态 | `submitted`、`processing`、`resolved`、`rejected` |
| 举报动作 | `processing`、`resolve`、`reject` |
| 用户处置 | `none`、`freeze`、`ban` |
如果现有数据库已经使用其他稳定枚举,开发前应与 Manager 对齐,不允许接口层静默转换出第三套状态。
## 13. 错误码建议
| HTTP | reason | 场景 |
| --- | --- | --- |
| 400 | `INVALID_ARGUMENT` | 参数或枚举错误 |
| 401 | `INTERNAL_AUTH_REQUIRED` | 缺少内部签名 |
| 403 | `INTERNAL_AUTH_FORBIDDEN` | 签名错误、服务未授权 |
| 404 | `TENANT_NOT_FOUND` | 商户不存在 |
| 404 | `RESOURCE_NOT_FOUND` | 花种、内容或举报不存在 |
| 409 | `STATE_CONFLICT` | 当前状态不允许执行动作 |
| 409 | `DUPLICATE_FLOWER_CODE` | 商户内花种编码重复 |
| 422 | `ASSET_REVIEW_PENDING` | 素材仍在审核中 |
| 422 | `ASSET_REVIEW_REJECTED` | 素材审核未通过 |
| 429 | `RATE_LIMITED` | 调用频率过高 |
| 503 | `DEPENDENCY_UNAVAILABLE` | IM 用户、文件、通知或治理服务不可用 |
| 500 | `INTERNAL_ERROR` | 未知内部错误 |
响应不得返回 SQL、数据库地址、堆栈或 Secret。
## 14. 数据一致性与安全要求
1. 每条 SQL 都必须显式过滤 `tenant_id`。
2. 更新语句同时按主键和 `tenant_id` 定位。
3. 写接口记录签名后的 `X-Operator-ID` 和 `X-Request-ID`。
4. 审核、举报处置和计数变更必须考虑重试幂等。
5. 批量操作最多 100 条,避免长事务。
6. 私有媒体只返回 `file_id`,访问地址由受控文件接口临时签发。
7. 关键操作日志永久保留;普通查询对已注销用户做匿名化。
8. 内部接口不可复用 C 端 Token 鉴权,必须单独使用服务间 HMAC。
9. 内部 API 路由应在普通 C 端认证中间件之前单独注册,不能误走 Redis 用户会话认证。
## 15. Cultivate Server 建议目录
按现有分层新增,禁止在 controller 中写数据库逻辑:
```text
api/internal/admin/
cultivate.go
internal/controller/admin/
cultivate.go
internal/service/
admin_cultivate.go
internal/logic/admin/
dashboard.go
flower.go
content_review.go
report.go
operation_log.go
internal/middleware/
internal_admin_auth.go
internal/model/
admin_cultivate.go
```
路由建议独立注册:
```text
/internal/admin/cultivate/*
```
不要将管理接口混入 `/v1/cultivate/*` C 端路由。
## 16. Manager 后续改造
Cultivate Server 接口完成后,Manager 侧将执行:
1. 新增 Cultivate 内部 API Client。
2. 将现有页面 API 转发到本文定义的内部接口。
3. 删除 Manager 内的 Cultivate 直接数据库逻辑。
4. 删除 Cultivate DAO、DO、Entity 等仅为直连数据库生成的代码。
5. 删除配置模板中的 `database.cultivate` 和 `CULTIVATE_MYSQL_DSN`。
6. 保持现有后台页面路径和前端交互不变。
服务地址建议支持配置,但 Kubernetes 环境可提供集群内默认值:
```text
http://cultivate-server.cultivate-system.svc.cluster.local:8080
```
该地址不是数据库配置。密钥复用双方已有内部 HMAC Secret,不增加 Cultivate 数据库 Secret。
## 17. 测试要求
### 17.1 单元测试
- HMAC 正确、错误、过期和 Nonce 重放。
- 商户编码与 ID 不匹配。
- 所有列表过滤和分页。
- 花种阶段链缺失、重复、成环和合法场景。
- 内容每种状态动作的合法与非法转换。
- 重复审核不重复增加评论数。
- 举报各种状态和联动处置。
- 跨商户 ID 查询和更新均返回不存在。
### 17.2 集成测试
- 使用真实 Cultivate 测试库验证全部接口。
- 模拟 IM 用户、文件、通知和治理内部接口。
- 验证事务失败不会留下半完成数据。
- 验证操作日志中的商户、管理员和请求 ID 正确。
- 验证接口响应不包含数据库信息和堆栈。
### 17.3 联调验收
1. Manager 不设置 `CULTIVATE_MYSQL_DSN` 可以正常启动。
2. Manager 登录、IM 管理和工作台管理均正常。
3. 养成概览、花种、内容审核、举报和操作日志页面可正常查询。
4. 花种新增、编辑、上下架生效。
5. 内容审核和举报处置生效,通知与审计记录正确。
6. 停止 Cultivate Server 后,Manager 仍保持健康,仅养成页面提示“养成服务暂不可用”。
7. Manager Pod 环境变量和 Secret 中不存在 Cultivate 数据库 DSN。
## 18. 交付清单
Cultivate Server 开发完成后需提供:
- 接口代码及单元测试。
- OpenAPI 或等价接口定义。
- 内部 HMAC 鉴权说明。
- 测试环境服务地址。
- 联调使用的服务名和 Secret 键名,不提供明文 Secret。
- 数据迁移说明,如无迁移需明确说明。
- 一份 curl 签名测试脚本或 Go 测试客户端。
上述内容齐备后,再开始 Manager 侧切换和删除直连代码。