feat: add forum and token store service skeletons

This commit is contained in:
Losita
2026-05-04 18:33:09 +08:00
parent 9742dc8b1c
commit 786c8925a0
25 changed files with 5344 additions and 203 deletions

View File

@@ -1,203 +0,0 @@
# 学习计划论坛与 Token 商店 PRD
## 1. 文档定位
本文只记录当前需要讨论和快速推进的核心产品口径,不展开完整交互稿、运营后台和支付细节。
本轮目标是新增两个终态服务模块:
1. `taskclass-forum`:支持用户分享 TaskClass 学习计划,并让其他用户一键导入。
2. `token-store`:支持 Token 商品购买、活动奖励和发放账本。
两个模块后续都放在 `backend/services` 下,以独立服务为目标设计;当前仓库工作区未干净前只讨论 PRD不进入代码实现。
## 2. 背景与目标
当前产品已经具备用户自建 TaskClass、智能排程和 Token 额度门禁能力。下一阶段希望补上社区化与商业化闭环:
1. 用户可以把自己的复习计划分享出去,形成可浏览、可点赞、可评论、可复用的学习计划论坛。
2. 其他用户可以一键导入计划模板,快速生成自己的 TaskClass。
3. 被点赞、被导入等社区行为可以转化为 Token 激励。
4. 用户可以通过 Token 商店购买或领取 Token为后续高频 Agent 使用建立基础商业闭环。
## 3. 模块一:学习计划论坛
### 3.1 产品定位
学习计划论坛不是普通帖子论坛,而是“帖子 + TaskClass 模板快照”的社区。
用户发布时,系统从用户自己的 TaskClass 复制一份模板快照。其他用户导入时,再从快照生成自己的 TaskClass 副本。
快照原则:
1. 发布后不直接引用原作者的 `task_classes` / `task_items`
2. 原作者后续修改自己的计划,不影响已发布模板。
3. 导入用户拿到的是自己的 TaskClass 副本,后续可自由编辑。
4. 不分享 `embedded_time`、schedule 绑定、用户私有排程状态。
### 3.2 P0 功能
1. 发布学习计划:用户选择一个 TaskClass填写标题、简介、标签后发布。
2. 浏览列表:支持分页查看公开计划,按最新、点赞数、导入数排序。
3. 查看详情展示计划说明、TaskClass 配置摘要和任务条目预览。
4. 点赞:同一用户对同一帖子只能点赞一次,可取消点赞。
5. 评论支持基础评论列表和发表评论P0 不做楼中楼。
6. 一键导入:从论坛模板复制出当前用户自己的 TaskClass。
7. 基础激励:模板获得点赞或导入后,可触发 Token 奖励事件。
### 3.3 P0 不做
1. 不做复杂推荐算法。
2. 不做关注、私信、用户主页。
3. 不做富文本编辑器,先用纯文本简介。
4. 不做审核后台,先预留状态字段。
5. 不直接把模板应用进 schedule导入后由用户走现有 TaskClass / 排程链路。
### 3.4 核心实体
1. `forum_posts`:帖子主体,记录作者、标题、简介、状态、点赞数、评论数、导入数。
2. `forum_post_templates`TaskClass 快照,记录模式、日期范围、策略、约束配置等。
3. `forum_post_template_items`TaskClassItem 快照,只记录 order/content 等模板信息。
4. `forum_likes`:点赞幂等记录。
5. `forum_comments`:评论记录。
6. `forum_imports`:导入记录,记录从哪个帖子导入到哪个用户和新 TaskClass ID。
### 3.5 关键流程
发布流程:
1. 用户选择自己的 TaskClass。
2. `taskclass-forum` 通过 TaskClass 读取端口拿到完整模板。
3. 服务过滤私有字段,生成论坛快照。
4. 写入帖子和模板快照。
导入流程:
1. 用户点击一键导入。
2. `taskclass-forum` 读取帖子模板快照。
3. 通过 TaskClass 写入端口为当前用户创建 TaskClass 副本。
4. 写入导入记录并增加导入计数。
5. 可异步发布 Token 奖励事件。
## 4. 模块二Token 商店
### 4.1 产品定位
Token 商店负责 Token 的购买、奖励、发放和账本,不负责登录鉴权,也不直接承载 Agent 消耗统计。
`user/auth` 继续负责用户 Token quota 的权威判断;`token-store` 只负责产生“发放 Token”的业务事实并通过跨服务契约通知 `user/auth` 增加用户额度。
### 4.2 P0 功能
1. 商品列表:展示可购买 Token 包。
2. 创建订单:用户选择商品生成订单。
3. 支付确认P0 先支持 mock paid 或管理端确认 paid不接真实支付网关。
4. Token 发放:订单支付成功后发放 Token。
5. 奖励发放:支持论坛点赞、导入等事件触发奖励。
6. 发放账本:所有发放必须有幂等 event_id避免重复加额度。
### 4.3 P0 不做
1. 不接真实微信 / 支付宝 / Stripe。
2. 不做退款、发票、优惠券。
3. 不做复杂会员体系。
4. 不直接改 `users.token_usage`,避免和消费统计混淆。
### 4.4 核心实体
1. `token_products`Token 商品。
2. `token_orders`:订单。
3. `token_grants`Token 发放账本,记录购买、奖励、补偿等来源。
4. `token_reward_rules`奖励规则P0 可先用配置或简单表。
### 4.5 关键流程
购买流程:
1. 用户选择商品并创建订单。
2. 订单进入 `pending`
3. P0 通过 mock paid 或管理端确认,把订单置为 `paid`
4. `token-store` 写入 token grant 账本。
5. `token-store` 调用 `user/auth` 的额度发放能力。
6. 发放成功后订单进入 `granted`
奖励流程:
1. 论坛产生点赞或导入事件。
2. `token-store` 按奖励规则判断是否发放。
3. 写入 token grant 账本。
4. 调用 `user/auth` 增加额度。
## 5. 服务边界
### 5.1 `taskclass-forum`
负责:
1. 论坛帖子、点赞、评论、导入记录。
2. TaskClass 模板快照。
3. 导入时的模板复制编排。
4. 发布社区行为事件,供 Token 激励消费。
不负责:
1. TaskClass 原始表所有权。
2. schedule 写入和排程应用。
3. Token 额度发放。
4. 用户登录鉴权。
### 5.2 `token-store`
负责:
1. 商品、订单、发放账本。
2. 社区奖励规则。
3. 幂等发放。
4. 调用 `user/auth` 增加 Token 额度。
不负责:
1. JWT、登录、注册。
2. Agent 消耗统计。
3. TaskClass 论坛内容。
4. 真实第三方支付回调P0 只预留状态机。
### 5.3 与现有服务关系
1. 论坛读取和导入 TaskClass 时,先通过端口适配旧 `TaskClassService/DAO`
2. 后续 `task-class` 独立成服务后,只替换端口适配器。
3. 论坛 P0 不直接写 schedule避免被 `schedule` 未拆服务影响。
4. Token 商店不直接改 users 表,通过 `user/auth` 契约发放额度。
## 6. 事件与激励
P0 建议事件:
1. `forum.post.liked`:帖子被点赞。
2. `forum.post.imported`:帖子被导入。
3. `token.grant.requested`:请求发放 Token。
4. `token.grant.completed`Token 发放完成。
奖励口径先从简单规则开始:
1. 每个帖子每个用户首次点赞只奖励一次。
2. 每个帖子每个用户首次导入只奖励一次。
3. 同一 event_id 的 Token 发放必须幂等。
4. 奖励额度先走配置,不在 PRD 阶段定死。
## 7. 当前推进策略
1. 当前工作区存在其它拆服务改动,本阶段只提交 PRD。
2. 等工作区干净后,从集成分支新开功能分支或单独 git worktree。
3. 实现时两个服务主体可以并行推进。
4. `gateway/router``shared/contracts``shared/ports``outbox route``config` 由主代理统一收口。
5. 先做 P0 闭环,再扩展审核、真实支付和推荐排序。
## 8. 待讨论问题
1. 论坛展示名使用“学习计划论坛”“计划广场”还是“模板市场”。
2. 点赞奖励是否给作者、点赞者,还是双方都给。
3. 导入奖励是否需要上限,避免刷导入。
4. 评论是否需要删除、举报、审核状态。
5. Token 发放应增加 `user/auth``GrantTokenQuota`,还是命名为 `AdjustTokenLimit`
6. P0 是否需要前端先隐藏评论,只保留后端能力。

