# 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 | ## 拆分原则 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: ```text services/wallet-service/internal/service/wallet/ ``` 第一阶段目标文件: ```text 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 的方式拆成嵌入式能力接口: ```go 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: ```text services/wallet-service/internal/storage/mysql/ ``` `repository.go` 第一阶段只保留: ```text Repository struct Open / Close / Ping 通用常量中真正跨业务域使用的部分 ``` 建议拆出的文件: ```text 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 ``` 已存在文件的后续整理: ```text 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 业务域拆文件: ```text 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` 可以低风险拆成: ```text 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,只拆文件: ```text 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: ```text 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/ ``` 依赖方向: ```text 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.go`、`host_salary.go`、`coin_seller.go`、`reward.go`、`game.go`、`recharge.go`、`vip.go`、`balance.go`、`projection.go`。 4. 每移动一个业务域,运行 `go test ./services/wallet-service/internal/service/wallet`。 ### 2. transport 跟随拆分 `server.go` 只保留 `Server`、`NewServer` 和通用 helper。按 service 文件域拆 handler 文件,mapper 超过 500 行时单独放 `*_mapper.go`。 验证: ```bash 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.go`、`gift_pricing.go`、`gift_receipt.go`、`host_diamond.go` 7. `admin_ledger.go`、`reward_ledger.go`、`game_ledger.go`、`coin_seller_ledger.go` 每一步只搬代码,不改 SQL,不改事务边界,不改 outbox payload。 验证: ```bash go test ./services/wallet-service/internal/storage/mysql go test ./services/wallet-service/... ``` ### 4. 拆 resource_repository.go `resource_repository.go` 单独作为第二个大 storage 任务处理,避免和核心账本拆分混在一起: ```text 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 边界清晰后再拆更不容易来回调整。 验证: ```bash 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 全包测试通过 | 最终验证: ```bash 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;第二阶段再决定是否抽资源权益端口 | ## 不建议做的事 1. 不要第一步创建多个 MySQL repository 实例。账务事务必须共享同一个连接池和同一个事务提交边界。 2. 不要让 `room-service`、`activity-service` 或 `notice-service` 直连钱包库查询交易明细。 3. 不要把 outbox 发布状态交给下游服务更新。wallet outbox 位点仍归 wallet-service。 4. 不要在 transport 层补业务幂等、扣费判断或支付状态机。 5. 不要为了拆目录改变 protobuf 字段或 gRPC 方法名;这是结构整理,不是协议重构。