16 KiB
Service Foundation Technical Design
本文档定义 user-service、wallet-service、activity-service 从占位骨架变成可正常开发服务前,必须补齐的基础架构。它只定义服务底座,不展开具体业务功能。
Current State
当前三个服务的基础成熟度不一致:
| Service | Current State | Main Gap |
|---|---|---|
user-service |
只有 gRPC 进程骨架和 health | 缺 proto、service 层、storage、错误模型、用户库 |
wallet-service |
已注册 wallet/v1 gRPC stub |
缺账务 domain、ledger storage、幂等、错误映射 |
activity-service |
只有 gRPC 进程骨架和 health | 缺 proto、consumer 边界、storage、事件处理骨架 |
补齐顺序不能按文件数量排,应该按跨服务依赖排:
- 统一 gRPC 错误模型。
- 补
user-service身份基础。 - 补
wallet-service账务基础。 - 补
activity-service消费和活动基础。
Non Goals
本阶段不做:
- 不实现完整用户资料、关系链、头像昵称体系。
- 不实现真实三方 provider SDK 细节。
- 不实现复杂钱包风控、充值提现、冻结解冻。
- 不实现完整活动规则引擎。
- 不引入重型 DI 框架。
- 不让 gateway、room、im 直接 import 这些服务的
internal包。
Service Baseline Layout
三个服务都应该收敛到同一种内部结构。
services/<service>/
cmd/server/main.go
configs/
config.yaml
config.docker.yaml
config.tencent.example.yaml
deploy/mysql/initdb/
001_<service>.sql
internal/
app/
app.go
config/
config.go
domain/
service/
storage/
mysql/
transport/
grpc/
server.go
integration/
目录职责:
| Directory | Responsibility |
|---|---|
app |
只做依赖装配、生命周期、health 状态切换 |
config |
只做配置加载和默认值,不做连接创建 |
domain |
领域实体、值对象、领域错误,不依赖 gRPC/MySQL |
service |
用例编排,调用 repository 和外部 integration |
storage/mysql |
MySQL repository 实现 |
transport/grpc |
protobuf 入参出参转换、错误映射、server 注册 |
integration |
调用其他服务或消费外部事件的 client/consumer |
transport/grpc 不能直接写复杂业务规则。业务规则进 service 或 domain。
Shared Config Baseline
每个服务的配置至少包含:
service_name: user-service
node_id: user-local
grpc_addr: ":13005"
mysql_dsn: "root:root@tcp(mysql:3306)/hyapp_user?parseTime=true"
mysql_auto_migrate: false
按服务扩展:
user-service:
jwt:
issuer: "hyapp"
access_token_ttl_sec: 1800
refresh_token_ttl_sec: 2592000
signing_alg: "HS256"
signing_secret: "dev-secret"
wallet-service:
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_wallet?parseTime=true&charset=utf8mb4&loc=UTC"
mysql_auto_migrate: false
activity-service:
consumer:
room_outbox_poll_interval_ms: 1000
batch_size: 100
线上真实密码、密钥和云资源地址只能通过环境或密钥系统注入,不能写进真实配置。
gRPC Error Model
内部服务不要返回 HTTP envelope。gRPC 成功时返回 protobuf response,失败时返回:
gRPC status code + google.rpc.ErrorInfo.reason
gateway 负责把内部错误映射成外部:
{
"code": "AUTH_FAILED",
"message": "authentication failed",
"request_id": "req_xxx",
"data": null
}
Standard Reasons
| Reason | gRPC Code | HTTP Mapping |
|---|---|---|
INVALID_ARGUMENT |
InvalidArgument |
400 |
NOT_FOUND |
NotFound |
404 |
CONFLICT |
AlreadyExists or FailedPrecondition |
409 |
UNAUTHORIZED |
Unauthenticated |
401 |
PERMISSION_DENIED |
PermissionDenied |
403 |
UNAVAILABLE |
Unavailable |
503 |
INTERNAL_ERROR |
Internal |
500 |
Auth 专用 reason:
| Reason | gRPC Code | HTTP Mapping |
|---|---|---|
AUTH_FAILED |
Unauthenticated |
401 |
PASSWORD_ALREADY_SET |
AlreadyExists |
409 |
USER_DISABLED |
PermissionDenied |
403 |
SESSION_EXPIRED |
Unauthenticated |
401 |
SESSION_REVOKED |
Unauthenticated |
401 |
User 专用 reason:
| Reason | gRPC Code | HTTP Mapping |
|---|---|---|
INVALID_INVITE_CODE |
InvalidArgument |
400 |
COUNTRY_CHANGE_COOLDOWN |
FailedPrecondition |
409 |
USER_PROFILE_REQUIRED |
FailedPrecondition |
403 |
Wallet 专用 reason:
| Reason | gRPC Code | HTTP Mapping |
|---|---|---|
INSUFFICIENT_BALANCE |
FailedPrecondition |
409 |
DUPLICATE_BILLING_COMMAND |
AlreadyExists |
409 |
LEDGER_CONFLICT |
Aborted |
409 |
Activity 专用 reason:
| Reason | gRPC Code | HTTP Mapping |
|---|---|---|
RULE_NOT_ACTIVE |
FailedPrecondition |
409 |
EVENT_ALREADY_CONSUMED |
AlreadyExists |
409 |
REWARD_PENDING |
FailedPrecondition |
409 |
Error Implementation Boundary
错误码统一由 pkg/xerr 的 catalog 注册,领域层仍不能 import gRPC 包。
目标结构:
type Error struct {
Reason string
Message string
}
transport/grpc 负责把 xerr.Error 转成 gRPC status,并附带 google.rpc.ErrorInfo。gRPC code 保持少量 canonical code;业务码只放在 ErrorInfo.reason 和 HTTP envelope code。
新增业务错误码时必须同时在 xerr catalog 注册:
- gRPC canonical code。
- HTTP status。
- App 可见 public code。
- App 可见 public message。
- 是否可重试。
不要在 service 层直接返回 status.Error(...)。
Request Meta
每个新 proto service 都必须有 meta,最少字段:
message RequestMeta {
string request_id = 1;
string caller = 2;
string gateway_node_id = 3;
int64 sent_at_ms = 4;
string app_code = 10;
}
身份相关接口可以扩展:
string device_id = 5;
string client_ip = 6;
string user_agent = 7;
request_id 是追踪字段,不是幂等字段。
app_code 是多 App 租户键。gateway 必须在外部 HTTP 入口根据包名、平台或 JWT claims 解析出 app_code,再写入所有内部 gRPC RequestMeta。服务端 repository 查询、唯一键和幂等键都必须带 app_code,不能只按 user_id、room_id 或 command_id 判断唯一。
幂等字段必须独立命名:
string command_id = 2;
string idempotency_key = 3;
user-service Foundation
user-service 首先承接身份与用户主数据。
Proto
必须新增:
api/proto/user/v1/auth.proto
api/proto/user/v1/user.proto
auth.proto 承载:
LoginPasswordLoginThirdPartySetPasswordRefreshTokenLogout
user.proto 首版只放基础查询:
GetUserBatchGetUsers
不要一开始把资料编辑、关系链、黑名单全塞进去。
Internal Packages
internal/domain/user
internal/domain/auth
internal/service/auth
internal/service/user
internal/storage/mysql
internal/transport/grpc
MySQL Tables
首批表:
apps
users
user_invite_codes
user_invite_relations
user_invite_validity_policies
user_invite_recharge_progress
user_invite_event_consumption
user_invite_counters
password_accounts
third_party_identities
auth_sessions
login_audit
user_country_change_logs
user_outbox
原则:
apps是 App 注册表,保存package_name/platform -> app_code映射,由 gateway 解析入口使用。users是用户主记录,包含注册资料和注册来源快照;不保存 credential、密码明文或 refresh token 原文。user_invite_codes保存用户自己的可分享邀请码,按app_code + code唯一。user_invite_relations保存注册邀请关系事实,按app_code + invited_user_id保证一个用户最多一个邀请人。user_invite_validity_policies保存有效邀请策略,默认累计充值80000COIN,但必须允许后台按 App/区域配置。user_invite_recharge_progress保存被邀请用户累计充值进度和是否已成为有效邀请。user_invite_event_consumption保存钱包充值事件消费幂等,避免 wallet outbox 重投导致重复累计。user_invite_counters保存我的页和后台列表使用的邀请计数 read model,包含invite_count和valid_invite_count。password_accounts保存已登录用户手动设置后的密码 hash,不承载独立账号体系。third_party_identities保存 provider 绑定。auth_sessions保存 refresh token hash 和会话状态。login_audit不影响主链路成功与否。user_country_change_logs保存国家修改审计,并作为滚动 30 天冷却判断来源。user_outbox保存UserRegistered、UserInvited、UserInviteBecameValid等用户领域事件,供奖励、活动、消息和风控异步消费。
邀请统计规则:
invite_count表示成功填写邀请码并绑定关系的人数,来源是user_invite_relations。valid_invite_count表示被邀请用户累计成功充值金币达到有效邀请策略门槛的人数,默认门槛是80000COIN。- 有效邀请只消费
wallet-service的充值事实事件;钱包不理解邀请关系,user-service invite module 不读取钱包余额作为判断依据。 - 后台修改有效邀请门槛只影响后续达成判断;已达成的有效邀请事实不自动重算。
Readiness
ready=true 需要:
- gRPC listener 已启动。
- MySQL ping 成功。
- token signing config 有效。
user-service 不应该依赖 room、wallet、im 的 ready。
wallet-service Foundation
wallet-service 首先承接送礼扣费的账务语义。
Proto
当前已有:
api/proto/wallet/v1/wallet.proto
需要检查并补齐:
DebitGift- 请求内必须有
command_id或idempotency_key - 响应内必须有
billing_receipt_id
后续再追加:
CreditLuckyRewardGetBalanceFreezeUnfreeze
Internal Packages
internal/domain/wallet
internal/domain/ledger
internal/service/wallet
internal/storage/mysql
internal/transport/grpc
MySQL Tables
首批表:
wallet_accounts
wallet_transactions
wallet_entries
wallet_outbox
wallet_gift_prices
账务规则:
- 钱包所有账户、交易、分录、礼物价格和 outbox 都必须带
app_code;同一个command_id在不同 App 下互不冲突。 - 钱包余额不能只存在内存。
- 每次账务动作必须写 transaction 和 entries。
- 每个账务命令必须通过
wallet_transactions.app_code + command_id幂等。 - 跨服务事件必须先写
wallet_outbox,再由 worker 投递。 - 幂等命中必须返回原始
billing_receipt_id,不能重复扣费。
Readiness
ready=true 需要:
- gRPC listener 已启动。
- MySQL ping 成功。
- ledger repository 可写或至少可执行轻量事务探测。
wallet-service 不依赖 room-service ready。room 调 wallet 是调用方依赖,不是被调用方依赖。
activity-service Foundation
activity-service 首先承接房间 outbox 事件消费,不进入 Room Cell 主链路。
Proto
建议新增:
api/proto/activity/v1/activity.proto
首版 proto 可以只定义:
PingActivityGetActivityStatus
活动消费本身可以先不走 gRPC,而是通过 room outbox / Redis Stream / MQ 接入。不要为了有 gRPC 而把事件消费伪装成同步 RPC。
Internal Packages
internal/domain/activity
internal/service/activity
internal/consumer/room
internal/storage/mysql
internal/transport/grpc
MySQL Tables
首批通用表:
activities
activity_event_consumption
activity_outbox
具体活动再建自己的业务表。例如幸运礼物使用独立规则、奖池、抽奖记录表。
消费规则:
- 消费 room outbox 事件必须幂等。
- 消费幂等键必须带
app_code,避免不同 App 的同名event_id互相阻断。 - 消费位点不能只存在内存。
- 处理失败不能阻塞 Room Cell。
- 活动结算失败要进入 activity 自己的 outbox 或 retry 表。
Readiness
ready=true 需要:
- gRPC listener 已启动。
- MySQL ping 成功。
- consumer 依赖可访问。
- 当前节点不在 draining。
如果 consumer 来源还没接入,readiness 不要假装消费健康,只报告 gRPC 和 MySQL。
Gateway Integration Baseline
gateway 接入这些服务时只做:
- HTTP JSON 入参解析。
- 生成
request_id并透传到内部调用。 - 调内部 gRPC。
- 把 gRPC status + reason 映射为 HTTP envelope。
gateway 不做:
- 密码 hash。
- 钱包余额计算。
- 活动规则判断。
- 用户主数据写入。
建议 client 包:
services/gateway-service/internal/client/user_client.go
services/gateway-service/internal/client/wallet_client.go
gateway 不一定直接调用 wallet。当前送礼仍是 gateway -> room -> wallet。
Storage Strategy
本地和测试:
- Docker MySQL。
- Docker Redis。
- 每个服务独立 schema 或表名前缀。
线上:
- 腾讯云 MySQL。
- 腾讯云 Redis。
- 服务启动不自动迁移线上 schema。
每个服务都需要自己的 MySQL init 目录:
services/user-service/deploy/mysql/initdb/001_user_service.sql
services/room-service/deploy/mysql/initdb/001_room_service.sql
services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql
services/activity-service/deploy/mysql/initdb/001_activity_service.sql
deploy/mysql/initdb/006_admin_database.sql
本地开发库可以整体清空后重放 initdb;线上 schema 迁移必须用专门 migration,不允许依赖容器 init hook。
memory storage 只用于单元测试,不作为本地联调默认路径。
Health And Lifecycle
三个服务都必须注册标准 gRPC Health Checking Protocol。
基础生命周期:
starting -> serving -> draining -> stopped
关闭流程:
- 标记 draining。
- health 变成
NOT_SERVING。 - 停止接收新请求。
- 等待当前请求完成。
- 关闭 gRPC server。
- 关闭数据库连接和 consumer。
activity-service 额外需要:
- 停止拉取新事件。
- 等当前批次处理结束。
- 提交或回滚消费位点。
Test Baseline
每个服务至少需要四类测试。
Contract Tests
覆盖 proto 契约和 transport 行为:
- 成功 response 字段稳定。
- 业务错误返回正确 gRPC code 和 reason。
- 未知枚举有安全默认行为。
Service Tests
覆盖 service 层:
- 不依赖真实 gRPC。
- 用隔离 MySQL schema 和生产 repository。
- 验证核心用例和错误分支。
Storage Tests
覆盖 MySQL repository:
- 唯一键冲突。
- 事务回滚。
- 幂等命中。
- 并发冲突。
Integration Tests
覆盖跨服务调用:
- gateway -> user。
- room -> wallet。
- room outbox -> activity。
首版可以先只跑单服务集成测试,跨服务集成后续再补。
Implementation Order
推荐顺序:
- 写
docs/gRPC错误模型.md或把本文件错误模型抽成独立文档。 - 扩展
pkg/xerr,支持 reason 和 transport 映射。 - 新增
api/proto/user/v1/auth.proto和user.proto。 - 补
user-service的 app/service/domain/storage/transport 骨架。 - 补
user-serviceMySQL schema。 - gateway 增加 user client 和
/api/v1/auth/*。 - 补
wallet-serviceledger/idempotency/domain/storage。 - 补
wallet-serviceMySQL schema。 - 补
activity-serviceproto、consumer、consumption storage。 - 补
activity-serviceMySQL schema。
不要同时把三个服务的业务都铺开。先让一个服务具备完整工程闭环,再复制结构到其他服务。
Acceptance Criteria
一个服务骨架完成的标准:
- 有稳定 protobuf contract。
- 有 gRPC server 注册真实 service。
- 有 domain/service/storage/transport 分层。
- 有 MySQL schema。
- 有 gRPC error reason 映射。
- 有 ready health 检查依赖。
- 有至少一组 service 单测和 transport 错误测试。
- 不依赖其他服务的
internal包。 - 本地 Docker 能以
13xxx端口启动。