# 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 方法名；这是结构整理，不是协议重构。

