38 KiB
计划广场与 Token 商店后端实施方案
1. 文档定位
本文记录当前需要讨论和快速推进的核心产品口径、前后端接口契约、服务边界、事件口径和实施计划,不展开完整交互稿、运营后台和真实支付细节。
本轮目标是新增两个终态服务模块:
taskclass-forum:支持用户分享 TaskClass 学习计划,并让其他用户一键导入。token-store:支持 Token 商品购买、活动奖励和发放账本。
两个模块后续都放在 backend/services 下,以独立服务为目标设计;当前仓库工作区未干净前只讨论方案,不进入代码实现。
2. 背景与目标
当前产品已经具备用户自建 TaskClass、智能排程和 Token 额度门禁能力。下一阶段希望补上社区化与商业化闭环:
- 用户可以把自己的复习计划分享出去,形成可浏览、可点赞、可评论、可复用的计划广场。
- 其他用户可以一键导入计划模板,快速生成自己的 TaskClass。
- 被点赞、被导入等社区行为可以转化为 Token 激励。
- 用户可以通过 Token 商店购买或领取 Token,为后续高频 Agent 使用建立基础商业闭环。
3. 模块一:学习计划论坛
3.1 产品定位
计划广场不是普通帖子论坛,而是“帖子 + TaskClass 模板快照”的社区。
对外展示名先使用“计划广场”。
用户发布时,系统从用户自己的 TaskClass 复制一份模板快照。其他用户导入时,再从快照生成自己的 TaskClass 副本。
快照原则:
- 发布后不直接引用原作者的
task_classes/task_items。 - 原作者后续修改自己的计划,不影响已发布模板。
- 导入用户拿到的是自己的 TaskClass 副本,后续可自由编辑。
- 不分享
embedded_time、schedule 绑定、用户私有排程状态。
3.2 P0 功能
- 发布学习计划:用户选择一个 TaskClass,填写标题、简介、标签后发布。
- 浏览列表:支持分页查看公开计划,按最新、点赞数、导入数排序。
- 查看详情:展示计划说明、TaskClass 配置摘要和任务条目预览。
- 点赞:同一用户对同一帖子只能点赞一次,可取消点赞。
- 评论:支持发表评论、多层回复和删除自己的评论,接口返回评论树 JSON。
- 一键导入:从论坛模板复制出当前用户自己的 TaskClass。
- 基础激励:模板获得点赞或导入后,可触发 Token 奖励事件。
3.3 P0 不做
- 不做复杂推荐算法。
- 不做关注、私信、用户主页。
- 不做富文本编辑器,先用纯文本简介。
- 不做审核后台和管理员删评,先预留状态字段。
- 不直接把模板应用进 schedule;导入后由用户走现有 TaskClass / 排程链路。
- P0 暂不做评论举报和管理员审核流。
3.4 核心实体
forum_posts:帖子主体,记录作者、标题、简介、状态、点赞数、评论数、导入数。forum_post_templates:TaskClass 快照,记录模式、日期范围、策略、约束配置等。forum_post_template_items:TaskClassItem 快照,只记录 order/content 等模板信息。forum_likes:点赞幂等记录。forum_comments:评论记录,使用parent_comment_id表达多层回复关系。forum_imports:导入记录,记录从哪个帖子导入到哪个用户和新 TaskClass ID。
3.5 关键流程
发布流程:
- 用户选择自己的 TaskClass。
taskclass-forum通过 TaskClass 读取端口拿到完整模板。- 服务过滤私有字段,生成论坛快照。
- 写入帖子和模板快照。
评论流程:
- 用户可以直接评论帖子,也可以回复任意一条评论。
- 评论表用
parent_comment_id记录父评论,根评论的parent_comment_id为空。 - 列表查询先按帖子读取扁平评论,再由服务层组装成多层评论树。
- 删除评论时 P0 只允许用户删除自己的评论,并采用软删除,保留子回复结构,避免整棵回复树断链。
- P0 暂不引入管理员删评、举报和审核流,后续如需治理再单独扩展。
导入流程:
- 用户点击一键导入。
taskclass-forum读取帖子模板快照。- 通过 TaskClass 写入端口为当前用户创建 TaskClass 副本。
- 写入导入记录并增加导入计数。
- 可异步发布 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 功能
- 商品列表:展示可购买 Token 包。
- 创建订单:用户选择商品生成订单。
- 支付确认:P0 先支持 mock paid 或管理端确认 paid,不接真实支付网关。
- Token 发放记录:订单支付成功后写入发放账本,并通过本服务内部端口封装后续发放出口。
- 奖励发放:支持论坛点赞、导入等事件触发奖励。
- 发放账本:所有发放必须有幂等 event_id,避免后续重复加额度。
4.3 P0 不做
- 不接真实微信 / 支付宝 / Stripe。
- 不做退款、发票、优惠券。
- 不做复杂会员体系。
- 不直接改
users.token_usage,避免和消费统计混淆。
4.4 核心实体
token_products:Token 商品,P0 从表读取,并通过 seed 初始化 2-3 个商品,不做管理后台。token_orders:订单。token_grants:Token 发放账本,记录购买、奖励、补偿等来源。token_reward_rules:奖励规则,P0 可先用配置或简单表。
4.5 关键流程
购买流程:
- 用户选择商品并创建订单。
- 订单进入
pending。 - P0 通过 mock paid 或管理端确认,把订单置为
paid。 token-store写入 token grant 账本。token-store通过内部发放端口记录本次 Token 获取事实,本轮不修改user/auth。- 发放记录写入成功后订单进入
granted;后续切到user/auth时只替换发放端口实现。
奖励流程:
- 论坛产生点赞或导入事件。
token-store按奖励规则判断是否发放。- 写入 token grant 账本。
- 通过内部发放端口记录奖励获取事实;后续切到
user/auth时只替换发放端口实现。
5. 服务边界
5.1 taskclass-forum
负责:
- 论坛帖子、点赞、评论、导入记录。
- TaskClass 模板快照。
- 导入时的模板复制编排。
- 发布社区行为事件,供 Token 激励消费。
不负责:
- TaskClass 原始表所有权。
- schedule 写入和排程应用。
- Token 额度发放。
- 用户登录鉴权。
5.2 token-store
负责:
- 商品、订单、发放账本。
- 社区奖励规则。
- 幂等发放。
- 封装 Token 获取途径和后续发放出口。
不负责:
- JWT、登录、注册。
- Agent 消耗统计。
- TaskClass 论坛内容。
- 真实第三方支付回调,P0 只预留状态机。
5.3 与现有服务关系
- 论坛读取和导入 TaskClass 时,先通过端口适配旧
TaskClassService/DAO。 - 后续
task-class独立成服务后,只替换端口适配器。 - 论坛 P0 不直接写 schedule,避免被
schedule未拆服务影响。 - Token 商店本轮不直接改 users 表,也不新增或修改
user/auth契约;先封装自己的 Token 获取途径和后续发放出口,等合并后再切到user/auth。
5.4 并行迁移细则
- 当前
task/task-class还没有完成服务迁移时,本轮不改它们的核心模块,也不提前给它们补新的 RPC 边界,避免和另一条拆分线发生合并冲突。 taskclass-forum的实现层可以先通过 legacy adapter 复用现有 DAO / Service,必要时也可以直接读旧表,但这类直连只能收敛在 adapter 内,不能扩散到论坛业务层。- 论坛业务层只依赖“读取 TaskClass 快照”和“写入 TaskClass 副本”这类端口,不关心底层是 DAO、Service 还是后续 RPC。
- 等
task-class侧完成独立服务后,论坛只替换 adapter,不回改论坛领域逻辑,不重写帖子、点赞、评论和导入编排。 - 这轮优先保证论坛和 Token 商店自己的边界稳定,
task相关能力保持当前状态即可,等主线合并后再按统一节奏推进下一步迁移。
5.5 理想内部结构
参考 userauth 样板和微服务迁移总纲,两个新模块都按“cmd 进程入口 + gateway/api HTTP 适配 + gateway/client RPC client + services 服务主体”的结构推进。
命名约定:
- 产品和事件域继续使用
taskclass-forum、token-store,方便和方案、topic、事件名保持一致。 - Go 目录和包名建议使用
taskclassforum、tokenstore,对齐当前userauth样板,避免包名里出现连字符。 - 如果后续统一目录规范改成带连字符目录,也只调整目录名和 import,不改变服务内职责分层。
推荐目标树:
这里是两套服务边缘入口:
gateway/api下放 HTTP API 入口,每个服务一个子目录。gateway/client下放 zrpc client / error 反解,每个服务一个子目录。- 旧
gateway/userapi、gateway/userauth暂不在本轮调整,后续合并时再统一处理。
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/
目录职责:
cmd/<service>/main.go只负责读取配置、初始化资源、启动 go-zero zrpc 服务,不承载业务规则。gateway/api/<service>只负责 HTTP 参数绑定、鉴权后用户 ID 注入、调用对应 client、复用respond返回前端,不直接访问服务数据库。gateway/client/<service>只放 zrpc client 和错误反解;不要把 client 放进cmd,也不要让 gateway 直接 import 服务端 DAO / model。services/<service>/sv是服务主业务编排层,负责事务顺序、幂等判断、状态流转和跨端口调用。services/<service>/dao只负责本服务数据库访问;model只放本服务私有表模型,不放跨服务 DTO。services/<service>/internal只放服务私有子能力,外部服务禁止直接 import。services/<service>/rpc对齐userauth,放 proto、server、handler、errors 和生成后的pb。shared/contracts、shared/events、shared/ports只放跨服务契约、事件 payload 和端口接口,不放 DAO、model、sv 或业务状态机。
taskclassforum/internal 建议职责:
adapter/:承载 TaskClass 端口实现。P0 先放 legacy adapter,复用旧 DAO / Service 或临时直读旧表;后续替换为 task-class RPC adapter。snapshot/:负责 TaskClass 原始数据到论坛模板快照的过滤、复制和字段白名单,避免分享embedded_time、schedule 绑定和私有排程状态。commenttree/:负责把forum_comments扁平记录组装成多层评论树,并处理软删除节点的展示兜底。event/:负责组装forum.post.liked、forum.post.imported等领域事件 payload;事件投递仍走全局 outbox 路由和服务 catalog。
tokenstore/internal 建议职责:
adapter/:承载后续user/auth额度发放适配点。P0 先封装token-store自己的 Token 获取途径和发放出口,不新增或修改user/auth契约,也禁止直接改users表。paymentmock/:只处理 P0 mock paid 状态确认,不承载真实支付网关逻辑。grant/:封装token_grants幂等账本写入、event_id 去重和 grant 状态流转。reward/:封装论坛点赞、导入等奖励规则判断,P0 可先走配置或简单表。event/:负责组装token.grant.requested、token.grant.completed等事件 payload;后续真实异步化时复用服务级 outbox 基线。
6. 事件与激励
P0 建议事件:
forum.post.liked:帖子被点赞。forum.post.imported:帖子被导入。token.grant.requested:请求发放 Token。token.grant.completed:Token 发放完成。
奖励口径先从简单规则开始:
- 点赞奖励先只给帖子作者,每个帖子每个用户首次点赞只奖励一次。
- 同一用户对同一计划只允许导入一次,导入奖励随该次导入记录一次。
- 同一 event_id 的 Token 发放必须幂等。
- 奖励额度先走配置,不在本方案阶段定死。
Outbox 使用口径:
- P0 用户可见主链路不强依赖 outbox:发布、点赞、评论、导入、下单、mock paid、写 grant 账本都应在服务自己的同步事务里完成。
- Outbox 用于异步副作用:论坛点赞 / 导入奖励事件、后续搜索索引同步、通知、排行榜刷新、运营统计,以及后续
token-store到user/auth的权威额度同步。 - 第一版如果为了赶进度先同步写奖励账本,也必须保留事件名、payload 和
event_id规则,后续切到 outbox 消费时不改业务语义。 - 新服务后续接入服务级 outbox 时,建议使用独立表、topic 和 consumer group,不回退到共享 outbox。
7. 后端契约初版
本节把前端对接草案收束成后端 P0 契约。若后续实现时发现字段和现有模型存在细小出入,允许在保持语义不变的前提下微调字段名,但必须同步更新 docs/frontend/计划广场与Token商店对接说明.md。
7.1 通用 HTTP 约定
- 所有接口统一挂在
/api/v1下。 - P0 默认都需要登录态,
user_id由 gateway 从 JWT 注入,前端不传user_id。 - 响应复用项目统一壳:
status、info、data。 - 写接口优先支持
X-Idempotency-Key,避免前端重复点击造成重复发布、重复评论、重复导入或重复下单。 - 时间字段统一返回 ISO 8601 字符串,带时区。
- 分页统一使用
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:
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:
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"`
}
字段口径:
ForumPostBrief.statusP0 先使用published,后续审核可扩展hidden、deleted、pending_review。ForumCommentNode.statusP0 使用visible、deleted。CanDelete由后端根据当前用户判断,前端只按字段展示删除入口。TemplateDetail.items_preview来自论坛快照,不读取原作者当前 TaskClass。- 一键导入成功只返回新 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:
type CreateTokenOrderRequest struct {
ProductID uint64 `json:"product_id"`
Quantity int `json:"quantity"`
}
type MockPaidOrderRequest struct {
MockChannel string `json:"mock_channel"`
}
核心响应 DTO:
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"`
}
字段口径:
TokenSummary.quota_sync_statusP0 返回not_connected,后续可扩展partial、synced。TokenOrderView.status使用pending、paid、granted、closed。TokenGrantView.status使用recorded、applied、skipped、failed。quota_appliedP0 默认为false,因为本轮不改user/auth。- 前端 P0 展示“累计获取 Token”,不要展示为权威可用额度。
7.4 RPC 服务契约
RPC 只承载 gateway 到服务主体的跨进程调用,不暴露给前端。字段可以按 proto 生成规则转成 snake_case JSON,但语义必须和 HTTP DTO 保持一致。
taskclassforum P0 方法:
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 方法:
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 入参通用规则:
- 所有需要用户态的方法都带
actor_user_id,由 gateway 注入。 - 写方法带
idempotency_key,由 gateway 从X-Idempotency-Key读取。 - 列表方法带
page、page_size、筛选字段和排序字段。 - RPC 层错误使用 go-zero / gRPC
error返回,gateway client 负责转成项目统一响应。
7.5 服务内部端口契约
taskclassforum 依赖 TaskClass 端口:
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)
}
端口口径:
GetOwnedTaskClassSnapshot只能读取当前用户自己的 TaskClass。TaskClassSnapshot必须剔除embedded_time、schedule 绑定和用户私有排程状态。- P0 实现为 legacy adapter,可复用旧 DAO / Service 或收敛式直读旧表。
- 后续
task-class服务拆出后,只替换 adapter。
tokenstore 内部发放端口:
type TokenGrantOutlet interface {
RecordAcquisition(ctx context.Context, grant TokenGrantRecord) error
}
端口口径:
- P0 只记录
token-store自己的获取事实和账本,不新增或修改user/auth契约。 - 禁止直接修改
users表。 - 后续切到
user/auth时新增 adapter 实现,服务编排层不重写。
7.6 事件契约
P0 事件按当前服务级 outbox 思路设计,但不要求主链路第一版强依赖异步投递;如果第一版先同步写账本,也必须按下面 event_id 生成规则保留幂等口径。
forum.post.liked:
{
"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:
{
"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:
{
"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:
{
"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"
}
事件口径:
- 点赞奖励只给作者,取消点赞不回滚已记录奖励。
- 同一用户对同一计划只允许导入一次,导入奖励随该次导入记录一次。
- Token 发放 P0 的
quota_applied为false,后续接入user/auth后再变成真实同步状态。 - 事件 payload 放
backend/shared/events;服务私有处理逻辑留在各自internal/event或sv。 - 事件不要阻塞用户主流程;异步消费失败时依赖
event_id和 grant 账本幂等重试。
7.7 幂等与唯一约束
论坛侧:
forum_likes对(post_id, user_id)建唯一约束。forum_imports对(post_id, user_id)建唯一约束,同一用户同一计划只允许导入一次,并保留event_id;奖励去重依赖token_grants.event_id。- 评论创建支持
X-Idempotency-Key,避免重复提交。 - 删除评论是软删除,重复删除同一条自己的评论返回成功态。
Token 侧:
token_orders.order_no唯一。token_grants.event_id唯一,是所有发放事实的最终幂等边界。- mock paid 重复调用同一订单时,不重复创建 grant。
- 创建订单使用
X-Idempotency-Key时,同一用户同一 key 返回同一订单。
7.8 错误码初版
| status | 场景 | 前端处理 |
|---|---|---|
40001 |
参数错误 | 展示 info |
40003 |
未登录或登录态失效 | 跳登录 |
40004 |
资源不存在或已下架 | 展示空态或返回列表 |
40009 |
重复操作,如已点赞 | 以返回状态刷新 UI |
40013 |
无权限,如删除他人评论 | 展示 info |
40029 |
请求过于频繁 | 展示稍后重试 |
50000 |
服务内部错误 | 展示通用失败提示 |
错误处理口径:
- 业务错误不要把 RPC 内部错误原文透给前端。
- gateway client 负责把服务错误转成统一响应。
- 幂等重复不应默认作为失败;能返回已有结果时优先返回已有结果。
8. 当前推进策略
- 当前工作区存在其它拆服务改动,本阶段只提交方案文档。
- 等工作区干净后,从集成分支新开功能分支或单独 git worktree。
- 实现时两个服务主体可以并行推进。
gateway/router、shared/contracts、shared/ports、outbox route、config由主代理统一收口。- 先做 P0 闭环,再扩展审核、真实支付和推荐排序。
task/task-class未迁出前,论坛侧优先走 legacy adapter,不提前把本轮推进绑死在 task 迁移完成之后。
9. 已确认口径
- 论坛展示名使用“计划广场”。
- 点赞奖励只给帖子作者。
- 同一用户对同一计划只允许导入一次,前端根据
imported_once刷新按钮状态,奖励随该次导入记录一次。 - 评论 P0 支持用户删除自己的评论,暂不引入管理员删评、举报和审核流。
- 评论层级后端不硬限制,数据库只存
parent_comment_id,服务层负责组装评论树;前端可以按体验做折叠或缩进。 - Token 发放本轮不改
user/auth,先在token-store内封装 Token 获取途径和后续发放出口,后续合并稳定后再切到user/auth的权威额度发放能力。 - Token 商品从
token_products表读取,P0 通过 seed 初始化商品,不做管理后台。 - 前端正常展示评论,不隐藏评论能力。
10. 实施计划
10.1 P0 实施原则
- 本轮先把
taskclass-forum和token-store两个新服务跑通,不改task/task-class核心模块,不改user/auth契约。 - 论坛依赖 TaskClass 的能力必须收敛在 legacy adapter 内,后续
task-classRPC 合并后只替换 adapter。 - Token 获取和发放先记录在
token-store自己的账本里,不直接改users表。 - 搜索 P0 先走 MySQL,服务层预留
PostSearchPort;后续需要中文分词、相关性排序或内容检索时,再替换为 Elasticsearch / OpenSearch adapter。 - 商品 P0 从
token_products表读取,通过 seed 初始化 2-3 个商品;不做商品管理后台。 - 同一用户对同一计划只允许导入一次,后端通过唯一约束兜底,前端通过
imported_once刷新按钮状态。 - 评论层级不硬限制,表里只存
parent_comment_id,服务层按帖子组装评论树。 - Outbox 总线只承载异步副作用,不承载 P0 用户可见主链路,避免联调期被 relay / consumer 状态拖慢。
10.2 表结构与迁移
第一步先落服务私有表和必要唯一约束:
- 计划广场表:
forum_posts、forum_post_templates、forum_post_template_items、forum_likes、forum_comments、forum_imports。 - Token 商店表:
token_products、token_orders、token_grants、token_reward_rules。 forum_likes建(post_id, user_id)唯一约束。forum_imports建(post_id, user_id)唯一约束,保证同一用户同一计划只导入一次。forum_comments建post_id、parent_comment_id、created_at索引,支撑按帖子读取扁平评论后组树。token_orders.order_no唯一,token_grants.event_id唯一。token_products通过 seed 初始化商品,后续如要管理后台再单独扩展。
10.3 服务骨架
第二步搭建目录和进程骨架:
- 新增
backend/cmd/taskclassforum、backend/cmd/tokenstore。 - 新增
backend/services/taskclassforum、backend/services/tokenstore,内部按sv、dao、model、internal、rpc分层。 - 新增
backend/gateway/api/taskclassforum、backend/gateway/api/tokenstore承载 HTTP 入口。 - 新增
backend/gateway/client/taskclassforum、backend/gateway/client/tokenstore承载 zrpc client 和错误反解。 gateway/router、配置、outbox route、shared contracts 由主代理统一收口,避免多处同时改同一个装配点。
10.4 计划广场 P0
第三步实现计划广场闭环:
- 发布计划:读取用户自己的 TaskClass,生成论坛模板快照,写入帖子和模板表。
- 列表和详情:先用 MySQL 查询
title、summary、tags、状态和计数字段,支持latest、likes、imports排序。 - 点赞 / 取消点赞:用唯一约束保证同一用户只点赞一次,点赞奖励事件只给作者。
- 评论:发表评论、回复任意评论、删除自己的评论;删除采用软删除,保留子回复结构。
- 评论树:按帖子读取扁平评论,服务层按
parent_comment_id组装树;后端不限制层级。 - 一键导入:同一用户同一帖子只允许导入一次,只生成当前用户自己的 TaskClass,不写 schedule。
10.5 Token 商店 P0
第四步实现 Token 商店闭环:
- 商品列表从
token_products表读取,P0 商品由 seed 提供。 - 创建订单:写入
token_orders,状态为pending。 - mock paid:把订单置为已支付,并写入
token_grants。 - 获取记录:从
token_grants分页查询购买、点赞奖励、导入奖励等记录。 - Token 概览:展示
token-store已记录的累计获取 Token;P0 不展示为user/auth权威可用额度。 - 预留
TokenGrantOutlet,后续切到user/auth时只替换发放出口。
10.6 Outbox 总线接入
第五步按复杂度分两档推进 outbox:
- P0 主链路同步闭环:计划发布、点赞状态、评论、导入、订单、mock paid 和 grant 账本都同步写入本服务数据库。
- P0 可同步写奖励账本,但必须按
forum.post.liked、forum.post.imported生成稳定event_id,保证后续切异步时可复用同一幂等边界。 - 若本轮顺手接入服务级 outbox,建议新增
taskclass_forum_outbox_messages、token_store_outbox_messages,对应 topic 为smartflow.taskclass-forum.outbox、smartflow.token-store.outbox。 - 对应 consumer group 建议为
smartflow-taskclass-forum-outbox-consumer、smartflow-token-store-outbox-consumer,延续当前服务级 topic / group 隔离规则。 taskclass-forum适合发布forum.post.liked、forum.post.imported、后续forum.post.published、forum.post.updated、forum.post.deleted。token-store适合发布token.grant.requested、token.grant.completed,后续接user/auth时再消费或转发到权威额度同步。- 搜索索引、排行榜、通知、运营统计都走 outbox 异步消费,不进入 HTTP 主事务。
- relay / consumer 失败时不得影响用户已经成功的主操作,通过 outbox 重试和
token_grants.event_id幂等兜底。
10.7 缓存策略
P0 不引入复杂缓存,但评论区读多写少,评论树需要接短 TTL 缓存:
- 评论树采用 cache-aside + 版本号失效,缓存粒度为
post_id + sort + page + page_size + version。版本 key 为forum:comments:{post_id}:version,数据 key 为forum:comments:{post_id}:v{version}:sort:{sort}:page:{page}:size:{page_size}。 - 缓存内容是“当前页根评论 + 子孙评论”组装后的去个性化评论树 JSON,并连同分页结果一起缓存;
can_delete这类当前用户视角字段不进共享缓存,返回前由 service 按actor_user_id补齐。 - 新增评论、回复或删除评论的 DB 事务成功后,递增
forum:comments:{post_id}:version。旧 data key 不扫描删除,依赖短 TTL 自然回收,避免写评论时阻塞 Redis。 - Redis 读取、写入或版本递增失败都不影响主链路:读失败直接回源 DB,写失败保持 DB 结果返回,版本递增失败则等待短 TTL 兜底。
- 评论接口仍按根评论分页,后端只读取当前页根评论及其子孙评论后组树,避免一次拉完整帖子全部评论。
- 帖子列表和详情 P0 可先不缓存;如果出现热点,再对列表首屏或详情头部做短 TTL 缓存,并在点赞、评论、导入后按帖子维度失效。
- 点赞数、评论数、导入数优先存
forum_posts计数字段,写操作事务内增减,避免每次列表都聚合统计。 token_products读取频率高、变化少,可做短 TTL 缓存;但 P0 直接读表也可以接受。- 后续若上 Elasticsearch,只缓存搜索索引,不改变前端接口和论坛业务编排。
10.8 联调与验收
最后按接口闭环做 smoke:
- 发布计划后,列表和详情能看到模板快照。
- 点赞成功后,点赞状态和计数刷新,作者获得一条 Token 获取记录。
- 评论支持多层回复;删除自己的评论后,子回复仍保留。
- 同一用户同一计划第二次导入被后端拒绝或返回已导入状态,前端按钮状态保持一致。
- mock paid 后订单进入
granted,token_grants只写一条幂等记录。 - Token 概览展示累计获取 Token,但不声称已经同步到
user/auth权威可用额度。 - 如果本轮接入 outbox,需要额外验证事件写入、投递、消费失败重试和幂等不重复发放。