hyapp-server/docs/wallet-service模块化拆分方案.md
2026-06-22 20:43:39 +08:00

19 KiB
Raw Blame History

wallet-service 模块化拆分方案

结论

wallet-service 需要一起拆三层:service 用例层、storage/mysql 持久化层、transport/grpc 协议适配层。只拆 repository.go 不够,因为当前 service 层的 Repository 接口已经把礼物扣费、充值、红包、资源、VIP、游戏账变、币商和投影混成一个大端口文件变小后边界仍然不清晰。

推荐路线是两阶段:

阶段 目标 变更方式
第一阶段 低风险整理文件,不改行为 保持 walletservice.Servicemysql.Repository 和 gRPC 注册方式不变,只在同 package 内按业务域拆文件,并把大 Repository 接口拆成嵌入式能力接口
第二阶段 真正目录模块化 抽出 portsusecase/* 子包,wallet.Service 只保留门面方法transport 继续依赖门面,避免 gRPC 层直接感知内部用例拆分

不要第一步就把 MySQL repository 拆成多个 Go package。钱包账务依赖同一个 *sql.Tx 保证账户、交易、分录、outbox 和幂等结果同事务提交;过早拆 package 会迫使大量事务 helper、metadata 和 outbox 构造函数导出,风险比收益大。

当前结构和问题

当前 wallet-service 的主要文件规模:

路径 行数 问题
internal/storage/mysql/repository.go 5089 混合核心账本、送礼、活动奖励、游戏账变、币商、工资兑换、outbox、账号锁、receipt 和 metadata
internal/storage/mysql/resource_repository.go 3987 资源目录、资源组、礼物配置、发放、穿戴、商城和资源 outbox 混在一起
internal/service/wallet/service.go 1259 service 门面、依赖注入、礼物、工资、币商、奖励、游戏、充值、VIP、投影混在一个文件
internal/service/wallet/service_test.go 5625 测试文件过大,说明 service 用例边界没有被稳定拆开
internal/transport/grpc/server.go 1389 gRPC handler 和 proto/domain mapper 混合,资源和红包已经拆出一部分,但主文件仍覆盖过多业务
internal/app/app.go 576 进程装配、MQ 生命周期、outbox worker、投影 consumer、三方支付补偿和红包过期 worker 混合
internal/domain/ledger/ledger.go 1407 账本领域对象混合所有业务命令和回执
internal/domain/resource/resource.go 729 资源目录、礼物配置、用户权益、商城和发放对象混合

当前最核心的问题不是单个文件长,而是变化原因互相交叉:

变化原因 当前落点 应独立出来的边界
送礼扣费和背包送礼 service.go + repository.go + resource_repository.go gift ledger
后台调账、活动奖励、游戏账变 service.go + repository.go ledger operations
Google、H5、MiFaPay、V5Pay、USDT service.go + external_recharge.go + third_party_payment_repository.go recharge
资源、礼物、穿戴、商城 resource.go + resource_repository.go + transport/grpc/resource.go resource
VIP 和钱包首页查询 service.go + app_wallet_repository.go app wallet
红包 red_packet.go + red_packet_repository.go + transport/grpc/red_packet.go red packet
钱包 outbox 和投影 app.go + service.go + repository.go + projection files outbox/projection
主播工资、币商库存和工资换币 service.go + repository.go + host_salary_settlement.go host salary / coin seller

拆分原则

  1. wallet-service 仍然是账务事实 owner。余额、交易、分录和 wallet_outbox 不拆到其他服务。
  2. wallet_outbox 仍然只由 wallet-service 直接读取和发布。下游服务只能消费 RocketMQ 或自己的投影表,不能回扫钱包库补业务事实。
  3. 第一阶段不改 protobuf、不改 gRPC 方法名、不改 walletservice.New(repository, activity) 装配方式。
  4. MySQL 第一阶段保持一个 mysql.Repository 类型,方法可以拆文件,但事务 helper 仍留在同 package。
  5. service 第一阶段保持一个 wallet.Service 门面,外部调用不变;先拆文件和接口,再考虑 usecase 子包。
  6. 每一步只搬同一业务域的方法、metadata、request hash、receipt 和 outbox event避免把一个事务链路拆成多次提交。

第一阶段目标结构

service 层

保留 package

services/wallet-service/internal/service/wallet/

第一阶段目标文件:

service.go                  # Service struct、New、依赖注入 setter、通用 guard
repository.go               # Repository 总接口和嵌入式能力接口
clients.go                  # ActivityBadgeClient、GooglePlayClient、MifaPayClient、V5PayClient、TronUSDTClient
gift.go                     # DebitGift、DebitRobotGift、BatchDebitGift
balance.go                  # GetBalances、GetWalletOverview、GetWalletValueSummary、GetUserGiftWall、ListWalletTransactions
host_salary.go              # GetActiveHostSalaryPolicy、GetHostSalaryProgress、ProcessHostSalarySettlementBatch
coin_seller.go              # AdminCreditCoinSellerStock、TransferCoinFromSeller、ExchangeSalaryToCoin、TransferSalaryToCoinSeller
admin_ledger.go             # AdminCreditAsset
reward.go                   # CreditTaskReward、CreditLuckyGiftReward、CreditWheelReward、CreditRoomTurnoverReward、CreditInviteActivityReward、CreditAgencyOpeningReward
game.go                     # ApplyGameCoinChange
recharge.go                 # ListRechargeBills、ListRechargeProducts、ListAdminRechargeProducts、Create/Update/DeleteRechargeProduct、ConfirmGooglePayment
external_recharge.go        # H5 三方充值、通知、查单补偿,保留现有文件但只放外部充值
vip.go                      # ListVipPackages、GetMyVip、PurchaseVip、GrantVip、ListAdminVipLevels、UpdateAdminVipLevels
red_packet.go               # 红包用例,保留现有文件
resource.go                 # 资源目录、资源组、礼物配置、发放、穿戴、商城,后续再细拆
projection.go               # ProjectPendingGiftWallEvents、ProcessWalletProjectionMessage、ProjectPendingBadgeGrantEvents

repository.go 建议按 room-service 的方式拆成嵌入式能力接口:

type Repository interface {
	GiftLedgerStore
	BalanceStore
	HostSalaryStore
	CoinSellerStore
	AdminLedgerStore
	RewardLedgerStore
	GameLedgerStore
	RechargeStore
	ExternalRechargeStore
	VIPStore
	RedPacketStore
	ResourceCatalogStore
	ResourceGrantStore
	ResourceEquipmentStore
	ResourceShopStore
	WalletProjectionStore
	BadgeProjectionStore
}

接口边界建议:

接口 方法范围
GiftLedgerStore DebitGiftBatchDebitGift
BalanceStore GetBalances、钱包首页、钱包流水、礼物墙
HostSalaryStore 工资政策、工资进度、工资结算批处理
CoinSellerStore 币商库存、币商转币、工资换币
AdminLedgerStore 后台调账
RewardLedgerStore 任务、幸运礼物、转盘、房间流水、邀请、开业奖励
GameLedgerStore 游戏扣款、返奖、退款、冲正
RechargeStore 充值档位、Google 支付、充值账单
ExternalRechargeStore H5 三方支付方式、下单、回调、查单补偿
VIPStore VIP 包、购买、发放、后台配置
RedPacketStore 红包配置、创建、领取、过期退款、后台重试
ResourceCatalogStore 资源、资源组、礼物配置、礼物类型
ResourceGrantStore 单资源发放、资源组发放、发放记录
ResourceEquipmentStore 用户权益、穿戴、卸下、批量读取穿戴
ResourceShopStore 资源商城档位、购买记录、购买
WalletProjectionStore 礼物墙投影和 wallet outbox message 投影入口
BadgeProjectionStore badge grant 投影 claim、delivered、failed

storage/mysql 层

保留 package

services/wallet-service/internal/storage/mysql/

repository.go 第一阶段只保留:

Repository struct
Open / Close / Ping
通用常量中真正跨业务域使用的部分

建议拆出的文件:

schema.go                     # ensure*Schema、重复列/重复索引兼容判断
account.go                    # walletAccount、lockAccount、lockAccountsOrdered、applyAccountDelta
transaction.go                # transactionRow、lookupTransaction、insertTransaction、transactionID、request hash 公共能力
entry.go                      # walletEntry、insertEntry、balanceAfterTransaction
outbox.go                     # WalletOutboxRecord、WalletOutboxClaimFilter、Claim/Mark/insertWalletOutbox、事件类型过滤
gift_ledger.go                # DebitGift、BatchDebitGift、giftDebitBizType、背包送礼消耗
gift_pricing.go               # resolveGiftPrice、resolveGiftDiamondRatio、resolveActiveGiftConfig
gift_receipt.go               # giftMetadata、receiptFromGiftMetadata、gift outbox events
host_diamond.go               # creditHostPeriodDiamonds、host period diamond account/entry
admin_ledger.go               # AdminCreditAsset、adminCreditMetadata、adminCreditRequestHash
reward_ledger.go              # 各活动奖励入账、metadata、receipt、request hash、outbox event
game_ledger.go                # ApplyGameCoinChange、gameCoinMetadata、gameBizTypeAndDelta
coin_seller_ledger.go         # stock、seller transfer、salary exchange、salary transfer、相关 metadata/hash/receipt

已存在文件的后续整理:

app_wallet_repository.go       # VIP + 钱包首页较大,后续拆 vip_repository.go / wallet_query_repository.go
resource_repository.go         # 后续拆 resource_catalog.go / resource_group.go / resource_grant.go / user_resource.go / resource_shop.go / resource_outbox.go
third_party_payment_repository.go # 后续拆 payment_method_repository.go / external_recharge_order_repository.go
red_packet_repository.go       # 已独立,但可继续拆 config/create/claim/refund/query

transport/grpc 层

保留 Server 类型和 protobuf 注册方式不变,先按 RPC 业务域拆文件:

server.go                    # Server struct、NewServer、通用 helper
gift.go                      # DebitGift、BatchDebitGift、DebitRobotGift、gift response mapper
balance.go                   # GetBalances、钱包首页、钱包流水、礼物墙 mapper
host_salary.go               # 工资政策、工资进度、cron 结果 mapper
admin_ledger.go              # AdminCreditAsset
coin_seller.go               # 币商库存、转币、工资换币
reward.go                    # 活动奖励 RPC
game.go                      # ApplyGameCoinChange
recharge.go                  # 充值账单、Google、充值档位
external_recharge.go         # H5 三方支付 RPC 和 mapper
vip.go                       # VIP RPC 和 mapper
resource.go                  # 资源 handler后续再拆 catalog/grant/shop/equipment
resource_mapper.go           # resource proto/domain mapper
red_packet.go                # 红包 handler
red_packet_mapper.go         # 红包 proto/domain mapper
cron_server.go               # cron gRPC server保留独立

transport/grpc 的拆分原则是handler 和 mapper 可以同业务域放一起,但一个文件超过 500 行时应把 mapper 单独拆出。transport 只能做 proto/domain 转换和错误透传,不要在这里补账务规则。

app 装配层

internal/app/app.go 可以低风险拆成:

app.go                       # App struct、New、Run、Close
clients.go                   # Activity、Google、MiFaPay、V5Pay、TronGrid 客户端装配
mq.go                        # RocketMQ producer/consumer 创建、start/shutdown
outbox_worker.go             # wallet outbox claim/publish/mark
projection_worker.go         # wallet projection consumer
external_recharge_worker.go  # 外部充值查单补偿 worker
red_packet_worker.go         # 红包过期退款 worker
health.go                    # health HTTP run/close

app 层不承载业务规则。worker 只负责任务循环和调用 service用例幂等必须仍在 service/storage 内保证。

domain 层

第一阶段不拆 package只拆文件

internal/domain/ledger/
  ledger.go                 # 通用常量、AssetBalance、基础类型
  gift.go                   # 送礼 command/receipt
  host_salary.go            # 工资 policy/progress/settlement
  coin_seller.go            # 币商库存、转币、工资换币
  recharge.go               # 充值档位、Google、充值账单
  external_recharge.go      # H5 三方订单、通知、支付方式
  wallet_query.go           # 钱包首页、流水、礼物墙
  vip.go                    # VIP
  reward.go                 # 活动奖励
  game.go                   # 游戏账变
  red_packet.go             # 红包

internal/domain/resource/
  resource.go               # 通用 Resource
  gift_config.go            # GiftConfig、GiftTypeConfig
  group.go                  # ResourceGroup
  grant.go                  # GrantResource、ResourceGrant
  entitlement.go            # UserResourceEntitlement、穿戴
  shop.go                   # ResourceShopItem、PurchaseOrder

领域对象拆文件的目标是让业务域可读,不要把领域对象拆成过细 package。否则 transport、service、storage 都要同时改 import收益不高。

第二阶段目标目录

第一阶段稳定后,再考虑 service 真正目录化。目标是 wallet.Service 作为门面,内部 usecase 子包只依赖 ports 和 domain

services/wallet-service/internal/service/wallet/
  service.go                 # 门面,保留现有对外方法
  ports/
    repository.go            # repository 能力接口
    clients.go               # 三方 client 能力接口
    clock.go                 # now/clock 抽象,如需要
  usecase/
    gift/
    balance/
    hostsalary/
    coinseller/
    adminledger/
    reward/
    game/
    recharge/
    externalrecharge/
    vip/
    redpacket/
    resource/
    projection/

依赖方向:

transport/grpc -> service/wallet facade -> service/wallet/usecase/* -> service/wallet/ports
storage/mysql  -> implements service/wallet/ports.Repository
client/*       -> implements service/wallet/ports third-party clients
domain/*       -> shared pure data and validation helpers

第二阶段不要让 transport/grpc 直接调用 usecase/*。gRPC 层直接感知所有 usecase 会把装配复杂度扩散到 transport后续新增 RPC 时更容易乱。

推荐实施顺序

1. service 低风险拆文件

先拆 internal/service/wallet/service.go,不改任何方法签名:

  1. 新建 repository.go,移动 Repository interface,并改成嵌入式能力接口。
  2. 新建 clients.go,移动 Activity、Google、MiFaPay、V5Pay、TronUSDT client 接口。
  3. 按业务域移动 Service 方法到 gift.gohost_salary.gocoin_seller.goreward.gogame.gorecharge.govip.gobalance.goprojection.go
  4. 每移动一个业务域,运行 go test ./services/wallet-service/internal/service/wallet

2. transport 跟随拆分

server.go 只保留 ServerNewServer 和通用 helper。按 service 文件域拆 handler 文件mapper 超过 500 行时单独放 *_mapper.go

验证:

go test ./services/wallet-service/internal/transport/grpc

3. storage/mysql 拆核心 repository

先拆基础设施,再拆业务:

  1. schema.go
  2. account.go
  3. transaction.go
  4. entry.go
  5. outbox.go
  6. gift_ledger.gogift_pricing.gogift_receipt.gohost_diamond.go
  7. admin_ledger.goreward_ledger.gogame_ledger.gocoin_seller_ledger.go

每一步只搬代码,不改 SQL不改事务边界不改 outbox payload。

验证:

go test ./services/wallet-service/internal/storage/mysql
go test ./services/wallet-service/...

4. 拆 resource_repository.go

resource_repository.go 单独作为第二个大 storage 任务处理,避免和核心账本拆分混在一起:

resource_catalog.go
resource_group.go
resource_grant.go
user_resource.go
resource_equipment.go
resource_shop.go
resource_outbox.go

这一步要特别保护背包送礼链路,因为 gift_ledger.go 会调用用户权益消耗相关 helper。

5. app worker 拆分

最后拆 internal/app/app.go。原因是 app 层只是装配和 worker 循环,等 service/storage 边界清晰后再拆更不容易来回调整。

验证:

go test ./services/wallet-service/...

每阶段完成标准

阶段 完成标准
service 拆分 service.go 只保留 Service 结构、构造和依赖 setterRepository 变成嵌入式能力接口service 单测通过
transport 拆分 server.go 不再承载大批业务 handlermapper 按业务域归位transport 单测通过
storage 核心拆分 repository.go 小于 250 行;核心账本 helper 有明确文件wallet storage 单测通过
resource storage 拆分 resource_repository.go 小于 500 行;资源目录、发放、穿戴、商城各自可定位;资源 storage 单测通过
app 拆分 app.go 只负责生命周期worker 文件按 MQ/outbox/projection/reconcile/expiry 分开wallet-service 全包测试通过

最终验证:

go test ./services/wallet-service/...
make test

如果 make test 暴露其他服务或既有配置失败,需要单独记录为基线问题,不能把它当成钱包拆分失败,也不能因为无关失败回滚已验证的 wallet-service 拆分。

风险点

风险 规避方式
事务 helper 被跨 package 导出 第一阶段 storage 保持同 package只拆文件
request hash 或 biz type 和原逻辑不一致 每个业务域移动时把 command、metadata、request hash、receipt、outbox event 一起移动
service 接口拆完但测试 mock 更复杂 Repository 保持嵌入式组合,测试按用例只依赖小接口
transport 拆分后 mapper 找不到 handler 和 mapper 同域移动,通用 mapper 留在 server.gomapper.go
app worker 拆早导致来回改 app 层最后拆,等 service 方法和 storage outbox 边界稳定
背包送礼跨 resource 和 gift ledger consumeGiftEntitlementForSend 先留在 gift ledger 同 package第二阶段再决定是否抽资源权益端口

不建议做的事

  1. 不要第一步创建多个 MySQL repository 实例。账务事务必须共享同一个连接池和同一个事务提交边界。
  2. 不要让 room-serviceactivity-servicenotice-service 直连钱包库查询交易明细。
  3. 不要把 outbox 发布状态交给下游服务更新。wallet outbox 位点仍归 wallet-service。
  4. 不要在 transport 层补业务幂等、扣费判断或支付状态机。
  5. 不要为了拆目录改变 protobuf 字段或 gRPC 方法名;这是结构整理,不是协议重构。