19 KiB
wallet-service 模块化拆分方案
结论
wallet-service 需要一起拆三层:service 用例层、storage/mysql 持久化层、transport/grpc 协议适配层。只拆 repository.go 不够,因为当前 service 层的 Repository 接口已经把礼物扣费、充值、红包、资源、VIP、游戏账变、币商和投影混成一个大端口,文件变小后边界仍然不清晰。
推荐路线是两阶段:
| 阶段 | 目标 | 变更方式 |
|---|---|---|
| 第一阶段 | 低风险整理文件,不改行为 | 保持 walletservice.Service、mysql.Repository 和 gRPC 注册方式不变,只在同 package 内按业务域拆文件,并把大 Repository 接口拆成嵌入式能力接口 |
| 第二阶段 | 真正目录模块化 | 抽出 ports 和 usecase/* 子包,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 |
拆分原则
wallet-service仍然是账务事实 owner。余额、交易、分录和wallet_outbox不拆到其他服务。wallet_outbox仍然只由 wallet-service 直接读取和发布。下游服务只能消费 RocketMQ 或自己的投影表,不能回扫钱包库补业务事实。- 第一阶段不改 protobuf、不改 gRPC 方法名、不改
walletservice.New(repository, activity)装配方式。 - MySQL 第一阶段保持一个
mysql.Repository类型,方法可以拆文件,但事务 helper 仍留在同 package。 - service 第一阶段保持一个
wallet.Service门面,外部调用不变;先拆文件和接口,再考虑 usecase 子包。 - 每一步只搬同一业务域的方法、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 |
DebitGift、BatchDebitGift |
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,不改任何方法签名:
- 新建
repository.go,移动Repository interface,并改成嵌入式能力接口。 - 新建
clients.go,移动 Activity、Google、MiFaPay、V5Pay、TronUSDT client 接口。 - 按业务域移动 Service 方法到
gift.go、host_salary.go、coin_seller.go、reward.go、game.go、recharge.go、vip.go、balance.go、projection.go。 - 每移动一个业务域,运行
go test ./services/wallet-service/internal/service/wallet。
2. transport 跟随拆分
server.go 只保留 Server、NewServer 和通用 helper。按 service 文件域拆 handler 文件,mapper 超过 500 行时单独放 *_mapper.go。
验证:
go test ./services/wallet-service/internal/transport/grpc
3. storage/mysql 拆核心 repository
先拆基础设施,再拆业务:
schema.goaccount.gotransaction.goentry.gooutbox.gogift_ledger.go、gift_pricing.go、gift_receipt.go、host_diamond.goadmin_ledger.go、reward_ledger.go、game_ledger.go、coin_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 结构、构造和依赖 setter;Repository 变成嵌入式能力接口;service 单测通过 |
| transport 拆分 | server.go 不再承载大批业务 handler;mapper 按业务域归位;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.go 或 mapper.go |
| app worker 拆早导致来回改 | app 层最后拆,等 service 方法和 storage outbox 边界稳定 |
| 背包送礼跨 resource 和 gift ledger | consumeGiftEntitlementForSend 先留在 gift ledger 同 package;第二阶段再决定是否抽资源权益端口 |
不建议做的事
- 不要第一步创建多个 MySQL repository 实例。账务事务必须共享同一个连接池和同一个事务提交边界。
- 不要让
room-service、activity-service或notice-service直连钱包库查询交易明细。 - 不要把 outbox 发布状态交给下游服务更新。wallet outbox 位点仍归 wallet-service。
- 不要在 transport 层补业务幂等、扣费判断或支付状态机。
- 不要为了拆目录改变 protobuf 字段或 gRPC 方法名;这是结构整理,不是协议重构。