feat: remove generic USD wallet balance
This commit is contained in:
parent
858b0993bc
commit
679d068a82
File diff suppressed because it is too large
Load Diff
@ -734,7 +734,6 @@ message ListRechargeBillsResponse {
|
|||||||
message WalletFeatureFlags {
|
message WalletFeatureFlags {
|
||||||
bool recharge_enabled = 1;
|
bool recharge_enabled = 1;
|
||||||
bool diamond_exchange_enabled = 2;
|
bool diamond_exchange_enabled = 2;
|
||||||
bool withdraw_enabled = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetWalletOverviewRequest {
|
message GetWalletOverviewRequest {
|
||||||
@ -976,32 +975,6 @@ message ListWalletTransactionsResponse {
|
|||||||
int64 total = 2;
|
int64 total = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WithdrawalRequest {
|
|
||||||
string withdrawal_id = 1;
|
|
||||||
int64 user_id = 2;
|
|
||||||
string asset_type = 3;
|
|
||||||
int64 amount = 4;
|
|
||||||
string status = 5;
|
|
||||||
string payout_account = 6;
|
|
||||||
string reason = 7;
|
|
||||||
int64 created_at_ms = 8;
|
|
||||||
int64 updated_at_ms = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ApplyWithdrawalRequest {
|
|
||||||
string command_id = 1;
|
|
||||||
string app_code = 2;
|
|
||||||
int64 user_id = 3;
|
|
||||||
int64 amount = 4;
|
|
||||||
string payout_account = 5;
|
|
||||||
string reason = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ApplyWithdrawalResponse {
|
|
||||||
WithdrawalRequest withdrawal = 1;
|
|
||||||
AssetBalance balance = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message VipRewardItem {
|
message VipRewardItem {
|
||||||
int64 resource_id = 1;
|
int64 resource_id = 1;
|
||||||
string resource_code = 2;
|
string resource_code = 2;
|
||||||
@ -1437,7 +1410,6 @@ service WalletService {
|
|||||||
rpc DeleteRechargeProduct(DeleteRechargeProductRequest) returns (DeleteRechargeProductResponse);
|
rpc DeleteRechargeProduct(DeleteRechargeProductRequest) returns (DeleteRechargeProductResponse);
|
||||||
rpc GetDiamondExchangeConfig(GetDiamondExchangeConfigRequest) returns (GetDiamondExchangeConfigResponse);
|
rpc GetDiamondExchangeConfig(GetDiamondExchangeConfigRequest) returns (GetDiamondExchangeConfigResponse);
|
||||||
rpc ListWalletTransactions(ListWalletTransactionsRequest) returns (ListWalletTransactionsResponse);
|
rpc ListWalletTransactions(ListWalletTransactionsRequest) returns (ListWalletTransactionsResponse);
|
||||||
rpc ApplyWithdrawal(ApplyWithdrawalRequest) returns (ApplyWithdrawalResponse);
|
|
||||||
rpc ListVipPackages(ListVipPackagesRequest) returns (ListVipPackagesResponse);
|
rpc ListVipPackages(ListVipPackagesRequest) returns (ListVipPackagesResponse);
|
||||||
rpc GetMyVip(GetMyVipRequest) returns (GetMyVipResponse);
|
rpc GetMyVip(GetMyVipRequest) returns (GetMyVipResponse);
|
||||||
rpc PurchaseVip(PurchaseVipRequest) returns (PurchaseVipResponse);
|
rpc PurchaseVip(PurchaseVipRequest) returns (PurchaseVipResponse);
|
||||||
|
|||||||
@ -243,7 +243,6 @@ const (
|
|||||||
WalletService_DeleteRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteRechargeProduct"
|
WalletService_DeleteRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteRechargeProduct"
|
||||||
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
|
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
|
||||||
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
|
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
|
||||||
WalletService_ApplyWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyWithdrawal"
|
|
||||||
WalletService_ListVipPackages_FullMethodName = "/hyapp.wallet.v1.WalletService/ListVipPackages"
|
WalletService_ListVipPackages_FullMethodName = "/hyapp.wallet.v1.WalletService/ListVipPackages"
|
||||||
WalletService_GetMyVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GetMyVip"
|
WalletService_GetMyVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GetMyVip"
|
||||||
WalletService_PurchaseVip_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseVip"
|
WalletService_PurchaseVip_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseVip"
|
||||||
@ -311,7 +310,6 @@ type WalletServiceClient interface {
|
|||||||
DeleteRechargeProduct(ctx context.Context, in *DeleteRechargeProductRequest, opts ...grpc.CallOption) (*DeleteRechargeProductResponse, error)
|
DeleteRechargeProduct(ctx context.Context, in *DeleteRechargeProductRequest, opts ...grpc.CallOption) (*DeleteRechargeProductResponse, error)
|
||||||
GetDiamondExchangeConfig(ctx context.Context, in *GetDiamondExchangeConfigRequest, opts ...grpc.CallOption) (*GetDiamondExchangeConfigResponse, error)
|
GetDiamondExchangeConfig(ctx context.Context, in *GetDiamondExchangeConfigRequest, opts ...grpc.CallOption) (*GetDiamondExchangeConfigResponse, error)
|
||||||
ListWalletTransactions(ctx context.Context, in *ListWalletTransactionsRequest, opts ...grpc.CallOption) (*ListWalletTransactionsResponse, error)
|
ListWalletTransactions(ctx context.Context, in *ListWalletTransactionsRequest, opts ...grpc.CallOption) (*ListWalletTransactionsResponse, error)
|
||||||
ApplyWithdrawal(ctx context.Context, in *ApplyWithdrawalRequest, opts ...grpc.CallOption) (*ApplyWithdrawalResponse, error)
|
|
||||||
ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error)
|
ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error)
|
||||||
GetMyVip(ctx context.Context, in *GetMyVipRequest, opts ...grpc.CallOption) (*GetMyVipResponse, error)
|
GetMyVip(ctx context.Context, in *GetMyVipRequest, opts ...grpc.CallOption) (*GetMyVipResponse, error)
|
||||||
PurchaseVip(ctx context.Context, in *PurchaseVipRequest, opts ...grpc.CallOption) (*PurchaseVipResponse, error)
|
PurchaseVip(ctx context.Context, in *PurchaseVipRequest, opts ...grpc.CallOption) (*PurchaseVipResponse, error)
|
||||||
@ -759,16 +757,6 @@ func (c *walletServiceClient) ListWalletTransactions(ctx context.Context, in *Li
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *walletServiceClient) ApplyWithdrawal(ctx context.Context, in *ApplyWithdrawalRequest, opts ...grpc.CallOption) (*ApplyWithdrawalResponse, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(ApplyWithdrawalResponse)
|
|
||||||
err := c.cc.Invoke(ctx, WalletService_ApplyWithdrawal_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *walletServiceClient) ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error) {
|
func (c *walletServiceClient) ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListVipPackagesResponse)
|
out := new(ListVipPackagesResponse)
|
||||||
@ -987,7 +975,6 @@ type WalletServiceServer interface {
|
|||||||
DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error)
|
DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error)
|
||||||
GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error)
|
GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error)
|
||||||
ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error)
|
ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error)
|
||||||
ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error)
|
|
||||||
ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error)
|
ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error)
|
||||||
GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error)
|
GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error)
|
||||||
PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error)
|
PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error)
|
||||||
@ -1141,9 +1128,6 @@ func (UnimplementedWalletServiceServer) GetDiamondExchangeConfig(context.Context
|
|||||||
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
|
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ListWalletTransactions not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ListWalletTransactions not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedWalletServiceServer) ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method ApplyWithdrawal not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
|
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ListVipPackages not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ListVipPackages not implemented")
|
||||||
}
|
}
|
||||||
@ -1972,24 +1956,6 @@ func _WalletService_ListWalletTransactions_Handler(srv interface{}, ctx context.
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _WalletService_ApplyWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(ApplyWithdrawalRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(WalletServiceServer).ApplyWithdrawal(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: WalletService_ApplyWithdrawal_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(WalletServiceServer).ApplyWithdrawal(ctx, req.(*ApplyWithdrawalRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _WalletService_ListVipPackages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _WalletService_ListVipPackages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListVipPackagesRequest)
|
in := new(ListVipPackagesRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -2471,10 +2437,6 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListWalletTransactions",
|
MethodName: "ListWalletTransactions",
|
||||||
Handler: _WalletService_ListWalletTransactions_Handler,
|
Handler: _WalletService_ListWalletTransactions_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "ApplyWithdrawal",
|
|
||||||
Handler: _WalletService_ApplyWithdrawal_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "ListVipPackages",
|
MethodName: "ListVipPackages",
|
||||||
Handler: _WalletService_ListVipPackages_Handler,
|
Handler: _WalletService_ListVipPackages_Handler,
|
||||||
|
|||||||
@ -138,7 +138,7 @@ GET /api/v1/wallet/me/balances
|
|||||||
示例:
|
示例:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=USD_BALANCE
|
GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=HOST_SALARY_USD&asset_type=AGENCY_SALARY_USD&asset_type=BD_SALARY_USD&asset_type=ADMIN_SALARY_USD
|
||||||
```
|
```
|
||||||
|
|
||||||
### 返回值 `data`
|
### 返回值 `data`
|
||||||
@ -158,7 +158,7 @@ GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=USD
|
|||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| ------------------ | ------ | ------------------------------------------------- |
|
| ------------------ | ------ | ------------------------------------------------- |
|
||||||
| `asset_type` | string | `COIN` / `DIAMOND` / `USD_BALANCE` / `GIFT_POINT` |
|
| `asset_type` | string | `COIN` / `DIAMOND` / `HOST_SALARY_USD` / `AGENCY_SALARY_USD` / `BD_SALARY_USD` / `ADMIN_SALARY_USD` / `GIFT_POINT` |
|
||||||
| `available_amount` | int64 | 可用余额 |
|
| `available_amount` | int64 | 可用余额 |
|
||||||
| `frozen_amount` | int64 | 冻结余额 |
|
| `frozen_amount` | int64 | 冻结余额 |
|
||||||
| `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 |
|
| `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 |
|
||||||
@ -201,7 +201,7 @@ GET /api/v1/wallet/me/overview
|
|||||||
"version": 4
|
"version": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"asset_type": "USD_BALANCE",
|
"asset_type": "HOST_SALARY_USD",
|
||||||
"available_amount": 1500,
|
"available_amount": 1500,
|
||||||
"frozen_amount": 0,
|
"frozen_amount": 0,
|
||||||
"version": 2
|
"version": 2
|
||||||
@ -209,8 +209,7 @@ GET /api/v1/wallet/me/overview
|
|||||||
],
|
],
|
||||||
"feature_flags": {
|
"feature_flags": {
|
||||||
"recharge_enabled": true,
|
"recharge_enabled": true,
|
||||||
"diamond_exchange_enabled": true,
|
"diamond_exchange_enabled": true
|
||||||
"withdraw_enabled": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -220,7 +219,6 @@ GET /api/v1/wallet/me/overview
|
|||||||
| `balances` | array | 钱包首页需要展示的资产余额 |
|
| `balances` | array | 钱包首页需要展示的资产余额 |
|
||||||
| `feature_flags.recharge_enabled` | bool | 是否允许充值 |
|
| `feature_flags.recharge_enabled` | bool | 是否允许充值 |
|
||||||
| `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 |
|
| `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 |
|
||||||
| `feature_flags.withdraw_enabled` | bool | 是否允许提现 |
|
|
||||||
|
|
||||||
### 可能错误
|
### 可能错误
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本文只定义后台/Admin 侧实现:政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。
|
本文只定义后台/Admin 侧实现:政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。
|
||||||
|
|
||||||
这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有,Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
|
这里的工资统一指发放到主播侧 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有,Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
|
||||||
|
|
||||||
本文不新增独立 `host-service` 微服务。host、Agency、BD、政策和工资后台能力属于 user-services,代码落在当前仓库的 `services/user-service` host domain。
|
本文不新增独立 `host-service` 微服务。host、Agency、BD、政策和工资后台能力属于 user-services,代码落在当前仓库的 `services/user-service` host domain。
|
||||||
|
|
||||||
@ -11,7 +11,7 @@
|
|||||||
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
|
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
|
||||||
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
|
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
|
||||||
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
|
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
|
||||||
- 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service` 给 `USD_BALANCE` 入账。
|
- 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service` 给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账。
|
||||||
- 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
|
- 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
|
||||||
- 后台可以创建或停用 BD Leader、BD、Agency,也可以关闭 Agency;后台可以查看和新增币商 `coin_seller`;所有动作必须有审计记录。
|
- 后台可以创建或停用 BD Leader、BD、Agency,也可以关闭 Agency;后台可以查看和新增币商 `coin_seller`;所有动作必须有审计记录。
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ graph LR
|
|||||||
| `hyapp-admin-server` | Admin HTTP 入口、后台鉴权、request envelope、后台操作审计;使用独立 `hyapp_admin` 库 |
|
| `hyapp-admin-server` | Admin HTTP 入口、后台鉴权、request envelope、后台操作审计;使用独立 `hyapp_admin` 库 |
|
||||||
| `user-service` | 用户、区域、账号状态查询;host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outbox;App/客户端不能提交区域,后台关系命令的 `region_id` 只能来自 `hyapp-admin-server` 鉴权审计入口 |
|
| `user-service` | 用户、区域、账号状态查询;host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outbox;App/客户端不能提交区域,后台关系命令的 `region_id` 只能来自 `hyapp-admin-server` 鉴权审计入口 |
|
||||||
| `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 |
|
| `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 |
|
||||||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`USD_BALANCE` 入账、提现冻结、提现审核状态和钱包流水 |
|
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账、提现冻结、提现审核状态和钱包流水 |
|
||||||
|
|
||||||
## Admin Permission Model
|
## Admin Permission Model
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ flowchart LR
|
|||||||
E --> F
|
E --> F
|
||||||
F --> G["approve"]
|
F --> G["approve"]
|
||||||
G --> H["wallet CreditSalaryBalance"]
|
G --> H["wallet CreditSalaryBalance"]
|
||||||
H --> I["USD_BALANCE"]
|
H --> I["identity salary wallet"]
|
||||||
```
|
```
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@ -353,7 +353,7 @@ Leader 直接拥有 Agency 下线时,推荐快照同时设置 `bd_user_id = le
|
|||||||
1. Admin 审批 cycle。
|
1. Admin 审批 cycle。
|
||||||
2. `user-service` host domain 查询所有 `approved` salary items。
|
2. `user-service` host domain 查询所有 `approved` salary items。
|
||||||
3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}`。
|
3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}`。
|
||||||
4. `wallet-service` credit `USD_BALANCE` 并返回 `wallet_transaction_id`。
|
4. `wallet-service` credit `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 并返回 `wallet_transaction_id`。
|
||||||
5. `user-service` host domain 保存 `wallet_transaction_id`,把 item 标记为 `posted`。
|
5. `user-service` host domain 保存 `wallet_transaction_id`,把 item 标记为 `posted`。
|
||||||
6. 如果中途失败,只重试未 posted item,幂等键保持不变。
|
6. 如果中途失败,只重试未 posted item,幂等键保持不变。
|
||||||
|
|
||||||
@ -488,7 +488,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
|||||||
| Host changes Agency during cycle | Salary uses `host_daily_stats` relation snapshot and can split across agencies |
|
| Host changes Agency during cycle | Salary uses `host_daily_stats` relation snapshot and can split across agencies |
|
||||||
| Late room event after posted cycle | Create adjustment item; do not mutate posted salary item |
|
| Late room event after posted cycle | Create adjustment item; do not mutate posted salary item |
|
||||||
| Wallet posting partially fails | Retry unposted items with same idempotency key |
|
| Wallet posting partially fails | Retry unposted items with same idempotency key |
|
||||||
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate `USD_BALANCE` credit |
|
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` credit |
|
||||||
| Agency closed mid-cycle | Work before close remains attributable; future App joins/search stop immediately |
|
| Agency closed mid-cycle | Work before close remains attributable; future App joins/search stop immediately |
|
||||||
| User region changed | Existing cycle uses event snapshots; new policy/cycle matching follows stats region |
|
| User region changed | Existing cycle uses event snapshots; new policy/cycle matching follows stats region |
|
||||||
| Negative adjustment exceeds balance | Move to manual handling; do not force wallet negative unless wallet explicitly supports debt |
|
| Negative adjustment exceeds balance | Move to manual handling; do not force wallet negative unless wallet explicitly supports debt |
|
||||||
@ -507,7 +507,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
|||||||
| Agency salary generated | Base uses host salary items under that Agency only |
|
| Agency salary generated | Base uses host salary items under that Agency only |
|
||||||
| BD Leader owns direct Agency | Source host salary counted once for the leader |
|
| BD Leader owns direct Agency | Source host salary counted once for the leader |
|
||||||
| BD under Leader owns Agency | Host salary contributes to BD and leader according to snapshot |
|
| BD under Leader owns Agency | Host salary contributes to BD and leader according to snapshot |
|
||||||
| Cycle posted twice | Wallet idempotency prevents duplicate `USD_BALANCE` credit |
|
| Cycle posted twice | Wallet idempotency prevents duplicate `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` credit |
|
||||||
| Posting fails after partial success | Retry posts only unposted items |
|
| Posting fails after partial success | Retry posts only unposted items |
|
||||||
| Posted item needs correction | Adjustment item created; original item remains immutable |
|
| Posted item needs correction | Adjustment item created; original item remains immutable |
|
||||||
| Admin closes Agency | Agency hidden from App search; existing facts remain auditable |
|
| Admin closes Agency | Agency hidden from App search; existing facts remain auditable |
|
||||||
@ -515,7 +515,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
|||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
||||||
- Admin owns policies and salary cycles; App owns user-initiated relationship flows.
|
- Admin owns policies and salary cycles; App owns user-initiated relationship flows.
|
||||||
- Wallet is the only owner of `USD_BALANCE` and withdrawal state.
|
- Wallet is the only owner of `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` and withdrawal state.
|
||||||
- Posted salary items are immutable.
|
- Posted salary items are immutable.
|
||||||
- BD/BD Leader base is downstream host salary only.
|
- BD/BD Leader base is downstream host salary only.
|
||||||
- Leader direct Agency salary source must be de-duplicated.
|
- Leader direct Agency salary source must be de-duplicated.
|
||||||
|
|||||||
@ -142,7 +142,7 @@
|
|||||||
- Agency salary:基于下属 host salary item 或明确配置的 agency 指标。
|
- Agency salary:基于下属 host salary item 或明确配置的 agency 指标。
|
||||||
- BD/BD Leader salary:基于 downstream host salary sum,且按 source host salary item 去重。
|
- BD/BD Leader salary:基于 downstream host salary sum,且按 source host salary item 去重。
|
||||||
- 后台审核工资周期。
|
- 后台审核工资周期。
|
||||||
- 调用 `wallet-service` 给 `USD_BALANCE` 入账,幂等键为 `salary:{salary_item_id}`。
|
- 调用 `wallet-service` 给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账,幂等键为 `salary:{salary_item_id}`。
|
||||||
- 支持调整单:迟到事件、人工修正、争议处理都走补差或冲正。
|
- 支持调整单:迟到事件、人工修正、争议处理都走补差或冲正。
|
||||||
- Salary posted 和 adjustment posted 事件接入 App message inbox,通知接收人查看收益明细;通知失败不影响钱包入账事实。
|
- Salary posted 和 adjustment posted 事件接入 App message inbox,通知接收人查看收益明细;通知失败不影响钱包入账事实。
|
||||||
|
|
||||||
@ -182,7 +182,7 @@
|
|||||||
| M3 | Phase 3 | 用户申请、审核、踢出、重新申请、邀请链路完整跑通 |
|
| M3 | Phase 3 | 用户申请、审核、踢出、重新申请、邀请链路完整跑通 |
|
||||||
| M3.5 | Phase 3.5 | 申请、邀请、后台创建关系结果能进入 App 系统消息 |
|
| M3.5 | Phase 3.5 | 申请、邀请、后台创建关系结果能进入 App 系统消息 |
|
||||||
| M4 | Phase 4 | 有效上麦和礼物统计按关系快照落库 |
|
| M4 | Phase 4 | 有效上麦和礼物统计按关系快照落库 |
|
||||||
| M5 | Phase 5 | 后台生成工资单,审批后入账 `USD_BALANCE` |
|
| M5 | Phase 5 | 后台生成工资单,审批后入账 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` |
|
||||||
| M6 | Phase 6 | 失败补偿、导出、重建和监控可用 |
|
| M6 | Phase 6 | 失败补偿、导出、重建和监控可用 |
|
||||||
|
|
||||||
## Stop Conditions
|
## Stop Conditions
|
||||||
@ -191,7 +191,7 @@
|
|||||||
- Phase 2 没有完成前,不做 App Agency 搜索上线,因为没有可信组织种子数据。
|
- Phase 2 没有完成前,不做 App Agency 搜索上线,因为没有可信组织种子数据。
|
||||||
- Phase 3 没有完成前,不做工资发放,因为 host/Agency/BD 归属还不稳定。
|
- Phase 3 没有完成前,不做工资发放,因为 host/Agency/BD 归属还不稳定。
|
||||||
- Phase 4 没有完成前,不做真实工资入账,因为缺少可审计统计输入。
|
- Phase 4 没有完成前,不做真实工资入账,因为缺少可审计统计输入。
|
||||||
- Phase 5 没有完成审批和幂等 posting 前,不允许写 `USD_BALANCE`。
|
- Phase 5 没有完成审批和幂等 posting 前,不允许写 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||||
|
|
||||||
## Verification Commands
|
## Verification Commands
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
- BD 默认不是主播;BD 可以加入别的 Agency 成为主播,也可以在未成为主播时邀请自己成为自己的 Agency。
|
- BD 默认不是主播;BD 可以加入别的 Agency 成为主播,也可以在未成为主播时邀请自己成为自己的 Agency。
|
||||||
- `coin_seller` 是独立币商身份,可以和 Agency、BD、BD Leader 身份并存;币商拥有专用金币账户,可以给玩家转普通金币。
|
- `coin_seller` 是独立币商身份,可以和 Agency、BD、BD Leader 身份并存;币商拥有专用金币账户,可以给玩家转普通金币。
|
||||||
- App 侧可以展示 host 统计、Agency 成员、申请、邀请和后台已经生成的收益结果。
|
- App 侧可以展示 host 统计、Agency 成员、申请、邀请和后台已经生成的收益结果。
|
||||||
- App 侧不能创建政策、计算工资周期、审核工资单或直接给 `USD_BALANCE` 入账。
|
- App 侧不能创建政策、计算工资周期、审核工资单或直接给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账。
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ graph LR
|
|||||||
| `gateway-service` | HTTP 入口、鉴权、request envelope、调用内部 gRPC |
|
| `gateway-service` | HTTP 入口、鉴权、request envelope、调用内部 gRPC |
|
||||||
| `user-service` | 用户主数据、国家、区域、账号状态;host 身份、Agency、BD、coin_seller 层级、申请邀请、关系快照、App 查询读模型 |
|
| `user-service` | 用户主数据、国家、区域、账号状态;host 身份、Agency、BD、coin_seller 层级、申请邀请、关系快照、App 查询读模型 |
|
||||||
| `room-service` | 房间事件:上麦、确认发流、下麦、送礼、离房 |
|
| `room-service` | 房间事件:上麦、确认发流、下麦、送礼、离房 |
|
||||||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`USD_BALANCE`、收益余额、提现冻结/审核/出账 |
|
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`、收益余额、提现冻结/审核/出账 |
|
||||||
|
|
||||||
`user-service` host domain 读取 `room-service` 和 `wallet-service` 事件,但不反向修改房间状态。App 查询只读取当前身份、关系、申请、邀请、统计和后台已生成的收益结果;结算写入链路见 Admin 文档。
|
`user-service` host domain 读取 `room-service` 和 `wallet-service` 事件,但不反向修改房间状态。App 查询只读取当前身份、关系、申请、邀请、统计和后台已生成的收益结果;结算写入链路见 Admin 文档。
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
- `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。
|
- `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。
|
||||||
- `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。
|
- `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。
|
||||||
- 两种类型都不能给币商发放 `USD_BALANCE`,也不能直接影响玩家普通 `COIN`。
|
- 两种类型都不能给币商发放 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,也不能直接影响玩家普通 `COIN`。
|
||||||
|
|
||||||
## 服务边界
|
## 服务边界
|
||||||
|
|
||||||
|
|||||||
@ -134,8 +134,8 @@ Authorization: Bearer <access_token>
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 消息 tab、系统消息、活动消息 | 我的页设计稿不展示消息红点 | 消息 tab 页面接口 |
|
| 消息 tab、系统消息、活动消息 | 我的页设计稿不展示消息红点 | 消息 tab 页面接口 |
|
||||||
| 邀请码入口和邀请统计 | 截图首屏没有邀请码入口 | 邀请页或活动入口 |
|
| 邀请码入口和邀请统计 | 截图首屏没有邀请码入口 | 邀请页或活动入口 |
|
||||||
| `wallet.recharge_enabled` / `withdraw_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` |
|
| `wallet.recharge_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` |
|
||||||
| `DIAMOND` / `USD_BALANCE` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 |
|
| `DIAMOND` / `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 |
|
||||||
| 邀请用户列表、钱包流水、工资明细、团队成员、背包完整列表、任务列表 | 都需要分页或聚合 | 对应二级页接口 |
|
| 邀请用户列表、钱包流水、工资明细、团队成员、背包完整列表、任务列表 | 都需要分页或聚合 | 对应二级页接口 |
|
||||||
|
|
||||||
## Performance Budget
|
## Performance Budget
|
||||||
@ -180,7 +180,6 @@ sequenceDiagram
|
|||||||
| `GET /api/v1/wallet/me/overview` | 钱包二级页余额和操作开关 |
|
| `GET /api/v1/wallet/me/overview` | 钱包二级页余额和操作开关 |
|
||||||
| `GET /api/v1/wallet/recharge/products` | 当前区域充值渠道和档位 |
|
| `GET /api/v1/wallet/recharge/products` | 当前区域充值渠道和档位 |
|
||||||
| `GET /api/v1/wallet/diamond-exchange/config` | 钻石兑换金币/余额配置 |
|
| `GET /api/v1/wallet/diamond-exchange/config` | 钻石兑换金币/余额配置 |
|
||||||
| `POST /api/v1/wallet/withdrawals/apply` | 创建待审核提现申请并冻结 `USD_BALANCE` |
|
|
||||||
| `GET /api/v1/wallet/transactions` | 钱包流水分页 |
|
| `GET /api/v1/wallet/transactions` | 钱包流水分页 |
|
||||||
| `GET /api/v1/vip/me` | 当前 VIP 状态 |
|
| `GET /api/v1/vip/me` | 当前 VIP 状态 |
|
||||||
| `GET /api/v1/vip/packages` | 可购买 VIP 包 |
|
| `GET /api/v1/vip/packages` | 可购买 VIP 包 |
|
||||||
|
|||||||
@ -99,7 +99,6 @@
|
|||||||
| GET | `/api/v1/wallet/me/balances` | wallet | `getMyBalances` | 查询当前用户钱包余额 |
|
| GET | `/api/v1/wallet/me/balances` | wallet | `getMyBalances` | 查询当前用户钱包余额 |
|
||||||
| GET | `/api/v1/wallet/recharge/products` | wallet | `listRechargeProducts` | 查询充值商品和渠道 |
|
| GET | `/api/v1/wallet/recharge/products` | wallet | `listRechargeProducts` | 查询充值商品和渠道 |
|
||||||
| GET | `/api/v1/wallet/diamond-exchange/config` | wallet | `getDiamondExchangeConfig` | 查询钻石兑换配置 |
|
| GET | `/api/v1/wallet/diamond-exchange/config` | wallet | `getDiamondExchangeConfig` | 查询钻石兑换配置 |
|
||||||
| POST | `/api/v1/wallet/withdrawals/apply` | wallet | `applyWithdrawal` | 提交美元余额提现申请 |
|
|
||||||
| GET | `/api/v1/wallet/coin-transactions` | wallet | `listCoinTransactions` | 分页查询当前用户金币流水 |
|
| GET | `/api/v1/wallet/coin-transactions` | wallet | `listCoinTransactions` | 分页查询当前用户金币流水 |
|
||||||
| GET | `/api/v1/wallet/transactions` | wallet | `listWalletTransactions` | 分页查询钱包流水 |
|
| GET | `/api/v1/wallet/transactions` | wallet | `listWalletTransactions` | 分页查询钱包流水 |
|
||||||
| POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 |
|
| POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 |
|
||||||
|
|||||||
@ -21,9 +21,9 @@
|
|||||||
| `COIN_SELLER_COIN` | 币商专用金币库存,只能转给玩家形成普通 `COIN` | yes, seller transfer only | no | wallet-service |
|
| `COIN_SELLER_COIN` | 币商专用金币库存,只能转给玩家形成普通 `COIN` | yes, seller transfer only | no | wallet-service |
|
||||||
| `DIAMOND` | 钻石,不可直接消费,只能按政策兑换金币或美元余额 | no | no | wallet-service |
|
| `DIAMOND` | 钻石,不可直接消费,只能按政策兑换金币或美元余额 | no | no | wallet-service |
|
||||||
| `GIFT_POINT` | 主播礼物积分,达到政策后结算美元奖励 | no | no | wallet-service |
|
| `GIFT_POINT` | 主播礼物积分,达到政策后结算美元奖励 | no | no | wallet-service |
|
||||||
| `USD_BALANCE` | 主播可提现美元余额,建议单位为 cent 或 micro cent | no | yes | wallet-service |
|
| `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` | 主播可提现美元余额,建议单位为 cent 或 micro cent | no | yes | wallet-service |
|
||||||
|
|
||||||
钻石和积分都不是直接消费资产。钻石兑换金币或美元余额时必须落兑换订单,记录政策版本、汇率和结果。主播积分达到政策后,由结算任务发起美元余额奖励入账,用户提现只能提现 `USD_BALANCE`。
|
钻石和积分都不是直接消费资产。钻石兑换金币或美元余额时必须落兑换订单,记录政策版本、汇率和结果。主播积分达到政策后,由结算任务发起美元余额奖励入账,用户提现只能提现 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||||
|
|
||||||
资源组可以通过资源域的 `wallet_asset` 组成员直接给用户发放 `COIN` 或 `DIAMOND`,但这仍然是钱包入账流水,不写用户资源权益;钻石后续兑换仍必须走独立兑换订单。
|
资源组可以通过资源域的 `wallet_asset` 组成员直接给用户发放 `COIN` 或 `DIAMOND`,但这仍然是钱包入账流水,不写用户资源权益;钻石后续兑换仍必须走独立兑换订单。
|
||||||
|
|
||||||
@ -40,7 +40,7 @@
|
|||||||
| 充值 | `PARTIAL` | 币商转账已写 `wallet_recharge_records`,provider order 未实现 | Apple、Google、线下订单和 provider 校验仍需补齐 |
|
| 充值 | `PARTIAL` | 币商转账已写 `wallet_recharge_records`,provider order 未实现 | Apple、Google、线下订单和 provider 校验仍需补齐 |
|
||||||
| 币商转金币 | `DONE` | `TransferCoinFromSeller` 扣 `COIN_SELLER_COIN`、加玩家 `COIN`,并按区域充值政策记录美元充值金额 | 限额、线下订单和风控审计仍需补齐 |
|
| 币商转金币 | `DONE` | `TransferCoinFromSeller` 扣 `COIN_SELLER_COIN`、加玩家 `COIN`,并按区域充值政策记录美元充值金额 | 限额、线下订单和风控审计仍需补齐 |
|
||||||
| 钻石兑换 | `TODO` | 无 diamond exchange order | 需要政策快照、兑换状态机和原子出入账 |
|
| 钻石兑换 | `TODO` | 无 diamond exchange order | 需要政策快照、兑换状态机和原子出入账 |
|
||||||
| 主播积分/奖励 | `PARTIAL` | 送礼实时给收礼人加 `GIFT_POINT` | 周期结算任务和 `USD_BALANCE` 奖励入账未落地 |
|
| 主播积分/奖励 | `PARTIAL` | 送礼实时给收礼人加 `GIFT_POINT` | 周期结算任务和 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 奖励入账未落地 |
|
||||||
| 提现 | `TODO` | 无 withdraw request | 需要冻结、审核、人工打款、失败回滚 |
|
| 提现 | `TODO` | 无 withdraw request | 需要冻结、审核、人工打款、失败回滚 |
|
||||||
| 钱包 outbox | `PARTIAL` | `wallet_outbox` 已和交易同事务写入 `WalletBalanceChanged/WalletGiftDebited/WalletCoinSellerTransferred/WalletRechargeRecorded` | outbox worker 和 MQ 投递适配未落地 |
|
| 钱包 outbox | `PARTIAL` | `wallet_outbox` 已和交易同事务写入 `WalletBalanceChanged/WalletGiftDebited/WalletCoinSellerTransferred/WalletRechargeRecorded` | outbox worker 和 MQ 投递适配未落地 |
|
||||||
|
|
||||||
@ -83,8 +83,8 @@
|
|||||||
### P4: Anchor Reward And Withdraw
|
### P4: Anchor Reward And Withdraw
|
||||||
|
|
||||||
- 送礼实时加主播 `GIFT_POINT`,但美元奖励由结算任务按政策转换,不能写死在送礼链路。
|
- 送礼实时加主播 `GIFT_POINT`,但美元奖励由结算任务按政策转换,不能写死在送礼链路。
|
||||||
- 奖励结算生成 `anchor_reward_settlements`,确认后加主播 `USD_BALANCE`。
|
- 奖励结算生成 `anchor_reward_settlements`,确认后加主播 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||||
- 提现申请必须先冻结 `USD_BALANCE`,审核拒绝解冻,审核通过后进入人工打款流程。
|
- 提现申请必须先冻结 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,审核拒绝解冻,审核通过后进入人工打款流程。
|
||||||
- 人工打款完成后从冻结余额出账;打款失败回到可重试状态,不能自动解冻后丢失审核上下文。
|
- 人工打款完成后从冻结余额出账;打款失败回到可重试状态,不能自动解冻后丢失审核上下文。
|
||||||
|
|
||||||
## Component Diagram
|
## Component Diagram
|
||||||
@ -239,7 +239,7 @@ wallet_recharge_records(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
`wallet_recharge_records.usd_minor_amount` 是用户充值额的权威统计口径;用户不会因此获得 `USD_BALANCE`。政策字段必须保存快照,后续修改区域汇率不能重算历史充值。
|
`wallet_recharge_records.usd_minor_amount` 是用户充值额的权威统计口径;用户不会因此获得 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。政策字段必须保存快照,后续修改区域汇率不能重算历史充值。
|
||||||
|
|
||||||
## Send Gift Flow
|
## Send Gift Flow
|
||||||
|
|
||||||
@ -321,7 +321,7 @@ stateDiagram-v2
|
|||||||
flowchart LR
|
flowchart LR
|
||||||
A["User DIAMOND account"] -->|"debit diamonds"| X["diamond_exchange_order"]
|
A["User DIAMOND account"] -->|"debit diamonds"| X["diamond_exchange_order"]
|
||||||
X -->|"policy: diamond to coin"| C["User COIN account"]
|
X -->|"policy: diamond to coin"| C["User COIN account"]
|
||||||
X -->|"policy: diamond to USD"| U["User USD_BALANCE account"]
|
X -->|"policy: diamond to USD"| U["User identity salary wallet account"]
|
||||||
```
|
```
|
||||||
|
|
||||||
钻石兑换必须冻结政策版本。订单里至少记录 `from_diamond_amount`、`to_asset_type`、`to_amount`、`policy_id`、`rate_snapshot` 和 `status`。兑换成功后写分录和 outbox;兑换失败不能出现钻石扣了但目标资产没到账。
|
钻石兑换必须冻结政策版本。订单里至少记录 `from_diamond_amount`、`to_asset_type`、`to_amount`、`policy_id`、`rate_snapshot` 和 `status`。兑换成功后写分录和 outbox;兑换失败不能出现钻石扣了但目标资产没到账。
|
||||||
@ -332,8 +332,8 @@ flowchart LR
|
|||||||
|
|
||||||
1. 送礼时钱包给主播增加 `GIFT_POINT`。
|
1. 送礼时钱包给主播增加 `GIFT_POINT`。
|
||||||
2. 结算任务读取积分和政策,生成 `anchor_reward_settlements`。
|
2. 结算任务读取积分和政策,生成 `anchor_reward_settlements`。
|
||||||
3. 结算单确认后调用钱包 `CreditRewardBalance`,增加主播 `USD_BALANCE`。
|
3. 结算单确认后调用钱包 `CreditRewardBalance`,增加主播 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||||
4. 主播提现时先冻结 `USD_BALANCE`,审核拒绝解冻,审核通过后等待人工转账。
|
4. 主播提现时先冻结 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,审核拒绝解冻,审核通过后等待人工转账。
|
||||||
5. 人工转账完成后把冻结余额出账,状态改为 `paid`。
|
5. 人工转账完成后把冻结余额出账,状态改为 `paid`。
|
||||||
|
|
||||||
提现状态机:
|
提现状态机:
|
||||||
@ -499,7 +499,7 @@ stateDiagram-v2
|
|||||||
|
|
||||||
提现:
|
提现:
|
||||||
|
|
||||||
- 提现申请成功后 `USD_BALANCE.available_amount` 减少,`frozen_amount` 增加。
|
- 提现申请成功后 `identity salary wallet.available_amount` 减少,`frozen_amount` 增加。
|
||||||
- 审核拒绝后冻结金额回到 available。
|
- 审核拒绝后冻结金额回到 available。
|
||||||
- 打款完成后 frozen 减少并写出账分录。
|
- 打款完成后 frozen 减少并写出账分录。
|
||||||
- 打款失败后状态可重试,不丢失冻结关系和审核上下文。
|
- 打款失败后状态可重试,不丢失冻结关系和审核上下文。
|
||||||
@ -533,7 +533,7 @@ docker compose config
|
|||||||
- 幂等键必须覆盖业务语义,同一个 command_id 带不同 payload 必须返回冲突。
|
- 幂等键必须覆盖业务语义,同一个 command_id 带不同 payload 必须返回冲突。
|
||||||
- Apple/Google 充值以 provider 真实交易号为最终幂等源,不能只信客户端 request id。
|
- Apple/Google 充值以 provider 真实交易号为最终幂等源,不能只信客户端 request id。
|
||||||
- 币商充值分两段:平台给币商发 `COIN_SELLER_COIN` 库存,币商再把库存转成用户 `COIN`;必须有币商金币余额、充值政策快照、限额和审计。
|
- 币商充值分两段:平台给币商发 `COIN_SELLER_COIN` 库存,币商再把库存转成用户 `COIN`;必须有币商金币余额、充值政策快照、限额和审计。
|
||||||
- `USD_BALANCE` 是可提现负债,调账和奖励发放需要更高权限和审计。
|
- `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 是可提现负债,调账和奖励发放需要更高权限和审计。
|
||||||
- 兑换汇率和奖励政策必须记录快照,不能只存当前 policy id。
|
- 兑换汇率和奖励政策必须记录快照,不能只存当前 policy id。
|
||||||
- 提现人工打款前必须冻结余额,打款失败要回到可重新处理状态。
|
- 提现人工打款前必须冻结余额,打款失败要回到可重新处理状态。
|
||||||
- 钱包事件投递使用 outbox,MQ 投递失败不回滚账务事实。
|
- 钱包事件投递使用 outbox,MQ 投递失败不回滚账务事实。
|
||||||
|
|||||||
55
scripts/mysql/034_remove_usd_balance_withdrawal_requests.sql
Normal file
55
scripts/mysql/034_remove_usd_balance_withdrawal_requests.sql
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
-- Remove the obsolete generic USD withdrawal request table from the wallet database.
|
||||||
|
-- Salary settlement now writes role-specific wallets only:
|
||||||
|
-- HOST_SALARY_USD, AGENCY_SALARY_USD, BD_SALARY_USD, ADMIN_SALARY_USD.
|
||||||
|
-- Keep an archive copy before dropping the source table so production history is not lost.
|
||||||
|
|
||||||
|
SET @source_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'wallet_withdrawal_requests'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @archive_exists := (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'wallet_withdrawal_requests_removed_20260602'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
@source_exists = 1 AND @archive_exists = 0,
|
||||||
|
'CREATE TABLE wallet_withdrawal_requests_removed_20260602 LIKE wallet_withdrawal_requests',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
@source_exists = 1,
|
||||||
|
'INSERT IGNORE INTO wallet_withdrawal_requests_removed_20260602 SELECT * FROM wallet_withdrawal_requests',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql := IF(
|
||||||
|
@source_exists = 1,
|
||||||
|
'DROP TABLE wallet_withdrawal_requests',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
SUM(TABLE_NAME = 'wallet_withdrawal_requests') AS source_table_exists,
|
||||||
|
SUM(TABLE_NAME = 'wallet_withdrawal_requests_removed_20260602') AS archive_table_exists
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME IN (
|
||||||
|
'wallet_withdrawal_requests',
|
||||||
|
'wallet_withdrawal_requests_removed_20260602'
|
||||||
|
);
|
||||||
@ -23,8 +23,9 @@ const (
|
|||||||
policyTypeAdmin = "admin"
|
policyTypeAdmin = "admin"
|
||||||
triggerAutomatic = "automatic"
|
triggerAutomatic = "automatic"
|
||||||
triggerManual = "manual"
|
triggerManual = "manual"
|
||||||
// assetUSDBalance 必须与 wallet-service 钱包资产类型保持一致,BD/Admin 工资直接入美元余额账户。
|
// BD/Admin 工资钱包资产类型必须与 wallet-service ledger 常量保持一致;admin-server 是独立 module,不能直接复用根 module 常量。
|
||||||
assetUSDBalance = "USD_BALANCE"
|
assetBDSalaryUSD = "BD_SALARY_USD"
|
||||||
|
assetAdminSalaryUSD = "ADMIN_SALARY_USD"
|
||||||
bizTypeTeamSettlement = "team_salary_settlement"
|
bizTypeTeamSettlement = "team_salary_settlement"
|
||||||
statusSucceeded = "succeeded"
|
statusSucceeded = "succeeded"
|
||||||
)
|
)
|
||||||
@ -97,6 +98,7 @@ type settlementMetadata struct {
|
|||||||
LevelNo int `json:"level_no"`
|
LevelNo int `json:"level_no"`
|
||||||
IncomeUSDMinor int64 `json:"income_usd_minor"`
|
IncomeUSDMinor int64 `json:"income_usd_minor"`
|
||||||
SalaryUSDMinorDelta int64 `json:"salary_usd_minor_delta"`
|
SalaryUSDMinorDelta int64 `json:"salary_usd_minor_delta"`
|
||||||
|
SalaryAssetType string `json:"salary_asset_type"`
|
||||||
SettledLevelNo int `json:"settled_level_no"`
|
SettledLevelNo int `json:"settled_level_no"`
|
||||||
SettledSalaryUSDMinor int64 `json:"settled_salary_usd_minor"`
|
SettledSalaryUSDMinor int64 `json:"settled_salary_usd_minor"`
|
||||||
ProcessedAtMS int64 `json:"processed_at_ms"`
|
ProcessedAtMS int64 `json:"processed_at_ms"`
|
||||||
@ -490,6 +492,10 @@ func (s *Service) settleCandidate(ctx context.Context, appCode string, operatorA
|
|||||||
MonthClosedAtMS: nowMS,
|
MonthClosedAtMS: nowMS,
|
||||||
Version: progress.Version,
|
Version: progress.Version,
|
||||||
}
|
}
|
||||||
|
salaryAssetType := teamSalaryAssetType(candidate.PolicyType)
|
||||||
|
if salaryAssetType == "" {
|
||||||
|
return recordDTO{}, false, fmt.Errorf("team salary asset type is invalid: %s", candidate.PolicyType)
|
||||||
|
}
|
||||||
metadata := settlementMetadata{
|
metadata := settlementMetadata{
|
||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
PolicyType: candidate.PolicyType,
|
PolicyType: candidate.PolicyType,
|
||||||
@ -502,6 +508,7 @@ func (s *Service) settleCandidate(ctx context.Context, appCode string, operatorA
|
|||||||
LevelNo: candidate.Level.LevelNo,
|
LevelNo: candidate.Level.LevelNo,
|
||||||
IncomeUSDMinor: candidate.IncomeUSDMinor,
|
IncomeUSDMinor: candidate.IncomeUSDMinor,
|
||||||
SalaryUSDMinorDelta: salaryDelta,
|
SalaryUSDMinorDelta: salaryDelta,
|
||||||
|
SalaryAssetType: salaryAssetType,
|
||||||
SettledLevelNo: nextProgress.SettledLevelNo,
|
SettledLevelNo: nextProgress.SettledLevelNo,
|
||||||
SettledSalaryUSDMinor: nextProgress.SettledSalaryUSDMinor,
|
SettledSalaryUSDMinor: nextProgress.SettledSalaryUSDMinor,
|
||||||
ProcessedAtMS: nowMS,
|
ProcessedAtMS: nowMS,
|
||||||
@ -513,11 +520,11 @@ func (s *Service) settleCandidate(ctx context.Context, appCode string, operatorA
|
|||||||
if err := s.insertWalletTransaction(ctx, tx, appCode, transactionID, commandID, metadata, nowMS); err != nil {
|
if err := s.insertWalletTransaction(ctx, tx, appCode, transactionID, commandID, metadata, nowMS); err != nil {
|
||||||
return recordDTO{}, false, err
|
return recordDTO{}, false, err
|
||||||
}
|
}
|
||||||
balanceAfter, version, err := s.creditWallet(ctx, tx, appCode, transactionID, candidate.UserID, salaryDelta, nowMS)
|
balanceAfter, version, err := s.creditWallet(ctx, tx, appCode, transactionID, candidate.UserID, metadata.SalaryAssetType, salaryDelta, nowMS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return recordDTO{}, false, err
|
return recordDTO{}, false, err
|
||||||
}
|
}
|
||||||
if err := s.insertWalletOutbox(ctx, tx, appCode, transactionID, commandID, candidate.UserID, salaryDelta, balanceAfter, version, metadata, nowMS); err != nil {
|
if err := s.insertWalletOutbox(ctx, tx, appCode, transactionID, commandID, candidate.UserID, metadata.SalaryAssetType, salaryDelta, balanceAfter, version, metadata, nowMS); err != nil {
|
||||||
return recordDTO{}, false, err
|
return recordDTO{}, false, err
|
||||||
}
|
}
|
||||||
if err := s.insertRecord(ctx, tx, appCode, transactionID, commandID, metadata, nowMS); err != nil {
|
if err := s.insertRecord(ctx, tx, appCode, transactionID, commandID, metadata, nowMS); err != nil {
|
||||||
@ -840,12 +847,12 @@ func (s *Service) insertWalletTransaction(ctx context.Context, tx *sql.Tx, appCo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// creditWallet 锁定美元账户并追加 wallet_entries,使余额、流水和版本在同一事务内一致。
|
// creditWallet 锁定对应角色工资钱包并追加 wallet_entries,使余额、流水和版本在同一事务内一致。
|
||||||
func (s *Service) creditWallet(ctx context.Context, tx *sql.Tx, appCode string, transactionID string, userID int64, amount int64, nowMS int64) (int64, int64, error) {
|
func (s *Service) creditWallet(ctx context.Context, tx *sql.Tx, appCode string, transactionID string, userID int64, assetType string, amount int64, nowMS int64) (int64, int64, error) {
|
||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
INSERT IGNORE INTO wallet_accounts (
|
INSERT IGNORE INTO wallet_accounts (
|
||||||
app_code, user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms
|
app_code, user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, 0, 0, 1, ?, ?)`, appCode, userID, assetUSDBalance, nowMS, nowMS); err != nil {
|
) VALUES (?, ?, ?, 0, 0, 1, ?, ?)`, appCode, userID, assetType, nowMS, nowMS); err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
var available int64
|
var available int64
|
||||||
@ -855,7 +862,7 @@ func (s *Service) creditWallet(ctx context.Context, tx *sql.Tx, appCode string,
|
|||||||
SELECT available_amount, frozen_amount, version
|
SELECT available_amount, frozen_amount, version
|
||||||
FROM wallet_accounts
|
FROM wallet_accounts
|
||||||
WHERE app_code = ? AND user_id = ? AND asset_type = ?
|
WHERE app_code = ? AND user_id = ? AND asset_type = ?
|
||||||
FOR UPDATE`, appCode, userID, assetUSDBalance,
|
FOR UPDATE`, appCode, userID, assetType,
|
||||||
).Scan(&available, &frozen, &version); err != nil {
|
).Scan(&available, &frozen, &version); err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
@ -868,7 +875,7 @@ func (s *Service) creditWallet(ctx context.Context, tx *sql.Tx, appCode string,
|
|||||||
UPDATE wallet_accounts
|
UPDATE wallet_accounts
|
||||||
SET available_amount = ?, version = version + 1, updated_at_ms = ?
|
SET available_amount = ?, version = version + 1, updated_at_ms = ?
|
||||||
WHERE app_code = ? AND user_id = ? AND asset_type = ? AND version = ?`,
|
WHERE app_code = ? AND user_id = ? AND asset_type = ? AND version = ?`,
|
||||||
after, nowMS, appCode, userID, assetUSDBalance, version,
|
after, nowMS, appCode, userID, assetType, version,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
@ -877,7 +884,7 @@ func (s *Service) creditWallet(ctx context.Context, tx *sql.Tx, appCode string,
|
|||||||
app_code, transaction_id, user_id, asset_type, available_delta, frozen_delta,
|
app_code, transaction_id, user_id, asset_type, available_delta, frozen_delta,
|
||||||
available_after, frozen_after, counterparty_user_id, room_id, created_at_ms
|
available_after, frozen_after, counterparty_user_id, room_id, created_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, 0, ?, ?, 0, '', ?)`,
|
) VALUES (?, ?, ?, ?, ?, 0, ?, ?, 0, '', ?)`,
|
||||||
appCode, transactionID, userID, assetUSDBalance, amount, after, frozen, nowMS,
|
appCode, transactionID, userID, assetType, amount, after, frozen, nowMS,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
@ -885,12 +892,12 @@ func (s *Service) creditWallet(ctx context.Context, tx *sql.Tx, appCode string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// insertWalletOutbox 投递余额变更事件,供后续 MQ/通知链路异步消费。
|
// insertWalletOutbox 投递余额变更事件,供后续 MQ/通知链路异步消费。
|
||||||
func (s *Service) insertWalletOutbox(ctx context.Context, tx *sql.Tx, appCode string, transactionID string, commandID string, userID int64, amount int64, balanceAfter int64, version int64, metadata settlementMetadata, nowMS int64) error {
|
func (s *Service) insertWalletOutbox(ctx context.Context, tx *sql.Tx, appCode string, transactionID string, commandID string, userID int64, assetType string, amount int64, balanceAfter int64, version int64, metadata settlementMetadata, nowMS int64) error {
|
||||||
payload, err := json.Marshal(map[string]any{
|
payload, err := json.Marshal(map[string]any{
|
||||||
"transaction_id": transactionID,
|
"transaction_id": transactionID,
|
||||||
"command_id": commandID,
|
"command_id": commandID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"asset_type": assetUSDBalance,
|
"asset_type": assetType,
|
||||||
"available_delta": amount,
|
"available_delta": amount,
|
||||||
"available_after": balanceAfter,
|
"available_after": balanceAfter,
|
||||||
"version": version,
|
"version": version,
|
||||||
@ -905,7 +912,7 @@ func (s *Service) insertWalletOutbox(ctx context.Context, tx *sql.Tx, appCode st
|
|||||||
app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
|
app_code, event_id, event_type, transaction_id, command_id, user_id, asset_type,
|
||||||
available_delta, frozen_delta, payload, status, created_at_ms, updated_at_ms
|
available_delta, frozen_delta, payload, status, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, 'WalletBalanceChanged', ?, ?, ?, ?, ?, 0, ?, 'pending', ?, ?)`,
|
) VALUES (?, ?, 'WalletBalanceChanged', ?, ?, ?, ?, ?, 0, ?, 'pending', ?, ?)`,
|
||||||
appCode, transactionID+":"+assetUSDBalance, transactionID, commandID, userID, assetUSDBalance, amount, string(payload), nowMS, nowMS,
|
appCode, transactionID+":"+assetType, transactionID, commandID, userID, assetType, amount, string(payload), nowMS, nowMS,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1233,6 +1240,18 @@ func normalizeTriggerMode(value string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func teamSalaryAssetType(policyType string) string {
|
||||||
|
// 用户可以同时拥有 BD/Admin 等多重身份,工资资产必须按本次结算角色拆分,不能混入其它身份钱包。
|
||||||
|
switch normalizePolicyType(policyType) {
|
||||||
|
case policyTypeBD:
|
||||||
|
return assetBDSalaryUSD
|
||||||
|
case policyTypeAdmin:
|
||||||
|
return assetAdminSalaryUSD
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// normalizeStatus 只允许记录表定义的状态值,避免前端筛选拼接任意 SQL 条件。
|
// normalizeStatus 只允许记录表定义的状态值,避免前端筛选拼接任意 SQL 条件。
|
||||||
func normalizeStatus(value string) string {
|
func normalizeStatus(value string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
|
|||||||
@ -72,8 +72,8 @@ func TestRealMySQLTeamSalaryFullFlow(t *testing.T) {
|
|||||||
if totalRecords != 2 || len(records) != 2 {
|
if totalRecords != 2 || len(records) != 2 {
|
||||||
t.Fatalf("record count mismatch: total=%d len=%d records=%+v", totalRecords, len(records), records)
|
t.Fatalf("record count mismatch: total=%d len=%d records=%+v", totalRecords, len(records), records)
|
||||||
}
|
}
|
||||||
assertWalletBalance(t, walletDB, 4001, 100_000)
|
assertWalletBalance(t, walletDB, 4001, assetBDSalaryUSD, 100_000)
|
||||||
assertWalletBalance(t, walletDB, 5001, 20_000)
|
assertWalletBalance(t, walletDB, 5001, assetAdminSalaryUSD, 20_000)
|
||||||
assertScalar(t, walletDB, `SELECT COUNT(*) FROM team_salary_settlement_records`, int64(2))
|
assertScalar(t, walletDB, `SELECT COUNT(*) FROM team_salary_settlement_records`, int64(2))
|
||||||
assertScalar(t, walletDB, `SELECT COUNT(*) FROM wallet_transactions WHERE biz_type = 'team_salary_settlement'`, int64(2))
|
assertScalar(t, walletDB, `SELECT COUNT(*) FROM wallet_transactions WHERE biz_type = 'team_salary_settlement'`, int64(2))
|
||||||
|
|
||||||
@ -354,14 +354,14 @@ func execAll(t *testing.T, db *sql.DB, statements ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertWalletBalance(t *testing.T, db *sql.DB, userID int64, expected int64) {
|
func assertWalletBalance(t *testing.T, db *sql.DB, userID int64, assetType string, expected int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var amount int64
|
var amount int64
|
||||||
if err := db.QueryRow(`SELECT available_amount FROM wallet_accounts WHERE app_code = 'lalu' AND user_id = ? AND asset_type = 'USD_BALANCE'`, userID).Scan(&amount); err != nil {
|
if err := db.QueryRow(`SELECT available_amount FROM wallet_accounts WHERE app_code = 'lalu' AND user_id = ? AND asset_type = ?`, userID, assetType).Scan(&amount); err != nil {
|
||||||
t.Fatalf("query wallet balance failed: %v", err)
|
t.Fatalf("query wallet balance failed: %v", err)
|
||||||
}
|
}
|
||||||
if amount != expected {
|
if amount != expected {
|
||||||
t.Fatalf("wallet balance mismatch for user %d: got=%d want=%d", userID, amount, expected)
|
t.Fatalf("wallet balance mismatch for user %d asset %s: got=%d want=%d", userID, assetType, amount, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,6 @@ type WalletClient interface {
|
|||||||
ConfirmGooglePayment(ctx context.Context, req *walletv1.ConfirmGooglePaymentRequest) (*walletv1.ConfirmGooglePaymentResponse, error)
|
ConfirmGooglePayment(ctx context.Context, req *walletv1.ConfirmGooglePaymentRequest) (*walletv1.ConfirmGooglePaymentResponse, error)
|
||||||
GetDiamondExchangeConfig(ctx context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error)
|
GetDiamondExchangeConfig(ctx context.Context, req *walletv1.GetDiamondExchangeConfigRequest) (*walletv1.GetDiamondExchangeConfigResponse, error)
|
||||||
ListWalletTransactions(ctx context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error)
|
ListWalletTransactions(ctx context.Context, req *walletv1.ListWalletTransactionsRequest) (*walletv1.ListWalletTransactionsResponse, error)
|
||||||
ApplyWithdrawal(ctx context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error)
|
|
||||||
ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error)
|
ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error)
|
||||||
GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error)
|
GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error)
|
||||||
PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error)
|
PurchaseVip(ctx context.Context, req *walletv1.PurchaseVipRequest) (*walletv1.PurchaseVipResponse, error)
|
||||||
@ -81,10 +80,6 @@ func (c *grpcWalletClient) ListWalletTransactions(ctx context.Context, req *wall
|
|||||||
return c.client.ListWalletTransactions(ctx, req)
|
return c.client.ListWalletTransactions(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *grpcWalletClient) ApplyWithdrawal(ctx context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error) {
|
|
||||||
return c.client.ApplyWithdrawal(ctx, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *grpcWalletClient) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
|
func (c *grpcWalletClient) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
|
||||||
return c.client.ListVipPackages(ctx, req)
|
return c.client.ListVipPackages(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,7 +167,6 @@ type WalletHandlers struct {
|
|||||||
ListRechargeProducts http.HandlerFunc
|
ListRechargeProducts http.HandlerFunc
|
||||||
ConfirmGooglePayment http.HandlerFunc
|
ConfirmGooglePayment http.HandlerFunc
|
||||||
GetDiamondExchangeConfig http.HandlerFunc
|
GetDiamondExchangeConfig http.HandlerFunc
|
||||||
ApplyWithdrawal http.HandlerFunc
|
|
||||||
ListCoinTransactions http.HandlerFunc
|
ListCoinTransactions http.HandlerFunc
|
||||||
ListWalletTransactions http.HandlerFunc
|
ListWalletTransactions http.HandlerFunc
|
||||||
ListCoinSellers http.HandlerFunc
|
ListCoinSellers http.HandlerFunc
|
||||||
@ -396,7 +395,6 @@ func (r routes) registerWalletRoutes() {
|
|||||||
r.profile("/wallet/recharge/products", http.MethodGet, h.ListRechargeProducts)
|
r.profile("/wallet/recharge/products", http.MethodGet, h.ListRechargeProducts)
|
||||||
r.profile("/wallet/payments/google/confirm", http.MethodPost, h.ConfirmGooglePayment)
|
r.profile("/wallet/payments/google/confirm", http.MethodPost, h.ConfirmGooglePayment)
|
||||||
r.profile("/wallet/diamond-exchange/config", http.MethodGet, h.GetDiamondExchangeConfig)
|
r.profile("/wallet/diamond-exchange/config", http.MethodGet, h.GetDiamondExchangeConfig)
|
||||||
r.profile("/wallet/withdrawals/apply", http.MethodPost, h.ApplyWithdrawal)
|
|
||||||
r.profile("/wallet/coin-transactions", http.MethodGet, h.ListCoinTransactions)
|
r.profile("/wallet/coin-transactions", http.MethodGet, h.ListCoinTransactions)
|
||||||
r.profile("/wallet/transactions", http.MethodGet, h.ListWalletTransactions)
|
r.profile("/wallet/transactions", http.MethodGet, h.ListWalletTransactions)
|
||||||
r.profile("/wallet/coin-sellers", http.MethodGet, h.ListCoinSellers)
|
r.profile("/wallet/coin-sellers", http.MethodGet, h.ListCoinSellers)
|
||||||
|
|||||||
@ -429,8 +429,6 @@ type fakeWalletClient struct {
|
|||||||
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
|
diamondExchangeResp *walletv1.GetDiamondExchangeConfigResponse
|
||||||
lastTransactions *walletv1.ListWalletTransactionsRequest
|
lastTransactions *walletv1.ListWalletTransactionsRequest
|
||||||
transactionsResp *walletv1.ListWalletTransactionsResponse
|
transactionsResp *walletv1.ListWalletTransactionsResponse
|
||||||
lastWithdrawal *walletv1.ApplyWithdrawalRequest
|
|
||||||
withdrawalResp *walletv1.ApplyWithdrawalResponse
|
|
||||||
lastVipPackages *walletv1.ListVipPackagesRequest
|
lastVipPackages *walletv1.ListVipPackagesRequest
|
||||||
vipPackagesResp *walletv1.ListVipPackagesResponse
|
vipPackagesResp *walletv1.ListVipPackagesResponse
|
||||||
lastMyVip *walletv1.GetMyVipRequest
|
lastMyVip *walletv1.GetMyVipRequest
|
||||||
@ -1147,7 +1145,6 @@ func (f *fakeWalletClient) GetWalletOverview(_ context.Context, req *walletv1.Ge
|
|||||||
FeatureFlags: &walletv1.WalletFeatureFlags{
|
FeatureFlags: &walletv1.WalletFeatureFlags{
|
||||||
RechargeEnabled: true,
|
RechargeEnabled: true,
|
||||||
DiamondExchangeEnabled: true,
|
DiamondExchangeEnabled: true,
|
||||||
WithdrawEnabled: true,
|
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -1230,17 +1227,6 @@ func (f *fakeWalletClient) ListWalletTransactions(_ context.Context, req *wallet
|
|||||||
return &walletv1.ListWalletTransactionsResponse{}, nil
|
return &walletv1.ListWalletTransactionsResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeWalletClient) ApplyWithdrawal(_ context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error) {
|
|
||||||
f.lastWithdrawal = req
|
|
||||||
if f.err != nil {
|
|
||||||
return nil, f.err
|
|
||||||
}
|
|
||||||
if f.withdrawalResp != nil {
|
|
||||||
return f.withdrawalResp, nil
|
|
||||||
}
|
|
||||||
return &walletv1.ApplyWithdrawalResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeWalletClient) ListVipPackages(_ context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
|
func (f *fakeWalletClient) ListVipPackages(_ context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
|
||||||
f.lastVipPackages = req
|
f.lastVipPackages = req
|
||||||
if f.vipErr != nil {
|
if f.vipErr != nil {
|
||||||
|
|||||||
@ -47,7 +47,6 @@ type walletOverviewData struct {
|
|||||||
type walletFeatureFlagsData struct {
|
type walletFeatureFlagsData struct {
|
||||||
RechargeEnabled bool `json:"recharge_enabled"`
|
RechargeEnabled bool `json:"recharge_enabled"`
|
||||||
DiamondExchangeEnabled bool `json:"diamond_exchange_enabled"`
|
DiamondExchangeEnabled bool `json:"diamond_exchange_enabled"`
|
||||||
WithdrawEnabled bool `json:"withdraw_enabled"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type walletTransactionData struct {
|
type walletTransactionData struct {
|
||||||
@ -96,15 +95,6 @@ type giftWallItemData struct {
|
|||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type withdrawalApplyRequestBody struct {
|
|
||||||
CommandID string `json:"command_id"`
|
|
||||||
CommandIDAlt string `json:"commandId"`
|
|
||||||
Amount int64 `json:"amount"`
|
|
||||||
PayoutAccount string `json:"payout_account"`
|
|
||||||
PayoutAlt string `json:"payoutAccount"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type googlePaymentConfirmRequestBody struct {
|
type googlePaymentConfirmRequestBody struct {
|
||||||
CommandID string `json:"command_id"`
|
CommandID string `json:"command_id"`
|
||||||
CommandIDAlt string `json:"commandId"`
|
CommandIDAlt string `json:"commandId"`
|
||||||
@ -134,18 +124,6 @@ type googlePaymentConfirmData struct {
|
|||||||
ConsumeState string `json:"consume_state"`
|
ConsumeState string `json:"consume_state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type withdrawalData struct {
|
|
||||||
WithdrawalID string `json:"withdrawal_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
AssetType string `json:"asset_type"`
|
|
||||||
Amount int64 `json:"amount"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
PayoutAccount string `json:"payout_account"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
CreatedAtMS int64 `json:"created_at_ms"`
|
|
||||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type vipPurchaseRequestBody struct {
|
type vipPurchaseRequestBody struct {
|
||||||
CommandID string `json:"command_id"`
|
CommandID string `json:"command_id"`
|
||||||
CommandIDAlt string `json:"commandId"`
|
CommandIDAlt string `json:"commandId"`
|
||||||
@ -387,46 +365,6 @@ func (h *Handler) listWalletTransactionsByAsset(writer http.ResponseWriter, requ
|
|||||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyWithdrawal 创建待人工审核的提现申请。
|
|
||||||
func (h *Handler) applyWithdrawal(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if h.walletClient == nil {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var body withdrawalApplyRequestBody
|
|
||||||
if !httpkit.Decode(writer, request, &body) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
commandID := strings.TrimSpace(body.CommandID)
|
|
||||||
if commandID == "" {
|
|
||||||
commandID = strings.TrimSpace(body.CommandIDAlt)
|
|
||||||
}
|
|
||||||
payoutAccount := strings.TrimSpace(body.PayoutAccount)
|
|
||||||
if payoutAccount == "" {
|
|
||||||
payoutAccount = strings.TrimSpace(body.PayoutAlt)
|
|
||||||
}
|
|
||||||
if commandID == "" || body.Amount <= 0 || payoutAccount == "" {
|
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.walletClient.ApplyWithdrawal(request.Context(), &walletv1.ApplyWithdrawalRequest{
|
|
||||||
CommandId: commandID,
|
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
|
||||||
UserId: auth.UserIDFromContext(request.Context()),
|
|
||||||
Amount: body.Amount,
|
|
||||||
PayoutAccount: payoutAccount,
|
|
||||||
Reason: strings.TrimSpace(body.Reason),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
httpkit.WriteOK(writer, request, map[string]any{
|
|
||||||
"withdrawal": withdrawalFromProto(resp.GetWithdrawal()),
|
|
||||||
"balance": balanceFromProto(resp.GetBalance()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func rechargeProductFromProto(product *walletv1.RechargeProduct) rechargeProductData {
|
func rechargeProductFromProto(product *walletv1.RechargeProduct) rechargeProductData {
|
||||||
if product == nil {
|
if product == nil {
|
||||||
return rechargeProductData{}
|
return rechargeProductData{}
|
||||||
@ -496,7 +434,6 @@ func walletOverviewFromProto(resp *walletv1.GetWalletOverviewResponse) walletOve
|
|||||||
FeatureFlags: walletFeatureFlagsData{
|
FeatureFlags: walletFeatureFlagsData{
|
||||||
RechargeEnabled: flags.GetRechargeEnabled(),
|
RechargeEnabled: flags.GetRechargeEnabled(),
|
||||||
DiamondExchangeEnabled: flags.GetDiamondExchangeEnabled(),
|
DiamondExchangeEnabled: flags.GetDiamondExchangeEnabled(),
|
||||||
WithdrawEnabled: flags.GetWithdrawEnabled(),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -566,23 +503,6 @@ func giftWallItemFromProto(item *walletv1.GiftWallItem) giftWallItemData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func withdrawalFromProto(item *walletv1.WithdrawalRequest) withdrawalData {
|
|
||||||
if item == nil {
|
|
||||||
return withdrawalData{}
|
|
||||||
}
|
|
||||||
return withdrawalData{
|
|
||||||
WithdrawalID: item.GetWithdrawalId(),
|
|
||||||
UserID: item.GetUserId(),
|
|
||||||
AssetType: item.GetAssetType(),
|
|
||||||
Amount: item.GetAmount(),
|
|
||||||
Status: item.GetStatus(),
|
|
||||||
PayoutAccount: item.GetPayoutAccount(),
|
|
||||||
Reason: item.GetReason(),
|
|
||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
|
||||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func balanceFromProto(balance *walletv1.AssetBalance) assetBalanceData {
|
func balanceFromProto(balance *walletv1.AssetBalance) assetBalanceData {
|
||||||
if balance == nil {
|
if balance == nil {
|
||||||
return assetBalanceData{}
|
return assetBalanceData{}
|
||||||
|
|||||||
@ -48,7 +48,6 @@ func (h *Handler) WalletHandlers() httproutes.WalletHandlers {
|
|||||||
ListRechargeProducts: h.listRechargeProducts,
|
ListRechargeProducts: h.listRechargeProducts,
|
||||||
ConfirmGooglePayment: h.confirmGooglePayment,
|
ConfirmGooglePayment: h.confirmGooglePayment,
|
||||||
GetDiamondExchangeConfig: h.getDiamondExchangeConfig,
|
GetDiamondExchangeConfig: h.getDiamondExchangeConfig,
|
||||||
ApplyWithdrawal: h.applyWithdrawal,
|
|
||||||
ListCoinTransactions: h.listCoinTransactions,
|
ListCoinTransactions: h.listCoinTransactions,
|
||||||
ListWalletTransactions: h.listWalletTransactions,
|
ListWalletTransactions: h.listWalletTransactions,
|
||||||
ListCoinSellers: h.listCoinSellers,
|
ListCoinSellers: h.listCoinSellers,
|
||||||
|
|||||||
@ -1046,28 +1046,6 @@ CREATE TABLE IF NOT EXISTS red_packet_user_daily_counters (
|
|||||||
PRIMARY KEY (app_code, user_id, send_day)
|
PRIMARY KEY (app_code, user_id, send_day)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包用户每日发送计数表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包用户每日发送计数表';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wallet_withdrawal_requests (
|
|
||||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
|
||||||
withdrawal_id VARCHAR(96) NOT NULL COMMENT '提现 ID',
|
|
||||||
command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
|
|
||||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
|
||||||
asset_type VARCHAR(32) NOT NULL COMMENT '钱包资产类型',
|
|
||||||
amount BIGINT NOT NULL COMMENT '金额或数量',
|
|
||||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
|
||||||
payout_account VARCHAR(256) NOT NULL COMMENT '出款账户',
|
|
||||||
reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '原因',
|
|
||||||
wallet_transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
|
|
||||||
reviewed_by_user_id BIGINT NULL COMMENT '审核用户 ID',
|
|
||||||
review_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '审核原因',
|
|
||||||
reviewed_at_ms BIGINT NULL COMMENT '审核时间,UTC epoch ms',
|
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
|
||||||
PRIMARY KEY (app_code, withdrawal_id),
|
|
||||||
UNIQUE KEY uk_withdrawal_command (app_code, command_id),
|
|
||||||
KEY idx_withdrawal_user_time (app_code, user_id, created_at_ms),
|
|
||||||
KEY idx_withdrawal_status_time (app_code, status, created_at_ms)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包提现申请表';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS vip_levels (
|
CREATE TABLE IF NOT EXISTS vip_levels (
|
||||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
level INT NOT NULL COMMENT '等级',
|
level INT NOT NULL COMMENT '等级',
|
||||||
|
|||||||
@ -11,8 +11,14 @@ const (
|
|||||||
AssetGiftPoint = "GIFT_POINT"
|
AssetGiftPoint = "GIFT_POINT"
|
||||||
// AssetHostPeriodDiamond 是主播工资周期钻石投影,只参与工资等级结算,不进入通用钱包余额。
|
// AssetHostPeriodDiamond 是主播工资周期钻石投影,只参与工资等级结算,不进入通用钱包余额。
|
||||||
AssetHostPeriodDiamond = "HOST_PERIOD_DIAMOND"
|
AssetHostPeriodDiamond = "HOST_PERIOD_DIAMOND"
|
||||||
// AssetUSDBalance 是主播可提现美元余额,单位由后续提现策略固定。
|
// AssetHostSalaryUSD 是主播工资美元钱包,只接收主播工资和月底剩余钻石折美元。
|
||||||
AssetUSDBalance = "USD_BALANCE"
|
AssetHostSalaryUSD = "HOST_SALARY_USD"
|
||||||
|
// AssetAgencySalaryUSD 是代理工资美元钱包,只接收主播结算同时产生的代理工资。
|
||||||
|
AssetAgencySalaryUSD = "AGENCY_SALARY_USD"
|
||||||
|
// AssetBDSalaryUSD 是 BD 工资美元钱包,只接收 BD 政策结算工资。
|
||||||
|
AssetBDSalaryUSD = "BD_SALARY_USD"
|
||||||
|
// AssetAdminSalaryUSD 是 Admin 工资美元钱包,只接收 Admin 政策结算工资。
|
||||||
|
AssetAdminSalaryUSD = "ADMIN_SALARY_USD"
|
||||||
|
|
||||||
// StockTypeUSDTPurchase 表示币商线下 USDT 进货后发放专用金币库存。
|
// StockTypeUSDTPurchase 表示币商线下 USDT 进货后发放专用金币库存。
|
||||||
StockTypeUSDTPurchase = "usdt_purchase"
|
StockTypeUSDTPurchase = "usdt_purchase"
|
||||||
@ -43,9 +49,6 @@ const (
|
|||||||
// GooglePurchaseStatePurchased 是 Google Play 已支付完成状态。
|
// GooglePurchaseStatePurchased 是 Google Play 已支付完成状态。
|
||||||
GooglePurchaseStatePurchased = "PURCHASED"
|
GooglePurchaseStatePurchased = "PURCHASED"
|
||||||
|
|
||||||
// WithdrawalStatusPending 表示主播美元余额提现已提交,等待后台人工审核和线下转账。
|
|
||||||
WithdrawalStatusPending = "pending"
|
|
||||||
|
|
||||||
// HostSalarySettlementModeDaily 表示主播工资按日结算等级增量。
|
// HostSalarySettlementModeDaily 表示主播工资按日结算等级增量。
|
||||||
HostSalarySettlementModeDaily = "daily"
|
HostSalarySettlementModeDaily = "daily"
|
||||||
// HostSalarySettlementModeHalfMonth 表示主播工资按半月周期结算等级增量。
|
// HostSalarySettlementModeHalfMonth 表示主播工资按半月周期结算等级增量。
|
||||||
@ -279,7 +282,6 @@ type ListRechargeBillsQuery struct {
|
|||||||
type WalletFeatureFlags struct {
|
type WalletFeatureFlags struct {
|
||||||
RechargeEnabled bool
|
RechargeEnabled bool
|
||||||
DiamondExchangeEnabled bool
|
DiamondExchangeEnabled bool
|
||||||
WithdrawEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WalletOverview 是我的页和钱包首页共同使用的轻量摘要。
|
// WalletOverview 是我的页和钱包首页共同使用的轻量摘要。
|
||||||
@ -473,29 +475,6 @@ type ListWalletTransactionsQuery struct {
|
|||||||
PageSize int32
|
PageSize int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithdrawalCommand 是主播余额提现申请命令。人工审核和线下转账不在 App 主链路完成。
|
|
||||||
type WithdrawalCommand struct {
|
|
||||||
AppCode string
|
|
||||||
CommandID string
|
|
||||||
UserID int64
|
|
||||||
Amount int64
|
|
||||||
PayoutAccount string
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithdrawalRequest 是提现申请事实。
|
|
||||||
type WithdrawalRequest struct {
|
|
||||||
WithdrawalID string
|
|
||||||
UserID int64
|
|
||||||
AssetType string
|
|
||||||
Amount int64
|
|
||||||
Status string
|
|
||||||
PayoutAccount string
|
|
||||||
Reason string
|
|
||||||
CreatedAtMS int64
|
|
||||||
UpdatedAtMS int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// VipRewardItem 是 VIP 资源组权益的轻量展示投影。
|
// VipRewardItem 是 VIP 资源组权益的轻量展示投影。
|
||||||
type VipRewardItem struct {
|
type VipRewardItem struct {
|
||||||
ResourceID int64
|
ResourceID int64
|
||||||
@ -786,7 +765,7 @@ type RedPacketExpireResult struct {
|
|||||||
// ValidAssetType 限定账本允许落账的资产类型,避免写入任意字符串资产。
|
// ValidAssetType 限定账本允许落账的资产类型,避免写入任意字符串资产。
|
||||||
func ValidAssetType(assetType string) bool {
|
func ValidAssetType(assetType string) bool {
|
||||||
switch assetType {
|
switch assetType {
|
||||||
case AssetCoin, AssetCoinSellerCoin, AssetGiftPoint, AssetUSDBalance:
|
case AssetCoin, AssetCoinSellerCoin, AssetGiftPoint, AssetHostSalaryUSD, AssetAgencySalaryUSD, AssetBDSalaryUSD, AssetAdminSalaryUSD:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -48,7 +48,6 @@ type Repository interface {
|
|||||||
DeleteRechargeProduct(ctx context.Context, appCode string, productID int64) error
|
DeleteRechargeProduct(ctx context.Context, appCode string, productID int64) error
|
||||||
GetDiamondExchangeConfig(ctx context.Context, userID int64) ([]ledger.DiamondExchangeRule, error)
|
GetDiamondExchangeConfig(ctx context.Context, userID int64) ([]ledger.DiamondExchangeRule, error)
|
||||||
ListWalletTransactions(ctx context.Context, query ledger.ListWalletTransactionsQuery) ([]ledger.WalletTransaction, int64, error)
|
ListWalletTransactions(ctx context.Context, query ledger.ListWalletTransactionsQuery) ([]ledger.WalletTransaction, int64, error)
|
||||||
ApplyWithdrawal(ctx context.Context, command ledger.WithdrawalCommand) (ledger.WithdrawalRequest, ledger.AssetBalance, error)
|
|
||||||
ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error)
|
ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error)
|
||||||
GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error)
|
GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error)
|
||||||
PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error)
|
PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error)
|
||||||
@ -738,24 +737,6 @@ func (s *Service) ListWalletTransactions(ctx context.Context, query ledger.ListW
|
|||||||
return s.repository.ListWalletTransactions(ctx, query)
|
return s.repository.ListWalletTransactions(ctx, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyWithdrawal 冻结主播美元余额并创建待人工审核提现申请。
|
|
||||||
func (s *Service) ApplyWithdrawal(ctx context.Context, command ledger.WithdrawalCommand) (ledger.WithdrawalRequest, ledger.AssetBalance, error) {
|
|
||||||
if command.CommandID == "" || command.UserID <= 0 || command.Amount <= 0 {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.InvalidArgument, "withdrawal command is incomplete")
|
|
||||||
}
|
|
||||||
command.PayoutAccount = strings.TrimSpace(command.PayoutAccount)
|
|
||||||
command.Reason = strings.TrimSpace(command.Reason)
|
|
||||||
if command.PayoutAccount == "" || len(command.PayoutAccount) > 256 || len(command.Reason) > 512 {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.InvalidArgument, "withdrawal payout account is invalid")
|
|
||||||
}
|
|
||||||
if s.repository == nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
|
||||||
}
|
|
||||||
command.AppCode = appcode.Normalize(command.AppCode)
|
|
||||||
ctx = appcode.WithContext(ctx, command.AppCode)
|
|
||||||
return s.repository.ApplyWithdrawal(ctx, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListVipPackages 返回可购买 VIP 包和当前会员状态。
|
// ListVipPackages 返回可购买 VIP 包和当前会员状态。
|
||||||
func (s *Service) ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error) {
|
func (s *Service) ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error) {
|
||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
|
|||||||
@ -264,9 +264,9 @@ func TestHostSalarySettlementCreditsIncrementalRewards(t *testing.T) {
|
|||||||
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
||||||
t.Fatalf("first settlement result mismatch: %+v", result)
|
t.Fatalf("first settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 150)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 150)
|
||||||
assertBalance(t, svc, 10002, ledger.AssetCoin, 90)
|
assertBalance(t, svc, 10002, ledger.AssetCoin, 90)
|
||||||
assertBalance(t, svc, 30001, ledger.AssetUSDBalance, 50)
|
assertBalance(t, svc, 30001, ledger.AssetAgencySalaryUSD, 50)
|
||||||
|
|
||||||
second, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
second, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
||||||
CommandID: "cmd-salary-gift-2",
|
CommandID: "cmd-salary-gift-2",
|
||||||
@ -301,9 +301,9 @@ func TestHostSalarySettlementCreditsIncrementalRewards(t *testing.T) {
|
|||||||
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
||||||
t.Fatalf("second settlement result mismatch: %+v", result)
|
t.Fatalf("second settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 300)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 300)
|
||||||
assertBalance(t, svc, 10002, ledger.AssetCoin, 198)
|
assertBalance(t, svc, 10002, ledger.AssetCoin, 198)
|
||||||
assertBalance(t, svc, 30001, ledger.AssetUSDBalance, 80)
|
assertBalance(t, svc, 30001, ledger.AssetAgencySalaryUSD, 80)
|
||||||
if got := repository.CountRows("host_salary_settlement_records", "user_id = ? AND cycle_key = ?", int64(10002), first.HostPeriodCycleKey); got != 2 {
|
if got := repository.CountRows("host_salary_settlement_records", "user_id = ? AND cycle_key = ?", int64(10002), first.HostPeriodCycleKey); got != 2 {
|
||||||
t.Fatalf("salary settlement records mismatch, got %d", got)
|
t.Fatalf("salary settlement records mismatch, got %d", got)
|
||||||
}
|
}
|
||||||
@ -391,8 +391,8 @@ func TestHostSalaryManualPolicyRequiresManualTrigger(t *testing.T) {
|
|||||||
if result.ProcessedCount != 1 || result.SuccessCount != 1 {
|
if result.ProcessedCount != 1 || result.SuccessCount != 1 {
|
||||||
t.Fatalf("manual settlement result mismatch: %+v", result)
|
t.Fatalf("manual settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 150)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 150)
|
||||||
assertBalance(t, svc, 30001, ledger.AssetUSDBalance, 50)
|
assertBalance(t, svc, 30001, ledger.AssetAgencySalaryUSD, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHostSalaryMonthEndCreditsResidualAndClearsCycle 验证月底结算补发剩余钻石折美元,并逻辑关闭当月周期。
|
// TestHostSalaryMonthEndCreditsResidualAndClearsCycle 验证月底结算补发剩余钻石折美元,并逻辑关闭当月周期。
|
||||||
@ -455,7 +455,7 @@ func TestHostSalaryMonthEndCreditsResidualAndClearsCycle(t *testing.T) {
|
|||||||
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
||||||
t.Fatalf("month-end settlement result mismatch: %+v", result)
|
t.Fatalf("month-end settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 400)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 400)
|
||||||
if got := repository.CountRows("host_salary_settlement_progress", "user_id = ? AND cycle_key = ? AND residual_usd_minor = ? AND month_end_cleared_at_ms > 0", int64(10002), receipt.HostPeriodCycleKey, int64(250)); got != 1 {
|
if got := repository.CountRows("host_salary_settlement_progress", "user_id = ? AND cycle_key = ? AND residual_usd_minor = ? AND month_end_cleared_at_ms > 0", int64(10002), receipt.HostPeriodCycleKey, int64(250)); got != 1 {
|
||||||
t.Fatalf("month-end progress should store residual and cleared marker, got %d", got)
|
t.Fatalf("month-end progress should store residual and cleared marker, got %d", got)
|
||||||
}
|
}
|
||||||
@ -534,9 +534,9 @@ func TestHostSalaryFullLifecycleFromPublishedPolicyToNextCycle(t *testing.T) {
|
|||||||
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
||||||
t.Fatalf("level 1 daily settlement result mismatch: %+v", result)
|
t.Fatalf("level 1 daily settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 150)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 150)
|
||||||
assertBalance(t, svc, 10002, ledger.AssetCoin, 90)
|
assertBalance(t, svc, 10002, ledger.AssetCoin, 90)
|
||||||
assertBalance(t, svc, 30001, ledger.AssetUSDBalance, 50)
|
assertBalance(t, svc, 30001, ledger.AssetAgencySalaryUSD, 50)
|
||||||
previousIDs := repository.HostSalarySettlementIDs(10002, previousCycle)
|
previousIDs := repository.HostSalarySettlementIDs(10002, previousCycle)
|
||||||
if len(previousIDs) != 1 || previousIDs[0] == "" {
|
if len(previousIDs) != 1 || previousIDs[0] == "" {
|
||||||
t.Fatalf("level 1 settlement id missing: %v", previousIDs)
|
t.Fatalf("level 1 settlement id missing: %v", previousIDs)
|
||||||
@ -598,9 +598,9 @@ func TestHostSalaryFullLifecycleFromPublishedPolicyToNextCycle(t *testing.T) {
|
|||||||
t.Fatalf("level 2 daily settlement result mismatch: %+v", result)
|
t.Fatalf("level 2 daily settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
// 第二次只补发累计 2 级与已发 1 级之间的差额,最终余额等于 2 级累计权益。
|
// 第二次只补发累计 2 级与已发 1 级之间的差额,最终余额等于 2 级累计权益。
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 300)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 300)
|
||||||
assertBalance(t, svc, 10002, ledger.AssetCoin, 198)
|
assertBalance(t, svc, 10002, ledger.AssetCoin, 198)
|
||||||
assertBalance(t, svc, 30001, ledger.AssetUSDBalance, 80)
|
assertBalance(t, svc, 30001, ledger.AssetAgencySalaryUSD, 80)
|
||||||
previousIDs = repository.HostSalarySettlementIDs(10002, previousCycle)
|
previousIDs = repository.HostSalarySettlementIDs(10002, previousCycle)
|
||||||
if len(previousIDs) != 2 {
|
if len(previousIDs) != 2 {
|
||||||
t.Fatalf("level 2 settlement should create second record: %v", previousIDs)
|
t.Fatalf("level 2 settlement should create second record: %v", previousIDs)
|
||||||
@ -623,7 +623,7 @@ func TestHostSalaryFullLifecycleFromPublishedPolicyToNextCycle(t *testing.T) {
|
|||||||
t.Fatalf("month-end lifecycle result mismatch: %+v", result)
|
t.Fatalf("month-end lifecycle result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
// 480 钻石达到 2 级后剩余 80 钻石,按 0.01 USD/diamond 折算为 80 美分并标记周期已清算。
|
// 480 钻石达到 2 级后剩余 80 钻石,按 0.01 USD/diamond 折算为 80 美分并标记周期已清算。
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 380)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 380)
|
||||||
if got := repository.CountRows("host_salary_settlement_progress", "user_id = ? AND cycle_key = ? AND last_settled_total_diamonds = ? AND residual_usd_minor = ? AND month_end_cleared_at_ms > 0", int64(10002), previousCycle, int64(480), int64(80)); got != 1 {
|
if got := repository.CountRows("host_salary_settlement_progress", "user_id = ? AND cycle_key = ? AND last_settled_total_diamonds = ? AND residual_usd_minor = ? AND month_end_cleared_at_ms > 0", int64(10002), previousCycle, int64(480), int64(80)); got != 1 {
|
||||||
t.Fatalf("month-end progress should store residual and clear marker, got %d", got)
|
t.Fatalf("month-end progress should store residual and clear marker, got %d", got)
|
||||||
}
|
}
|
||||||
@ -683,9 +683,9 @@ func TestHostSalaryFullLifecycleFromPublishedPolicyToNextCycle(t *testing.T) {
|
|||||||
t.Fatalf("next-cycle daily result mismatch: %+v", result)
|
t.Fatalf("next-cycle daily result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
// 新 cycle_key 使用独立进度,因此月末清算后的下一周期从 1 级累计权益重新开始发放。
|
// 新 cycle_key 使用独立进度,因此月末清算后的下一周期从 1 级累计权益重新开始发放。
|
||||||
assertBalance(t, svc, 10002, ledger.AssetUSDBalance, 530)
|
assertBalance(t, svc, 10002, ledger.AssetHostSalaryUSD, 530)
|
||||||
assertBalance(t, svc, 10002, ledger.AssetCoin, 288)
|
assertBalance(t, svc, 10002, ledger.AssetCoin, 288)
|
||||||
assertBalance(t, svc, 30001, ledger.AssetUSDBalance, 130)
|
assertBalance(t, svc, 30001, ledger.AssetAgencySalaryUSD, 130)
|
||||||
nextIDs := repository.HostSalarySettlementIDs(10002, third.HostPeriodCycleKey)
|
nextIDs := repository.HostSalarySettlementIDs(10002, third.HostPeriodCycleKey)
|
||||||
if len(nextIDs) != 1 || nextIDs[0] == "" {
|
if len(nextIDs) != 1 || nextIDs[0] == "" {
|
||||||
t.Fatalf("next-cycle settlement id missing: %v", nextIDs)
|
t.Fatalf("next-cycle settlement id missing: %v", nextIDs)
|
||||||
@ -757,9 +757,9 @@ func TestHostSalaryHalfMonthSettlementUsesPolicyMode(t *testing.T) {
|
|||||||
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
if result.ProcessedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
||||||
t.Fatalf("half-month settlement result mismatch: %+v", result)
|
t.Fatalf("half-month settlement result mismatch: %+v", result)
|
||||||
}
|
}
|
||||||
assertBalance(t, svc, 10003, ledger.AssetUSDBalance, 150)
|
assertBalance(t, svc, 10003, ledger.AssetHostSalaryUSD, 150)
|
||||||
assertBalance(t, svc, 10003, ledger.AssetCoin, 90)
|
assertBalance(t, svc, 10003, ledger.AssetCoin, 90)
|
||||||
assertBalance(t, svc, 30002, ledger.AssetUSDBalance, 50)
|
assertBalance(t, svc, 30002, ledger.AssetAgencySalaryUSD, 50)
|
||||||
if ids := repository.HostSalarySettlementIDs(10003, receipt.HostPeriodCycleKey); len(ids) != 1 || ids[0] == "" {
|
if ids := repository.HostSalarySettlementIDs(10003, receipt.HostPeriodCycleKey); len(ids) != 1 || ids[0] == "" {
|
||||||
t.Fatalf("half-month settlement id missing: %v", ids)
|
t.Fatalf("half-month settlement id missing: %v", ids)
|
||||||
}
|
}
|
||||||
@ -2499,36 +2499,6 @@ func TestEquipUserResourceRequiresActiveEquipableEntitlement(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestApplyWithdrawalFreezesUSDBalance 验证提现申请只创建待审核事实,并把可提现美元余额转入冻结。
|
|
||||||
func TestApplyWithdrawalFreezesUSDBalance(t *testing.T) {
|
|
||||||
repository := mysqltest.NewRepository(t)
|
|
||||||
repository.SetAssetBalance(50001, ledger.AssetUSDBalance, 3000)
|
|
||||||
svc := walletservice.New(repository)
|
|
||||||
|
|
||||||
withdrawal, balance, err := svc.ApplyWithdrawal(context.Background(), ledger.WithdrawalCommand{
|
|
||||||
CommandID: "cmd-withdraw-usd",
|
|
||||||
UserID: 50001,
|
|
||||||
Amount: 1200,
|
|
||||||
PayoutAccount: "paypal:host@example.com",
|
|
||||||
Reason: "host payout",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ApplyWithdrawal failed: %v", err)
|
|
||||||
}
|
|
||||||
if withdrawal.WithdrawalID == "" || withdrawal.Status != ledger.WithdrawalStatusPending || withdrawal.Amount != 1200 {
|
|
||||||
t.Fatalf("withdrawal response mismatch: %+v", withdrawal)
|
|
||||||
}
|
|
||||||
if balance.AvailableAmount != 1800 || balance.FrozenAmount != 1200 {
|
|
||||||
t.Fatalf("withdrawal balance mismatch: %+v", balance)
|
|
||||||
}
|
|
||||||
if got := repository.CountRows("wallet_withdrawal_requests", "user_id = ? AND status = ?", int64(50001), ledger.WithdrawalStatusPending); got != 1 {
|
|
||||||
t.Fatalf("withdrawal request should be persisted once, got %d", got)
|
|
||||||
}
|
|
||||||
if got := repository.CountRows("wallet_transactions", "command_id = ? AND biz_type = ?", "cmd-withdraw-usd", "withdrawal_apply"); got != 1 {
|
|
||||||
t.Fatalf("withdrawal should write one transaction, got %d", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPurchaseVipDebitsCoinAndGrantsReward 验证 VIP 购买在同一账务事务中扣金币、更新会员和发权益。
|
// TestPurchaseVipDebitsCoinAndGrantsReward 验证 VIP 购买在同一账务事务中扣金币、更新会员和发权益。
|
||||||
func TestPurchaseVipDebitsCoinAndGrantsReward(t *testing.T) {
|
func TestPurchaseVipDebitsCoinAndGrantsReward(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
@ -2845,7 +2815,7 @@ func TestWalletOverviewDisablesDiamondExchange(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetWalletOverview failed: %v", err)
|
t.Fatalf("GetWalletOverview failed: %v", err)
|
||||||
}
|
}
|
||||||
if !overview.FeatureFlags.RechargeEnabled || overview.FeatureFlags.DiamondExchangeEnabled || !overview.FeatureFlags.WithdrawEnabled {
|
if !overview.FeatureFlags.RechargeEnabled || overview.FeatureFlags.DiamondExchangeEnabled {
|
||||||
t.Fatalf("wallet feature flags mismatch: %+v", overview.FeatureFlags)
|
t.Fatalf("wallet feature flags mismatch: %+v", overview.FeatureFlags)
|
||||||
}
|
}
|
||||||
if balanceAmount(overview.Balances, ledger.AssetCoin) != 700 || balanceAmount(overview.Balances, "DIAMOND") != 0 {
|
if balanceAmount(overview.Balances, ledger.AssetCoin) != 700 || balanceAmount(overview.Balances, "DIAMOND") != 0 {
|
||||||
|
|||||||
@ -16,15 +16,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
bizTypeWithdrawalApply = "withdrawal_apply"
|
bizTypeVIPPurchase = "vip_purchase"
|
||||||
bizTypeVIPPurchase = "vip_purchase"
|
bizTypeVIPGrant = "vip_grant"
|
||||||
bizTypeVIPGrant = "vip_grant"
|
vipOutboxAsset = "VIP"
|
||||||
vipOutboxAsset = "VIP"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetWalletOverview 返回钱包首页摘要;开关放在 wallet-service 内,避免 gateway 硬编码账务能力。
|
// GetWalletOverview 返回钱包首页摘要;开关放在 wallet-service 内,避免 gateway 硬编码账务能力。
|
||||||
func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error) {
|
func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error) {
|
||||||
balances, err := r.GetBalances(ctx, userID, []string{ledger.AssetCoin, ledger.AssetUSDBalance})
|
// 钱包首页返回所有可展示的资金口袋;工资钱包按身份拆分,0 余额也返回,便于客户端固定布局。
|
||||||
|
balances, err := r.GetBalances(ctx, userID, []string{
|
||||||
|
ledger.AssetCoin,
|
||||||
|
ledger.AssetHostSalaryUSD,
|
||||||
|
ledger.AssetAgencySalaryUSD,
|
||||||
|
ledger.AssetBDSalaryUSD,
|
||||||
|
ledger.AssetAdminSalaryUSD,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.WalletOverview{}, err
|
return ledger.WalletOverview{}, err
|
||||||
}
|
}
|
||||||
@ -33,7 +39,6 @@ func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledge
|
|||||||
FeatureFlags: ledger.WalletFeatureFlags{
|
FeatureFlags: ledger.WalletFeatureFlags{
|
||||||
RechargeEnabled: true,
|
RechargeEnabled: true,
|
||||||
DiamondExchangeEnabled: false,
|
DiamondExchangeEnabled: false,
|
||||||
WithdrawEnabled: true,
|
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -196,124 +201,6 @@ func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.Li
|
|||||||
return items, total, rows.Err()
|
return items, total, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyWithdrawal 冻结 USD_BALANCE 并写待审核提现申请。后台审核和人工转账是后续独立命令。
|
|
||||||
func (r *Repository) ApplyWithdrawal(ctx context.Context, command ledger.WithdrawalCommand) (ledger.WithdrawalRequest, ledger.AssetBalance, error) {
|
|
||||||
if r == nil || r.db == nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
|
||||||
}
|
|
||||||
ctx = contextWithCommandApp(ctx, command.AppCode)
|
|
||||||
command.AppCode = appcode.FromContext(ctx)
|
|
||||||
|
|
||||||
tx, err := r.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback() }()
|
|
||||||
|
|
||||||
requestHash := stableHash(fmt.Sprintf("withdrawal|%s|%d|%d|%s", command.AppCode, command.UserID, command.Amount, command.PayoutAccount))
|
|
||||||
if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeWithdrawalApply); err != nil || exists {
|
|
||||||
if err != nil || !exists {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
withdrawal, balance, receiptErr := r.withdrawalReceiptForTransaction(ctx, tx, txRow.TransactionID, command.UserID)
|
|
||||||
return withdrawal, balance, receiptErr
|
|
||||||
}
|
|
||||||
|
|
||||||
nowMs := time.Now().UnixMilli()
|
|
||||||
account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetUSDBalance, false, nowMs)
|
|
||||||
if err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
if account.AvailableAmount < command.Amount {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
|
|
||||||
}
|
|
||||||
transactionID := transactionID(command.AppCode, command.CommandID)
|
|
||||||
withdrawalID := "wdr_" + stableHash(command.AppCode+"|"+command.CommandID)
|
|
||||||
availableAfter := account.AvailableAmount - command.Amount
|
|
||||||
frozenAfter := account.FrozenAmount + command.Amount
|
|
||||||
metadata := map[string]any{
|
|
||||||
"app_code": command.AppCode,
|
|
||||||
"withdrawal_id": withdrawalID,
|
|
||||||
"user_id": command.UserID,
|
|
||||||
"asset_type": ledger.AssetUSDBalance,
|
|
||||||
"amount": command.Amount,
|
|
||||||
"payout_account": command.PayoutAccount,
|
|
||||||
"reason": command.Reason,
|
|
||||||
}
|
|
||||||
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeWithdrawalApply, requestHash, withdrawalID, metadata, nowMs); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
if err := r.applyAccountDelta(ctx, tx, account, -command.Amount, command.Amount, nowMs); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
if err := r.insertEntry(ctx, tx, walletEntry{
|
|
||||||
TransactionID: transactionID,
|
|
||||||
UserID: command.UserID,
|
|
||||||
AssetType: ledger.AssetUSDBalance,
|
|
||||||
AvailableDelta: -command.Amount,
|
|
||||||
FrozenDelta: command.Amount,
|
|
||||||
AvailableAfter: availableAfter,
|
|
||||||
FrozenAfter: frozenAfter,
|
|
||||||
CreatedAtMS: nowMs,
|
|
||||||
}); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
withdrawal := ledger.WithdrawalRequest{
|
|
||||||
WithdrawalID: withdrawalID,
|
|
||||||
UserID: command.UserID,
|
|
||||||
AssetType: ledger.AssetUSDBalance,
|
|
||||||
Amount: command.Amount,
|
|
||||||
Status: ledger.WithdrawalStatusPending,
|
|
||||||
PayoutAccount: command.PayoutAccount,
|
|
||||||
Reason: command.Reason,
|
|
||||||
CreatedAtMS: nowMs,
|
|
||||||
UpdatedAtMS: nowMs,
|
|
||||||
}
|
|
||||||
if _, err := tx.ExecContext(ctx, `
|
|
||||||
INSERT INTO wallet_withdrawal_requests (
|
|
||||||
app_code, withdrawal_id, command_id, user_id, asset_type, amount, status,
|
|
||||||
payout_account, reason, wallet_transaction_id, created_at_ms, updated_at_ms
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
command.AppCode, withdrawal.WithdrawalID, command.CommandID, command.UserID, withdrawal.AssetType,
|
|
||||||
withdrawal.Amount, withdrawal.Status, withdrawal.PayoutAccount, withdrawal.Reason, transactionID, nowMs, nowMs,
|
|
||||||
); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
|
|
||||||
balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetUSDBalance, -command.Amount, command.Amount, availableAfter, frozenAfter, account.Version+1, metadata, nowMs),
|
|
||||||
}); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return withdrawal, ledger.AssetBalance{
|
|
||||||
AppCode: command.AppCode,
|
|
||||||
UserID: command.UserID,
|
|
||||||
AssetType: ledger.AssetUSDBalance,
|
|
||||||
AvailableAmount: availableAfter,
|
|
||||||
FrozenAmount: frozenAfter,
|
|
||||||
Version: account.Version + 1,
|
|
||||||
UpdatedAtMs: nowMs,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) withdrawalReceiptForTransaction(ctx context.Context, tx *sql.Tx, transactionID string, userID int64) (ledger.WithdrawalRequest, ledger.AssetBalance, error) {
|
|
||||||
row := tx.QueryRowContext(ctx, `
|
|
||||||
SELECT withdrawal_id, user_id, asset_type, amount, status, payout_account, reason, created_at_ms, updated_at_ms
|
|
||||||
FROM wallet_withdrawal_requests
|
|
||||||
WHERE app_code = ? AND wallet_transaction_id = ?`,
|
|
||||||
appcode.FromContext(ctx), transactionID,
|
|
||||||
)
|
|
||||||
var withdrawal ledger.WithdrawalRequest
|
|
||||||
if err := row.Scan(&withdrawal.WithdrawalID, &withdrawal.UserID, &withdrawal.AssetType, &withdrawal.Amount, &withdrawal.Status, &withdrawal.PayoutAccount, &withdrawal.Reason, &withdrawal.CreatedAtMS, &withdrawal.UpdatedAtMS); err != nil {
|
|
||||||
return ledger.WithdrawalRequest{}, ledger.AssetBalance{}, err
|
|
||||||
}
|
|
||||||
balance, err := r.balanceAfterTransaction(ctx, tx, transactionID, userID, ledger.AssetUSDBalance)
|
|
||||||
return withdrawal, balance, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMyVip 返回用户当前 VIP 状态;过期只影响 active 投影,不直接修改历史事实。
|
// GetMyVip 返回用户当前 VIP 状态;过期只影响 active 投影,不直接修改历史事实。
|
||||||
func (r *Repository) GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error) {
|
func (r *Repository) GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
|
|||||||
@ -57,6 +57,8 @@ type hostSalarySettlementMetadata struct {
|
|||||||
HostUSDBalanceAfter int64 `json:"host_usd_balance_after"`
|
HostUSDBalanceAfter int64 `json:"host_usd_balance_after"`
|
||||||
HostCoinBalanceAfter int64 `json:"host_coin_balance_after"`
|
HostCoinBalanceAfter int64 `json:"host_coin_balance_after"`
|
||||||
AgencyUSDBalanceAfter int64 `json:"agency_usd_balance_after"`
|
AgencyUSDBalanceAfter int64 `json:"agency_usd_balance_after"`
|
||||||
|
HostSalaryAssetType string `json:"host_salary_asset_type"`
|
||||||
|
AgencySalaryAssetType string `json:"agency_salary_asset_type"`
|
||||||
SettledLevelNo int `json:"settled_level_no"`
|
SettledLevelNo int `json:"settled_level_no"`
|
||||||
SettledHostSalaryUSDMinor int64 `json:"settled_host_salary_usd_minor"`
|
SettledHostSalaryUSDMinor int64 `json:"settled_host_salary_usd_minor"`
|
||||||
SettledHostCoinReward int64 `json:"settled_host_coin_reward"`
|
SettledHostCoinReward int64 `json:"settled_host_coin_reward"`
|
||||||
@ -262,6 +264,8 @@ func (r *Repository) processHostSalaryCandidate(ctx context.Context, command led
|
|||||||
HostCoinRewardDelta: hostCoinDelta,
|
HostCoinRewardDelta: hostCoinDelta,
|
||||||
AgencySalaryUSDMinorDelta: agencyPaidDelta,
|
AgencySalaryUSDMinorDelta: agencyPaidDelta,
|
||||||
ResidualUSDMinorDelta: residualDelta,
|
ResidualUSDMinorDelta: residualDelta,
|
||||||
|
HostSalaryAssetType: ledger.AssetHostSalaryUSD,
|
||||||
|
AgencySalaryAssetType: ledger.AssetAgencySalaryUSD,
|
||||||
SettledLevelNo: nextProgress.SettledLevelNo,
|
SettledLevelNo: nextProgress.SettledLevelNo,
|
||||||
SettledHostSalaryUSDMinor: nextProgress.SettledHostSalaryUSDMinor,
|
SettledHostSalaryUSDMinor: nextProgress.SettledHostSalaryUSDMinor,
|
||||||
SettledHostCoinReward: nextProgress.SettledHostCoinReward,
|
SettledHostCoinReward: nextProgress.SettledHostCoinReward,
|
||||||
@ -407,12 +411,13 @@ func (r *Repository) applyHostSalaryCredits(ctx context.Context, tx *sql.Tx, tra
|
|||||||
// 主播美元、主播金币、代理美元共用同一 transaction_id,确保账务上可视为一次工资结算。
|
// 主播美元、主播金币、代理美元共用同一 transaction_id,确保账务上可视为一次工资结算。
|
||||||
events := make([]walletOutboxEvent, 0, 3)
|
events := make([]walletOutboxEvent, 0, 3)
|
||||||
if hostUSDDelta > 0 {
|
if hostUSDDelta > 0 {
|
||||||
balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.HostUserID, ledger.AssetUSDBalance, hostUSDDelta, 0, nowMs)
|
// 主播工资和月底剩余钻石折美元只进入主播工资钱包,不混入代理或团队工资钱包。
|
||||||
|
balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.HostUserID, ledger.AssetHostSalaryUSD, hostUSDDelta, 0, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
metadata.HostUSDBalanceAfter = balanceAfter
|
metadata.HostUSDBalanceAfter = balanceAfter
|
||||||
events = append(events, balanceChangedEvent(transactionID, commandID, metadata.HostUserID, ledger.AssetUSDBalance, hostUSDDelta, 0, balanceAfter, 0, version, metadata, nowMs))
|
events = append(events, balanceChangedEvent(transactionID, commandID, metadata.HostUserID, ledger.AssetHostSalaryUSD, hostUSDDelta, 0, balanceAfter, 0, version, metadata, nowMs))
|
||||||
}
|
}
|
||||||
if hostCoinDelta > 0 {
|
if hostCoinDelta > 0 {
|
||||||
balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.HostUserID, ledger.AssetCoin, hostCoinDelta, 0, nowMs)
|
balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.HostUserID, ledger.AssetCoin, hostCoinDelta, 0, nowMs)
|
||||||
@ -423,12 +428,13 @@ func (r *Repository) applyHostSalaryCredits(ctx context.Context, tx *sql.Tx, tra
|
|||||||
events = append(events, balanceChangedEvent(transactionID, commandID, metadata.HostUserID, ledger.AssetCoin, hostCoinDelta, 0, balanceAfter, 0, version, metadata, nowMs))
|
events = append(events, balanceChangedEvent(transactionID, commandID, metadata.HostUserID, ledger.AssetCoin, hostCoinDelta, 0, balanceAfter, 0, version, metadata, nowMs))
|
||||||
}
|
}
|
||||||
if agencyUSDDelta > 0 {
|
if agencyUSDDelta > 0 {
|
||||||
balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.AgencyOwnerUserID, ledger.AssetUSDBalance, agencyUSDDelta, metadata.HostUserID, nowMs)
|
// 代理工资进入代理工资钱包,同一用户若同时是主播也不会和主播工资钱包合并。
|
||||||
|
balanceAfter, version, err := r.creditSettlementAsset(ctx, tx, transactionID, metadata.AgencyOwnerUserID, ledger.AssetAgencySalaryUSD, agencyUSDDelta, metadata.HostUserID, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
metadata.AgencyUSDBalanceAfter = balanceAfter
|
metadata.AgencyUSDBalanceAfter = balanceAfter
|
||||||
events = append(events, balanceChangedEvent(transactionID, commandID, metadata.AgencyOwnerUserID, ledger.AssetUSDBalance, agencyUSDDelta, 0, balanceAfter, 0, version, metadata, nowMs))
|
events = append(events, balanceChangedEvent(transactionID, commandID, metadata.AgencyOwnerUserID, ledger.AssetAgencySalaryUSD, agencyUSDDelta, 0, balanceAfter, 0, version, metadata, nowMs))
|
||||||
}
|
}
|
||||||
return events, nil
|
return events, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -534,7 +534,7 @@ func validateTableName(table string) string {
|
|||||||
case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records",
|
case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records",
|
||||||
"host_period_diamond_accounts", "host_period_diamond_entries", "host_agency_salary_policies",
|
"host_period_diamond_accounts", "host_period_diamond_entries", "host_agency_salary_policies",
|
||||||
"host_agency_salary_policy_levels", "host_salary_settlement_progress", "host_salary_settlement_records",
|
"host_agency_salary_policy_levels", "host_salary_settlement_progress", "host_salary_settlement_records",
|
||||||
"coin_seller_stock_records", "gift_diamond_ratio_configs", "wallet_diamond_exchange_rules", "wallet_withdrawal_requests",
|
"coin_seller_stock_records", "gift_diamond_ratio_configs", "wallet_diamond_exchange_rules",
|
||||||
"vip_levels", "user_vip_memberships", "vip_purchase_orders", "user_vip_history",
|
"vip_levels", "user_vip_memberships", "vip_purchase_orders", "user_vip_history",
|
||||||
"resources", "resource_groups", "resource_group_items", "resource_shop_items", "resource_shop_purchase_orders", "gift_type_configs", "gift_configs", "gift_config_regions", "user_gift_wall", "wallet_projection_events",
|
"resources", "resource_groups", "resource_group_items", "resource_shop_items", "resource_shop_purchase_orders", "gift_type_configs", "gift_configs", "gift_config_regions", "user_gift_wall", "wallet_projection_events",
|
||||||
"resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment":
|
"resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment":
|
||||||
|
|||||||
@ -329,26 +329,6 @@ func (s *Server) ListWalletTransactions(ctx context.Context, req *walletv1.ListW
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyWithdrawal 创建提现申请并冻结美元余额。
|
|
||||||
func (s *Server) ApplyWithdrawal(ctx context.Context, req *walletv1.ApplyWithdrawalRequest) (*walletv1.ApplyWithdrawalResponse, error) {
|
|
||||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
|
||||||
withdrawal, balance, err := s.svc.ApplyWithdrawal(ctx, ledger.WithdrawalCommand{
|
|
||||||
AppCode: req.GetAppCode(),
|
|
||||||
CommandID: req.GetCommandId(),
|
|
||||||
UserID: req.GetUserId(),
|
|
||||||
Amount: req.GetAmount(),
|
|
||||||
PayoutAccount: req.GetPayoutAccount(),
|
|
||||||
Reason: req.GetReason(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, xerr.ToGRPCError(err)
|
|
||||||
}
|
|
||||||
return &walletv1.ApplyWithdrawalResponse{
|
|
||||||
Withdrawal: withdrawalToProto(withdrawal),
|
|
||||||
Balance: balanceToProto(balance),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListVipPackages 返回当前 VIP 和可购买包列表。
|
// ListVipPackages 返回当前 VIP 和可购买包列表。
|
||||||
func (s *Server) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
|
func (s *Server) ListVipPackages(ctx context.Context, req *walletv1.ListVipPackagesRequest) (*walletv1.ListVipPackagesResponse, error) {
|
||||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
@ -562,21 +542,6 @@ func walletFeatureFlagsToProto(flags ledger.WalletFeatureFlags) *walletv1.Wallet
|
|||||||
return &walletv1.WalletFeatureFlags{
|
return &walletv1.WalletFeatureFlags{
|
||||||
RechargeEnabled: flags.RechargeEnabled,
|
RechargeEnabled: flags.RechargeEnabled,
|
||||||
DiamondExchangeEnabled: flags.DiamondExchangeEnabled,
|
DiamondExchangeEnabled: flags.DiamondExchangeEnabled,
|
||||||
WithdrawEnabled: flags.WithdrawEnabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withdrawalToProto(withdrawal ledger.WithdrawalRequest) *walletv1.WithdrawalRequest {
|
|
||||||
return &walletv1.WithdrawalRequest{
|
|
||||||
WithdrawalId: withdrawal.WithdrawalID,
|
|
||||||
UserId: withdrawal.UserID,
|
|
||||||
AssetType: withdrawal.AssetType,
|
|
||||||
Amount: withdrawal.Amount,
|
|
||||||
Status: withdrawal.Status,
|
|
||||||
PayoutAccount: withdrawal.PayoutAccount,
|
|
||||||
Reason: withdrawal.Reason,
|
|
||||||
CreatedAtMs: withdrawal.CreatedAtMS,
|
|
||||||
UpdatedAtMs: withdrawal.UpdatedAtMS,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user