hyapp-server/docs/服务基础技术设计.md
2026-05-27 17:27:13 +08:00

16 KiB
Raw Permalink Blame History

Service Foundation Technical Design

本文档定义 user-servicewallet-serviceactivity-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、事件处理骨架

补齐顺序不能按文件数量排,应该按跨服务依赖排:

  1. 统一 gRPC 错误模型。
  2. user-service 身份基础。
  3. wallet-service 账务基础。
  4. 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 不能直接写复杂业务规则。业务规则进 servicedomain

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_idroom_idcommand_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 承载:

  • LoginPassword
  • LoginThirdParty
  • SetPassword
  • RefreshToken
  • Logout

user.proto 首版只放基础查询:

  • GetUser
  • BatchGetUsers

不要一开始把资料编辑、关系链、黑名单全塞进去。

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 保存有效邀请策略,默认累计充值 80000 COIN,但必须允许后台按 App/区域配置。
  • user_invite_recharge_progress 保存被邀请用户累计充值进度和是否已成为有效邀请。
  • user_invite_event_consumption 保存钱包充值事件消费幂等,避免 wallet outbox 重投导致重复累计。
  • user_invite_counters 保存我的页和后台列表使用的邀请计数 read model包含 invite_countvalid_invite_count
  • password_accounts 保存已登录用户手动设置后的密码 hash不承载独立账号体系。
  • third_party_identities 保存 provider 绑定。
  • auth_sessions 保存 refresh token hash 和会话状态。
  • login_audit 不影响主链路成功与否。
  • user_country_change_logs 保存国家修改审计,并作为滚动 30 天冷却判断来源。
  • user_outbox 保存 UserRegisteredUserInvitedUserInviteBecameValid 等用户领域事件,供奖励、活动、消息和风控异步消费。

邀请统计规则:

  • invite_count 表示成功填写邀请码并绑定关系的人数,来源是 user_invite_relations
  • valid_invite_count 表示被邀请用户累计成功充值金币达到有效邀请策略门槛的人数,默认门槛是 80000 COIN
  • 有效邀请只消费 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_ididempotency_key
  • 响应内必须有 billing_receipt_id

后续再追加:

  • CreditLuckyReward
  • GetBalance
  • Freeze
  • Unfreeze

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 可以只定义:

  • PingActivity
  • GetActivityStatus

活动消费本身可以先不走 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-platform/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

关闭流程:

  1. 标记 draining。
  2. health 变成 NOT_SERVING
  3. 停止接收新请求。
  4. 等待当前请求完成。
  5. 关闭 gRPC server。
  6. 关闭数据库连接和 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

推荐顺序:

  1. docs/gRPC错误模型.md 或把本文件错误模型抽成独立文档。
  2. 扩展 pkg/xerr,支持 reason 和 transport 映射。
  3. 新增 api/proto/user/v1/auth.protouser.proto
  4. user-service 的 app/service/domain/storage/transport 骨架。
  5. user-service MySQL schema。
  6. gateway 增加 user client 和 /api/v1/auth/*
  7. wallet-service ledger/idempotency/domain/storage。
  8. wallet-service MySQL schema。
  9. activity-service proto、consumer、consumption storage。
  10. activity-service MySQL schema。

不要同时把三个服务的业务都铺开。先让一个服务具备完整工程闭环,再复制结构到其他服务。

Acceptance Criteria

一个服务骨架完成的标准:

  • 有稳定 protobuf contract。
  • 有 gRPC server 注册真实 service。
  • 有 domain/service/storage/transport 分层。
  • 有 MySQL schema。
  • 有 gRPC error reason 映射。
  • 有 ready health 检查依赖。
  • 有至少一组 service 单测和 transport 错误测试。
  • 不依赖其他服务的 internal 包。
  • 本地 Docker 能以 13xxx 端口启动。