388 lines
19 KiB
Markdown
388 lines
19 KiB
Markdown
# 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 方法名;这是结构整理,不是协议重构。
|
||
|