View File

@@ -0,0 +1,816 @@
# 计划广场与 Token 商店后端实施方案
## 1. 文档定位
本文记录当前需要讨论和快速推进的核心产品口径、前后端接口契约、服务边界、事件口径和实施计划,不展开完整交互稿、运营后台和真实支付细节。
本轮目标是新增两个终态服务模块:
1. `taskclass-forum`:支持用户分享 TaskClass 学习计划,并让其他用户一键导入。
2. `token-store`:支持 Token 商品购买、活动奖励和发放账本。
两个模块后续都放在 `backend/services` 下,以独立服务为目标设计;当前仓库工作区未干净前只讨论方案,不进入代码实现。
## 2. 背景与目标
当前产品已经具备用户自建 TaskClass、智能排程和 Token 额度门禁能力。下一阶段希望补上社区化与商业化闭环:
1. 用户可以把自己的复习计划分享出去,形成可浏览、可点赞、可评论、可复用的计划广场。
2. 其他用户可以一键导入计划模板,快速生成自己的 TaskClass。
3. 被点赞、被导入等社区行为可以转化为 Token 激励。
4. 用户可以通过 Token 商店购买或领取 Token为后续高频 Agent 使用建立基础商业闭环。
## 3. 模块一:学习计划论坛
### 3.1 产品定位
计划广场不是普通帖子论坛,而是“帖子 + TaskClass 模板快照”的社区。
对外展示名先使用“计划广场”。
用户发布时,系统从用户自己的 TaskClass 复制一份模板快照。其他用户导入时,再从快照生成自己的 TaskClass 副本。
快照原则:
1. 发布后不直接引用原作者的 `task_classes` / `task_items`
2. 原作者后续修改自己的计划,不影响已发布模板。
3. 导入用户拿到的是自己的 TaskClass 副本,后续可自由编辑。
4. 不分享 `embedded_time`、schedule 绑定、用户私有排程状态。
### 3.2 P0 功能
1. 发布学习计划:用户选择一个 TaskClass填写标题、简介、标签后发布。
2. 浏览列表:支持分页查看公开计划,按最新、点赞数、导入数排序。
3. 查看详情展示计划说明、TaskClass 配置摘要和任务条目预览。
4. 点赞:同一用户对同一帖子只能点赞一次,可取消点赞。
5. 评论:支持发表评论、多层回复和删除自己的评论,接口返回评论树 JSON。
6. 一键导入:从论坛模板复制出当前用户自己的 TaskClass。
7. 基础激励:模板获得点赞或导入后,可触发 Token 奖励事件。
### 3.3 P0 不做
1. 不做复杂推荐算法。
2. 不做关注、私信、用户主页。
3. 不做富文本编辑器,先用纯文本简介。
4. 不做审核后台和管理员删评,先预留状态字段。
5. 不直接把模板应用进 schedule导入后由用户走现有 TaskClass / 排程链路。
6. P0 暂不做评论举报和管理员审核流。
### 3.4 核心实体
1. `forum_posts`:帖子主体,记录作者、标题、简介、状态、点赞数、评论数、导入数。
2. `forum_post_templates`TaskClass 快照,记录模式、日期范围、策略、约束配置等。
3. `forum_post_template_items`TaskClassItem 快照,只记录 order/content 等模板信息。
4. `forum_likes`:点赞幂等记录。
5. `forum_comments`:评论记录,使用 `parent_comment_id` 表达多层回复关系。
6. `forum_imports`:导入记录,记录从哪个帖子导入到哪个用户和新 TaskClass ID。
### 3.5 关键流程
发布流程:
1. 用户选择自己的 TaskClass。
2. `taskclass-forum` 通过 TaskClass 读取端口拿到完整模板。
3. 服务过滤私有字段,生成论坛快照。
4. 写入帖子和模板快照。
评论流程:
1. 用户可以直接评论帖子,也可以回复任意一条评论。
2. 评论表用 `parent_comment_id` 记录父评论,根评论的 `parent_comment_id` 为空。
3. 列表查询先按帖子读取扁平评论,再由服务层组装成多层评论树。
4. 删除评论时 P0 只允许用户删除自己的评论,并采用软删除,保留子回复结构,避免整棵回复树断链。
5. P0 暂不引入管理员删评、举报和审核流,后续如需治理再单独扩展。
导入流程:
1. 用户点击一键导入。
2. `taskclass-forum` 读取帖子模板快照。
3. 通过 TaskClass 写入端口为当前用户创建 TaskClass 副本。
4. 写入导入记录并增加导入计数。
5. 可异步发布 Token 奖励事件。
## 4. 模块二Token 商店
### 4.1 产品定位
Token 商店负责 Token 的购买、奖励、发放和账本,不负责登录鉴权,也不直接承载 Agent 消耗统计。
`user/auth` 继续负责用户 Token quota 的权威判断;`token-store` 只负责产生“获取 Token / 发放 Token”的业务事实和账本。本轮不新增或修改 `user/auth` 契约,先在 `token-store` 内封装 Token 获取途径和后续发放出口,等主线合并稳定后再切到 `user/auth` 的权威额度发放能力。
### 4.2 P0 功能
1. 商品列表:展示可购买 Token 包。
2. 创建订单:用户选择商品生成订单。
3. 支付确认P0 先支持 mock paid 或管理端确认 paid不接真实支付网关。
4. Token 发放记录:订单支付成功后写入发放账本,并通过本服务内部端口封装后续发放出口。
5. 奖励发放:支持论坛点赞、导入等事件触发奖励。
6. 发放账本:所有发放必须有幂等 event_id避免后续重复加额度。
### 4.3 P0 不做
1. 不接真实微信 / 支付宝 / Stripe。
2. 不做退款、发票、优惠券。
3. 不做复杂会员体系。
4. 不直接改 `users.token_usage`,避免和消费统计混淆。
### 4.4 核心实体
1. `token_products`Token 商品P0 从表读取,并通过 seed 初始化 2-3 个商品,不做管理后台。
2. `token_orders`:订单。
3. `token_grants`Token 发放账本,记录购买、奖励、补偿等来源。
4. `token_reward_rules`奖励规则P0 可先用配置或简单表。
### 4.5 关键流程
购买流程:
1. 用户选择商品并创建订单。
2. 订单进入 `pending`
3. P0 通过 mock paid 或管理端确认,把订单置为 `paid`
4. `token-store` 写入 token grant 账本。
5. `token-store` 通过内部发放端口记录本次 Token 获取事实,本轮不修改 `user/auth`
6. 发放记录写入成功后订单进入 `granted`;后续切到 `user/auth` 时只替换发放端口实现。
奖励流程:
1. 论坛产生点赞或导入事件。
2. `token-store` 按奖励规则判断是否发放。
3. 写入 token grant 账本。
4. 通过内部发放端口记录奖励获取事实;后续切到 `user/auth` 时只替换发放端口实现。
## 5. 服务边界
### 5.1 `taskclass-forum`
负责:
1. 论坛帖子、点赞、评论、导入记录。
2. TaskClass 模板快照。
3. 导入时的模板复制编排。
4. 发布社区行为事件,供 Token 激励消费。
不负责:
1. TaskClass 原始表所有权。
2. schedule 写入和排程应用。
3. Token 额度发放。
4. 用户登录鉴权。
### 5.2 `token-store`
负责:
1. 商品、订单、发放账本。
2. 社区奖励规则。
3. 幂等发放。
4. 封装 Token 获取途径和后续发放出口。
不负责:
1. JWT、登录、注册。
2. Agent 消耗统计。
3. TaskClass 论坛内容。
4. 真实第三方支付回调P0 只预留状态机。
### 5.3 与现有服务关系
1. 论坛读取和导入 TaskClass 时,先通过端口适配旧 `TaskClassService/DAO`
2. 后续 `task-class` 独立成服务后,只替换端口适配器。
3. 论坛 P0 不直接写 schedule避免被 `schedule` 未拆服务影响。
4. Token 商店本轮不直接改 users 表,也不新增或修改 `user/auth` 契约;先封装自己的 Token 获取途径和后续发放出口,等合并后再切到 `user/auth`
### 5.4 并行迁移细则
1. 当前 `task` / `task-class` 还没有完成服务迁移时,本轮不改它们的核心模块,也不提前给它们补新的 RPC 边界,避免和另一条拆分线发生合并冲突。
2. `taskclass-forum` 的实现层可以先通过 legacy adapter 复用现有 DAO / Service必要时也可以直接读旧表但这类直连只能收敛在 adapter 内,不能扩散到论坛业务层。
3. 论坛业务层只依赖“读取 TaskClass 快照”和“写入 TaskClass 副本”这类端口,不关心底层是 DAO、Service 还是后续 RPC。
4.`task-class` 侧完成独立服务后,论坛只替换 adapter不回改论坛领域逻辑不重写帖子、点赞、评论和导入编排。
5. 这轮优先保证论坛和 Token 商店自己的边界稳定,`task` 相关能力保持当前状态即可,等主线合并后再按统一节奏推进下一步迁移。
### 5.5 理想内部结构
参考 `userauth` 样板和微服务迁移总纲,两个新模块都按“`cmd` 进程入口 + `gateway/api` HTTP 适配 + `gateway/client` RPC client + `services` 服务主体”的结构推进。
命名约定:
1. 产品和事件域继续使用 `taskclass-forum``token-store`方便和方案、topic、事件名保持一致。
2. Go 目录和包名建议使用 `taskclassforum``tokenstore`,对齐当前 `userauth` 样板,避免包名里出现连字符。
3. 如果后续统一目录规范改成带连字符目录,也只调整目录名和 import不改变服务内职责分层。
推荐目标树:
这里是两套服务边缘入口:
1. `gateway/api` 下放 HTTP API 入口,每个服务一个子目录。
2. `gateway/client` 下放 zrpc client / error 反解,每个服务一个子目录。
3.`gateway/userapi``gateway/userauth` 暂不在本轮调整,后续合并时再统一处理。
```text
backend/
├── cmd/
│ ├── taskclassforum/
│ │ └── main.go
│ └── tokenstore/
│ └── main.go
├── gateway/
│ ├── api/
│ │ ├── taskclassforum/
│ │ │ ├── handler.go
│ │ │ └── routes.go
│ │ └── tokenstore/
│ │ ├── handler.go
│ │ └── routes.go
│ └── client/
│ ├── taskclassforum/
│ │ ├── client.go
│ │ └── errors.go
│ └── tokenstore/
│ ├── client.go
│ └── errors.go
├── services/
│ ├── taskclassforum/
│ │ ├── sv/
│ │ │ ├── service.go
│ │ │ ├── post.go
│ │ │ ├── like.go
│ │ │ ├── comment.go
│ │ │ └── import.go
│ │ ├── dao/
│ │ │ ├── connect.go
│ │ │ ├── post.go
│ │ │ ├── template.go
│ │ │ ├── like.go
│ │ │ ├── comment.go
│ │ │ └── import.go
│ │ ├── model/
│ │ │ ├── forum_post.go
│ │ │ ├── forum_post_template.go
│ │ │ ├── forum_post_template_item.go
│ │ │ ├── forum_like.go
│ │ │ ├── forum_comment.go
│ │ │ └── forum_import.go
│ │ ├── internal/
│ │ │ ├── adapter/
│ │ │ ├── snapshot/
│ │ │ ├── commenttree/
│ │ │ └── event/
│ │ └── rpc/
│ │ ├── pb/
│ │ ├── taskclassforum.proto
│ │ ├── errors.go
│ │ ├── handler.go
│ │ └── server.go
│ └── tokenstore/
│ ├── sv/
│ │ ├── service.go
│ │ ├── product.go
│ │ ├── order.go
│ │ ├── grant.go
│ │ └── reward.go
│ ├── dao/
│ │ ├── connect.go
│ │ ├── product.go
│ │ ├── order.go
│ │ ├── grant.go
│ │ └── reward_rule.go
│ ├── model/
│ │ ├── token_product.go
│ │ ├── token_order.go
│ │ ├── token_grant.go
│ │ └── token_reward_rule.go
│ ├── internal/
│ │ ├── adapter/
│ │ ├── paymentmock/
│ │ ├── grant/
│ │ ├── reward/
│ │ └── event/
│ └── rpc/
│ ├── pb/
│ ├── tokenstore.proto
│ ├── errors.go
│ ├── handler.go
│ └── server.go
└── shared/
├── contracts/
│ ├── taskclassforum/
│ └── tokenstore/
├── events/
└── ports/
```
目录职责:
1. `cmd/<service>/main.go` 只负责读取配置、初始化资源、启动 go-zero zrpc 服务,不承载业务规则。
2. `gateway/api/<service>` 只负责 HTTP 参数绑定、鉴权后用户 ID 注入、调用对应 client、复用 `respond` 返回前端,不直接访问服务数据库。
3. `gateway/client/<service>` 只放 zrpc client 和错误反解;不要把 client 放进 `cmd`,也不要让 gateway 直接 import 服务端 DAO / model。
4. `services/<service>/sv` 是服务主业务编排层,负责事务顺序、幂等判断、状态流转和跨端口调用。
5. `services/<service>/dao` 只负责本服务数据库访问;`model` 只放本服务私有表模型,不放跨服务 DTO。
6. `services/<service>/internal` 只放服务私有子能力,外部服务禁止直接 import。
7. `services/<service>/rpc` 对齐 `userauth`,放 proto、server、handler、errors 和生成后的 `pb`
8. `shared/contracts``shared/events``shared/ports` 只放跨服务契约、事件 payload 和端口接口,不放 DAO、model、sv 或业务状态机。
`taskclassforum/internal` 建议职责:
1. `adapter/`:承载 TaskClass 端口实现。P0 先放 legacy adapter复用旧 DAO / Service 或临时直读旧表;后续替换为 task-class RPC adapter。
2. `snapshot/`:负责 TaskClass 原始数据到论坛模板快照的过滤、复制和字段白名单,避免分享 `embedded_time`、schedule 绑定和私有排程状态。
3. `commenttree/`:负责把 `forum_comments` 扁平记录组装成多层评论树,并处理软删除节点的展示兜底。
4. `event/`:负责组装 `forum.post.liked``forum.post.imported` 等领域事件 payload事件投递仍走全局 outbox 路由和服务 catalog。
`tokenstore/internal` 建议职责:
1. `adapter/`:承载后续 `user/auth` 额度发放适配点。P0 先封装 `token-store` 自己的 Token 获取途径和发放出口,不新增或修改 `user/auth` 契约,也禁止直接改 `users` 表。
2. `paymentmock/`:只处理 P0 mock paid 状态确认,不承载真实支付网关逻辑。
3. `grant/`:封装 `token_grants` 幂等账本写入、event_id 去重和 grant 状态流转。
4. `reward/`封装论坛点赞、导入等奖励规则判断P0 可先走配置或简单表。
5. `event/`:负责组装 `token.grant.requested``token.grant.completed` 等事件 payload后续真实异步化时复用服务级 outbox 基线。
## 6. 事件与激励
P0 建议事件:
1. `forum.post.liked`:帖子被点赞。
2. `forum.post.imported`:帖子被导入。
3. `token.grant.requested`:请求发放 Token。
4. `token.grant.completed`Token 发放完成。
奖励口径先从简单规则开始:
1. 点赞奖励先只给帖子作者,每个帖子每个用户首次点赞只奖励一次。
2. 同一用户对同一计划只允许导入一次,导入奖励随该次导入记录一次。
3. 同一 event_id 的 Token 发放必须幂等。
4. 奖励额度先走配置,不在本方案阶段定死。
Outbox 使用口径:
1. P0 用户可见主链路不强依赖 outbox发布、点赞、评论、导入、下单、mock paid、写 grant 账本都应在服务自己的同步事务里完成。
2. Outbox 用于异步副作用:论坛点赞 / 导入奖励事件、后续搜索索引同步、通知、排行榜刷新、运营统计,以及后续 `token-store``user/auth` 的权威额度同步。
3. 第一版如果为了赶进度先同步写奖励账本也必须保留事件名、payload 和 `event_id` 规则,后续切到 outbox 消费时不改业务语义。
4. 新服务后续接入服务级 outbox 时建议使用独立表、topic 和 consumer group不回退到共享 outbox。
## 7. 后端契约初版
本节把前端对接草案收束成后端 P0 契约。若后续实现时发现字段和现有模型存在细小出入,允许在保持语义不变的前提下微调字段名,但必须同步更新 `docs/frontend/计划广场与Token商店对接说明.md`
### 7.1 通用 HTTP 约定
1. 所有接口统一挂在 `/api/v1` 下。
2. P0 默认都需要登录态,`user_id` 由 gateway 从 JWT 注入,前端不传 `user_id`
3. 响应复用项目统一壳:`status``info``data`
4. 写接口优先支持 `X-Idempotency-Key`,避免前端重复点击造成重复发布、重复评论、重复导入或重复下单。
5. 时间字段统一返回 ISO 8601 字符串,带时区。
6. 分页统一使用 `page``page_size``total``has_more`
### 7.2 计划广场 HTTP 契约
| 功能 | 方法 | 路径 | 幂等 |
| --- | --- | --- | --- |
| 计划列表 | `GET` | `/api/v1/plan-square/posts` | 否 |
| 热门标签 | `GET` | `/api/v1/plan-square/tags` | 否 |
| 发布计划 | `POST` | `/api/v1/plan-square/posts` | 是 |
| 计划详情 | `GET` | `/api/v1/plan-square/posts/{post_id}` | 否 |
| 点赞 | `POST` | `/api/v1/plan-square/posts/{post_id}/like` | 由唯一约束兜底 |
| 取消点赞 | `DELETE` | `/api/v1/plan-square/posts/{post_id}/like` | 否 |
| 评论树 | `GET` | `/api/v1/plan-square/posts/{post_id}/comments` | 否 |
| 发表评论 / 回复 | `POST` | `/api/v1/plan-square/posts/{post_id}/comments` | 是 |
| 删除自己的评论 | `DELETE` | `/api/v1/plan-square/comments/{comment_id}` | 否 |
| 一键导入 | `POST` | `/api/v1/plan-square/posts/{post_id}/import` | 是 |
核心请求 DTO
```go
type CreateForumPostRequest struct {
TaskClassID uint64 `json:"task_class_id"`
Title string `json:"title"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
}
type CreateForumCommentRequest struct {
Content string `json:"content"`
ParentCommentID *uint64 `json:"parent_comment_id"`
}
type ImportForumPostRequest struct {
TargetTitle string `json:"target_title"`
}
```
核心响应 DTO
```go
type ForumPostBrief struct {
PostID uint64 `json:"post_id"`
Title string `json:"title"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
Author UserBrief `json:"author"`
TemplateSummary TemplateSummary `json:"template_summary"`
Counters ForumPostCounters `json:"counters"`
ViewerState ForumPostViewerState `json:"viewer_state"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
type ForumPostDetail struct {
Post ForumPostBrief `json:"post"`
Template TemplateDetail `json:"template"`
}
type ForumCommentNode struct {
CommentID uint64 `json:"comment_id"`
PostID uint64 `json:"post_id"`
ParentCommentID *uint64 `json:"parent_comment_id"`
Content string `json:"content"`
Status string `json:"status"`
Author UserBrief `json:"author"`
CanDelete bool `json:"can_delete"`
CreatedAt string `json:"created_at"`
DeletedAt *string `json:"deleted_at"`
Children []ForumCommentNode `json:"children"`
}
```
字段口径:
1. `ForumPostBrief.status` P0 先使用 `published`,后续审核可扩展 `hidden``deleted``pending_review`
2. `ForumCommentNode.status` P0 使用 `visible``deleted`
3. `CanDelete` 由后端根据当前用户判断,前端只按字段展示删除入口。
4. `TemplateDetail.items_preview` 来自论坛快照,不读取原作者当前 TaskClass。
5. 一键导入成功只返回新 TaskClass ID不直接写 schedule。
### 7.3 Token 商店 HTTP 契约
| 功能 | 方法 | 路径 | 幂等 |
| --- | --- | --- | --- |
| Token 概览 | `GET` | `/api/v1/token-store/summary` | 否 |
| 商品列表 | `GET` | `/api/v1/token-store/products` | 否 |
| 创建订单 | `POST` | `/api/v1/token-store/orders` | 是 |
| 订单列表 | `GET` | `/api/v1/token-store/orders` | 否 |
| 订单详情 | `GET` | `/api/v1/token-store/orders/{order_id}` | 否 |
| mock paid | `POST` | `/api/v1/token-store/orders/{order_id}/mock-paid` | 是 |
| Token 获取记录 | `GET` | `/api/v1/token-store/grants` | 否 |
核心请求 DTO
```go
type CreateTokenOrderRequest struct {
ProductID uint64 `json:"product_id"`
Quantity int `json:"quantity"`
}
type MockPaidOrderRequest struct {
MockChannel string `json:"mock_channel"`
}
```
核心响应 DTO
```go
type TokenSummary struct {
RecordedTokenTotal int64 `json:"recorded_token_total"`
AppliedTokenTotal int64 `json:"applied_token_total"`
PendingApplyTokenTotal int64 `json:"pending_apply_token_total"`
QuotaSyncStatus string `json:"quota_sync_status"`
Tip string `json:"tip"`
}
type TokenProductView struct {
ProductID uint64 `json:"product_id"`
Name string `json:"name"`
Description string `json:"description"`
TokenAmount int64 `json:"token_amount"`
PriceCent int64 `json:"price_cent"`
PriceText string `json:"price_text"`
Currency string `json:"currency"`
Badge string `json:"badge"`
Status string `json:"status"`
SortOrder int `json:"sort_order"`
}
type TokenOrderView struct {
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
Status string `json:"status"`
TokenAmount int64 `json:"token_amount"`
AmountCent int64 `json:"amount_cent"`
PriceText string `json:"price_text"`
Currency string `json:"currency"`
PaymentMode string `json:"payment_mode"`
Grant *TokenGrantView `json:"grant"`
CreatedAt string `json:"created_at"`
PaidAt *string `json:"paid_at"`
GrantedAt *string `json:"granted_at"`
}
type TokenGrantView struct {
GrantID uint64 `json:"grant_id"`
EventID string `json:"event_id"`
Source string `json:"source"`
SourceLabel string `json:"source_label"`
Amount int64 `json:"amount"`
Status string `json:"status"`
QuotaApplied bool `json:"quota_applied"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
}
```
字段口径:
1. `TokenSummary.quota_sync_status` P0 返回 `not_connected`,后续可扩展 `partial``synced`
2. `TokenOrderView.status` 使用 `pending``paid``granted``closed`
3. `TokenGrantView.status` 使用 `recorded``applied``skipped``failed`
4. `quota_applied` P0 默认为 `false`,因为本轮不改 `user/auth`
5. 前端 P0 展示“累计获取 Token”不要展示为权威可用额度。
### 7.4 RPC 服务契约
RPC 只承载 gateway 到服务主体的跨进程调用,不暴露给前端。字段可以按 proto 生成规则转成 snake_case JSON但语义必须和 HTTP DTO 保持一致。
`taskclassforum` P0 方法:
```protobuf
service TaskClassForumService {
rpc ListPosts(ListForumPostsRequest) returns (ListForumPostsResponse);
rpc ListTags(ListForumTagsRequest) returns (ListForumTagsResponse);
rpc CreatePost(CreateForumPostRPCRequest) returns (CreateForumPostRPCResponse);
rpc GetPost(GetForumPostRequest) returns (GetForumPostResponse);
rpc LikePost(LikeForumPostRequest) returns (LikeForumPostResponse);
rpc UnlikePost(UnlikeForumPostRequest) returns (UnlikeForumPostResponse);
rpc ListComments(ListForumCommentsRequest) returns (ListForumCommentsResponse);
rpc CreateComment(CreateForumCommentRPCRequest) returns (CreateForumCommentRPCResponse);
rpc DeleteComment(DeleteForumCommentRequest) returns (DeleteForumCommentResponse);
rpc ImportPost(ImportForumPostRPCRequest) returns (ImportForumPostRPCResponse);
}
```
`tokenstore` P0 方法:
```protobuf
service TokenStoreService {
rpc GetSummary(GetTokenSummaryRequest) returns (GetTokenSummaryResponse);
rpc ListProducts(ListTokenProductsRequest) returns (ListTokenProductsResponse);
rpc CreateOrder(CreateTokenOrderRPCRequest) returns (CreateTokenOrderRPCResponse);
rpc ListOrders(ListTokenOrdersRequest) returns (ListTokenOrdersResponse);
rpc GetOrder(GetTokenOrderRequest) returns (GetTokenOrderResponse);
rpc MockPaidOrder(MockPaidOrderRequest) returns (MockPaidOrderResponse);
rpc ListGrants(ListTokenGrantsRequest) returns (ListTokenGrantsResponse);
}
```
RPC 入参通用规则:
1. 所有需要用户态的方法都带 `actor_user_id`,由 gateway 注入。
2. 写方法带 `idempotency_key`,由 gateway 从 `X-Idempotency-Key` 读取。
3. 列表方法带 `page``page_size`、筛选字段和排序字段。
4. RPC 层错误使用 go-zero / gRPC `error` 返回gateway client 负责转成项目统一响应。
### 7.5 服务内部端口契约
`taskclassforum` 依赖 TaskClass 端口:
```go
type TaskClassSnapshotPort interface {
GetOwnedTaskClassSnapshot(ctx context.Context, userID uint64, taskClassID uint64) (*TaskClassSnapshot, error)
CreateTaskClassFromSnapshot(ctx context.Context, userID uint64, snapshot TaskClassSnapshot, targetTitle string) (*CreatedTaskClass, error)
}
```
端口口径:
1. `GetOwnedTaskClassSnapshot` 只能读取当前用户自己的 TaskClass。
2. `TaskClassSnapshot` 必须剔除 `embedded_time`、schedule 绑定和用户私有排程状态。
3. P0 实现为 legacy adapter可复用旧 DAO / Service 或收敛式直读旧表。
4. 后续 `task-class` 服务拆出后,只替换 adapter。
`tokenstore` 内部发放端口:
```go
type TokenGrantOutlet interface {
RecordAcquisition(ctx context.Context, grant TokenGrantRecord) error
}
```
端口口径:
1. P0 只记录 `token-store` 自己的获取事实和账本,不新增或修改 `user/auth` 契约。
2. 禁止直接修改 `users` 表。
3. 后续切到 `user/auth` 时新增 adapter 实现,服务编排层不重写。
### 7.6 事件契约
P0 事件按当前服务级 outbox 思路设计,但不要求主链路第一版强依赖异步投递;如果第一版先同步写账本,也必须按下面 event_id 生成规则保留幂等口径。
`forum.post.liked`
```json
{
"event_id": "forum.post.liked:{post_id}:{actor_user_id}",
"post_id": 10001,
"author_user_id": 88,
"actor_user_id": 91,
"reward_receiver_user_id": 88,
"reward_amount": 1,
"occurred_at": "2026-05-04T21:05:00+08:00"
}
```
`forum.post.imported`
```json
{
"event_id": "forum.post.imported:{post_id}:{actor_user_id}",
"post_id": 10001,
"import_id": 70001,
"author_user_id": 88,
"actor_user_id": 91,
"new_task_class_id": 430,
"reward_receiver_user_id": 88,
"reward_amount": 2,
"occurred_at": "2026-05-04T21:10:00+08:00"
}
```
`token.grant.requested`
```json
{
"event_id": "order:80001:paid",
"user_id": 91,
"source": "purchase",
"amount": 100,
"description": "购买基础 Token 包",
"occurred_at": "2026-05-04T21:00:00+08:00"
}
```
`token.grant.completed`
```json
{
"event_id": "order:80001:paid",
"grant_id": 90001,
"user_id": 91,
"source": "purchase",
"amount": 100,
"status": "recorded",
"quota_applied": false,
"occurred_at": "2026-05-04T21:00:01+08:00"
}
```
事件口径:
1. 点赞奖励只给作者,取消点赞不回滚已记录奖励。
2. 同一用户对同一计划只允许导入一次,导入奖励随该次导入记录一次。
3. Token 发放 P0 的 `quota_applied``false`,后续接入 `user/auth` 后再变成真实同步状态。
4. 事件 payload 放 `backend/shared/events`;服务私有处理逻辑留在各自 `internal/event``sv`
5. 事件不要阻塞用户主流程;异步消费失败时依赖 `event_id` 和 grant 账本幂等重试。
### 7.7 幂等与唯一约束
论坛侧:
1. `forum_likes``(post_id, user_id)` 建唯一约束。
2. `forum_imports``(post_id, user_id)` 建唯一约束,同一用户同一计划只允许导入一次,并保留 `event_id`;奖励去重依赖 `token_grants.event_id`
3. 评论创建支持 `X-Idempotency-Key`,避免重复提交。
4. 删除评论是软删除,重复删除同一条自己的评论返回成功态。
Token 侧:
1. `token_orders.order_no` 唯一。
2. `token_grants.event_id` 唯一,是所有发放事实的最终幂等边界。
3. mock paid 重复调用同一订单时,不重复创建 grant。
4. 创建订单使用 `X-Idempotency-Key` 时,同一用户同一 key 返回同一订单。
### 7.8 错误码初版
| status | 场景 | 前端处理 |
| --- | --- | --- |
| `40001` | 参数错误 | 展示 `info` |
| `40003` | 未登录或登录态失效 | 跳登录 |
| `40004` | 资源不存在或已下架 | 展示空态或返回列表 |
| `40009` | 重复操作,如已点赞 | 以返回状态刷新 UI |
| `40013` | 无权限,如删除他人评论 | 展示 `info` |
| `40029` | 请求过于频繁 | 展示稍后重试 |
| `50000` | 服务内部错误 | 展示通用失败提示 |
错误处理口径:
1. 业务错误不要把 RPC 内部错误原文透给前端。
2. gateway client 负责把服务错误转成统一响应。
3. 幂等重复不应默认作为失败;能返回已有结果时优先返回已有结果。
## 8. 当前推进策略
1. 当前工作区存在其它拆服务改动,本阶段只提交方案文档。
2. 等工作区干净后,从集成分支新开功能分支或单独 git worktree。
3. 实现时两个服务主体可以并行推进。
4. `gateway/router``shared/contracts``shared/ports``outbox route``config` 由主代理统一收口。
5. 先做 P0 闭环,再扩展审核、真实支付和推荐排序。
6. `task` / `task-class` 未迁出前,论坛侧优先走 legacy adapter不提前把本轮推进绑死在 task 迁移完成之后。
## 9. 已确认口径
1. 论坛展示名使用“计划广场”。
2. 点赞奖励只给帖子作者。
3. 同一用户对同一计划只允许导入一次,前端根据 `imported_once` 刷新按钮状态,奖励随该次导入记录一次。
4. 评论 P0 支持用户删除自己的评论,暂不引入管理员删评、举报和审核流。
5. 评论层级后端不硬限制,数据库只存 `parent_comment_id`,服务层负责组装评论树;前端可以按体验做折叠或缩进。
6. Token 发放本轮不改 `user/auth`,先在 `token-store` 内封装 Token 获取途径和后续发放出口,后续合并稳定后再切到 `user/auth` 的权威额度发放能力。
7. Token 商品从 `token_products` 表读取P0 通过 seed 初始化商品,不做管理后台。
8. 前端正常展示评论,不隐藏评论能力。
## 10. 实施计划
### 10.1 P0 实施原则
1. 本轮先把 `taskclass-forum``token-store` 两个新服务跑通,不改 `task` / `task-class` 核心模块,不改 `user/auth` 契约。
2. 论坛依赖 TaskClass 的能力必须收敛在 legacy adapter 内,后续 `task-class` RPC 合并后只替换 adapter。
3. Token 获取和发放先记录在 `token-store` 自己的账本里,不直接改 `users` 表。
4. 搜索 P0 先走 MySQL服务层预留 `PostSearchPort`;后续需要中文分词、相关性排序或内容检索时,再替换为 Elasticsearch / OpenSearch adapter。
5. 商品 P0 从 `token_products` 表读取,通过 seed 初始化 2-3 个商品;不做商品管理后台。
6. 同一用户对同一计划只允许导入一次,后端通过唯一约束兜底,前端通过 `imported_once` 刷新按钮状态。
7. 评论层级不硬限制,表里只存 `parent_comment_id`,服务层按帖子组装评论树。
8. Outbox 总线只承载异步副作用,不承载 P0 用户可见主链路,避免联调期被 relay / consumer 状态拖慢。
### 10.2 表结构与迁移
第一步先落服务私有表和必要唯一约束:
1. 计划广场表:`forum_posts``forum_post_templates``forum_post_template_items``forum_likes``forum_comments``forum_imports`
2. Token 商店表:`token_products``token_orders``token_grants``token_reward_rules`
3. `forum_likes``(post_id, user_id)` 唯一约束。
4. `forum_imports``(post_id, user_id)` 唯一约束,保证同一用户同一计划只导入一次。
5. `forum_comments``post_id``parent_comment_id``created_at` 索引,支撑按帖子读取扁平评论后组树。
6. `token_orders.order_no` 唯一,`token_grants.event_id` 唯一。
7. `token_products` 通过 seed 初始化商品,后续如要管理后台再单独扩展。
### 10.3 服务骨架
第二步搭建目录和进程骨架:
1. 新增 `backend/cmd/taskclassforum``backend/cmd/tokenstore`
2. 新增 `backend/services/taskclassforum``backend/services/tokenstore`,内部按 `sv``dao``model``internal``rpc` 分层。
3. 新增 `backend/gateway/api/taskclassforum``backend/gateway/api/tokenstore` 承载 HTTP 入口。
4. 新增 `backend/gateway/client/taskclassforum``backend/gateway/client/tokenstore` 承载 zrpc client 和错误反解。
5. `gateway/router`、配置、outbox route、shared contracts 由主代理统一收口,避免多处同时改同一个装配点。
### 10.4 计划广场 P0
第三步实现计划广场闭环:
1. 发布计划:读取用户自己的 TaskClass生成论坛模板快照写入帖子和模板表。
2. 列表和详情:先用 MySQL 查询 `title``summary``tags`、状态和计数字段,支持 `latest``likes``imports` 排序。
3. 点赞 / 取消点赞:用唯一约束保证同一用户只点赞一次,点赞奖励事件只给作者。
4. 评论:发表评论、回复任意评论、删除自己的评论;删除采用软删除,保留子回复结构。
5. 评论树:按帖子读取扁平评论,服务层按 `parent_comment_id` 组装树;后端不限制层级。
6. 一键导入:同一用户同一帖子只允许导入一次,只生成当前用户自己的 TaskClass不写 schedule。
### 10.5 Token 商店 P0
第四步实现 Token 商店闭环:
1. 商品列表从 `token_products` 表读取P0 商品由 seed 提供。
2. 创建订单:写入 `token_orders`,状态为 `pending`
3. mock paid把订单置为已支付并写入 `token_grants`
4. 获取记录:从 `token_grants` 分页查询购买、点赞奖励、导入奖励等记录。
5. Token 概览:展示 `token-store` 已记录的累计获取 TokenP0 不展示为 `user/auth` 权威可用额度。
6. 预留 `TokenGrantOutlet`,后续切到 `user/auth` 时只替换发放出口。
### 10.6 Outbox 总线接入
第五步按复杂度分两档推进 outbox
1. P0 主链路同步闭环计划发布、点赞状态、评论、导入、订单、mock paid 和 grant 账本都同步写入本服务数据库。
2. P0 可同步写奖励账本,但必须按 `forum.post.liked``forum.post.imported` 生成稳定 `event_id`,保证后续切异步时可复用同一幂等边界。
3. 若本轮顺手接入服务级 outbox建议新增 `taskclass_forum_outbox_messages``token_store_outbox_messages`,对应 topic 为 `smartflow.taskclass-forum.outbox``smartflow.token-store.outbox`
4. 对应 consumer group 建议为 `smartflow-taskclass-forum-outbox-consumer``smartflow-token-store-outbox-consumer`,延续当前服务级 topic / group 隔离规则。
5. `taskclass-forum` 适合发布 `forum.post.liked``forum.post.imported`、后续 `forum.post.published``forum.post.updated``forum.post.deleted`
6. `token-store` 适合发布 `token.grant.requested``token.grant.completed`,后续接 `user/auth` 时再消费或转发到权威额度同步。
7. 搜索索引、排行榜、通知、运营统计都走 outbox 异步消费,不进入 HTTP 主事务。
8. relay / consumer 失败时不得影响用户已经成功的主操作,通过 outbox 重试和 `token_grants.event_id` 幂等兜底。
### 10.7 缓存策略
P0 不引入复杂缓存,优先靠表结构、索引和分页控制复杂度:
1. 评论树 P0 不做整树缓存。评论是强互动数据,新增、回复、删除都会影响树结构,缓存失效成本高;当前场景多数用户看完即切,直接查库并组树更简单。
2. 评论接口按根评论分页,后端读取当前页根评论及其子孙评论后组树,避免一次拉完整帖子全部评论。
3. 帖子列表和详情 P0 可先不缓存;如果出现热点,再对列表首屏或详情头部做短 TTL 缓存,并在点赞、评论、导入后按帖子维度失效。
4. 点赞数、评论数、导入数优先存 `forum_posts` 计数字段,写操作事务内增减,避免每次列表都聚合统计。
5. `token_products` 读取频率高、变化少,可做短 TTL 缓存;但 P0 直接读表也可以接受。
6. 后续若上 Elasticsearch只缓存搜索索引不改变前端接口和论坛业务编排。
### 10.8 联调与验收
最后按接口闭环做 smoke
1. 发布计划后,列表和详情能看到模板快照。
2. 点赞成功后,点赞状态和计数刷新,作者获得一条 Token 获取记录。
3. 评论支持多层回复;删除自己的评论后,子回复仍保留。
4. 同一用户同一计划第二次导入被后端拒绝或返回已导入状态,前端按钮状态保持一致。
5. mock paid 后订单进入 `granted``token_grants` 只写一条幂等记录。
6. Token 概览展示累计获取 Token但不声称已经同步到 `user/auth` 权威可用额度。
7. 如果本轮接入 outbox需要额外验证事件写入、投递、消费失败重试和幂等不重复发放。

File diff suppressed because it is too large Load Diff