积分收益政策h5
This commit is contained in:
parent
4794507044
commit
ca15f09850
File diff suppressed because it is too large
Load Diff
@ -698,6 +698,39 @@ message UpdateAgencyProfileResponse {
|
|||||||
Agency agency = 1;
|
Agency agency = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostEngagementStats 是 Fami 等公会中心使用的用户侧统计聚合;金额/礼物流水由 wallet-service 单独拥有。
|
||||||
|
message HostEngagementStats {
|
||||||
|
int64 online_duration_ms = 1;
|
||||||
|
int64 valid_mic_duration_ms = 2;
|
||||||
|
int64 valid_mic_days = 3;
|
||||||
|
int64 private_message_senders = 4;
|
||||||
|
int64 new_followers = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHostEngagementStatsRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
int64 host_user_id = 2;
|
||||||
|
int64 start_at_ms = 3;
|
||||||
|
int64 end_at_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHostEngagementStatsResponse {
|
||||||
|
HostEngagementStats stats = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordPrivateMessageEvent 由经过腾讯回调鉴权的 gateway 写入私信事实;重复 event_id 必须幂等。
|
||||||
|
message RecordPrivateMessageEventRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
string event_id = 2;
|
||||||
|
int64 sender_user_id = 3;
|
||||||
|
int64 target_user_id = 4;
|
||||||
|
int64 occurred_at_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RecordPrivateMessageEventResponse {
|
||||||
|
bool created = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// UserHostService 是 user-service 内部 host domain 的 gRPC 面,不是独立微服务。
|
// UserHostService 是 user-service 内部 host domain 的 gRPC 面,不是独立微服务。
|
||||||
service UserHostService {
|
service UserHostService {
|
||||||
rpc SearchAgencies(SearchAgenciesRequest) returns (SearchAgenciesResponse);
|
rpc SearchAgencies(SearchAgenciesRequest) returns (SearchAgenciesResponse);
|
||||||
@ -731,6 +764,8 @@ service UserHostService {
|
|||||||
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
|
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
|
||||||
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
|
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
|
||||||
rpc UpdateAgencyProfile(UpdateAgencyProfileRequest) returns (UpdateAgencyProfileResponse);
|
rpc UpdateAgencyProfile(UpdateAgencyProfileRequest) returns (UpdateAgencyProfileResponse);
|
||||||
|
rpc GetHostEngagementStats(GetHostEngagementStatsRequest) returns (GetHostEngagementStatsResponse);
|
||||||
|
rpc RecordPrivateMessageEvent(RecordPrivateMessageEventRequest) returns (RecordPrivateMessageEventResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserHostAdminService 是后台关系管理入口;公网 admin 鉴权由 admin-server 承担。
|
// UserHostAdminService 是后台关系管理入口;公网 admin 鉴权由 admin-server 承担。
|
||||||
|
|||||||
@ -49,6 +49,8 @@ const (
|
|||||||
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
|
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
|
||||||
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
|
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
|
||||||
UserHostService_UpdateAgencyProfile_FullMethodName = "/hyapp.user.v1.UserHostService/UpdateAgencyProfile"
|
UserHostService_UpdateAgencyProfile_FullMethodName = "/hyapp.user.v1.UserHostService/UpdateAgencyProfile"
|
||||||
|
UserHostService_GetHostEngagementStats_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostEngagementStats"
|
||||||
|
UserHostService_RecordPrivateMessageEvent_FullMethodName = "/hyapp.user.v1.UserHostService/RecordPrivateMessageEvent"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserHostServiceClient is the client API for UserHostService service.
|
// UserHostServiceClient is the client API for UserHostService service.
|
||||||
@ -88,6 +90,8 @@ type UserHostServiceClient interface {
|
|||||||
GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error)
|
GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error)
|
||||||
GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error)
|
GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error)
|
||||||
UpdateAgencyProfile(ctx context.Context, in *UpdateAgencyProfileRequest, opts ...grpc.CallOption) (*UpdateAgencyProfileResponse, error)
|
UpdateAgencyProfile(ctx context.Context, in *UpdateAgencyProfileRequest, opts ...grpc.CallOption) (*UpdateAgencyProfileResponse, error)
|
||||||
|
GetHostEngagementStats(ctx context.Context, in *GetHostEngagementStatsRequest, opts ...grpc.CallOption) (*GetHostEngagementStatsResponse, error)
|
||||||
|
RecordPrivateMessageEvent(ctx context.Context, in *RecordPrivateMessageEventRequest, opts ...grpc.CallOption) (*RecordPrivateMessageEventResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userHostServiceClient struct {
|
type userHostServiceClient struct {
|
||||||
@ -398,6 +402,26 @@ func (c *userHostServiceClient) UpdateAgencyProfile(ctx context.Context, in *Upd
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *userHostServiceClient) GetHostEngagementStats(ctx context.Context, in *GetHostEngagementStatsRequest, opts ...grpc.CallOption) (*GetHostEngagementStatsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetHostEngagementStatsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserHostService_GetHostEngagementStats_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userHostServiceClient) RecordPrivateMessageEvent(ctx context.Context, in *RecordPrivateMessageEventRequest, opts ...grpc.CallOption) (*RecordPrivateMessageEventResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(RecordPrivateMessageEventResponse)
|
||||||
|
err := c.cc.Invoke(ctx, UserHostService_RecordPrivateMessageEvent_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UserHostServiceServer is the server API for UserHostService service.
|
// UserHostServiceServer is the server API for UserHostService service.
|
||||||
// All implementations must embed UnimplementedUserHostServiceServer
|
// All implementations must embed UnimplementedUserHostServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@ -435,6 +459,8 @@ type UserHostServiceServer interface {
|
|||||||
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
|
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
|
||||||
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
|
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
|
||||||
UpdateAgencyProfile(context.Context, *UpdateAgencyProfileRequest) (*UpdateAgencyProfileResponse, error)
|
UpdateAgencyProfile(context.Context, *UpdateAgencyProfileRequest) (*UpdateAgencyProfileResponse, error)
|
||||||
|
GetHostEngagementStats(context.Context, *GetHostEngagementStatsRequest) (*GetHostEngagementStatsResponse, error)
|
||||||
|
RecordPrivateMessageEvent(context.Context, *RecordPrivateMessageEventRequest) (*RecordPrivateMessageEventResponse, error)
|
||||||
mustEmbedUnimplementedUserHostServiceServer()
|
mustEmbedUnimplementedUserHostServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,6 +561,12 @@ func (UnimplementedUserHostServiceServer) GetAgencyApplications(context.Context,
|
|||||||
func (UnimplementedUserHostServiceServer) UpdateAgencyProfile(context.Context, *UpdateAgencyProfileRequest) (*UpdateAgencyProfileResponse, error) {
|
func (UnimplementedUserHostServiceServer) UpdateAgencyProfile(context.Context, *UpdateAgencyProfileRequest) (*UpdateAgencyProfileResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAgencyProfile not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method UpdateAgencyProfile not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedUserHostServiceServer) GetHostEngagementStats(context.Context, *GetHostEngagementStatsRequest) (*GetHostEngagementStatsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetHostEngagementStats not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedUserHostServiceServer) RecordPrivateMessageEvent(context.Context, *RecordPrivateMessageEventRequest) (*RecordPrivateMessageEventResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RecordPrivateMessageEvent not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedUserHostServiceServer) mustEmbedUnimplementedUserHostServiceServer() {}
|
func (UnimplementedUserHostServiceServer) mustEmbedUnimplementedUserHostServiceServer() {}
|
||||||
func (UnimplementedUserHostServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedUserHostServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@ -1096,6 +1128,42 @@ func _UserHostService_UpdateAgencyProfile_Handler(srv interface{}, ctx context.C
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _UserHostService_GetHostEngagementStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetHostEngagementStatsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserHostServiceServer).GetHostEngagementStats(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserHostService_GetHostEngagementStats_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserHostServiceServer).GetHostEngagementStats(ctx, req.(*GetHostEngagementStatsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _UserHostService_RecordPrivateMessageEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RecordPrivateMessageEventRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(UserHostServiceServer).RecordPrivateMessageEvent(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: UserHostService_RecordPrivateMessageEvent_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(UserHostServiceServer).RecordPrivateMessageEvent(ctx, req.(*RecordPrivateMessageEventRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// UserHostService_ServiceDesc is the grpc.ServiceDesc for UserHostService service.
|
// UserHostService_ServiceDesc is the grpc.ServiceDesc for UserHostService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -1223,6 +1291,14 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "UpdateAgencyProfile",
|
MethodName: "UpdateAgencyProfile",
|
||||||
Handler: _UserHostService_UpdateAgencyProfile_Handler,
|
Handler: _UserHostService_UpdateAgencyProfile_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetHostEngagementStats",
|
||||||
|
Handler: _UserHostService_GetHostEngagementStats_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RecordPrivateMessageEvent",
|
||||||
|
Handler: _UserHostService_RecordPrivateMessageEvent_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/user/v1/host.proto",
|
Metadata: "proto/user/v1/host.proto",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2763,6 +2763,122 @@ message GetGiftCatalogVersionResponse {
|
|||||||
int64 version = 1;
|
int64 version = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HostRevenueStats 聚合主播在自然日区间内的收益 POINT 和送礼用户;gateway 对外仍按产品文案展示为钻石。
|
||||||
|
message HostRevenueStats {
|
||||||
|
int64 diamond_earnings = 1;
|
||||||
|
int64 diamond_exchanged = 2;
|
||||||
|
int64 gift_senders = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHostRevenueStatsRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 host_user_id = 3;
|
||||||
|
int64 start_at_ms = 4;
|
||||||
|
int64 end_at_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHostRevenueStatsResponse {
|
||||||
|
HostRevenueStats stats = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgencyPointShareStats 只统计独立 Agency 分成流水,不把 owner 自己的主播礼物收益重复算入分成。
|
||||||
|
message AgencyPointShareStats {
|
||||||
|
int64 share_income = 1;
|
||||||
|
int64 gifted_host_count = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyPointShareStatsRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 agency_owner_user_id = 3;
|
||||||
|
int64 start_at_ms = 4;
|
||||||
|
int64 end_at_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyPointShareStatsResponse {
|
||||||
|
AgencyPointShareStats stats = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AgencyHostGiftStats {
|
||||||
|
int64 gift_income = 1;
|
||||||
|
int64 gifted_host_count = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyHostGiftStatsRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
repeated int64 host_user_ids = 3;
|
||||||
|
int64 start_at_ms = 4;
|
||||||
|
int64 end_at_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetAgencyHostGiftStatsResponse {
|
||||||
|
AgencyHostGiftStats stats = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointWithdrawalCoinSellerConfig 是按 App 隔离的 POINT 提现币商白名单与兑换比例。
|
||||||
|
// 比例用整数分子/分母保存,避免浮点金额在 H5、gateway 和账本间产生舍入分歧。
|
||||||
|
message PointWithdrawalCoinSellerConfig {
|
||||||
|
string app_code = 1;
|
||||||
|
int64 seller_user_id = 2;
|
||||||
|
int32 sort_order = 3;
|
||||||
|
int64 point_amount = 4;
|
||||||
|
int64 seller_coin_amount = 5;
|
||||||
|
repeated string service_country_codes = 6;
|
||||||
|
string status = 7;
|
||||||
|
int64 created_at_ms = 8;
|
||||||
|
int64 updated_at_ms = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPointWithdrawalCoinSellersRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
// country_code 由 gateway 从登录用户资料注入;为空只用于 Admin/内部查询。
|
||||||
|
string country_code = 3;
|
||||||
|
bool include_disabled = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPointWithdrawalCoinSellersResponse {
|
||||||
|
repeated PointWithdrawalCoinSellerConfig sellers = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransferPointToCoinSellerRequest {
|
||||||
|
string command_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 source_user_id = 3;
|
||||||
|
int64 seller_user_id = 4;
|
||||||
|
int64 point_amount = 5;
|
||||||
|
// source_country_code 由 gateway 从用户资料读取,wallet 会再次校验币商服务国家。
|
||||||
|
string source_country_code = 6;
|
||||||
|
string reason = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransferPointToCoinSellerResponse {
|
||||||
|
string transaction_id = 1;
|
||||||
|
int64 source_point_balance_after = 2;
|
||||||
|
int64 seller_balance_after = 3;
|
||||||
|
int64 point_amount = 4;
|
||||||
|
int64 seller_coin_amount = 5;
|
||||||
|
int64 ratio_point_amount = 6;
|
||||||
|
int64 ratio_seller_coin_amount = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPointWithdrawalConfigRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
int64 region_id = 3;
|
||||||
|
int64 now_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPointWithdrawalConfigResponse {
|
||||||
|
bool found = 1;
|
||||||
|
int64 points_per_usd = 2;
|
||||||
|
int32 fee_bps = 3;
|
||||||
|
int64 minimum_points = 4;
|
||||||
|
string policy_instance_code = 5;
|
||||||
|
}
|
||||||
|
|
||||||
// WalletCronService 只给 cron-service 调用;账务状态仍由 wallet-service owner 修改。
|
// WalletCronService 只给 cron-service 调用;账务状态仍由 wallet-service owner 修改。
|
||||||
service WalletCronService {
|
service WalletCronService {
|
||||||
rpc ProcessHostSalaryDailySettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
rpc ProcessHostSalaryDailySettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||||
@ -2780,6 +2896,9 @@ service WalletService {
|
|||||||
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
||||||
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
|
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
|
||||||
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
|
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
|
||||||
|
rpc GetHostRevenueStats(GetHostRevenueStatsRequest) returns (GetHostRevenueStatsResponse);
|
||||||
|
rpc GetAgencyPointShareStats(GetAgencyPointShareStatsRequest) returns (GetAgencyPointShareStatsResponse);
|
||||||
|
rpc GetAgencyHostGiftStats(GetAgencyHostGiftStatsRequest) returns (GetAgencyHostGiftStatsResponse);
|
||||||
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
||||||
rpc GetTeamHostSalaryStats(GetTeamHostSalaryStatsRequest) returns (GetTeamHostSalaryStatsResponse);
|
rpc GetTeamHostSalaryStats(GetTeamHostSalaryStatsRequest) returns (GetTeamHostSalaryStatsResponse);
|
||||||
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
||||||
@ -2789,6 +2908,9 @@ service WalletService {
|
|||||||
rpc ListCoinSellerSalaryExchangeRateTiers(ListCoinSellerSalaryExchangeRateTiersRequest) returns (ListCoinSellerSalaryExchangeRateTiersResponse);
|
rpc ListCoinSellerSalaryExchangeRateTiers(ListCoinSellerSalaryExchangeRateTiersRequest) returns (ListCoinSellerSalaryExchangeRateTiersResponse);
|
||||||
rpc ExchangeSalaryToCoin(ExchangeSalaryToCoinRequest) returns (ExchangeSalaryToCoinResponse);
|
rpc ExchangeSalaryToCoin(ExchangeSalaryToCoinRequest) returns (ExchangeSalaryToCoinResponse);
|
||||||
rpc TransferSalaryToCoinSeller(TransferSalaryToCoinSellerRequest) returns (TransferSalaryToCoinSellerResponse);
|
rpc TransferSalaryToCoinSeller(TransferSalaryToCoinSellerRequest) returns (TransferSalaryToCoinSellerResponse);
|
||||||
|
rpc ListPointWithdrawalCoinSellers(ListPointWithdrawalCoinSellersRequest) returns (ListPointWithdrawalCoinSellersResponse);
|
||||||
|
rpc TransferPointToCoinSeller(TransferPointToCoinSellerRequest) returns (TransferPointToCoinSellerResponse);
|
||||||
|
rpc GetPointWithdrawalConfig(GetPointWithdrawalConfigRequest) returns (GetPointWithdrawalConfigResponse);
|
||||||
rpc FreezeSalaryWithdrawal(FreezeSalaryWithdrawalRequest) returns (FreezeSalaryWithdrawalResponse);
|
rpc FreezeSalaryWithdrawal(FreezeSalaryWithdrawalRequest) returns (FreezeSalaryWithdrawalResponse);
|
||||||
rpc SettleSalaryWithdrawal(SettleSalaryWithdrawalRequest) returns (SettleSalaryWithdrawalResponse);
|
rpc SettleSalaryWithdrawal(SettleSalaryWithdrawalRequest) returns (SettleSalaryWithdrawalResponse);
|
||||||
rpc ReleaseSalaryWithdrawal(ReleaseSalaryWithdrawalRequest) returns (ReleaseSalaryWithdrawalResponse);
|
rpc ReleaseSalaryWithdrawal(ReleaseSalaryWithdrawalRequest) returns (ReleaseSalaryWithdrawalResponse);
|
||||||
|
|||||||
@ -246,6 +246,9 @@ const (
|
|||||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||||
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
|
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
|
||||||
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
|
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
|
||||||
|
WalletService_GetHostRevenueStats_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostRevenueStats"
|
||||||
|
WalletService_GetAgencyPointShareStats_FullMethodName = "/hyapp.wallet.v1.WalletService/GetAgencyPointShareStats"
|
||||||
|
WalletService_GetAgencyHostGiftStats_FullMethodName = "/hyapp.wallet.v1.WalletService/GetAgencyHostGiftStats"
|
||||||
WalletService_GetTeamHostSalaryStats_FullMethodName = "/hyapp.wallet.v1.WalletService/GetTeamHostSalaryStats"
|
WalletService_GetTeamHostSalaryStats_FullMethodName = "/hyapp.wallet.v1.WalletService/GetTeamHostSalaryStats"
|
||||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||||
@ -254,6 +257,9 @@ const (
|
|||||||
WalletService_ListCoinSellerSalaryExchangeRateTiers_FullMethodName = "/hyapp.wallet.v1.WalletService/ListCoinSellerSalaryExchangeRateTiers"
|
WalletService_ListCoinSellerSalaryExchangeRateTiers_FullMethodName = "/hyapp.wallet.v1.WalletService/ListCoinSellerSalaryExchangeRateTiers"
|
||||||
WalletService_ExchangeSalaryToCoin_FullMethodName = "/hyapp.wallet.v1.WalletService/ExchangeSalaryToCoin"
|
WalletService_ExchangeSalaryToCoin_FullMethodName = "/hyapp.wallet.v1.WalletService/ExchangeSalaryToCoin"
|
||||||
WalletService_TransferSalaryToCoinSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferSalaryToCoinSeller"
|
WalletService_TransferSalaryToCoinSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferSalaryToCoinSeller"
|
||||||
|
WalletService_ListPointWithdrawalCoinSellers_FullMethodName = "/hyapp.wallet.v1.WalletService/ListPointWithdrawalCoinSellers"
|
||||||
|
WalletService_TransferPointToCoinSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferPointToCoinSeller"
|
||||||
|
WalletService_GetPointWithdrawalConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetPointWithdrawalConfig"
|
||||||
WalletService_FreezeSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/FreezeSalaryWithdrawal"
|
WalletService_FreezeSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/FreezeSalaryWithdrawal"
|
||||||
WalletService_SettleSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/SettleSalaryWithdrawal"
|
WalletService_SettleSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/SettleSalaryWithdrawal"
|
||||||
WalletService_ReleaseSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ReleaseSalaryWithdrawal"
|
WalletService_ReleaseSalaryWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ReleaseSalaryWithdrawal"
|
||||||
@ -372,6 +378,9 @@ type WalletServiceClient interface {
|
|||||||
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
||||||
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
|
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
|
||||||
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
|
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
|
||||||
|
GetHostRevenueStats(ctx context.Context, in *GetHostRevenueStatsRequest, opts ...grpc.CallOption) (*GetHostRevenueStatsResponse, error)
|
||||||
|
GetAgencyPointShareStats(ctx context.Context, in *GetAgencyPointShareStatsRequest, opts ...grpc.CallOption) (*GetAgencyPointShareStatsResponse, error)
|
||||||
|
GetAgencyHostGiftStats(ctx context.Context, in *GetAgencyHostGiftStatsRequest, opts ...grpc.CallOption) (*GetAgencyHostGiftStatsResponse, error)
|
||||||
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
||||||
GetTeamHostSalaryStats(ctx context.Context, in *GetTeamHostSalaryStatsRequest, opts ...grpc.CallOption) (*GetTeamHostSalaryStatsResponse, error)
|
GetTeamHostSalaryStats(ctx context.Context, in *GetTeamHostSalaryStatsRequest, opts ...grpc.CallOption) (*GetTeamHostSalaryStatsResponse, error)
|
||||||
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
||||||
@ -381,6 +390,9 @@ type WalletServiceClient interface {
|
|||||||
ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, in *ListCoinSellerSalaryExchangeRateTiersRequest, opts ...grpc.CallOption) (*ListCoinSellerSalaryExchangeRateTiersResponse, error)
|
ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, in *ListCoinSellerSalaryExchangeRateTiersRequest, opts ...grpc.CallOption) (*ListCoinSellerSalaryExchangeRateTiersResponse, error)
|
||||||
ExchangeSalaryToCoin(ctx context.Context, in *ExchangeSalaryToCoinRequest, opts ...grpc.CallOption) (*ExchangeSalaryToCoinResponse, error)
|
ExchangeSalaryToCoin(ctx context.Context, in *ExchangeSalaryToCoinRequest, opts ...grpc.CallOption) (*ExchangeSalaryToCoinResponse, error)
|
||||||
TransferSalaryToCoinSeller(ctx context.Context, in *TransferSalaryToCoinSellerRequest, opts ...grpc.CallOption) (*TransferSalaryToCoinSellerResponse, error)
|
TransferSalaryToCoinSeller(ctx context.Context, in *TransferSalaryToCoinSellerRequest, opts ...grpc.CallOption) (*TransferSalaryToCoinSellerResponse, error)
|
||||||
|
ListPointWithdrawalCoinSellers(ctx context.Context, in *ListPointWithdrawalCoinSellersRequest, opts ...grpc.CallOption) (*ListPointWithdrawalCoinSellersResponse, error)
|
||||||
|
TransferPointToCoinSeller(ctx context.Context, in *TransferPointToCoinSellerRequest, opts ...grpc.CallOption) (*TransferPointToCoinSellerResponse, error)
|
||||||
|
GetPointWithdrawalConfig(ctx context.Context, in *GetPointWithdrawalConfigRequest, opts ...grpc.CallOption) (*GetPointWithdrawalConfigResponse, error)
|
||||||
FreezeSalaryWithdrawal(ctx context.Context, in *FreezeSalaryWithdrawalRequest, opts ...grpc.CallOption) (*FreezeSalaryWithdrawalResponse, error)
|
FreezeSalaryWithdrawal(ctx context.Context, in *FreezeSalaryWithdrawalRequest, opts ...grpc.CallOption) (*FreezeSalaryWithdrawalResponse, error)
|
||||||
SettleSalaryWithdrawal(ctx context.Context, in *SettleSalaryWithdrawalRequest, opts ...grpc.CallOption) (*SettleSalaryWithdrawalResponse, error)
|
SettleSalaryWithdrawal(ctx context.Context, in *SettleSalaryWithdrawalRequest, opts ...grpc.CallOption) (*SettleSalaryWithdrawalResponse, error)
|
||||||
ReleaseSalaryWithdrawal(ctx context.Context, in *ReleaseSalaryWithdrawalRequest, opts ...grpc.CallOption) (*ReleaseSalaryWithdrawalResponse, error)
|
ReleaseSalaryWithdrawal(ctx context.Context, in *ReleaseSalaryWithdrawalRequest, opts ...grpc.CallOption) (*ReleaseSalaryWithdrawalResponse, error)
|
||||||
@ -564,6 +576,36 @@ func (c *walletServiceClient) GetHostSalaryProgress(ctx context.Context, in *Get
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) GetHostRevenueStats(ctx context.Context, in *GetHostRevenueStatsRequest, opts ...grpc.CallOption) (*GetHostRevenueStatsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetHostRevenueStatsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_GetHostRevenueStats_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) GetAgencyPointShareStats(ctx context.Context, in *GetAgencyPointShareStatsRequest, opts ...grpc.CallOption) (*GetAgencyPointShareStatsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetAgencyPointShareStatsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_GetAgencyPointShareStats_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) GetAgencyHostGiftStats(ctx context.Context, in *GetAgencyHostGiftStatsRequest, opts ...grpc.CallOption) (*GetAgencyHostGiftStatsResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetAgencyHostGiftStatsResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_GetAgencyHostGiftStats_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *walletServiceClient) GetTeamHostSalaryStats(ctx context.Context, in *GetTeamHostSalaryStatsRequest, opts ...grpc.CallOption) (*GetTeamHostSalaryStatsResponse, error) {
|
func (c *walletServiceClient) GetTeamHostSalaryStats(ctx context.Context, in *GetTeamHostSalaryStatsRequest, opts ...grpc.CallOption) (*GetTeamHostSalaryStatsResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(GetTeamHostSalaryStatsResponse)
|
out := new(GetTeamHostSalaryStatsResponse)
|
||||||
@ -644,6 +686,36 @@ func (c *walletServiceClient) TransferSalaryToCoinSeller(ctx context.Context, in
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) ListPointWithdrawalCoinSellers(ctx context.Context, in *ListPointWithdrawalCoinSellersRequest, opts ...grpc.CallOption) (*ListPointWithdrawalCoinSellersResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListPointWithdrawalCoinSellersResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_ListPointWithdrawalCoinSellers_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) TransferPointToCoinSeller(ctx context.Context, in *TransferPointToCoinSellerRequest, opts ...grpc.CallOption) (*TransferPointToCoinSellerResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(TransferPointToCoinSellerResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_TransferPointToCoinSeller_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) GetPointWithdrawalConfig(ctx context.Context, in *GetPointWithdrawalConfigRequest, opts ...grpc.CallOption) (*GetPointWithdrawalConfigResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetPointWithdrawalConfigResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_GetPointWithdrawalConfig_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *walletServiceClient) FreezeSalaryWithdrawal(ctx context.Context, in *FreezeSalaryWithdrawalRequest, opts ...grpc.CallOption) (*FreezeSalaryWithdrawalResponse, error) {
|
func (c *walletServiceClient) FreezeSalaryWithdrawal(ctx context.Context, in *FreezeSalaryWithdrawalRequest, opts ...grpc.CallOption) (*FreezeSalaryWithdrawalResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(FreezeSalaryWithdrawalResponse)
|
out := new(FreezeSalaryWithdrawalResponse)
|
||||||
@ -1687,6 +1759,9 @@ type WalletServiceServer interface {
|
|||||||
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
||||||
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
|
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
|
||||||
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
|
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
|
||||||
|
GetHostRevenueStats(context.Context, *GetHostRevenueStatsRequest) (*GetHostRevenueStatsResponse, error)
|
||||||
|
GetAgencyPointShareStats(context.Context, *GetAgencyPointShareStatsRequest) (*GetAgencyPointShareStatsResponse, error)
|
||||||
|
GetAgencyHostGiftStats(context.Context, *GetAgencyHostGiftStatsRequest) (*GetAgencyHostGiftStatsResponse, error)
|
||||||
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
// GetTeamHostSalaryStats 按 Agency 收款人集合聚合主播预收入工资;经理中心团队工资卡片使用。
|
||||||
GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error)
|
GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error)
|
||||||
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
||||||
@ -1696,6 +1771,9 @@ type WalletServiceServer interface {
|
|||||||
ListCoinSellerSalaryExchangeRateTiers(context.Context, *ListCoinSellerSalaryExchangeRateTiersRequest) (*ListCoinSellerSalaryExchangeRateTiersResponse, error)
|
ListCoinSellerSalaryExchangeRateTiers(context.Context, *ListCoinSellerSalaryExchangeRateTiersRequest) (*ListCoinSellerSalaryExchangeRateTiersResponse, error)
|
||||||
ExchangeSalaryToCoin(context.Context, *ExchangeSalaryToCoinRequest) (*ExchangeSalaryToCoinResponse, error)
|
ExchangeSalaryToCoin(context.Context, *ExchangeSalaryToCoinRequest) (*ExchangeSalaryToCoinResponse, error)
|
||||||
TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error)
|
TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error)
|
||||||
|
ListPointWithdrawalCoinSellers(context.Context, *ListPointWithdrawalCoinSellersRequest) (*ListPointWithdrawalCoinSellersResponse, error)
|
||||||
|
TransferPointToCoinSeller(context.Context, *TransferPointToCoinSellerRequest) (*TransferPointToCoinSellerResponse, error)
|
||||||
|
GetPointWithdrawalConfig(context.Context, *GetPointWithdrawalConfigRequest) (*GetPointWithdrawalConfigResponse, error)
|
||||||
FreezeSalaryWithdrawal(context.Context, *FreezeSalaryWithdrawalRequest) (*FreezeSalaryWithdrawalResponse, error)
|
FreezeSalaryWithdrawal(context.Context, *FreezeSalaryWithdrawalRequest) (*FreezeSalaryWithdrawalResponse, error)
|
||||||
SettleSalaryWithdrawal(context.Context, *SettleSalaryWithdrawalRequest) (*SettleSalaryWithdrawalResponse, error)
|
SettleSalaryWithdrawal(context.Context, *SettleSalaryWithdrawalRequest) (*SettleSalaryWithdrawalResponse, error)
|
||||||
ReleaseSalaryWithdrawal(context.Context, *ReleaseSalaryWithdrawalRequest) (*ReleaseSalaryWithdrawalResponse, error)
|
ReleaseSalaryWithdrawal(context.Context, *ReleaseSalaryWithdrawalRequest) (*ReleaseSalaryWithdrawalResponse, error)
|
||||||
@ -1830,6 +1908,15 @@ func (UnimplementedWalletServiceServer) GetActiveHostSalaryPolicy(context.Contex
|
|||||||
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
|
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) GetHostRevenueStats(context.Context, *GetHostRevenueStatsRequest) (*GetHostRevenueStatsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetHostRevenueStats not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) GetAgencyPointShareStats(context.Context, *GetAgencyPointShareStatsRequest) (*GetAgencyPointShareStatsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyPointShareStats not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) GetAgencyHostGiftStats(context.Context, *GetAgencyHostGiftStatsRequest) (*GetAgencyHostGiftStatsResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyHostGiftStats not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedWalletServiceServer) GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error) {
|
func (UnimplementedWalletServiceServer) GetTeamHostSalaryStats(context.Context, *GetTeamHostSalaryStatsRequest) (*GetTeamHostSalaryStatsResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetTeamHostSalaryStats not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GetTeamHostSalaryStats not implemented")
|
||||||
}
|
}
|
||||||
@ -1854,6 +1941,15 @@ func (UnimplementedWalletServiceServer) ExchangeSalaryToCoin(context.Context, *E
|
|||||||
func (UnimplementedWalletServiceServer) TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error) {
|
func (UnimplementedWalletServiceServer) TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) ListPointWithdrawalCoinSellers(context.Context, *ListPointWithdrawalCoinSellersRequest) (*ListPointWithdrawalCoinSellersResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListPointWithdrawalCoinSellers not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) TransferPointToCoinSeller(context.Context, *TransferPointToCoinSellerRequest) (*TransferPointToCoinSellerResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method TransferPointToCoinSeller not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) GetPointWithdrawalConfig(context.Context, *GetPointWithdrawalConfigRequest) (*GetPointWithdrawalConfigResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetPointWithdrawalConfig not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedWalletServiceServer) FreezeSalaryWithdrawal(context.Context, *FreezeSalaryWithdrawalRequest) (*FreezeSalaryWithdrawalResponse, error) {
|
func (UnimplementedWalletServiceServer) FreezeSalaryWithdrawal(context.Context, *FreezeSalaryWithdrawalRequest) (*FreezeSalaryWithdrawalResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method FreezeSalaryWithdrawal not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method FreezeSalaryWithdrawal not implemented")
|
||||||
}
|
}
|
||||||
@ -2310,6 +2406,60 @@ func _WalletService_GetHostSalaryProgress_Handler(srv interface{}, ctx context.C
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _WalletService_GetHostRevenueStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetHostRevenueStatsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).GetHostRevenueStats(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_GetHostRevenueStats_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).GetHostRevenueStats(ctx, req.(*GetHostRevenueStatsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _WalletService_GetAgencyPointShareStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetAgencyPointShareStatsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).GetAgencyPointShareStats(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_GetAgencyPointShareStats_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).GetAgencyPointShareStats(ctx, req.(*GetAgencyPointShareStatsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _WalletService_GetAgencyHostGiftStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetAgencyHostGiftStatsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).GetAgencyHostGiftStats(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_GetAgencyHostGiftStats_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).GetAgencyHostGiftStats(ctx, req.(*GetAgencyHostGiftStatsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _WalletService_GetTeamHostSalaryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _WalletService_GetTeamHostSalaryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(GetTeamHostSalaryStatsRequest)
|
in := new(GetTeamHostSalaryStatsRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -2454,6 +2604,60 @@ func _WalletService_TransferSalaryToCoinSeller_Handler(srv interface{}, ctx cont
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _WalletService_ListPointWithdrawalCoinSellers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListPointWithdrawalCoinSellersRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).ListPointWithdrawalCoinSellers(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_ListPointWithdrawalCoinSellers_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).ListPointWithdrawalCoinSellers(ctx, req.(*ListPointWithdrawalCoinSellersRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _WalletService_TransferPointToCoinSeller_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(TransferPointToCoinSellerRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).TransferPointToCoinSeller(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_TransferPointToCoinSeller_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).TransferPointToCoinSeller(ctx, req.(*TransferPointToCoinSellerRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _WalletService_GetPointWithdrawalConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetPointWithdrawalConfigRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).GetPointWithdrawalConfig(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_GetPointWithdrawalConfig_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).GetPointWithdrawalConfig(ctx, req.(*GetPointWithdrawalConfigRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _WalletService_FreezeSalaryWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _WalletService_FreezeSalaryWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(FreezeSalaryWithdrawalRequest)
|
in := new(FreezeSalaryWithdrawalRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -4343,6 +4547,18 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "GetHostSalaryProgress",
|
MethodName: "GetHostSalaryProgress",
|
||||||
Handler: _WalletService_GetHostSalaryProgress_Handler,
|
Handler: _WalletService_GetHostSalaryProgress_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetHostRevenueStats",
|
||||||
|
Handler: _WalletService_GetHostRevenueStats_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetAgencyPointShareStats",
|
||||||
|
Handler: _WalletService_GetAgencyPointShareStats_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetAgencyHostGiftStats",
|
||||||
|
Handler: _WalletService_GetAgencyHostGiftStats_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "GetTeamHostSalaryStats",
|
MethodName: "GetTeamHostSalaryStats",
|
||||||
Handler: _WalletService_GetTeamHostSalaryStats_Handler,
|
Handler: _WalletService_GetTeamHostSalaryStats_Handler,
|
||||||
@ -4375,6 +4591,18 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "TransferSalaryToCoinSeller",
|
MethodName: "TransferSalaryToCoinSeller",
|
||||||
Handler: _WalletService_TransferSalaryToCoinSeller_Handler,
|
Handler: _WalletService_TransferSalaryToCoinSeller_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListPointWithdrawalCoinSellers",
|
||||||
|
Handler: _WalletService_ListPointWithdrawalCoinSellers_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "TransferPointToCoinSeller",
|
||||||
|
Handler: _WalletService_TransferPointToCoinSeller_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetPointWithdrawalConfig",
|
||||||
|
Handler: _WalletService_GetPointWithdrawalConfig_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "FreezeSalaryWithdrawal",
|
MethodName: "FreezeSalaryWithdrawal",
|
||||||
Handler: _WalletService_FreezeSalaryWithdrawal_Handler,
|
Handler: _WalletService_FreezeSalaryWithdrawal_Handler,
|
||||||
|
|||||||
@ -63,6 +63,7 @@ import (
|
|||||||
menumodule "hyapp-admin-server/internal/modules/menu"
|
menumodule "hyapp-admin-server/internal/modules/menu"
|
||||||
opscentermodule "hyapp-admin-server/internal/modules/opscenter"
|
opscentermodule "hyapp-admin-server/internal/modules/opscenter"
|
||||||
paymentmodule "hyapp-admin-server/internal/modules/payment"
|
paymentmodule "hyapp-admin-server/internal/modules/payment"
|
||||||
|
pointwithdrawalconfigmodule "hyapp-admin-server/internal/modules/pointwithdrawalconfig"
|
||||||
policyconfigmodule "hyapp-admin-server/internal/modules/policyconfig"
|
policyconfigmodule "hyapp-admin-server/internal/modules/policyconfig"
|
||||||
prettyidmodule "hyapp-admin-server/internal/modules/prettyid"
|
prettyidmodule "hyapp-admin-server/internal/modules/prettyid"
|
||||||
rbacmodule "hyapp-admin-server/internal/modules/rbac"
|
rbacmodule "hyapp-admin-server/internal/modules/rbac"
|
||||||
@ -397,40 +398,41 @@ func main() {
|
|||||||
gamemanagementmodule.WithRobotProfileSource(robotProfileSource),
|
gamemanagementmodule.WithRobotProfileSource(robotProfileSource),
|
||||||
gamemanagementmodule.WithRobotAppearanceServices(walletclient.NewGRPC(walletConn), activityclient.NewGRPC(activityConn)),
|
gamemanagementmodule.WithRobotAppearanceServices(walletclient.NewGRPC(walletConn), activityclient.NewGRPC(activityConn)),
|
||||||
),
|
),
|
||||||
GiftDiamond: giftdiamondmodule.New(walletDB, auditHandler),
|
GiftDiamond: giftdiamondmodule.New(walletDB, auditHandler),
|
||||||
Health: healthmodule.New(store, redisClient, jobStatus),
|
Health: healthmodule.New(store, redisClient, jobStatus),
|
||||||
HostAgencyPolicy: hostagencypolicymodule.New(store, walletDB, auditHandler),
|
HostAgencyPolicy: hostagencypolicymodule.New(store, walletDB, auditHandler),
|
||||||
HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
|
HostOrg: hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
|
||||||
HostSalarySettlement: hostsalarysettlementmodule.New(walletDB, userDB, walletv1.NewWalletCronServiceClient(walletConn), cfg.WalletService.RequestTimeout, auditHandler),
|
HostSalarySettlement: hostsalarysettlementmodule.New(walletDB, userDB, walletv1.NewWalletCronServiceClient(walletConn), cfg.WalletService.RequestTimeout, auditHandler),
|
||||||
HostWithdrawal: hostwithdrawalmodule.New(sqlDB, walletDB, userDB),
|
HostWithdrawal: hostwithdrawalmodule.New(sqlDB, walletDB, userDB),
|
||||||
InviteActivityReward: inviteactivityrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
InviteActivityReward: inviteactivityrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
Job: jobmodule.New(store, cfg, auditHandler),
|
Job: jobmodule.New(store, cfg, auditHandler),
|
||||||
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), walletclient.NewGRPC(walletConn), auditHandler),
|
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), walletclient.NewGRPC(walletConn), auditHandler),
|
||||||
Menu: menumodule.New(store, auditHandler),
|
Menu: menumodule.New(store, auditHandler),
|
||||||
OpsCenter: opscentermodule.New(appRegistryService, luckyGiftHandler),
|
OpsCenter: opscentermodule.New(appRegistryService, luckyGiftHandler),
|
||||||
Payment: paymentHandler,
|
Payment: paymentHandler,
|
||||||
PolicyConfig: policyconfigmodule.New(sqlDB, walletDB, userDB, activityclient.NewGRPC(activityConn), auditHandler),
|
PolicyConfig: policyconfigmodule.New(sqlDB, walletDB, userDB, activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
PrettyID: prettyidmodule.New(userclient.NewGRPC(userConn), auditHandler),
|
PointWithdrawalConfig: pointwithdrawalconfigmodule.New(walletDB, userDB, auditHandler),
|
||||||
RBAC: rbacmodule.New(store, auditHandler),
|
PrettyID: prettyidmodule.New(userclient.NewGRPC(userConn), auditHandler),
|
||||||
RedPacket: redpacketmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
RBAC: rbacmodule.New(store, auditHandler),
|
||||||
Report: reportmodule.New(userDB, roomClient),
|
RedPacket: redpacketmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||||
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
Report: reportmodule.New(userDB, roomClient),
|
||||||
RegionBlock: regionblockmodule.New(userDB, userclient.NewGRPC(userConn), auditHandler),
|
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler),
|
RegionBlock: regionblockmodule.New(userDB, userclient.NewGRPC(userConn), auditHandler),
|
||||||
RiskConfig: riskconfigmodule.New(userclient.NewGRPC(userConn), auditHandler),
|
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler),
|
||||||
RoomAdmin: roomadminmodule.New(userDB, store, roomClient, robotClient, auditHandler),
|
RiskConfig: riskconfigmodule.New(userclient.NewGRPC(userConn), auditHandler),
|
||||||
RoomRocket: roomrocketmodule.New(roomClient, auditHandler),
|
RoomAdmin: roomadminmodule.New(userDB, store, roomClient, robotClient, auditHandler),
|
||||||
RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
RoomRocket: roomrocketmodule.New(roomClient, auditHandler),
|
||||||
Search: searchmodule.New(store),
|
RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
SevenDayCheckIn: sevendaycheckinmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
Search: searchmodule.New(store),
|
||||||
Team: teammodule.New(store, auditHandler),
|
SevenDayCheckIn: sevendaycheckinmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
TeamSalaryPolicy: teamsalarypolicymodule.New(store, auditHandler),
|
Team: teammodule.New(store, auditHandler),
|
||||||
TeamSalarySettlement: teamSalarySettlementHandler,
|
TeamSalaryPolicy: teamsalarypolicymodule.New(store, auditHandler),
|
||||||
Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
|
TeamSalarySettlement: teamSalarySettlementHandler,
|
||||||
UserLeaderboard: userleaderboardmodule.New(walletDB, userDB, roomClient),
|
Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
|
||||||
VIPConfig: vipconfigmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
UserLeaderboard: userleaderboardmodule.New(walletDB, userDB, roomClient),
|
||||||
WeeklyStar: weeklystarmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
VIPConfig: vipconfigmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||||
Wheel: wheelmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler),
|
WeeklyStar: weeklystarmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||||
|
Wheel: wheelmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler),
|
||||||
}
|
}
|
||||||
engine := router.New(cfg, auth, handlers)
|
engine := router.New(cfg, auth, handlers)
|
||||||
|
|
||||||
|
|||||||
@ -653,6 +653,11 @@ type UserWithdrawalApplication struct {
|
|||||||
SalaryAssetType string `gorm:"size:64;not null;default:''" json:"salaryAssetType"`
|
SalaryAssetType string `gorm:"size:64;not null;default:''" json:"salaryAssetType"`
|
||||||
WithdrawAmount string `gorm:"type:decimal(18,2);not null;default:0.00" json:"withdrawAmount"`
|
WithdrawAmount string `gorm:"type:decimal(18,2);not null;default:0.00" json:"withdrawAmount"`
|
||||||
WithdrawAmountMinor int64 `gorm:"not null;default:0" json:"withdrawAmountMinor"`
|
WithdrawAmountMinor int64 `gorm:"not null;default:0" json:"withdrawAmountMinor"`
|
||||||
|
PointFeeAmount int64 `gorm:"not null;default:0" json:"pointFeeAmount"`
|
||||||
|
PointNetAmount int64 `gorm:"not null;default:0" json:"pointNetAmount"`
|
||||||
|
PointsPerUSD int64 `gorm:"not null;default:100000" json:"pointsPerUsd"`
|
||||||
|
PointFeeBPS int32 `gorm:"not null;default:500" json:"pointFeeBps"`
|
||||||
|
PointPolicyInstance string `gorm:"column:point_policy_instance_code;size:96;not null;default:''" json:"pointPolicyInstanceCode"`
|
||||||
WithdrawMethod string `gorm:"size:64;index:idx_admin_withdrawal_method;not null;default:''" json:"withdrawMethod"`
|
WithdrawMethod string `gorm:"size:64;index:idx_admin_withdrawal_method;not null;default:''" json:"withdrawMethod"`
|
||||||
WithdrawAddress string `gorm:"size:255;not null;default:''" json:"withdrawAddress"`
|
WithdrawAddress string `gorm:"size:255;not null;default:''" json:"withdrawAddress"`
|
||||||
FreezeCommandID string `gorm:"size:128;not null;default:''" json:"freezeCommandId"`
|
FreezeCommandID string `gorm:"size:128;not null;default:''" json:"freezeCommandId"`
|
||||||
|
|||||||
@ -15,6 +15,7 @@ type withdrawalApplicationDTO struct {
|
|||||||
PointNetAmount int64 `json:"pointNetAmount,omitempty"`
|
PointNetAmount int64 `json:"pointNetAmount,omitempty"`
|
||||||
PointsPerUSD int64 `json:"pointsPerUsd,omitempty"`
|
PointsPerUSD int64 `json:"pointsPerUsd,omitempty"`
|
||||||
PointFeeBPS int32 `json:"pointFeeBps,omitempty"`
|
PointFeeBPS int32 `json:"pointFeeBps,omitempty"`
|
||||||
|
PointPolicyInstance string `json:"pointPolicyInstanceCode,omitempty"`
|
||||||
WithdrawMethod string `json:"withdrawMethod"`
|
WithdrawMethod string `json:"withdrawMethod"`
|
||||||
WithdrawAddress string `json:"withdrawAddress"`
|
WithdrawAddress string `json:"withdrawAddress"`
|
||||||
FreezeTransactionID string `json:"freezeTransactionId"`
|
FreezeTransactionID string `json:"freezeTransactionId"`
|
||||||
@ -52,17 +53,35 @@ func withdrawalApplicationDTOFromModel(item model.UserWithdrawalApplication) wit
|
|||||||
UpdatedAtMS: item.UpdatedAtMS,
|
UpdatedAtMS: item.UpdatedAtMS,
|
||||||
}
|
}
|
||||||
if isPointWithdrawalAssetType(item.SalaryAssetType) {
|
if isPointWithdrawalAssetType(item.SalaryAssetType) {
|
||||||
grossPoints := item.WithdrawAmountMinor
|
feePoints, netPoints, pointsPerUSD, feeBPS := pointWithdrawalSnapshot(item)
|
||||||
feePoints := grossPoints * int64(pointWithdrawalDefaultFeeBPS) / 10000
|
dto.PointGrossAmount = item.WithdrawAmountMinor
|
||||||
dto.PointGrossAmount = grossPoints
|
|
||||||
dto.PointFeeAmount = feePoints
|
dto.PointFeeAmount = feePoints
|
||||||
dto.PointNetAmount = grossPoints - feePoints
|
dto.PointNetAmount = netPoints
|
||||||
dto.PointsPerUSD = pointWithdrawalDefaultPointsPerUSD
|
dto.PointsPerUSD = pointsPerUSD
|
||||||
dto.PointFeeBPS = pointWithdrawalDefaultFeeBPS
|
dto.PointFeeBPS = feeBPS
|
||||||
|
dto.PointPolicyInstance = item.PointPolicyInstance
|
||||||
}
|
}
|
||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pointWithdrawalSnapshot(item model.UserWithdrawalApplication) (feePoints int64, netPoints int64, pointsPerUSD int64, feeBPS int32) {
|
||||||
|
pointsPerUSD = item.PointsPerUSD
|
||||||
|
if pointsPerUSD <= 0 {
|
||||||
|
pointsPerUSD = pointWithdrawalDefaultPointsPerUSD
|
||||||
|
}
|
||||||
|
feeBPS = item.PointFeeBPS
|
||||||
|
if feeBPS < 0 || feeBPS > 10000 {
|
||||||
|
feeBPS = pointWithdrawalDefaultFeeBPS
|
||||||
|
}
|
||||||
|
feePoints, netPoints = item.PointFeeAmount, item.PointNetAmount
|
||||||
|
if feePoints < 0 || netPoints < 0 || feePoints+netPoints != item.WithdrawAmountMinor {
|
||||||
|
// 090 之前的存量 POINT 申请没有快照;只对这些不一致旧行按历史 5% 默认回填。
|
||||||
|
feePoints = item.WithdrawAmountMinor * int64(feeBPS) / 10000
|
||||||
|
netPoints = item.WithdrawAmountMinor - feePoints
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func withdrawalApplicationDTOsFromModel(items []model.UserWithdrawalApplication) []withdrawalApplicationDTO {
|
func withdrawalApplicationDTOsFromModel(items []model.UserWithdrawalApplication) []withdrawalApplicationDTO {
|
||||||
out := make([]withdrawalApplicationDTO, 0, len(items))
|
out := make([]withdrawalApplicationDTO, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
|||||||
@ -92,7 +92,7 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
|||||||
}
|
}
|
||||||
commandID := withdrawalAuditCommandID(id, decision)
|
commandID := withdrawalAuditCommandID(id, decision)
|
||||||
amountMinor := item.WithdrawAmountMinor
|
amountMinor := item.WithdrawAmountMinor
|
||||||
transactionID, err := s.applyWalletDecision(ctx, decision, commandID, appCode, userID, item.SalaryAssetType, amountMinor, actor, strings.TrimSpace(remark), id)
|
transactionID, err := s.applyWalletDecision(ctx, decision, commandID, appCode, userID, item.SalaryAssetType, amountMinor, item.PointFeeAmount, item.PointNetAmount, item.PointsPerUSD, item.PointFeeBPS, actor, strings.TrimSpace(remark), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ func (s *Service) auditApplication(ctx context.Context, actor shared.Actor, id u
|
|||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) applyWalletDecision(ctx context.Context, decision string, commandID string, appCode string, userID int64, assetType string, amountMinor int64, actor shared.Actor, remark string, applicationID uint) (string, error) {
|
func (s *Service) applyWalletDecision(ctx context.Context, decision string, commandID string, appCode string, userID int64, assetType string, amountMinor int64, storedFeePoints int64, storedNetPoints int64, storedPointsPerUSD int64, storedFeeBPS int32, actor shared.Actor, remark string, applicationID uint) (string, error) {
|
||||||
reason := strings.TrimSpace(remark)
|
reason := strings.TrimSpace(remark)
|
||||||
if reason == "" {
|
if reason == "" {
|
||||||
reason = "salary withdrawal " + decision
|
reason = "salary withdrawal " + decision
|
||||||
@ -141,8 +141,10 @@ func (s *Service) applyWalletDecision(ctx context.Context, decision string, comm
|
|||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("point withdrawal wallet executor is not configured")
|
return "", errors.New("point withdrawal wallet executor is not configured")
|
||||||
}
|
}
|
||||||
feePoints := amountMinor * int64(pointWithdrawalDefaultFeeBPS) / 10000
|
feePoints, netPoints, pointsPerUSD, feeBPS := pointWithdrawalSnapshot(model.UserWithdrawalApplication{
|
||||||
netPoints := amountMinor - feePoints
|
WithdrawAmountMinor: amountMinor, PointFeeAmount: storedFeePoints, PointNetAmount: storedNetPoints,
|
||||||
|
PointsPerUSD: storedPointsPerUSD, PointFeeBPS: storedFeeBPS,
|
||||||
|
})
|
||||||
switch decision {
|
switch decision {
|
||||||
case model.WithdrawalApplicationStatusApproved:
|
case model.WithdrawalApplicationStatusApproved:
|
||||||
resp, err := pointWallet.SettlePointWithdrawal(ctx, &walletv1.SettlePointWithdrawalRequest{
|
resp, err := pointWallet.SettlePointWithdrawal(ctx, &walletv1.SettlePointWithdrawalRequest{
|
||||||
@ -152,8 +154,8 @@ func (s *Service) applyWalletDecision(ctx context.Context, decision string, comm
|
|||||||
GrossPointAmount: amountMinor,
|
GrossPointAmount: amountMinor,
|
||||||
FeePointAmount: feePoints,
|
FeePointAmount: feePoints,
|
||||||
NetPointAmount: netPoints,
|
NetPointAmount: netPoints,
|
||||||
PointsPerUsd: pointWithdrawalDefaultPointsPerUSD,
|
PointsPerUsd: pointsPerUSD,
|
||||||
FeeBps: pointWithdrawalDefaultFeeBPS,
|
FeeBps: feeBPS,
|
||||||
OperatorUserId: int64(actor.UserID),
|
OperatorUserId: int64(actor.UserID),
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
@ -171,8 +173,8 @@ func (s *Service) applyWalletDecision(ctx context.Context, decision string, comm
|
|||||||
GrossPointAmount: amountMinor,
|
GrossPointAmount: amountMinor,
|
||||||
FeePointAmount: feePoints,
|
FeePointAmount: feePoints,
|
||||||
NetPointAmount: netPoints,
|
NetPointAmount: netPoints,
|
||||||
PointsPerUsd: pointWithdrawalDefaultPointsPerUSD,
|
PointsPerUsd: pointsPerUSD,
|
||||||
FeeBps: pointWithdrawalDefaultFeeBPS,
|
FeeBps: feeBPS,
|
||||||
OperatorUserId: int64(actor.UserID),
|
OperatorUserId: int64(actor.UserID),
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
AppCode: appCode,
|
AppCode: appCode,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ func TestApplyWalletDecisionRoutesPointApprovalToPointSettlement(t *testing.T) {
|
|||||||
wallet := &fakeAuditWalletClient{}
|
wallet := &fakeAuditWalletClient{}
|
||||||
svc := &Service{wallet: wallet}
|
svc := &Service{wallet: wallet}
|
||||||
|
|
||||||
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:77:approved", "huwaa", 42001, "POINT", 1_000_000, shared.Actor{UserID: 90001}, "paid", 77)
|
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:77:approved", "fami", 42001, "POINT", 1_000_000, 25_000, 975_000, 200_000, 250, shared.Actor{UserID: 90001}, "paid", 77)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("apply point approval failed: %v", err)
|
t.Fatalf("apply point approval failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -23,12 +23,14 @@ func TestApplyWalletDecisionRoutesPointApprovalToPointSettlement(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if wallet.settlePoint == nil ||
|
if wallet.settlePoint == nil ||
|
||||||
wallet.settlePoint.GetCommandId() != "audit:77:approved" ||
|
wallet.settlePoint.GetCommandId() != "audit:77:approved" ||
|
||||||
wallet.settlePoint.GetAppCode() != "huwaa" ||
|
wallet.settlePoint.GetAppCode() != "fami" ||
|
||||||
wallet.settlePoint.GetUserId() != 42001 ||
|
wallet.settlePoint.GetUserId() != 42001 ||
|
||||||
wallet.settlePoint.GetAssetType() != "POINT" ||
|
wallet.settlePoint.GetAssetType() != "POINT" ||
|
||||||
wallet.settlePoint.GetGrossPointAmount() != 1_000_000 ||
|
wallet.settlePoint.GetGrossPointAmount() != 1_000_000 ||
|
||||||
wallet.settlePoint.GetFeePointAmount() != 50_000 ||
|
wallet.settlePoint.GetFeePointAmount() != 25_000 ||
|
||||||
wallet.settlePoint.GetNetPointAmount() != 950_000 ||
|
wallet.settlePoint.GetNetPointAmount() != 975_000 ||
|
||||||
|
wallet.settlePoint.GetPointsPerUsd() != 200_000 ||
|
||||||
|
wallet.settlePoint.GetFeeBps() != 250 ||
|
||||||
wallet.settlePoint.GetOperatorUserId() != 90001 ||
|
wallet.settlePoint.GetOperatorUserId() != 90001 ||
|
||||||
wallet.settlePoint.GetWithdrawalApplicationId() != "77" {
|
wallet.settlePoint.GetWithdrawalApplicationId() != "77" {
|
||||||
t.Fatalf("SettlePointWithdrawal payload mismatch: %+v", wallet.settlePoint)
|
t.Fatalf("SettlePointWithdrawal payload mismatch: %+v", wallet.settlePoint)
|
||||||
@ -42,7 +44,7 @@ func TestApplyWalletDecisionRoutesPointRejectionToPointRelease(t *testing.T) {
|
|||||||
wallet := &fakeAuditWalletClient{}
|
wallet := &fakeAuditWalletClient{}
|
||||||
svc := &Service{wallet: wallet}
|
svc := &Service{wallet: wallet}
|
||||||
|
|
||||||
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusRejected, "audit:78:rejected", "huwaa", 42002, "COIN_SELLER_POINT", 2_000_000, shared.Actor{UserID: 90002}, "bad address", 78)
|
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusRejected, "audit:78:rejected", "huwaa", 42002, "COIN_SELLER_POINT", 2_000_000, 100_000, 1_900_000, 100_000, 500, shared.Actor{UserID: 90002}, "bad address", 78)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("apply point rejection failed: %v", err)
|
t.Fatalf("apply point rejection failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -71,7 +73,7 @@ func TestApplyWalletDecisionKeepsLegacySalaryAssetsOnSalaryRPC(t *testing.T) {
|
|||||||
wallet := &fakeAuditWalletClient{}
|
wallet := &fakeAuditWalletClient{}
|
||||||
svc := &Service{wallet: wallet}
|
svc := &Service{wallet: wallet}
|
||||||
|
|
||||||
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:79:approved", "lalu", 42003, "HOST_SALARY_USD", 12_345, shared.Actor{UserID: 90003}, "", 79)
|
transactionID, err := svc.applyWalletDecision(context.Background(), model.WithdrawalApplicationStatusApproved, "audit:79:approved", "lalu", 42003, "HOST_SALARY_USD", 12_345, 0, 0, 0, 0, shared.Actor{UserID: 90003}, "", 79)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("apply salary approval failed: %v", err)
|
t.Fatalf("apply salary approval failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
26
server/admin/internal/modules/pointwithdrawalconfig/dto.go
Normal file
26
server/admin/internal/modules/pointwithdrawalconfig/dto.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package pointwithdrawalconfig
|
||||||
|
|
||||||
|
type itemDTO struct {
|
||||||
|
AppCode string `json:"appCode"`
|
||||||
|
SellerUserID string `json:"sellerUserId"`
|
||||||
|
DisplayUserID string `json:"displayUserId"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
CountryName string `json:"countryName"`
|
||||||
|
SortOrder int `json:"sortOrder"`
|
||||||
|
PointAmount int64 `json:"pointAmount"`
|
||||||
|
SellerCoinAmount int64 `json:"sellerCoinAmount"`
|
||||||
|
ServiceCountryCodes []string `json:"serviceCountryCodes"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedAtMS int64 `json:"createdAtMs"`
|
||||||
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type upsertRequest struct {
|
||||||
|
SellerUserID int64 `json:"sellerUserId" binding:"required"`
|
||||||
|
SortOrder int `json:"sortOrder"`
|
||||||
|
PointAmount int64 `json:"pointAmount" binding:"required"`
|
||||||
|
SellerCoinAmount int64 `json:"sellerCoinAmount" binding:"required"`
|
||||||
|
ServiceCountryCodes []string `json:"serviceCountryCodes"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package pointwithdrawalconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
|
"hyapp-admin-server/internal/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
audit shared.OperationLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(walletDB *sql.DB, userDB *sql.DB, audit shared.OperationLogger) *Handler {
|
||||||
|
return &Handler{service: NewService(walletDB, userDB), audit: audit}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) List(c *gin.Context) {
|
||||||
|
items, err := h.service.List(c.Request.Context(), appctx.FromContext(c.Request.Context()))
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取币商提现白名单失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, map[string]any{"items": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Upsert(c *gin.Context) {
|
||||||
|
var req upsertRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "币商提现配置参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := h.service.Upsert(c.Request.Context(), appctx.FromContext(c.Request.Context()), req, int64(shared.ActorFromContext(c).UserID))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "point-withdrawal-coin-seller-upsert", "point_withdrawal_coin_seller_configs", item.AppCode+":"+item.SellerUserID, "success", "ratio="+strconv.FormatInt(item.PointAmount, 10)+":"+strconv.FormatInt(item.SellerCoinAmount, 10))
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Delete(c *gin.Context) {
|
||||||
|
sellerUserID, err := strconv.ParseInt(strings.TrimSpace(c.Param("seller_user_id")), 10, 64)
|
||||||
|
if err != nil || sellerUserID <= 0 {
|
||||||
|
response.BadRequest(c, "币商 ID 不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appCode := appctx.FromContext(c.Request.Context())
|
||||||
|
if err := h.service.Delete(c.Request.Context(), appCode, sellerUserID); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
response.NotFound(c, "配置不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ServerError(c, "删除币商提现配置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "point-withdrawal-coin-seller-delete", "point_withdrawal_coin_seller_configs", appCode+":"+strconv.FormatInt(sellerUserID, 10), "success", "")
|
||||||
|
response.OK(c, map[string]any{"deleted": true})
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package pointwithdrawalconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
protected.GET("/admin/point-withdrawal/coin-sellers", middleware.RequirePermission("point-withdrawal-config:view"), h.List)
|
||||||
|
protected.PUT("/admin/point-withdrawal/coin-sellers", middleware.RequirePermission("point-withdrawal-config:update"), h.Upsert)
|
||||||
|
protected.DELETE("/admin/point-withdrawal/coin-sellers/:seller_user_id", middleware.RequirePermission("point-withdrawal-config:update"), h.Delete)
|
||||||
|
}
|
||||||
201
server/admin/internal/modules/pointwithdrawalconfig/service.go
Normal file
201
server/admin/internal/modules/pointwithdrawalconfig/service.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package pointwithdrawalconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
walletDB *sql.DB
|
||||||
|
userDB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(walletDB *sql.DB, userDB *sql.DB) *Service {
|
||||||
|
return &Service{walletDB: walletDB, userDB: userDB}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 按 app_code 读取同一套公共配置表,并补齐用户资料;孤儿配置仍返回,便于运营删除而不是静默隐藏。
|
||||||
|
func (s *Service) List(ctx context.Context, appCode string) ([]itemDTO, error) {
|
||||||
|
if err := s.ensureSchema(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rows, err := s.walletDB.QueryContext(ctx, `
|
||||||
|
SELECT app_code, seller_user_id, sort_order, point_amount, seller_coin_amount,
|
||||||
|
service_country_codes, status, created_at_ms, updated_at_ms
|
||||||
|
FROM point_withdrawal_coin_seller_configs
|
||||||
|
WHERE app_code = ?
|
||||||
|
ORDER BY sort_order ASC, seller_user_id ASC`, normalizeAppCode(appCode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := make([]itemDTO, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item itemDTO
|
||||||
|
var sellerUserID int64
|
||||||
|
var countriesJSON []byte
|
||||||
|
if err := rows.Scan(&item.AppCode, &sellerUserID, &item.SortOrder, &item.PointAmount, &item.SellerCoinAmount, &countriesJSON, &item.Status, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.SellerUserID = strconv.FormatInt(sellerUserID, 10)
|
||||||
|
if err := json.Unmarshal(countriesJSON, &item.ServiceCountryCodes); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse service countries for seller %d: %w", sellerUserID, err)
|
||||||
|
}
|
||||||
|
item.ServiceCountryCodes = normalizeCountries(item.ServiceCountryCodes)
|
||||||
|
displayUserID, nickname, avatar, countryName, lookupErr := s.lookupSeller(ctx, item.AppCode, sellerUserID, false)
|
||||||
|
if lookupErr != nil && !errors.Is(lookupErr, sql.ErrNoRows) {
|
||||||
|
return nil, lookupErr
|
||||||
|
}
|
||||||
|
item.DisplayUserID, item.Nickname, item.Avatar, item.CountryName = displayUserID, nickname, avatar, countryName
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert 在写白名单前验证同 App 的 active 币商身份,防止普通用户被配置成账务收款方。
|
||||||
|
func (s *Service) Upsert(ctx context.Context, appCode string, req upsertRequest, adminID int64) (itemDTO, error) {
|
||||||
|
appCode = normalizeAppCode(appCode)
|
||||||
|
if req.SellerUserID <= 0 || req.PointAmount <= 0 || req.SellerCoinAmount <= 0 || req.SortOrder < 0 {
|
||||||
|
return itemDTO{}, errors.New("币商、排序和兑换比例必须是正数")
|
||||||
|
}
|
||||||
|
status := strings.ToLower(strings.TrimSpace(req.Status))
|
||||||
|
if status == "" {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
if status != "active" && status != "disabled" {
|
||||||
|
return itemDTO{}, errors.New("状态只能是 active 或 disabled")
|
||||||
|
}
|
||||||
|
if _, _, _, _, err := s.lookupSeller(ctx, appCode, req.SellerUserID, true); err != nil {
|
||||||
|
return itemDTO{}, err
|
||||||
|
}
|
||||||
|
if err := s.ensureSchema(ctx); err != nil {
|
||||||
|
return itemDTO{}, err
|
||||||
|
}
|
||||||
|
countries := normalizeCountries(req.ServiceCountryCodes)
|
||||||
|
countriesJSON, err := json.Marshal(countries)
|
||||||
|
if err != nil {
|
||||||
|
return itemDTO{}, err
|
||||||
|
}
|
||||||
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
|
_, err = s.walletDB.ExecContext(ctx, `
|
||||||
|
INSERT INTO point_withdrawal_coin_seller_configs (
|
||||||
|
app_code, seller_user_id, sort_order, point_amount, seller_coin_amount, service_country_codes,
|
||||||
|
status, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
sort_order = VALUES(sort_order), point_amount = VALUES(point_amount),
|
||||||
|
seller_coin_amount = VALUES(seller_coin_amount), service_country_codes = VALUES(service_country_codes),
|
||||||
|
status = VALUES(status), updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
|
appCode, req.SellerUserID, req.SortOrder, req.PointAmount, req.SellerCoinAmount, countriesJSON,
|
||||||
|
status, adminID, adminID, nowMS, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return itemDTO{}, err
|
||||||
|
}
|
||||||
|
return s.Get(ctx, appCode, req.SellerUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Get(ctx context.Context, appCode string, sellerUserID int64) (itemDTO, error) {
|
||||||
|
items, err := s.List(ctx, appCode)
|
||||||
|
if err != nil {
|
||||||
|
return itemDTO{}, err
|
||||||
|
}
|
||||||
|
needle := strconv.FormatInt(sellerUserID, 10)
|
||||||
|
for _, item := range items {
|
||||||
|
if item.SellerUserID == needle {
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itemDTO{}, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, appCode string, sellerUserID int64) error {
|
||||||
|
if sellerUserID <= 0 {
|
||||||
|
return errors.New("币商 ID 不正确")
|
||||||
|
}
|
||||||
|
if err := s.ensureSchema(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := s.walletDB.ExecContext(ctx, `DELETE FROM point_withdrawal_coin_seller_configs WHERE app_code = ? AND seller_user_id = ?`, normalizeAppCode(appCode), sellerUserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) lookupSeller(ctx context.Context, appCode string, sellerUserID int64, requireActive bool) (displayUserID string, nickname string, avatar string, countryName string, err error) {
|
||||||
|
if s.userDB == nil {
|
||||||
|
return "", "", "", "", errors.New("user db is not configured")
|
||||||
|
}
|
||||||
|
query := `
|
||||||
|
SELECT COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''),
|
||||||
|
COALESCE(c.country_display_name, c.country_name, u.country, '')
|
||||||
|
FROM coin_seller_profiles csp
|
||||||
|
JOIN users u ON u.user_id = csp.user_id AND u.app_code = csp.app_code
|
||||||
|
LEFT JOIN countries c ON c.app_code = u.app_code AND c.country_code = u.country
|
||||||
|
WHERE csp.app_code = ? AND csp.user_id = ?`
|
||||||
|
if requireActive {
|
||||||
|
query += " AND csp.status = 'active' AND u.status = 'active'"
|
||||||
|
}
|
||||||
|
query += " LIMIT 1"
|
||||||
|
err = s.userDB.QueryRowContext(ctx, query, appCode, sellerUserID).Scan(&displayUserID, &nickname, &avatar, &countryName)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) && requireActive {
|
||||||
|
return "", "", "", "", errors.New("目标用户不是当前 App 的启用币商")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureSchema(ctx context.Context) error {
|
||||||
|
if s == nil || s.walletDB == nil {
|
||||||
|
return errors.New("wallet db is not configured")
|
||||||
|
}
|
||||||
|
_, err := s.walletDB.ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL, seller_user_id BIGINT NOT NULL, sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
point_amount BIGINT NOT NULL, seller_coin_amount BIGINT NOT NULL, service_country_codes JSON NOT NULL,
|
||||||
|
status VARCHAR(24) NOT NULL DEFAULT 'active', created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at_ms BIGINT NOT NULL, updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (app_code, seller_user_id),
|
||||||
|
KEY idx_point_withdraw_seller_list (app_code, status, sort_order, seller_user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT 提现币商白名单与兑换比例'`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCountries(values []string) []string {
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
out := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
if value == "" || len(value) > 16 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAppCode(value string) string {
|
||||||
|
value = strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
return "lalu"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package pointwithdrawalconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeCountriesCanonicalizesWhitelist(t *testing.T) {
|
||||||
|
got := normalizeCountries([]string{" sa ", "TR", "sa", "", "country-code-that-is-too-long"})
|
||||||
|
want := []string{"SA", "TR"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("normalizeCountries() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeAppCodeKeepsLegacyDefault(t *testing.T) {
|
||||||
|
if got := normalizeAppCode(""); got != "lalu" {
|
||||||
|
t.Fatalf("empty app code = %q, want lalu", got)
|
||||||
|
}
|
||||||
|
if got := normalizeAppCode(" FAMI "); got != "fami" {
|
||||||
|
t.Fatalf("Fami app code = %q, want fami", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
"hyapp-admin-server/internal/integration/activityclient"
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||||
@ -64,6 +65,7 @@ type policyInstanceRow struct {
|
|||||||
|
|
||||||
type compiledPolicy struct {
|
type compiledPolicy struct {
|
||||||
HostPointRatioPPM int64
|
HostPointRatioPPM int64
|
||||||
|
AgencyPointRatioPPM int64
|
||||||
PointsPerUSD int64
|
PointsPerUSD int64
|
||||||
WithdrawFeeBPS int64
|
WithdrawFeeBPS int64
|
||||||
TaskRewardAssetType string
|
TaskRewardAssetType string
|
||||||
@ -496,12 +498,12 @@ func (s *Service) publishWalletRuntime(ctx context.Context, item policyInstanceR
|
|||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO wallet_policy_instances (
|
INSERT INTO wallet_policy_instances (
|
||||||
app_code, instance_code, template_code, template_version, region_id, status,
|
app_code, instance_code, template_code, template_version, region_id, status,
|
||||||
effective_from_ms, effective_to_ms, host_point_asset_type, host_point_ratio_ppm,
|
effective_from_ms, effective_to_ms, host_point_asset_type, host_point_ratio_ppm, agency_point_ratio_ppm,
|
||||||
points_per_usd, withdraw_fee_bps, rule_json, published_by_admin_id,
|
points_per_usd, withdraw_fee_bps, rule_json, published_by_admin_id,
|
||||||
published_at_ms, created_at_ms, updated_at_ms
|
published_at_ms, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'POINT', ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'POINT', ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?, ?)`,
|
||||||
item.AppCode, item.InstanceCode, item.TemplateCode, item.TemplateVersion, regionID, item.Status,
|
item.AppCode, item.InstanceCode, item.TemplateCode, item.TemplateVersion, regionID, item.Status,
|
||||||
item.EffectiveFromMS, item.EffectiveToMS, compiled.HostPointRatioPPM, compiled.PointsPerUSD,
|
item.EffectiveFromMS, item.EffectiveToMS, compiled.HostPointRatioPPM, compiled.AgencyPointRatioPPM, compiled.PointsPerUSD,
|
||||||
compiled.WithdrawFeeBPS, string(compiled.RuleJSON), actorID, nowMS, nowMS, nowMS,
|
compiled.WithdrawFeeBPS, string(compiled.RuleJSON), actorID, nowMS, nowMS, nowMS,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -563,6 +565,7 @@ func ensureWalletPolicyRuntimeTable(ctx context.Context, db *sql.DB) error {
|
|||||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
||||||
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
||||||
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
||||||
|
agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '有效付费礼物给 Agency owner 的 POINT 分成比例,ppm',
|
||||||
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
||||||
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
||||||
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
||||||
@ -573,7 +576,19 @@ func ensureWalletPolicyRuntimeTable(ctx context.Context, db *sql.DB) error {
|
|||||||
PRIMARY KEY (app_code, instance_code, region_id),
|
PRIMARY KEY (app_code, instance_code, region_id),
|
||||||
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表'`)
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表'`)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 运行库可能是旧版本建表;幂等补列保证发布新策略时不会因列缺失中断。
|
||||||
|
if _, err := db.ExecContext(ctx, `ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '有效付费礼物给 Agency owner 的 POINT 分成比例,ppm' AFTER host_point_ratio_ppm`); err != nil && !isDuplicateColumnError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDuplicateColumnError(err error) bool {
|
||||||
|
var mysqlErr *mysqlDriver.MySQLError
|
||||||
|
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1060
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) insertPublishItems(ctx context.Context, jobID uint64, appCode string, regionIDs []int64, activitySucceeded bool, nowMS int64) error {
|
func (s *Service) insertPublishItems(ctx context.Context, jobID uint64, appCode string, regionIDs []int64, activitySucceeded bool, nowMS int64) error {
|
||||||
@ -635,11 +650,15 @@ func compilePolicy(raw json.RawMessage) (compiledPolicy, error) {
|
|||||||
pointsPerUSD := int64FromJSON(decoded["points_per_usd"], 100000)
|
pointsPerUSD := int64FromJSON(decoded["points_per_usd"], 100000)
|
||||||
host, _ := decoded["host"].(map[string]any)
|
host, _ := decoded["host"].(map[string]any)
|
||||||
hostRatioPercent := floatFromJSON(host["point_ratio_percent"], 70)
|
hostRatioPercent := floatFromJSON(host["point_ratio_percent"], 70)
|
||||||
|
agency, _ := decoded["agency"].(map[string]any)
|
||||||
|
// Agency 比例由跨 App 共用的策略模板显式配置;旧模板缺字段时保持 0,避免发布动作隐式改变存量 App 账务。
|
||||||
|
agencyRatioPercent := floatFromJSON(agency["point_ratio_percent"], 0)
|
||||||
withdrawFeeBPS := int64FromJSON(host["withdraw_fee_bps"], 500)
|
withdrawFeeBPS := int64FromJSON(host["withdraw_fee_bps"], 500)
|
||||||
ratioPPM := int64(math.Round(hostRatioPercent * 10000))
|
ratioPPM := int64(math.Round(hostRatioPercent * 10000))
|
||||||
|
agencyRatioPPM := int64(math.Round(agencyRatioPercent * 10000))
|
||||||
tasks, _ := decoded["tasks"].(map[string]any)
|
tasks, _ := decoded["tasks"].(map[string]any)
|
||||||
taskRewardAssetType := strings.ToUpper(strings.TrimSpace(stringFromJSON(tasks["reward_asset_type"], policyTaskRewardAssetCoin)))
|
taskRewardAssetType := strings.ToUpper(strings.TrimSpace(stringFromJSON(tasks["reward_asset_type"], policyTaskRewardAssetCoin)))
|
||||||
if pointsPerUSD <= 0 || ratioPPM <= 0 || ratioPPM > 1000000 || withdrawFeeBPS < 0 || withdrawFeeBPS > 10000 {
|
if pointsPerUSD <= 0 || ratioPPM <= 0 || ratioPPM > 1000000 || agencyRatioPPM < 0 || agencyRatioPPM > 1000000 || ratioPPM+agencyRatioPPM > 1000000 || withdrawFeeBPS < 0 || withdrawFeeBPS > 10000 {
|
||||||
return compiledPolicy{}, errors.New("rule_json host point policy is invalid")
|
return compiledPolicy{}, errors.New("rule_json host point policy is invalid")
|
||||||
}
|
}
|
||||||
if taskRewardAssetType != policyTaskRewardAssetCoin && taskRewardAssetType != "POINT" {
|
if taskRewardAssetType != policyTaskRewardAssetCoin && taskRewardAssetType != "POINT" {
|
||||||
@ -647,6 +666,7 @@ func compilePolicy(raw json.RawMessage) (compiledPolicy, error) {
|
|||||||
}
|
}
|
||||||
return compiledPolicy{
|
return compiledPolicy{
|
||||||
HostPointRatioPPM: ratioPPM,
|
HostPointRatioPPM: ratioPPM,
|
||||||
|
AgencyPointRatioPPM: agencyRatioPPM,
|
||||||
PointsPerUSD: pointsPerUSD,
|
PointsPerUSD: pointsPerUSD,
|
||||||
WithdrawFeeBPS: withdrawFeeBPS,
|
WithdrawFeeBPS: withdrawFeeBPS,
|
||||||
TaskRewardAssetType: taskRewardAssetType,
|
TaskRewardAssetType: taskRewardAssetType,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package policyconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
@ -60,6 +61,7 @@ func TestPublishRuntimeWritesWalletRowsAndActivityTaskRewardPolicy(t *testing.T)
|
|||||||
}
|
}
|
||||||
compiled := compiledPolicy{
|
compiled := compiledPolicy{
|
||||||
HostPointRatioPPM: 700000,
|
HostPointRatioPPM: 700000,
|
||||||
|
AgencyPointRatioPPM: 0,
|
||||||
PointsPerUSD: 100000,
|
PointsPerUSD: 100000,
|
||||||
WithdrawFeeBPS: 500,
|
WithdrawFeeBPS: 500,
|
||||||
TaskRewardAssetType: "POINT",
|
TaskRewardAssetType: "POINT",
|
||||||
@ -67,6 +69,7 @@ func TestPublishRuntimeWritesWalletRowsAndActivityTaskRewardPolicy(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
|
walletMock.ExpectExec(`(?s)ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm`).WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
walletMock.ExpectBegin()
|
walletMock.ExpectBegin()
|
||||||
walletMock.ExpectExec(`DELETE FROM wallet_policy_instances WHERE app_code = \? AND instance_code = \?`).
|
walletMock.ExpectExec(`DELETE FROM wallet_policy_instances WHERE app_code = \? AND instance_code = \?`).
|
||||||
WithArgs("huwaa", "huwaa-point-live").
|
WithArgs("huwaa", "huwaa-point-live").
|
||||||
@ -83,6 +86,7 @@ func TestPublishRuntimeWritesWalletRowsAndActivityTaskRewardPolicy(t *testing.T)
|
|||||||
int64(1700000000000),
|
int64(1700000000000),
|
||||||
int64(0),
|
int64(0),
|
||||||
int64(700000),
|
int64(700000),
|
||||||
|
int64(0),
|
||||||
int64(100000),
|
int64(100000),
|
||||||
int64(500),
|
int64(500),
|
||||||
string(compiled.RuleJSON),
|
string(compiled.RuleJSON),
|
||||||
@ -147,6 +151,7 @@ func TestUpdateInstanceStatusSyncsWalletRuntimeAndActivityRuntime(t *testing.T)
|
|||||||
WithArgs(policyStatusDisabled, uint(90002), sqlmock.AnyArg(), "huwaa", uint64(9)).
|
WithArgs(policyStatusDisabled, uint(90002), sqlmock.AnyArg(), "huwaa", uint64(9)).
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
walletMock.ExpectExec(`(?s)CREATE TABLE IF NOT EXISTS wallet_policy_instances`).WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
|
walletMock.ExpectExec(`(?s)ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm`).WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
walletMock.ExpectExec(`(?s)UPDATE wallet_policy_instances\s+SET status = \?, updated_at_ms = \?\s+WHERE app_code = \? AND instance_code = \?`).
|
walletMock.ExpectExec(`(?s)UPDATE wallet_policy_instances\s+SET status = \?, updated_at_ms = \?\s+WHERE app_code = \? AND instance_code = \?`).
|
||||||
WithArgs(policyStatusDisabled, sqlmock.AnyArg(), "huwaa", "huwaa-point-live").
|
WithArgs(policyStatusDisabled, sqlmock.AnyArg(), "huwaa", "huwaa-point-live").
|
||||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
@ -191,6 +196,24 @@ func TestUpdateInstanceStatusSyncsWalletRuntimeAndActivityRuntime(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompilePolicyUsesExplicitAgencyRatioAndKeepsLegacyDefaultZero(t *testing.T) {
|
||||||
|
ruleJSON := json.RawMessage(`{"host":{"point_ratio_percent":70}}`)
|
||||||
|
fami, err := compilePolicy(ruleJSON)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compile Fami policy failed: %v", err)
|
||||||
|
}
|
||||||
|
if fami.AgencyPointRatioPPM != 0 {
|
||||||
|
t.Fatalf("legacy policy without agency section must remain zero: %+v", fami)
|
||||||
|
}
|
||||||
|
configured, err := compilePolicy(json.RawMessage(`{"host":{"point_ratio_percent":70},"agency":{"point_ratio_percent":20}}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("compile configured policy failed: %v", err)
|
||||||
|
}
|
||||||
|
if configured.AgencyPointRatioPPM != 200000 {
|
||||||
|
t.Fatalf("configured agency ratio mismatch: %+v", configured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type fakePolicyActivityClient struct {
|
type fakePolicyActivityClient struct {
|
||||||
activityclient.Client
|
activityclient.Client
|
||||||
last *activityv1.PublishTaskRewardPolicyRequest
|
last *activityv1.PublishTaskRewardPolicyRequest
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/modules/menu"
|
"hyapp-admin-server/internal/modules/menu"
|
||||||
"hyapp-admin-server/internal/modules/opscenter"
|
"hyapp-admin-server/internal/modules/opscenter"
|
||||||
"hyapp-admin-server/internal/modules/payment"
|
"hyapp-admin-server/internal/modules/payment"
|
||||||
|
"hyapp-admin-server/internal/modules/pointwithdrawalconfig"
|
||||||
"hyapp-admin-server/internal/modules/policyconfig"
|
"hyapp-admin-server/internal/modules/policyconfig"
|
||||||
"hyapp-admin-server/internal/modules/prettyid"
|
"hyapp-admin-server/internal/modules/prettyid"
|
||||||
"hyapp-admin-server/internal/modules/rbac"
|
"hyapp-admin-server/internal/modules/rbac"
|
||||||
@ -66,62 +67,63 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
AdminUser *adminuser.Handler
|
AdminUser *adminuser.Handler
|
||||||
AgencyOpening *agencyopening.Handler
|
AgencyOpening *agencyopening.Handler
|
||||||
AchievementConfig *achievementconfig.Handler
|
AchievementConfig *achievementconfig.Handler
|
||||||
AppConfig *appconfig.Handler
|
AppConfig *appconfig.Handler
|
||||||
AppRegistry *appregistry.Handler
|
AppRegistry *appregistry.Handler
|
||||||
AppUser *appuser.Handler
|
AppUser *appuser.Handler
|
||||||
Audit *audit.Handler
|
Audit *audit.Handler
|
||||||
Auth *authroutes.Handler
|
Auth *authroutes.Handler
|
||||||
CoinLedger *coinledger.Handler
|
CoinLedger *coinledger.Handler
|
||||||
CountryRegion *countryregion.Handler
|
CountryRegion *countryregion.Handler
|
||||||
CPRelation *cprelation.Handler
|
CPRelation *cprelation.Handler
|
||||||
CPWeeklyRank *cpweeklyrank.Handler
|
CPWeeklyRank *cpweeklyrank.Handler
|
||||||
CumulativeRecharge *cumulativerechargereward.Handler
|
CumulativeRecharge *cumulativerechargereward.Handler
|
||||||
DailyTask *dailytask.Handler
|
DailyTask *dailytask.Handler
|
||||||
Dashboard *dashboard.Handler
|
Dashboard *dashboard.Handler
|
||||||
Databi *databi.Handler
|
Databi *databi.Handler
|
||||||
FirstRechargeReward *firstrechargereward.Handler
|
FirstRechargeReward *firstrechargereward.Handler
|
||||||
FinanceApplication *financeapplication.Handler
|
FinanceApplication *financeapplication.Handler
|
||||||
FinanceOrder *financeorder.Handler
|
FinanceOrder *financeorder.Handler
|
||||||
FinanceWithdrawal *financewithdrawal.Handler
|
FinanceWithdrawal *financewithdrawal.Handler
|
||||||
FullServerNotice *fullservernotice.Handler
|
FullServerNotice *fullservernotice.Handler
|
||||||
Game *gamemanagement.Handler
|
Game *gamemanagement.Handler
|
||||||
GiftDiamond *giftdiamond.Handler
|
GiftDiamond *giftdiamond.Handler
|
||||||
Health *health.Handler
|
Health *health.Handler
|
||||||
HostAgencyPolicy *hostagencypolicy.Handler
|
HostAgencyPolicy *hostagencypolicy.Handler
|
||||||
HostOrg *hostorg.Handler
|
HostOrg *hostorg.Handler
|
||||||
HostSalarySettlement *hostsalarysettlement.Handler
|
HostSalarySettlement *hostsalarysettlement.Handler
|
||||||
HostWithdrawal *hostwithdrawal.Handler
|
HostWithdrawal *hostwithdrawal.Handler
|
||||||
InviteActivityReward *inviteactivityreward.Handler
|
InviteActivityReward *inviteactivityreward.Handler
|
||||||
Job *job.Handler
|
Job *job.Handler
|
||||||
LevelConfig *levelconfig.Handler
|
LevelConfig *levelconfig.Handler
|
||||||
Menu *menu.Handler
|
Menu *menu.Handler
|
||||||
OpsCenter *opscenter.Handler
|
OpsCenter *opscenter.Handler
|
||||||
Payment *payment.Handler
|
Payment *payment.Handler
|
||||||
PolicyConfig *policyconfig.Handler
|
PolicyConfig *policyconfig.Handler
|
||||||
PrettyID *prettyid.Handler
|
PointWithdrawalConfig *pointwithdrawalconfig.Handler
|
||||||
RBAC *rbac.Handler
|
PrettyID *prettyid.Handler
|
||||||
RedPacket *redpacket.Handler
|
RBAC *rbac.Handler
|
||||||
Report *reportmodule.Handler
|
RedPacket *redpacket.Handler
|
||||||
RegistrationReward *registrationreward.Handler
|
Report *reportmodule.Handler
|
||||||
RegionBlock *regionblock.Handler
|
RegistrationReward *registrationreward.Handler
|
||||||
Resource *resourcemodule.Handler
|
RegionBlock *regionblock.Handler
|
||||||
RiskConfig *riskconfig.Handler
|
Resource *resourcemodule.Handler
|
||||||
RoomAdmin *roomadmin.Handler
|
RiskConfig *riskconfig.Handler
|
||||||
RoomRocket *roomrocket.Handler
|
RoomAdmin *roomadmin.Handler
|
||||||
RoomTurnoverReward *roomturnoverreward.Handler
|
RoomRocket *roomrocket.Handler
|
||||||
Search *search.Handler
|
RoomTurnoverReward *roomturnoverreward.Handler
|
||||||
SevenDayCheckIn *sevendaycheckin.Handler
|
Search *search.Handler
|
||||||
Team *team.Handler
|
SevenDayCheckIn *sevendaycheckin.Handler
|
||||||
TeamSalaryPolicy *teamsalarypolicy.Handler
|
Team *team.Handler
|
||||||
TeamSalarySettlement *teamsalarysettlement.Handler
|
TeamSalaryPolicy *teamsalarypolicy.Handler
|
||||||
Upload *upload.Handler
|
TeamSalarySettlement *teamsalarysettlement.Handler
|
||||||
UserLeaderboard *userleaderboard.Handler
|
Upload *upload.Handler
|
||||||
VIPConfig *vipconfig.Handler
|
UserLeaderboard *userleaderboard.Handler
|
||||||
WeeklyStar *weeklystar.Handler
|
VIPConfig *vipconfig.Handler
|
||||||
Wheel *wheel.Handler
|
WeeklyStar *weeklystar.Handler
|
||||||
|
Wheel *wheel.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
||||||
@ -176,6 +178,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
|||||||
audit.RegisterRoutes(protected, h.Audit)
|
audit.RegisterRoutes(protected, h.Audit)
|
||||||
payment.RegisterRoutes(protected, h.Payment)
|
payment.RegisterRoutes(protected, h.Payment)
|
||||||
policyconfig.RegisterRoutes(protected, h.PolicyConfig)
|
policyconfig.RegisterRoutes(protected, h.PolicyConfig)
|
||||||
|
pointwithdrawalconfig.RegisterRoutes(protected, h.PointWithdrawalConfig)
|
||||||
prettyid.RegisterRoutes(protected, h.PrettyID)
|
prettyid.RegisterRoutes(protected, h.PrettyID)
|
||||||
job.RegisterRoutes(protected, h.Job)
|
job.RegisterRoutes(protected, h.Job)
|
||||||
levelconfig.RegisterRoutes(protected, h.LevelConfig)
|
levelconfig.RegisterRoutes(protected, h.LevelConfig)
|
||||||
|
|||||||
66
server/admin/migrations/088_fami_guild_revenue_policy.sql
Normal file
66
server/admin/migrations/088_fami_guild_revenue_policy.sql
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||||
|
|
||||||
|
-- 收益策略模板是 Lalu/Huwaa/Fami 共用的产品配置入口。存量模板显式补 0,避免旧实例重发时产生新分成。
|
||||||
|
UPDATE admin_policy_templates
|
||||||
|
SET rule_json = JSON_SET(rule_json, '$.agency.point_ratio_percent', 0),
|
||||||
|
updated_at_ms = @now_ms
|
||||||
|
WHERE template_code = 'first_google70000_coin_seller_92000_100000_v1'
|
||||||
|
AND template_version = 'v1'
|
||||||
|
AND JSON_EXTRACT(rule_json, '$.agency.point_ratio_percent') IS NULL;
|
||||||
|
|
||||||
|
UPDATE admin_policy_template_versions
|
||||||
|
SET rule_json = JSON_SET(rule_json, '$.agency.point_ratio_percent', 0),
|
||||||
|
updated_at_ms = @now_ms
|
||||||
|
WHERE template_code = 'first_google70000_coin_seller_92000_100000_v1'
|
||||||
|
AND template_version = 'v1'
|
||||||
|
AND JSON_EXTRACT(rule_json, '$.agency.point_ratio_percent') IS NULL;
|
||||||
|
|
||||||
|
-- Fami 从同一份公共政策结构派生独立模板,默认 20%;运营可在 Admin 编辑并发布,不需要改代码。
|
||||||
|
INSERT INTO admin_policy_templates (
|
||||||
|
template_code, template_version, name, status, rule_json, description,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'fami_guild_revenue_policy', 'v1', 'Fami 公会收益政策', 'active',
|
||||||
|
JSON_SET(rule_json, '$.agency.point_ratio_percent', 20),
|
||||||
|
'Fami 主播与 Agency POINT 收益政策;Agency 默认 20%,以发布后的运行快照为准。',
|
||||||
|
0, 0, @now_ms, @now_ms
|
||||||
|
FROM admin_policy_templates
|
||||||
|
WHERE template_code = 'first_google70000_coin_seller_92000_100000_v1' AND template_version = 'v1'
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
status = VALUES(status),
|
||||||
|
description = VALUES(description),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_policy_template_versions (
|
||||||
|
template_code, template_version, status, rule_json,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
template_code, template_version, status, rule_json,
|
||||||
|
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
FROM admin_policy_templates
|
||||||
|
WHERE template_code = 'fami_guild_revenue_policy' AND template_version = 'v1'
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
status = VALUES(status),
|
||||||
|
rule_json = VALUES(rule_json),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_policy_instances (
|
||||||
|
app_code, instance_code, template_code, template_version, region_scope, status,
|
||||||
|
effective_from_ms, effective_to_ms, publish_status, created_by_admin_id, updated_by_admin_id,
|
||||||
|
created_at_ms, updated_at_ms
|
||||||
|
) VALUES (
|
||||||
|
'fami', 'fami_guild_revenue_policy', 'fami_guild_revenue_policy', 'v1',
|
||||||
|
'all_active_regions', 'active', @now_ms, 0, 'draft', 0, 0, @now_ms, @now_ms
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
template_code = VALUES(template_code),
|
||||||
|
template_version = VALUES(template_version),
|
||||||
|
region_scope = VALUES(region_scope),
|
||||||
|
status = VALUES(status),
|
||||||
|
effective_to_ms = VALUES(effective_to_ms),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 币商提现白名单是按当前后台 App 范围编辑的公共能力;不在表中复制 Who/Sofun 等固定开关,
|
||||||
|
-- 新增 App 时复用 X-App-Code 作用域即可,避免每增加一套公会页面就增加一列。
|
||||||
|
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||||
|
|
||||||
|
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||||
|
('币商提现白名单查看', 'point-withdrawal-config:view', 'menu', '查看当前 App 的 POINT 提现币商、服务国家和兑换比例', @now_ms, @now_ms),
|
||||||
|
('币商提现白名单编辑', 'point-withdrawal-config:update', 'button', '新增、编辑、禁用或删除当前 App 的 POINT 提现币商配置', @now_ms, @now_ms)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name), kind = VALUES(kind), description = VALUES(description), updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||||
|
SELECT parent.id, '币商提现白名单', 'host-org-point-withdrawal-config', '/host/point-withdrawal-config', 'wallet', 'point-withdrawal-config:view', 89, TRUE, @now_ms, @now_ms
|
||||||
|
FROM admin_menus parent
|
||||||
|
WHERE parent.code = 'host-org'
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
parent_id = VALUES(parent_id), title = VALUES(title), path = VALUES(path), icon = VALUES(icon),
|
||||||
|
permission_code = VALUES(permission_code), sort = VALUES(sort), visible = VALUES(visible), updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT role.id, permission.id
|
||||||
|
FROM admin_roles role
|
||||||
|
JOIN admin_permissions permission
|
||||||
|
WHERE role.code IN ('platform-admin', 'ops-admin')
|
||||||
|
AND permission.code IN ('point-withdrawal-config:view', 'point-withdrawal-config:update');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT role.id, permission.id
|
||||||
|
FROM admin_roles role
|
||||||
|
JOIN admin_permissions permission
|
||||||
|
WHERE role.code IN ('auditor', 'readonly')
|
||||||
|
AND permission.code = 'point-withdrawal-config:view';
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- POINT 提现审核必须使用用户提交时的已发布政策快照;否则 Admin 后续修改汇率/手续费会让冻结、展示和审核扣款口径不一致。
|
||||||
|
ALTER TABLE admin_user_withdrawal_applications
|
||||||
|
ADD COLUMN point_fee_amount BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT 提现手续费积分快照' AFTER withdraw_amount_minor,
|
||||||
|
ADD COLUMN point_net_amount BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT 提现预计到账积分快照' AFTER point_fee_amount,
|
||||||
|
ADD COLUMN points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 汇率快照' AFTER point_net_amount,
|
||||||
|
ADD COLUMN point_fee_bps INT NOT NULL DEFAULT 500 COMMENT 'POINT 提现手续费 bps 快照' AFTER points_per_usd,
|
||||||
|
ADD COLUMN point_policy_instance_code VARCHAR(96) NOT NULL DEFAULT '' COMMENT '已发布钱包政策实例快照' AFTER point_fee_bps;
|
||||||
@ -635,6 +635,14 @@ func (c *grpcUserHostClient) KickAgencyHost(ctx context.Context, req *userv1.Kic
|
|||||||
return c.client.KickAgencyHost(ctx, req)
|
return c.client.KickAgencyHost(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcUserHostClient) GetHostEngagementStats(ctx context.Context, req *userv1.GetHostEngagementStatsRequest) (*userv1.GetHostEngagementStatsResponse, error) {
|
||||||
|
return c.client.GetHostEngagementStats(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcUserHostClient) RecordPrivateMessageEvent(ctx context.Context, req *userv1.RecordPrivateMessageEventRequest) (*userv1.RecordPrivateMessageEventResponse, error) {
|
||||||
|
return c.client.RecordPrivateMessageEvent(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcUserHostAdminClient) CreateBDLeader(ctx context.Context, req *userv1.CreateBDLeaderRequest) (*userv1.CreateBDLeaderResponse, error) {
|
func (c *grpcUserHostAdminClient) CreateBDLeader(ctx context.Context, req *userv1.CreateBDLeaderRequest) (*userv1.CreateBDLeaderResponse, error) {
|
||||||
return c.client.CreateBDLeader(ctx, req)
|
return c.client.CreateBDLeader(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,6 +99,18 @@ func (c *grpcWalletClient) GetHostSalaryProgress(ctx context.Context, req *walle
|
|||||||
return c.client.GetHostSalaryProgress(ctx, req)
|
return c.client.GetHostSalaryProgress(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) GetHostRevenueStats(ctx context.Context, req *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error) {
|
||||||
|
return c.client.GetHostRevenueStats(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) GetAgencyPointShareStats(ctx context.Context, req *walletv1.GetAgencyPointShareStatsRequest) (*walletv1.GetAgencyPointShareStatsResponse, error) {
|
||||||
|
return c.client.GetAgencyPointShareStats(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) GetAgencyHostGiftStats(ctx context.Context, req *walletv1.GetAgencyHostGiftStatsRequest) (*walletv1.GetAgencyHostGiftStatsResponse, error) {
|
||||||
|
return c.client.GetAgencyHostGiftStats(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcWalletClient) GetTeamHostSalaryStats(ctx context.Context, req *walletv1.GetTeamHostSalaryStatsRequest) (*walletv1.GetTeamHostSalaryStatsResponse, error) {
|
func (c *grpcWalletClient) GetTeamHostSalaryStats(ctx context.Context, req *walletv1.GetTeamHostSalaryStatsRequest) (*walletv1.GetTeamHostSalaryStatsResponse, error) {
|
||||||
return c.client.GetTeamHostSalaryStats(ctx, req)
|
return c.client.GetTeamHostSalaryStats(ctx, req)
|
||||||
}
|
}
|
||||||
@ -251,6 +263,18 @@ func (c *grpcWalletClient) TransferSalaryToCoinSeller(ctx context.Context, req *
|
|||||||
return c.client.TransferSalaryToCoinSeller(ctx, req)
|
return c.client.TransferSalaryToCoinSeller(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) ListPointWithdrawalCoinSellers(ctx context.Context, req *walletv1.ListPointWithdrawalCoinSellersRequest) (*walletv1.ListPointWithdrawalCoinSellersResponse, error) {
|
||||||
|
return c.client.ListPointWithdrawalCoinSellers(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) TransferPointToCoinSeller(ctx context.Context, req *walletv1.TransferPointToCoinSellerRequest) (*walletv1.TransferPointToCoinSellerResponse, error) {
|
||||||
|
return c.client.TransferPointToCoinSeller(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcWalletClient) GetPointWithdrawalConfig(ctx context.Context, req *walletv1.GetPointWithdrawalConfigRequest) (*walletv1.GetPointWithdrawalConfigResponse, error) {
|
||||||
|
return c.client.GetPointWithdrawalConfig(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcWalletClient) FreezeSalaryWithdrawal(ctx context.Context, req *walletv1.FreezeSalaryWithdrawalRequest) (*walletv1.FreezeSalaryWithdrawalResponse, error) {
|
func (c *grpcWalletClient) FreezeSalaryWithdrawal(ctx context.Context, req *walletv1.FreezeSalaryWithdrawalRequest) (*walletv1.FreezeSalaryWithdrawalResponse, error) {
|
||||||
return c.client.FreezeSalaryWithdrawal(ctx, req)
|
return c.client.FreezeSalaryWithdrawal(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,11 @@ type CreateApplicationCommand struct {
|
|||||||
SalaryAssetType string
|
SalaryAssetType string
|
||||||
WithdrawAmount string
|
WithdrawAmount string
|
||||||
WithdrawAmountMinor int64
|
WithdrawAmountMinor int64
|
||||||
|
PointFeeAmount int64
|
||||||
|
PointNetAmount int64
|
||||||
|
PointsPerUSD int64
|
||||||
|
PointFeeBPS int32
|
||||||
|
PointPolicyInstance string
|
||||||
WithdrawMethod string
|
WithdrawMethod string
|
||||||
WithdrawAddress string
|
WithdrawAddress string
|
||||||
FreezeCommandID string
|
FreezeCommandID string
|
||||||
@ -40,6 +45,11 @@ type Application struct {
|
|||||||
SalaryAssetType string
|
SalaryAssetType string
|
||||||
WithdrawAmount string
|
WithdrawAmount string
|
||||||
WithdrawAmountMinor int64
|
WithdrawAmountMinor int64
|
||||||
|
PointFeeAmount int64
|
||||||
|
PointNetAmount int64
|
||||||
|
PointsPerUSD int64
|
||||||
|
PointFeeBPS int32
|
||||||
|
PointPolicyInstance string
|
||||||
WithdrawMethod string
|
WithdrawMethod string
|
||||||
WithdrawAddress string
|
WithdrawAddress string
|
||||||
FreezeCommandID string
|
FreezeCommandID string
|
||||||
@ -106,14 +116,21 @@ func (w *MySQLWriter) CreateApplication(ctx context.Context, command CreateAppli
|
|||||||
|
|
||||||
result, err := w.db.ExecContext(ctx, `
|
result, err := w.db.ExecContext(ctx, `
|
||||||
INSERT INTO admin_user_withdrawal_applications
|
INSERT INTO admin_user_withdrawal_applications
|
||||||
(app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor, withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id, status, created_at_ms, updated_at_ms)
|
(app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor,
|
||||||
|
point_fee_amount, point_net_amount, points_per_usd, point_fee_bps, point_policy_instance_code,
|
||||||
|
withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id, status, created_at_ms, updated_at_ms)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
command.AppCode,
|
command.AppCode,
|
||||||
formatUserID(command.UserID),
|
formatUserID(command.UserID),
|
||||||
command.SalaryAssetType,
|
command.SalaryAssetType,
|
||||||
command.WithdrawAmount,
|
command.WithdrawAmount,
|
||||||
command.WithdrawAmountMinor,
|
command.WithdrawAmountMinor,
|
||||||
|
command.PointFeeAmount,
|
||||||
|
command.PointNetAmount,
|
||||||
|
command.PointsPerUSD,
|
||||||
|
command.PointFeeBPS,
|
||||||
|
command.PointPolicyInstance,
|
||||||
command.WithdrawMethod,
|
command.WithdrawMethod,
|
||||||
command.WithdrawAddress,
|
command.WithdrawAddress,
|
||||||
command.FreezeCommandID,
|
command.FreezeCommandID,
|
||||||
@ -144,6 +161,11 @@ VALUES
|
|||||||
SalaryAssetType: command.SalaryAssetType,
|
SalaryAssetType: command.SalaryAssetType,
|
||||||
WithdrawAmount: command.WithdrawAmount,
|
WithdrawAmount: command.WithdrawAmount,
|
||||||
WithdrawAmountMinor: command.WithdrawAmountMinor,
|
WithdrawAmountMinor: command.WithdrawAmountMinor,
|
||||||
|
PointFeeAmount: command.PointFeeAmount,
|
||||||
|
PointNetAmount: command.PointNetAmount,
|
||||||
|
PointsPerUSD: command.PointsPerUSD,
|
||||||
|
PointFeeBPS: command.PointFeeBPS,
|
||||||
|
PointPolicyInstance: command.PointPolicyInstance,
|
||||||
WithdrawMethod: command.WithdrawMethod,
|
WithdrawMethod: command.WithdrawMethod,
|
||||||
WithdrawAddress: command.WithdrawAddress,
|
WithdrawAddress: command.WithdrawAddress,
|
||||||
FreezeCommandID: command.FreezeCommandID,
|
FreezeCommandID: command.FreezeCommandID,
|
||||||
@ -157,6 +179,7 @@ VALUES
|
|||||||
func (w *MySQLWriter) getApplicationByFreezeCommand(ctx context.Context, appCode string, freezeCommandID string) (Application, bool, error) {
|
func (w *MySQLWriter) getApplicationByFreezeCommand(ctx context.Context, appCode string, freezeCommandID string) (Application, bool, error) {
|
||||||
row := w.db.QueryRowContext(ctx, `
|
row := w.db.QueryRowContext(ctx, `
|
||||||
SELECT id, app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor,
|
SELECT id, app_code, user_id, salary_asset_type, withdraw_amount, withdraw_amount_minor,
|
||||||
|
point_fee_amount, point_net_amount, points_per_usd, point_fee_bps, point_policy_instance_code,
|
||||||
withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id,
|
withdraw_method, withdraw_address, freeze_command_id, freeze_transaction_id,
|
||||||
status, created_at_ms, updated_at_ms
|
status, created_at_ms, updated_at_ms
|
||||||
FROM admin_user_withdrawal_applications
|
FROM admin_user_withdrawal_applications
|
||||||
@ -171,6 +194,11 @@ LIMIT 1`, appCode, freezeCommandID)
|
|||||||
&item.SalaryAssetType,
|
&item.SalaryAssetType,
|
||||||
&item.WithdrawAmount,
|
&item.WithdrawAmount,
|
||||||
&item.WithdrawAmountMinor,
|
&item.WithdrawAmountMinor,
|
||||||
|
&item.PointFeeAmount,
|
||||||
|
&item.PointNetAmount,
|
||||||
|
&item.PointsPerUSD,
|
||||||
|
&item.PointFeeBPS,
|
||||||
|
&item.PointPolicyInstance,
|
||||||
&item.WithdrawMethod,
|
&item.WithdrawMethod,
|
||||||
&item.WithdrawAddress,
|
&item.WithdrawAddress,
|
||||||
&item.FreezeCommandID,
|
&item.FreezeCommandID,
|
||||||
@ -205,6 +233,7 @@ func normalizeCreateApplicationCommand(command CreateApplicationCommand) CreateA
|
|||||||
command.WithdrawAddress = strings.TrimSpace(command.WithdrawAddress)
|
command.WithdrawAddress = strings.TrimSpace(command.WithdrawAddress)
|
||||||
command.FreezeCommandID = strings.TrimSpace(command.FreezeCommandID)
|
command.FreezeCommandID = strings.TrimSpace(command.FreezeCommandID)
|
||||||
command.FreezeTransactionID = strings.TrimSpace(command.FreezeTransactionID)
|
command.FreezeTransactionID = strings.TrimSpace(command.FreezeTransactionID)
|
||||||
|
command.PointPolicyInstance = strings.TrimSpace(command.PointPolicyInstance)
|
||||||
if command.WithdrawMethod == "" {
|
if command.WithdrawMethod == "" {
|
||||||
command.WithdrawMethod = MethodUSDTTRC20
|
command.WithdrawMethod = MethodUSDTTRC20
|
||||||
}
|
}
|
||||||
@ -234,6 +263,12 @@ func validateCreateApplicationCommand(command CreateApplicationCommand) error {
|
|||||||
case command.CreatedAtMS <= 0:
|
case command.CreatedAtMS <= 0:
|
||||||
return errors.New("created_at_ms is required")
|
return errors.New("created_at_ms is required")
|
||||||
default:
|
default:
|
||||||
|
if command.SalaryAssetType == "POINT" || command.SalaryAssetType == "COIN_SELLER_POINT" {
|
||||||
|
// POINT 的政策快照会被财务审核原样复用;写入前先验证内部一致性,避免冻结成功但审核无法结算。
|
||||||
|
if command.PointsPerUSD <= 0 || command.PointFeeBPS < 0 || command.PointFeeBPS > 10000 || command.PointFeeAmount < 0 || command.PointNetAmount < 0 || command.PointFeeAmount+command.PointNetAmount != command.WithdrawAmountMinor || command.PointFeeAmount != command.WithdrawAmountMinor*int64(command.PointFeeBPS)/10000 {
|
||||||
|
return errors.New("point withdrawal policy snapshot is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,11 @@ func TestCreateApplicationReturnsExistingByAppAndFreezeCommand(t *testing.T) {
|
|||||||
"POINT",
|
"POINT",
|
||||||
"9.50",
|
"9.50",
|
||||||
int64(1000000),
|
int64(1000000),
|
||||||
|
int64(50000),
|
||||||
|
int64(950000),
|
||||||
|
int64(100000),
|
||||||
|
int32(500),
|
||||||
|
"huwaa-policy-v1",
|
||||||
MethodUSDTTRC20,
|
MethodUSDTTRC20,
|
||||||
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||||
"cmd-point-freeze",
|
"cmd-point-freeze",
|
||||||
@ -54,6 +59,11 @@ func TestCreateApplicationReturnsExistingByAppAndFreezeCommand(t *testing.T) {
|
|||||||
SalaryAssetType: "POINT",
|
SalaryAssetType: "POINT",
|
||||||
WithdrawAmount: "9.50",
|
WithdrawAmount: "9.50",
|
||||||
WithdrawAmountMinor: 1_000_000,
|
WithdrawAmountMinor: 1_000_000,
|
||||||
|
PointFeeAmount: 50_000,
|
||||||
|
PointNetAmount: 950_000,
|
||||||
|
PointsPerUSD: 100_000,
|
||||||
|
PointFeeBPS: 500,
|
||||||
|
PointPolicyInstance: "huwaa-policy-v1",
|
||||||
WithdrawMethod: MethodUSDTTRC20,
|
WithdrawMethod: MethodUSDTTRC20,
|
||||||
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||||
FreezeCommandID: "cmd-point-freeze",
|
FreezeCommandID: "cmd-point-freeze",
|
||||||
@ -86,6 +96,11 @@ func TestCreateApplicationInsertsAfterAppScopedMiss(t *testing.T) {
|
|||||||
"POINT",
|
"POINT",
|
||||||
"9.50",
|
"9.50",
|
||||||
int64(1000000),
|
int64(1000000),
|
||||||
|
int64(50000),
|
||||||
|
int64(950000),
|
||||||
|
int64(100000),
|
||||||
|
int32(500),
|
||||||
|
"huwaa-policy-v1",
|
||||||
MethodUSDTTRC20,
|
MethodUSDTTRC20,
|
||||||
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
"TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||||
"cmd-point-freeze",
|
"cmd-point-freeze",
|
||||||
@ -102,6 +117,11 @@ func TestCreateApplicationInsertsAfterAppScopedMiss(t *testing.T) {
|
|||||||
SalaryAssetType: "point",
|
SalaryAssetType: "point",
|
||||||
WithdrawAmount: "9.50",
|
WithdrawAmount: "9.50",
|
||||||
WithdrawAmountMinor: 1_000_000,
|
WithdrawAmountMinor: 1_000_000,
|
||||||
|
PointFeeAmount: 50_000,
|
||||||
|
PointNetAmount: 950_000,
|
||||||
|
PointsPerUSD: 100_000,
|
||||||
|
PointFeeBPS: 500,
|
||||||
|
PointPolicyInstance: "huwaa-policy-v1",
|
||||||
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
WithdrawAddress: "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF",
|
||||||
FreezeCommandID: "cmd-point-freeze",
|
FreezeCommandID: "cmd-point-freeze",
|
||||||
FreezeTransactionID: "tx-freeze",
|
FreezeTransactionID: "tx-freeze",
|
||||||
@ -126,6 +146,11 @@ func withdrawalApplicationRows() *sqlmock.Rows {
|
|||||||
"salary_asset_type",
|
"salary_asset_type",
|
||||||
"withdraw_amount",
|
"withdraw_amount",
|
||||||
"withdraw_amount_minor",
|
"withdraw_amount_minor",
|
||||||
|
"point_fee_amount",
|
||||||
|
"point_net_amount",
|
||||||
|
"points_per_usd",
|
||||||
|
"point_fee_bps",
|
||||||
|
"point_policy_instance_code",
|
||||||
"withdraw_method",
|
"withdraw_method",
|
||||||
"withdraw_address",
|
"withdraw_address",
|
||||||
"freeze_command_id",
|
"freeze_command_id",
|
||||||
|
|||||||
@ -28,6 +28,7 @@ type Handler struct {
|
|||||||
roomClient client.RoomClient
|
roomClient client.RoomClient
|
||||||
roomGuardClient client.RoomGuardClient
|
roomGuardClient client.RoomGuardClient
|
||||||
userProfileClient client.UserProfileClient
|
userProfileClient client.UserProfileClient
|
||||||
|
userHostClient client.UserHostClient
|
||||||
tencentIM TencentIMConfig
|
tencentIM TencentIMConfig
|
||||||
tencentRTC TencentRTCConfig
|
tencentRTC TencentRTCConfig
|
||||||
}
|
}
|
||||||
@ -36,6 +37,7 @@ type Config struct {
|
|||||||
RoomClient client.RoomClient
|
RoomClient client.RoomClient
|
||||||
RoomGuardClient client.RoomGuardClient
|
RoomGuardClient client.RoomGuardClient
|
||||||
UserProfileClient client.UserProfileClient
|
UserProfileClient client.UserProfileClient
|
||||||
|
UserHostClient client.UserHostClient
|
||||||
TencentIM TencentIMConfig
|
TencentIM TencentIMConfig
|
||||||
TencentRTC TencentRTCConfig
|
TencentRTC TencentRTCConfig
|
||||||
}
|
}
|
||||||
@ -45,6 +47,7 @@ func New(config Config) *Handler {
|
|||||||
roomClient: config.RoomClient,
|
roomClient: config.RoomClient,
|
||||||
roomGuardClient: config.RoomGuardClient,
|
roomGuardClient: config.RoomGuardClient,
|
||||||
userProfileClient: config.UserProfileClient,
|
userProfileClient: config.UserProfileClient,
|
||||||
|
userHostClient: config.UserHostClient,
|
||||||
tencentIM: config.TencentIM,
|
tencentIM: config.TencentIM,
|
||||||
tencentRTC: config.TencentRTC,
|
tencentRTC: config.TencentRTC,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
package callbackapi
|
package callbackapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -18,6 +21,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
tencentCallbackJoinGroup = "Group.CallbackBeforeApplyJoinGroup"
|
tencentCallbackJoinGroup = "Group.CallbackBeforeApplyJoinGroup"
|
||||||
tencentCallbackSendMsg = "Group.CallbackBeforeSendMsg"
|
tencentCallbackSendMsg = "Group.CallbackBeforeSendMsg"
|
||||||
|
tencentCallbackC2CSent = "C2C.CallbackAfterSendMsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tencentIMCallbackBody 覆盖本阶段需要处理的腾讯云 IM 群回调字段。
|
// tencentIMCallbackBody 覆盖本阶段需要处理的腾讯云 IM 群回调字段。
|
||||||
@ -28,6 +32,11 @@ type tencentIMCallbackBody struct {
|
|||||||
Requestor string `json:"Requestor_Account"`
|
Requestor string `json:"Requestor_Account"`
|
||||||
FromAccount string `json:"From_Account"`
|
FromAccount string `json:"From_Account"`
|
||||||
OperatorAccount string `json:"Operator_Account"`
|
OperatorAccount string `json:"Operator_Account"`
|
||||||
|
ToAccount string `json:"To_Account"`
|
||||||
|
MsgKey string `json:"MsgKey"`
|
||||||
|
MsgSeq int64 `json:"MsgSeq"`
|
||||||
|
MsgRandom int64 `json:"MsgRandom"`
|
||||||
|
MsgTime int64 `json:"MsgTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// tencentIMCallbackResponse 是腾讯云 IM 回调要求的原生响应结构。
|
// tencentIMCallbackResponse 是腾讯云 IM 回调要求的原生响应结构。
|
||||||
@ -86,11 +95,60 @@ func (h *Handler) handleTencentIMCallback(writer http.ResponseWriter, request *h
|
|||||||
h.handleTencentIMJoinCallback(writer, request, body)
|
h.handleTencentIMJoinCallback(writer, request, body)
|
||||||
case tencentCallbackSendMsg:
|
case tencentCallbackSendMsg:
|
||||||
h.handleTencentIMSendCallback(writer, request, body)
|
h.handleTencentIMSendCallback(writer, request, body)
|
||||||
|
case tencentCallbackC2CSent:
|
||||||
|
h.handleTencentIMC2CSentCallback(writer, request, body)
|
||||||
default:
|
default:
|
||||||
writeTencentCallback(writer, http.StatusOK, 0, "")
|
writeTencentCallback(writer, http.StatusOK, 0, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleTencentIMC2CSentCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) {
|
||||||
|
// 服务端系统通知不属于主播“收到私信的人数”,否则批量余额/VIP 通知会污染运营指标。
|
||||||
|
if h.tencentIMCallbackSenderIsServer(body) {
|
||||||
|
writeTencentCallback(writer, http.StatusOK, 0, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
senderUserID, senderOK := parseTencentIMUserID(body.FromAccount)
|
||||||
|
targetUserID, targetOK := parseTencentIMUserID(body.ToAccount)
|
||||||
|
callbackAppCode := appcode.Normalize(request.URL.Query().Get("app_code"))
|
||||||
|
if callbackAppCode == "" {
|
||||||
|
callbackAppCode = appcode.FromContext(request.Context())
|
||||||
|
}
|
||||||
|
recorder, recorderOK := h.userHostClient.(interface {
|
||||||
|
RecordPrivateMessageEvent(context.Context, *userv1.RecordPrivateMessageEventRequest) (*userv1.RecordPrivateMessageEventResponse, error)
|
||||||
|
})
|
||||||
|
if !senderOK || !targetOK || senderUserID == targetUserID || !recorderOK {
|
||||||
|
// 统计旁路不能阻断用户已经发送成功的私信;缺配置只记告警,腾讯仍收到成功响应。
|
||||||
|
slog.Warn("tencent_im_private_message_stats_skipped", "app_code", callbackAppCode, "from", body.FromAccount, "to", body.ToAccount)
|
||||||
|
writeTencentCallback(writer, http.StatusOK, 0, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
occurredAtMS := body.MsgTime * 1000
|
||||||
|
if occurredAtMS <= 0 {
|
||||||
|
occurredAtMS = time.Now().UTC().UnixMilli()
|
||||||
|
}
|
||||||
|
eventID := strings.TrimSpace(body.MsgKey)
|
||||||
|
if eventID == "" {
|
||||||
|
eventID = fmt.Sprintf("c2c:%d:%d:%d:%d:%d", senderUserID, targetUserID, body.MsgSeq, body.MsgRandom, occurredAtMS)
|
||||||
|
}
|
||||||
|
_, err := recorder.RecordPrivateMessageEvent(appcode.WithContext(request.Context(), callbackAppCode), &userv1.RecordPrivateMessageEventRequest{
|
||||||
|
Meta: &userv1.RequestMeta{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
|
Caller: "gateway-service",
|
||||||
|
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||||
|
AppCode: callbackAppCode,
|
||||||
|
},
|
||||||
|
EventId: eventID,
|
||||||
|
SenderUserId: senderUserID,
|
||||||
|
TargetUserId: targetUserID,
|
||||||
|
OccurredAtMs: occurredAtMS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("tencent_im_private_message_stats_failed", "app_code", callbackAppCode, "event_id", eventID, "error", err)
|
||||||
|
}
|
||||||
|
writeTencentCallback(writer, http.StatusOK, 0, "")
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) handleTencentIMJoinCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) {
|
func (h *Handler) handleTencentIMJoinCallback(writer http.ResponseWriter, request *http.Request, body tencentIMCallbackBody) {
|
||||||
parsed := imgroup.ParseWithPrefix(body.GroupID, h.tencentIM.GroupIDPrefix)
|
parsed := imgroup.ParseWithPrefix(body.GroupID, h.tencentIM.GroupIDPrefix)
|
||||||
userID, ok := parseTencentIMUserID(body.Requestor)
|
userID, ok := parseTencentIMUserID(body.Requestor)
|
||||||
@ -294,7 +352,7 @@ func resolveTencentIMCallbackCommand(queryCommand string, bodyCommand string) (s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func knownTencentIMCallback(command string) bool {
|
func knownTencentIMCallback(command string) bool {
|
||||||
return command == tencentCallbackJoinGroup || command == tencentCallbackSendMsg
|
return command == tencentCallbackJoinGroup || command == tencentCallbackSendMsg || command == tencentCallbackC2CSent
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTencentIMUserID(value string) (int64, bool) {
|
func parseTencentIMUserID(value string) (int64, bool) {
|
||||||
|
|||||||
@ -90,8 +90,10 @@ type UserHandlers struct {
|
|||||||
ListAgencyRooms http.HandlerFunc
|
ListAgencyRooms http.HandlerFunc
|
||||||
UpdateAgencyProfile http.HandlerFunc
|
UpdateAgencyProfile http.HandlerFunc
|
||||||
GetHostCenterAgency http.HandlerFunc
|
GetHostCenterAgency http.HandlerFunc
|
||||||
|
GetHostCenterStats http.HandlerFunc
|
||||||
GetHostCenterPlatformPolicy http.HandlerFunc
|
GetHostCenterPlatformPolicy http.HandlerFunc
|
||||||
GetAgencyCenterOverview http.HandlerFunc
|
GetAgencyCenterOverview http.HandlerFunc
|
||||||
|
GetAgencyCenterStats http.HandlerFunc
|
||||||
GetAgencyCenterPlatformPolicy http.HandlerFunc
|
GetAgencyCenterPlatformPolicy http.HandlerFunc
|
||||||
ListAgencyCenterHosts http.HandlerFunc
|
ListAgencyCenterHosts http.HandlerFunc
|
||||||
ListAgencyCenterApplications http.HandlerFunc
|
ListAgencyCenterApplications http.HandlerFunc
|
||||||
@ -104,6 +106,7 @@ type UserHandlers struct {
|
|||||||
InviteBDLeaderBD http.HandlerFunc
|
InviteBDLeaderBD http.HandlerFunc
|
||||||
InviteBDLeaderAgency http.HandlerFunc
|
InviteBDLeaderAgency http.HandlerFunc
|
||||||
GetBDCenterOverview http.HandlerFunc
|
GetBDCenterOverview http.HandlerFunc
|
||||||
|
GetBDCenterStats http.HandlerFunc
|
||||||
ListBDCenterAgencies http.HandlerFunc
|
ListBDCenterAgencies http.HandlerFunc
|
||||||
InviteBDAgency http.HandlerFunc
|
InviteBDAgency http.HandlerFunc
|
||||||
ListMyRoleInvitations http.HandlerFunc
|
ListMyRoleInvitations http.HandlerFunc
|
||||||
@ -287,6 +290,7 @@ type WalletHandlers struct {
|
|||||||
WithdrawSalaryWallet http.HandlerFunc
|
WithdrawSalaryWallet http.HandlerFunc
|
||||||
GetPointWalletOverview http.HandlerFunc
|
GetPointWalletOverview http.HandlerFunc
|
||||||
WithdrawPointWallet http.HandlerFunc
|
WithdrawPointWallet http.HandlerFunc
|
||||||
|
TransferPointWalletToCoinSeller http.HandlerFunc
|
||||||
GetRedPacketConfig http.HandlerFunc
|
GetRedPacketConfig http.HandlerFunc
|
||||||
ListRoomRedPackets http.HandlerFunc
|
ListRoomRedPackets http.HandlerFunc
|
||||||
CreateRoomRedPacket http.HandlerFunc
|
CreateRoomRedPacket http.HandlerFunc
|
||||||
@ -470,8 +474,10 @@ func (r routes) registerUserRoutes() {
|
|||||||
r.profile("/agencies/rooms", http.MethodGet, h.ListAgencyRooms)
|
r.profile("/agencies/rooms", http.MethodGet, h.ListAgencyRooms)
|
||||||
r.profile("/agencies/profile/update", http.MethodPost, h.UpdateAgencyProfile)
|
r.profile("/agencies/profile/update", http.MethodPost, h.UpdateAgencyProfile)
|
||||||
r.profile("/host-center/agency", http.MethodGet, h.GetHostCenterAgency)
|
r.profile("/host-center/agency", http.MethodGet, h.GetHostCenterAgency)
|
||||||
|
r.profile("/host-center/stats", http.MethodGet, h.GetHostCenterStats)
|
||||||
r.profile("/host-center/platform-policy", http.MethodGet, h.GetHostCenterPlatformPolicy)
|
r.profile("/host-center/platform-policy", http.MethodGet, h.GetHostCenterPlatformPolicy)
|
||||||
r.profile("/agency-center/overview", http.MethodGet, h.GetAgencyCenterOverview)
|
r.profile("/agency-center/overview", http.MethodGet, h.GetAgencyCenterOverview)
|
||||||
|
r.profile("/agency-center/stats", http.MethodGet, h.GetAgencyCenterStats)
|
||||||
r.profile("/agency-center/platform-policy", http.MethodGet, h.GetAgencyCenterPlatformPolicy)
|
r.profile("/agency-center/platform-policy", http.MethodGet, h.GetAgencyCenterPlatformPolicy)
|
||||||
r.profile("/agency-center/hosts", http.MethodGet, h.ListAgencyCenterHosts)
|
r.profile("/agency-center/hosts", http.MethodGet, h.ListAgencyCenterHosts)
|
||||||
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
|
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
|
||||||
@ -484,6 +490,7 @@ func (r routes) registerUserRoutes() {
|
|||||||
r.profile("/bd-leader/invitations/bd", http.MethodPost, h.InviteBDLeaderBD)
|
r.profile("/bd-leader/invitations/bd", http.MethodPost, h.InviteBDLeaderBD)
|
||||||
r.profile("/bd-leader/invitations/agency", http.MethodPost, h.InviteBDLeaderAgency)
|
r.profile("/bd-leader/invitations/agency", http.MethodPost, h.InviteBDLeaderAgency)
|
||||||
r.profile("/bd-center/overview", http.MethodGet, h.GetBDCenterOverview)
|
r.profile("/bd-center/overview", http.MethodGet, h.GetBDCenterOverview)
|
||||||
|
r.profile("/bd-center/stats", http.MethodGet, h.GetBDCenterStats)
|
||||||
r.profile("/bd-center/agencies", http.MethodGet, h.ListBDCenterAgencies)
|
r.profile("/bd-center/agencies", http.MethodGet, h.ListBDCenterAgencies)
|
||||||
r.profile("/bd/invitations/agency", http.MethodPost, h.InviteBDAgency)
|
r.profile("/bd/invitations/agency", http.MethodPost, h.InviteBDAgency)
|
||||||
r.profile("/role-invitations", http.MethodGet, h.ListMyRoleInvitations)
|
r.profile("/role-invitations", http.MethodGet, h.ListMyRoleInvitations)
|
||||||
@ -690,6 +697,7 @@ func (r routes) registerWalletRoutes() {
|
|||||||
r.profile("/salary-wallet/withdraw", http.MethodPost, h.WithdrawSalaryWallet)
|
r.profile("/salary-wallet/withdraw", http.MethodPost, h.WithdrawSalaryWallet)
|
||||||
r.profile("/point-wallet/overview", http.MethodGet, h.GetPointWalletOverview)
|
r.profile("/point-wallet/overview", http.MethodGet, h.GetPointWalletOverview)
|
||||||
r.profile("/point-wallet/withdraw", http.MethodPost, h.WithdrawPointWallet)
|
r.profile("/point-wallet/withdraw", http.MethodPost, h.WithdrawPointWallet)
|
||||||
|
r.profile("/point-wallet/transfer-to-coin-seller", http.MethodPost, h.TransferPointWalletToCoinSeller)
|
||||||
r.profile("/red-packets/config", http.MethodGet, h.GetRedPacketConfig)
|
r.profile("/red-packets/config", http.MethodGet, h.GetRedPacketConfig)
|
||||||
r.profile("/rooms/{room_id}/red-packets", http.MethodGet, h.ListRoomRedPackets)
|
r.profile("/rooms/{room_id}/red-packets", http.MethodGet, h.ListRoomRedPackets)
|
||||||
r.profile("/rooms/{room_id}/red-packets/create", http.MethodPost, h.CreateRoomRedPacket)
|
r.profile("/rooms/{room_id}/red-packets/create", http.MethodPost, h.CreateRoomRedPacket)
|
||||||
|
|||||||
@ -134,6 +134,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
RoomClient: h.roomClient,
|
RoomClient: h.roomClient,
|
||||||
RoomGuardClient: h.roomGuardClient,
|
RoomGuardClient: h.roomGuardClient,
|
||||||
UserProfileClient: h.userProfileClient,
|
UserProfileClient: h.userProfileClient,
|
||||||
|
UserHostClient: h.userHostClient,
|
||||||
TencentIM: callbackapi.TencentIMConfig{
|
TencentIM: callbackapi.TencentIMConfig{
|
||||||
SDKAppID: h.tencentIM.SDKAppID,
|
SDKAppID: h.tencentIM.SDKAppID,
|
||||||
AdminIdentifier: h.tencentIM.AdminIdentifier,
|
AdminIdentifier: h.tencentIM.AdminIdentifier,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ type agencyCenterUserData struct {
|
|||||||
DisplayUserID string `json:"display_user_id"`
|
DisplayUserID string `json:"display_user_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type agencyCenterHostData struct {
|
type agencyCenterHostData struct {
|
||||||
@ -465,6 +466,7 @@ func agencyCenterUserFromProto(user *userv1.User) agencyCenterUserData {
|
|||||||
DisplayUserID: user.GetDisplayUserId(),
|
DisplayUserID: user.GetDisplayUserId(),
|
||||||
Username: user.GetUsername(),
|
Username: user.GetUsername(),
|
||||||
Avatar: user.GetAvatar(),
|
Avatar: user.GetAvatar(),
|
||||||
|
Country: firstBDLeaderNonBlank(user.GetCountryDisplayName(), user.GetCountryName(), user.GetCountry()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,165 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agencyRevenueStatsClient interface {
|
||||||
|
GetHostRevenueStats(context.Context, *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error)
|
||||||
|
GetAgencyPointShareStats(context.Context, *walletv1.GetAgencyPointShareStatsRequest) (*walletv1.GetAgencyPointShareStatsResponse, error)
|
||||||
|
GetAgencyHostGiftStats(context.Context, *walletv1.GetAgencyHostGiftStatsRequest) (*walletv1.GetAgencyHostGiftStatsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyCenterStatsSummaryData struct {
|
||||||
|
TotalEarnings int64 `json:"total_earnings"`
|
||||||
|
GiftIncome int64 `json:"gift_income"`
|
||||||
|
ShareIncome int64 `json:"share_income"`
|
||||||
|
TotalHosts int `json:"total_hosts"`
|
||||||
|
GiftedHostCount int64 `json:"gifted_host_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type agencyCenterHostStatsData struct {
|
||||||
|
MembershipID string `json:"membership_id"`
|
||||||
|
HostUserID string `json:"host_user_id"`
|
||||||
|
DisplayUserID string `json:"display_user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
JoinedAtMS int64 `json:"joined_at_ms"`
|
||||||
|
DiamondEarnings int64 `json:"diamond_earnings"`
|
||||||
|
DiamondExchanged int64 `json:"diamond_exchanged"`
|
||||||
|
GiftSenders int64 `json:"gift_senders"`
|
||||||
|
OnlineDurationMS int64 `json:"online_duration_ms"`
|
||||||
|
ValidMicDurationMS int64 `json:"valid_mic_duration_ms"`
|
||||||
|
ValidMicDays int64 `json:"valid_mic_days"`
|
||||||
|
Removable bool `json:"removable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAgencyCenterStats 只做跨 owner 编排:成员关系/互动来自 user-service,金额和分成来自 wallet-service。
|
||||||
|
// 每个主播的两次 RPC 使用有界并发,避免大公会串行超时,也避免无上限 goroutine 压垮下游。
|
||||||
|
func (h *Handler) getAgencyCenterStats(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
revenueClient, revenueOK := h.walletClient.(agencyRevenueStatsClient)
|
||||||
|
engagementClient, engagementOK := h.userHostClient.(hostEngagementStatsClient)
|
||||||
|
if !revenueOK || !engagementOK {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startDate, endDate, startAtMS, endAtMS, ok := parseHostCenterStatsRange(request)
|
||||||
|
if !ok {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid date range")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
membersResp, err := h.userHostClient.GetAgencyMembers(request.Context(), &userv1.GetAgencyMembersRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""), AgencyId: agency.GetAgencyId(), Status: membershipStatusActive,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memberships := make([]*userv1.AgencyMembership, 0, len(membersResp.GetMemberships()))
|
||||||
|
userIDs := make([]int64, 0, len(membersResp.GetMemberships()))
|
||||||
|
for _, membership := range membersResp.GetMemberships() {
|
||||||
|
memberships = append(memberships, membership)
|
||||||
|
userIDs = append(userIDs, membership.GetHostUserId())
|
||||||
|
}
|
||||||
|
profiles, err := h.userProfiles(request, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestID := httpkit.RequestIDFromContext(request.Context())
|
||||||
|
appCode := appcode.FromContext(request.Context())
|
||||||
|
shareResp, err := revenueClient.GetAgencyPointShareStats(request.Context(), &walletv1.GetAgencyPointShareStatsRequest{
|
||||||
|
RequestId: requestID, AppCode: appCode, AgencyOwnerUserId: agency.GetOwnerUserId(), StartAtMs: startAtMS, EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
giftResp, err := revenueClient.GetAgencyHostGiftStats(request.Context(), &walletv1.GetAgencyHostGiftStatsRequest{
|
||||||
|
RequestId: requestID, AppCode: appCode, HostUserIds: userIDs, StartAtMs: startAtMS, EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]agencyCenterHostStatsData, len(memberships))
|
||||||
|
ctx, cancel := context.WithCancel(request.Context())
|
||||||
|
defer cancel()
|
||||||
|
semaphore := make(chan struct{}, 8)
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
var firstErr error
|
||||||
|
var errorMu sync.Mutex
|
||||||
|
for index, membership := range memberships {
|
||||||
|
index, membership := index, membership
|
||||||
|
wait.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wait.Done()
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
revenue, callErr := revenueClient.GetHostRevenueStats(ctx, &walletv1.GetHostRevenueStatsRequest{
|
||||||
|
RequestId: requestID, AppCode: appCode, HostUserId: membership.GetHostUserId(), StartAtMs: startAtMS, EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if callErr == nil {
|
||||||
|
var engagement *userv1.GetHostEngagementStatsResponse
|
||||||
|
engagement, callErr = engagementClient.GetHostEngagementStats(ctx, &userv1.GetHostEngagementStatsRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""), HostUserId: membership.GetHostUserId(), StartAtMs: startAtMS, EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if callErr == nil {
|
||||||
|
profile := profiles[membership.GetHostUserId()]
|
||||||
|
items[index] = agencyCenterHostStatsData{
|
||||||
|
MembershipID: int64String(membership.GetMembershipId()), HostUserID: int64String(membership.GetHostUserId()),
|
||||||
|
DisplayUserID: profile.DisplayUserID, Username: profile.Username, Avatar: profile.Avatar, JoinedAtMS: membership.GetJoinedAtMs(),
|
||||||
|
DiamondEarnings: revenue.GetStats().GetDiamondEarnings(), DiamondExchanged: revenue.GetStats().GetDiamondExchanged(), GiftSenders: revenue.GetStats().GetGiftSenders(),
|
||||||
|
OnlineDurationMS: engagement.GetStats().GetOnlineDurationMs(), ValidMicDurationMS: engagement.GetStats().GetValidMicDurationMs(), ValidMicDays: engagement.GetStats().GetValidMicDays(),
|
||||||
|
Removable: membership.GetHostUserId() != agency.GetOwnerUserId() && membership.GetMembershipType() != membershipTypeOwner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if callErr != nil {
|
||||||
|
errorMu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = callErr
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
errorMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
if firstErr != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, firstErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shareIncome := shareResp.GetStats().GetShareIncome()
|
||||||
|
// 公会长同时是主播时,其 POINT 流水包含 Agency 分成;主播行只展示个人收益,必须扣除同区间分成。
|
||||||
|
for index := range items {
|
||||||
|
if items[index].HostUserID == int64String(agency.GetOwnerUserId()) {
|
||||||
|
items[index].DiamondEarnings -= shareIncome
|
||||||
|
if items[index].DiamondEarnings < 0 {
|
||||||
|
items[index].DiamondEarnings = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
giftIncome := giftResp.GetStats().GetGiftIncome()
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"range": hostCenterStatsRangeData{StartDate: startDate, EndDate: endDate, Timezone: "UTC"},
|
||||||
|
"summary": agencyCenterStatsSummaryData{
|
||||||
|
TotalEarnings: giftIncome + shareIncome, GiftIncome: giftIncome, ShareIncome: shareIncome,
|
||||||
|
TotalHosts: len(items), GiftedHostCount: giftResp.GetStats().GetGiftedHostCount(),
|
||||||
|
},
|
||||||
|
"hosts": items,
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/gateway-service/internal/auth"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agencyHostGiftStatsClient interface {
|
||||||
|
GetAgencyHostGiftStats(context.Context, *walletv1.GetAgencyHostGiftStatsRequest) (*walletv1.GetAgencyHostGiftStatsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type bdCenterAgencyStatsData struct {
|
||||||
|
Agency hostAgencyData `json:"agency"`
|
||||||
|
Owner agencyCenterUserData `json:"owner"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
TotalHosts int `json:"total_hosts"`
|
||||||
|
GiftedHostCount int64 `json:"gifted_host_count"`
|
||||||
|
DiamondEarnings int64 `json:"diamond_earnings"`
|
||||||
|
JoinedAtMS int64 `json:"joined_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBDCenterStats 返回 BD 直属 Agency 的收礼统计;工资和 Agency 分成不属于设计稿口径,刻意不返回。
|
||||||
|
func (h *Handler) getBDCenterStats(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
if _, ok := h.resolveActiveBD(writer, request, userID); !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statsClient, ok := h.walletClient.(agencyHostGiftStatsClient)
|
||||||
|
if !ok {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startDate, endDate, startAtMS, endAtMS, ok := parseHostCenterStatsRange(request)
|
||||||
|
if !ok {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid date range")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agenciesResp, err := h.userHostClient.ListBDAgencies(request.Context(), &userv1.ListBDAgenciesRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""), BdUserId: userID, Status: agencyStatusActive, PageSize: 100,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ownerIDs := make([]int64, 0, len(agenciesResp.GetAgencies()))
|
||||||
|
for _, agency := range agenciesResp.GetAgencies() {
|
||||||
|
ownerIDs = append(ownerIDs, agency.GetOwnerUserId())
|
||||||
|
}
|
||||||
|
owners, err := h.userProfiles(request, ownerIDs)
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]bdCenterAgencyStatsData, len(agenciesResp.GetAgencies()))
|
||||||
|
ctx, cancel := context.WithCancel(request.Context())
|
||||||
|
defer cancel()
|
||||||
|
semaphore := make(chan struct{}, 8)
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
var firstErr error
|
||||||
|
var errorMu sync.Mutex
|
||||||
|
for index, agency := range agenciesResp.GetAgencies() {
|
||||||
|
index, agency := index, agency
|
||||||
|
wait.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wait.Done()
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
members, callErr := h.userHostClient.GetAgencyMembers(ctx, &userv1.GetAgencyMembersRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""), AgencyId: agency.GetAgencyId(), Status: membershipStatusActive,
|
||||||
|
})
|
||||||
|
hostIDs := make([]int64, 0, len(members.GetMemberships()))
|
||||||
|
if callErr == nil {
|
||||||
|
for _, membership := range members.GetMemberships() {
|
||||||
|
hostIDs = append(hostIDs, membership.GetHostUserId())
|
||||||
|
}
|
||||||
|
var stats *walletv1.GetAgencyHostGiftStatsResponse
|
||||||
|
stats, callErr = statsClient.GetAgencyHostGiftStats(ctx, &walletv1.GetAgencyHostGiftStatsRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
|
||||||
|
HostUserIds: hostIDs, StartAtMs: startAtMS, EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if callErr == nil {
|
||||||
|
owner := owners[agency.GetOwnerUserId()]
|
||||||
|
items[index] = bdCenterAgencyStatsData{
|
||||||
|
Agency: hostAgencyFromProto(agency), Owner: owner, Country: owner.Country,
|
||||||
|
TotalHosts: len(hostIDs), GiftedHostCount: stats.GetStats().GetGiftedHostCount(),
|
||||||
|
DiamondEarnings: stats.GetStats().GetGiftIncome(), JoinedAtMS: agency.GetCreatedAtMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if callErr != nil {
|
||||||
|
errorMu.Lock()
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = callErr
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
errorMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
if firstErr != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, firstErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"range": hostCenterStatsRangeData{StartDate: startDate, EndDate: endDate, Timezone: "UTC"},
|
||||||
|
"items": items, "total": len(items),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -73,8 +73,10 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
|||||||
ListAgencyRooms: h.listAgencyRooms,
|
ListAgencyRooms: h.listAgencyRooms,
|
||||||
UpdateAgencyProfile: h.updateAgencyProfile,
|
UpdateAgencyProfile: h.updateAgencyProfile,
|
||||||
GetHostCenterAgency: h.getHostCenterAgency,
|
GetHostCenterAgency: h.getHostCenterAgency,
|
||||||
|
GetHostCenterStats: h.getHostCenterStats,
|
||||||
GetHostCenterPlatformPolicy: h.getHostCenterPlatformPolicy,
|
GetHostCenterPlatformPolicy: h.getHostCenterPlatformPolicy,
|
||||||
GetAgencyCenterOverview: h.getAgencyCenterOverview,
|
GetAgencyCenterOverview: h.getAgencyCenterOverview,
|
||||||
|
GetAgencyCenterStats: h.getAgencyCenterStats,
|
||||||
GetAgencyCenterPlatformPolicy: h.getAgencyCenterPlatformPolicy,
|
GetAgencyCenterPlatformPolicy: h.getAgencyCenterPlatformPolicy,
|
||||||
ListAgencyCenterHosts: h.listAgencyCenterHosts,
|
ListAgencyCenterHosts: h.listAgencyCenterHosts,
|
||||||
ListAgencyCenterApplications: h.listAgencyCenterApplications,
|
ListAgencyCenterApplications: h.listAgencyCenterApplications,
|
||||||
@ -87,6 +89,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
|||||||
InviteBDLeaderBD: h.inviteBDLeaderBD,
|
InviteBDLeaderBD: h.inviteBDLeaderBD,
|
||||||
InviteBDLeaderAgency: h.inviteBDLeaderAgency,
|
InviteBDLeaderAgency: h.inviteBDLeaderAgency,
|
||||||
GetBDCenterOverview: h.getBDCenterOverview,
|
GetBDCenterOverview: h.getBDCenterOverview,
|
||||||
|
GetBDCenterStats: h.getBDCenterStats,
|
||||||
ListBDCenterAgencies: h.listBDCenterAgencies,
|
ListBDCenterAgencies: h.listBDCenterAgencies,
|
||||||
InviteBDAgency: h.inviteBDAgency,
|
InviteBDAgency: h.inviteBDAgency,
|
||||||
ListMyRoleInvitations: h.listMyRoleInvitations,
|
ListMyRoleInvitations: h.listMyRoleInvitations,
|
||||||
|
|||||||
@ -8,10 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type hostCenterAgencyData struct {
|
type hostCenterAgencyData struct {
|
||||||
AgencyID string `json:"agency_id"`
|
AgencyID string `json:"agency_id"`
|
||||||
ShortID string `json:"short_id"`
|
ShortID string `json:"short_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Avatar string `json:"avatar,omitempty"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
JoinedAtMS int64 `json:"joined_at_ms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getHostCenterAgency(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getHostCenterAgency(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -49,6 +50,21 @@ func (h *Handler) getHostCenterAgency(writer http.ResponseWriter, request *http.
|
|||||||
Name: agency.GetName(),
|
Name: agency.GetName(),
|
||||||
Avatar: agency.GetAvatar(),
|
Avatar: agency.GetAvatar(),
|
||||||
}
|
}
|
||||||
|
// 加入时间来自当前 membership 事实,不使用 host 首次成为主播时间;主播换公会后两者语义不同。
|
||||||
|
membersResp, err := h.userHostClient.GetAgencyMembers(request.Context(), &userv1.GetAgencyMembersRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
AgencyId: agency.GetAgencyId(),
|
||||||
|
Status: "active",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
// 加入时间是新增展示字段;该补充查询失败时保留旧响应,不能让 Lalu 历史页面因非关键字段不可用而整体失败。
|
||||||
|
for _, membership := range membersResp.GetMemberships() {
|
||||||
|
if membership.GetMembershipId() == profile.GetCurrentMembershipId() && membership.GetHostUserId() == profile.GetUserId() {
|
||||||
|
data.JoinedAtMS = membership.GetJoinedAtMs()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if agency.GetOwnerUserId() > 0 {
|
if agency.GetOwnerUserId() > 0 {
|
||||||
profiles, err := h.userProfiles(request, []int64{agency.GetOwnerUserId()})
|
profiles, err := h.userProfiles(request, []int64{agency.GetOwnerUserId()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -0,0 +1,114 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hostEngagementStatsClient interface {
|
||||||
|
GetHostEngagementStats(context.Context, *userv1.GetHostEngagementStatsRequest) (*userv1.GetHostEngagementStatsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostRevenueStatsClient interface {
|
||||||
|
GetHostRevenueStats(context.Context, *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostCenterMaxStatsDays = 366
|
||||||
|
|
||||||
|
type hostCenterStatsRangeData struct {
|
||||||
|
StartDate string `json:"start_date"`
|
||||||
|
EndDate string `json:"end_date"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hostCenterStatsMetricsData struct {
|
||||||
|
DiamondEarnings int64 `json:"diamond_earnings"`
|
||||||
|
DiamondExchanged int64 `json:"diamond_exchanged"`
|
||||||
|
GiftSenders int64 `json:"gift_senders"`
|
||||||
|
OnlineDurationMS int64 `json:"online_duration_ms"`
|
||||||
|
ValidMicDurationMS int64 `json:"valid_mic_duration_ms"`
|
||||||
|
ValidMicDays int64 `json:"valid_mic_days"`
|
||||||
|
PrivateMessageSenders int64 `json:"private_message_senders"`
|
||||||
|
NewFollowers int64 `json:"new_followers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHostCenterStats 只编排 user/wallet 两个 owner 的只读聚合,不在 gateway 重算账务或在线时长。
|
||||||
|
func (h *Handler) getHostCenterStats(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
profile, ok := h.resolveCurrentActiveHostProfile(writer, request)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
engagementClient, engagementOK := h.userHostClient.(hostEngagementStatsClient)
|
||||||
|
revenueClient, revenueOK := h.walletClient.(hostRevenueStatsClient)
|
||||||
|
if !engagementOK || !revenueOK {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startDate, endDate, startAtMS, endAtMS, ok := parseHostCenterStatsRange(request)
|
||||||
|
if !ok {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid date range")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
engagementResp, err := engagementClient.GetHostEngagementStats(request.Context(), &userv1.GetHostEngagementStatsRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
HostUserId: profile.GetUserId(),
|
||||||
|
StartAtMs: startAtMS,
|
||||||
|
EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
revenueResp, err := revenueClient.GetHostRevenueStats(request.Context(), &walletv1.GetHostRevenueStatsRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
HostUserId: profile.GetUserId(),
|
||||||
|
StartAtMs: startAtMS,
|
||||||
|
EndAtMs: endAtMS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
engagement := engagementResp.GetStats()
|
||||||
|
revenue := revenueResp.GetStats()
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"range": hostCenterStatsRangeData{StartDate: startDate, EndDate: endDate, Timezone: "UTC"},
|
||||||
|
"metrics": hostCenterStatsMetricsData{
|
||||||
|
DiamondEarnings: revenue.GetDiamondEarnings(),
|
||||||
|
DiamondExchanged: revenue.GetDiamondExchanged(),
|
||||||
|
GiftSenders: revenue.GetGiftSenders(),
|
||||||
|
OnlineDurationMS: engagement.GetOnlineDurationMs(),
|
||||||
|
ValidMicDurationMS: engagement.GetValidMicDurationMs(),
|
||||||
|
ValidMicDays: engagement.GetValidMicDays(),
|
||||||
|
PrivateMessageSenders: engagement.GetPrivateMessageSenders(),
|
||||||
|
NewFollowers: engagement.GetNewFollowers(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHostCenterStatsRange(request *http.Request) (string, string, int64, int64, bool) {
|
||||||
|
startText := strings.TrimSpace(request.URL.Query().Get("start_date"))
|
||||||
|
endText := strings.TrimSpace(request.URL.Query().Get("end_date"))
|
||||||
|
if startText == "" || endText == "" {
|
||||||
|
return "", "", 0, 0, false
|
||||||
|
}
|
||||||
|
start, startErr := time.Parse("2006-01-02", startText)
|
||||||
|
end, endErr := time.Parse("2006-01-02", endText)
|
||||||
|
if startErr != nil || endErr != nil || end.Before(start) {
|
||||||
|
return "", "", 0, 0, false
|
||||||
|
}
|
||||||
|
endExclusive := end.Add(24 * time.Hour)
|
||||||
|
if int(endExclusive.Sub(start)/(24*time.Hour)) > hostCenterMaxStatsDays {
|
||||||
|
return "", "", 0, 0, false
|
||||||
|
}
|
||||||
|
return startText, endText, start.UnixMilli(), endExclusive.UnixMilli(), true
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package userapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseHostCenterStatsRangeUsesInclusiveNaturalDays(t *testing.T) {
|
||||||
|
request := httptest.NewRequest("GET", "/api/v1/host-center/stats?start_date=2026-07-06&end_date=2026-07-12", nil)
|
||||||
|
startDate, endDate, startAtMS, endAtMS, ok := parseHostCenterStatsRange(request)
|
||||||
|
if !ok || startDate != "2026-07-06" || endDate != "2026-07-12" {
|
||||||
|
t.Fatalf("range parse mismatch: ok=%t start=%s end=%s", ok, startDate, endDate)
|
||||||
|
}
|
||||||
|
if got := endAtMS - startAtMS; got != 7*24*60*60*1000 {
|
||||||
|
t.Fatalf("end must be exclusive after seven inclusive days, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHostCenterStatsRangeRejectsInvalidOrOversizedRanges(t *testing.T) {
|
||||||
|
for _, rawURL := range []string{
|
||||||
|
"/api/v1/host-center/stats?start_date=2026-07-12&end_date=2026-07-01",
|
||||||
|
"/api/v1/host-center/stats?start_date=2026-07-12",
|
||||||
|
"/api/v1/host-center/stats?start_date=2025-01-01&end_date=2026-07-12",
|
||||||
|
} {
|
||||||
|
request := httptest.NewRequest("GET", rawURL, nil)
|
||||||
|
if _, _, _, _, ok := parseHostCenterStatsRange(request); ok {
|
||||||
|
t.Fatalf("expected invalid range for %s", rawURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -118,6 +118,7 @@ func (h *Handler) WalletHandlers() httproutes.WalletHandlers {
|
|||||||
WithdrawSalaryWallet: h.withdrawSalaryWallet,
|
WithdrawSalaryWallet: h.withdrawSalaryWallet,
|
||||||
GetPointWalletOverview: h.getPointWalletOverview,
|
GetPointWalletOverview: h.getPointWalletOverview,
|
||||||
WithdrawPointWallet: h.withdrawPointWallet,
|
WithdrawPointWallet: h.withdrawPointWallet,
|
||||||
|
TransferPointWalletToCoinSeller: h.transferPointWalletToCoinSeller,
|
||||||
GetRedPacketConfig: h.getRedPacketConfig,
|
GetRedPacketConfig: h.getRedPacketConfig,
|
||||||
ListRoomRedPackets: h.listRoomRedPackets,
|
ListRoomRedPackets: h.listRoomRedPackets,
|
||||||
CreateRoomRedPacket: h.createRoomRedPacket,
|
CreateRoomRedPacket: h.createRoomRedPacket,
|
||||||
|
|||||||
@ -18,16 +18,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pointWalletAssetType = "POINT"
|
pointWalletAssetType = "POINT"
|
||||||
pointWalletMinimumGross = int64(1000000)
|
|
||||||
pointWalletPointsPerUSD = int64(100000)
|
|
||||||
pointWalletWithdrawFeeBPS = int32(500)
|
|
||||||
pointWalletWithdrawalScope = "huwaa"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type pointWithdrawalGatewayClient interface {
|
type pointWithdrawalGatewayClient interface {
|
||||||
FreezePointWithdrawal(ctx context.Context, req *walletv1.FreezePointWithdrawalRequest) (*walletv1.FreezePointWithdrawalResponse, error)
|
FreezePointWithdrawal(ctx context.Context, req *walletv1.FreezePointWithdrawalRequest) (*walletv1.FreezePointWithdrawalResponse, error)
|
||||||
ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error)
|
ReleasePointWithdrawal(ctx context.Context, req *walletv1.ReleasePointWithdrawalRequest) (*walletv1.ReleasePointWithdrawalResponse, error)
|
||||||
|
ListPointWithdrawalCoinSellers(ctx context.Context, req *walletv1.ListPointWithdrawalCoinSellersRequest) (*walletv1.ListPointWithdrawalCoinSellersResponse, error)
|
||||||
|
TransferPointToCoinSeller(ctx context.Context, req *walletv1.TransferPointToCoinSellerRequest) (*walletv1.TransferPointToCoinSellerResponse, error)
|
||||||
|
GetPointWithdrawalConfig(ctx context.Context, req *walletv1.GetPointWithdrawalConfigRequest) (*walletv1.GetPointWithdrawalConfigResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pointWalletOverviewData struct {
|
type pointWalletOverviewData struct {
|
||||||
@ -37,6 +36,19 @@ type pointWalletOverviewData struct {
|
|||||||
PointsPerUSD int64 `json:"points_per_usd"`
|
PointsPerUSD int64 `json:"points_per_usd"`
|
||||||
FeeBPS int32 `json:"fee_bps"`
|
FeeBPS int32 `json:"fee_bps"`
|
||||||
MinimumPoints int64 `json:"minimum_points"`
|
MinimumPoints int64 `json:"minimum_points"`
|
||||||
|
CoinSellers []pointWalletCoinSellerData `json:"coin_sellers"`
|
||||||
|
PolicyInstance string `json:"policy_instance_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pointWalletCoinSellerData struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
DisplayUserID string `json:"display_user_id"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
PointAmount int64 `json:"point_amount"`
|
||||||
|
SellerCoinAmount int64 `json:"seller_coin_amount"`
|
||||||
|
ServiceCountryCodes []string `json:"service_country_codes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type pointWalletBalanceData struct {
|
type pointWalletBalanceData struct {
|
||||||
@ -66,6 +78,36 @@ type pointWalletWithdrawBody struct {
|
|||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pointWalletCoinSellerTransferBody struct {
|
||||||
|
CommandID string `json:"command_id"`
|
||||||
|
CommandIDCamel string `json:"commandId"`
|
||||||
|
SellerUserID flexibleJSONInt64 `json:"seller_user_id"`
|
||||||
|
SellerUserIDCamel flexibleJSONInt64 `json:"sellerUserId"`
|
||||||
|
PointAmount int64 `json:"point_amount"`
|
||||||
|
PointAmountCamel int64 `json:"pointAmount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b pointWalletCoinSellerTransferBody) commandID() string {
|
||||||
|
if value := strings.TrimSpace(b.CommandID); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(b.CommandIDCamel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b pointWalletCoinSellerTransferBody) sellerUserID() int64 {
|
||||||
|
if b.SellerUserID.Int64() > 0 {
|
||||||
|
return b.SellerUserID.Int64()
|
||||||
|
}
|
||||||
|
return b.SellerUserIDCamel.Int64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b pointWalletCoinSellerTransferBody) amount() int64 {
|
||||||
|
if b.PointAmount > 0 {
|
||||||
|
return b.PointAmount
|
||||||
|
}
|
||||||
|
return b.PointAmountCamel
|
||||||
|
}
|
||||||
|
|
||||||
func (b pointWalletWithdrawBody) commandID() string {
|
func (b pointWalletWithdrawBody) commandID() string {
|
||||||
if value := strings.TrimSpace(b.CommandID); value != "" {
|
if value := strings.TrimSpace(b.CommandID); value != "" {
|
||||||
return value
|
return value
|
||||||
@ -102,7 +144,7 @@ func (b pointWalletWithdrawBody) usdtTRC20Address() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getPointWalletOverview(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getPointWalletOverview(writer http.ResponseWriter, request *http.Request) {
|
||||||
if !isHuwaaPointWalletRequest(request) {
|
if !isPointWalletRequest(request) {
|
||||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -110,7 +152,37 @@ func (h *Handler) getPointWalletOverview(writer http.ResponseWriter, request *ht
|
|||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
pointWallet, ok := h.walletClient.(pointWithdrawalGatewayClient)
|
||||||
|
if !ok {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""), UserId: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile := profileResp.GetUser()
|
||||||
|
if profile == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config, err := pointWallet.GetPointWithdrawalConfig(request.Context(), &walletv1.GetPointWithdrawalConfigRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
|
||||||
|
RegionId: profile.GetRegionId(), NowMs: time.Now().UTC().UnixMilli(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !config.GetFound() {
|
||||||
|
// 没有已发布政策就关闭资金入口;不能回退到编译期常量造成 Admin 看似配置但账务仍走旧值。
|
||||||
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "point wallet policy is not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
@ -121,26 +193,33 @@ func (h *Handler) getPointWalletOverview(writer http.ResponseWriter, request *ht
|
|||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
|
sellerResp, err := pointWallet.ListPointWithdrawalCoinSellers(request.Context(), &walletv1.ListPointWithdrawalCoinSellersRequest{
|
||||||
Meta: httpkit.UserMeta(request, ""),
|
RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
|
||||||
UserId: userID,
|
CountryCode: pointWalletCountryCode(profile),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
sellers, err := h.pointWalletCoinSellerData(request, sellerResp.GetSellers())
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
httpkit.WriteOK(writer, request, pointWalletOverviewData{
|
httpkit.WriteOK(writer, request, pointWalletOverviewData{
|
||||||
AssetType: pointWalletAssetType,
|
AssetType: pointWalletAssetType,
|
||||||
Balance: pointBalanceFromProto(firstBalance(balanceResp.GetBalances())),
|
Balance: pointBalanceFromProto(firstBalance(balanceResp.GetBalances()), config.GetPointsPerUsd()),
|
||||||
WithdrawAddress: salaryWithdrawAddressFromProto(profileResp.GetUser()),
|
WithdrawAddress: salaryWithdrawAddressFromProto(profileResp.GetUser()),
|
||||||
PointsPerUSD: pointWalletPointsPerUSD,
|
PointsPerUSD: config.GetPointsPerUsd(),
|
||||||
FeeBPS: pointWalletWithdrawFeeBPS,
|
FeeBPS: config.GetFeeBps(),
|
||||||
MinimumPoints: pointWalletMinimumGross,
|
MinimumPoints: config.GetMinimumPoints(),
|
||||||
|
CoinSellers: sellers,
|
||||||
|
PolicyInstance: config.GetPolicyInstanceCode(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.Request) {
|
||||||
if !isHuwaaPointWalletRequest(request) {
|
if !isPointWalletRequest(request) {
|
||||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -164,13 +243,31 @@ func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
grossPoints := body.grossPointAmount()
|
grossPoints := body.grossPointAmount()
|
||||||
if grossPoints < pointWalletMinimumGross {
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{Meta: httpkit.UserMeta(request, ""), UserId: userID})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile := profileResp.GetUser()
|
||||||
|
if profile == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config, err := pointWallet.GetPointWithdrawalConfig(request.Context(), &walletv1.GetPointWithdrawalConfigRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
|
||||||
|
RegionId: profile.GetRegionId(), NowMs: time.Now().UTC().UnixMilli(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !config.GetFound() || grossPoints < config.GetMinimumPoints() {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
feePoints := grossPoints * int64(pointWalletWithdrawFeeBPS) / 10000
|
feePoints := grossPoints * int64(config.GetFeeBps()) / 10000
|
||||||
netPoints := grossPoints - feePoints
|
netPoints := grossPoints - feePoints
|
||||||
userID := auth.UserIDFromContext(request.Context())
|
|
||||||
balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
UserId: userID,
|
UserId: userID,
|
||||||
@ -199,8 +296,8 @@ func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.
|
|||||||
GrossPointAmount: grossPoints,
|
GrossPointAmount: grossPoints,
|
||||||
FeePointAmount: feePoints,
|
FeePointAmount: feePoints,
|
||||||
NetPointAmount: netPoints,
|
NetPointAmount: netPoints,
|
||||||
PointsPerUsd: pointWalletPointsPerUSD,
|
PointsPerUsd: config.GetPointsPerUsd(),
|
||||||
FeeBps: pointWalletWithdrawFeeBPS,
|
FeeBps: config.GetFeeBps(),
|
||||||
Reason: "point withdrawal submitted",
|
Reason: "point withdrawal submitted",
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
WithdrawalRef: httpkit.RequestIDFromContext(request.Context()),
|
WithdrawalRef: httpkit.RequestIDFromContext(request.Context()),
|
||||||
@ -213,8 +310,13 @@ func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.
|
|||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
SalaryAssetType: pointWalletAssetType,
|
SalaryAssetType: pointWalletAssetType,
|
||||||
WithdrawAmount: formatPointUSD(netPoints, pointWalletPointsPerUSD),
|
WithdrawAmount: formatPointUSD(netPoints, config.GetPointsPerUsd()),
|
||||||
WithdrawAmountMinor: grossPoints,
|
WithdrawAmountMinor: grossPoints,
|
||||||
|
PointFeeAmount: feePoints,
|
||||||
|
PointNetAmount: netPoints,
|
||||||
|
PointsPerUSD: config.GetPointsPerUsd(),
|
||||||
|
PointFeeBPS: config.GetFeeBps(),
|
||||||
|
PointPolicyInstance: config.GetPolicyInstanceCode(),
|
||||||
WithdrawMethod: financewithdrawal.MethodUSDTTRC20,
|
WithdrawMethod: financewithdrawal.MethodUSDTTRC20,
|
||||||
WithdrawAddress: address,
|
WithdrawAddress: address,
|
||||||
FreezeCommandID: freezeCommandID,
|
FreezeCommandID: freezeCommandID,
|
||||||
@ -222,7 +324,7 @@ func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.
|
|||||||
CreatedAtMS: nowMS,
|
CreatedAtMS: nowMS,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rollbackPointWithdrawalFreeze(request, pointWallet, freezeCommandID, userID, grossPoints, feePoints, netPoints)
|
rollbackPointWithdrawalFreeze(request, pointWallet, freezeCommandID, userID, grossPoints, feePoints, netPoints, config.GetPointsPerUsd(), config.GetFeeBps())
|
||||||
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -239,12 +341,13 @@ func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.
|
|||||||
"gross_point_amount": grossPoints,
|
"gross_point_amount": grossPoints,
|
||||||
"fee_point_amount": feePoints,
|
"fee_point_amount": feePoints,
|
||||||
"net_point_amount": netPoints,
|
"net_point_amount": netPoints,
|
||||||
"points_per_usd": pointWalletPointsPerUSD,
|
"points_per_usd": config.GetPointsPerUsd(),
|
||||||
"fee_bps": pointWalletWithdrawFeeBPS,
|
"fee_bps": config.GetFeeBps(),
|
||||||
"gross_usd": formatPointUSD(grossPoints, pointWalletPointsPerUSD),
|
"policy_instance_code": config.GetPolicyInstanceCode(),
|
||||||
"fee_usd": formatPointUSD(feePoints, pointWalletPointsPerUSD),
|
"gross_usd": formatPointUSD(grossPoints, config.GetPointsPerUsd()),
|
||||||
"net_usd": formatPointUSD(netPoints, pointWalletPointsPerUSD),
|
"fee_usd": formatPointUSD(feePoints, config.GetPointsPerUsd()),
|
||||||
"balance_after": pointBalanceFromProto(freezeResp.GetBalance()),
|
"net_usd": formatPointUSD(netPoints, config.GetPointsPerUsd()),
|
||||||
|
"balance_after": pointBalanceFromProto(freezeResp.GetBalance(), config.GetPointsPerUsd()),
|
||||||
"withdraw_method": application.WithdrawMethod,
|
"withdraw_method": application.WithdrawMethod,
|
||||||
"withdraw_address": application.WithdrawAddress,
|
"withdraw_address": application.WithdrawAddress,
|
||||||
"freeze_transaction_id": freezeResp.GetTransactionId(),
|
"freeze_transaction_id": freezeResp.GetTransactionId(),
|
||||||
@ -252,11 +355,17 @@ func (h *Handler) withdrawPointWallet(writer http.ResponseWriter, request *http.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHuwaaPointWalletRequest(request *http.Request) bool {
|
func isPointWalletRequest(request *http.Request) bool {
|
||||||
return strings.EqualFold(appcode.FromContext(request.Context()), pointWalletWithdrawalScope)
|
// 新钱包首先服务 Huwaa 与 Fami;Lalu 旧 gonghui 页面和既有工资钱包路由不做行为变更。
|
||||||
|
switch strings.ToLower(strings.TrimSpace(appcode.FromContext(request.Context()))) {
|
||||||
|
case "huwaa", "fami":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pointBalanceFromProto(balance *walletv1.AssetBalance) pointWalletBalanceData {
|
func pointBalanceFromProto(balance *walletv1.AssetBalance, pointsPerUSD int64) pointWalletBalanceData {
|
||||||
if balance == nil {
|
if balance == nil {
|
||||||
return pointWalletBalanceData{AssetType: pointWalletAssetType, DisplayUSD: "0.00"}
|
return pointWalletBalanceData{AssetType: pointWalletAssetType, DisplayUSD: "0.00"}
|
||||||
}
|
}
|
||||||
@ -265,7 +374,7 @@ func pointBalanceFromProto(balance *walletv1.AssetBalance) pointWalletBalanceDat
|
|||||||
AvailableAmount: balance.GetAvailableAmount(),
|
AvailableAmount: balance.GetAvailableAmount(),
|
||||||
FrozenAmount: balance.GetFrozenAmount(),
|
FrozenAmount: balance.GetFrozenAmount(),
|
||||||
Version: balance.GetVersion(),
|
Version: balance.GetVersion(),
|
||||||
DisplayUSD: formatPointUSD(balance.GetAvailableAmount(), pointWalletPointsPerUSD),
|
DisplayUSD: formatPointUSD(balance.GetAvailableAmount(), pointsPerUSD),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,7 +397,7 @@ func pointWithdrawalFreezeCommandID(submittedCommandID string, requestID string,
|
|||||||
return fmt.Sprintf("point-withdrawal:%d", nowMS)
|
return fmt.Sprintf("point-withdrawal:%d", nowMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rollbackPointWithdrawalFreeze(request *http.Request, walletClient pointWithdrawalGatewayClient, freezeCommandID string, userID int64, grossPoints int64, feePoints int64, netPoints int64) {
|
func rollbackPointWithdrawalFreeze(request *http.Request, walletClient pointWithdrawalGatewayClient, freezeCommandID string, userID int64, grossPoints int64, feePoints int64, netPoints int64, pointsPerUSD int64, feeBPS int32) {
|
||||||
rollbackCommandID := strings.TrimSpace(freezeCommandID) + ":rollback"
|
rollbackCommandID := strings.TrimSpace(freezeCommandID) + ":rollback"
|
||||||
if len(rollbackCommandID) > 128 {
|
if len(rollbackCommandID) > 128 {
|
||||||
rollbackCommandID = fmt.Sprintf("point-withdrawal-rollback:%s", httpkit.RequestIDFromContext(request.Context()))
|
rollbackCommandID = fmt.Sprintf("point-withdrawal-rollback:%s", httpkit.RequestIDFromContext(request.Context()))
|
||||||
@ -302,8 +411,8 @@ func rollbackPointWithdrawalFreeze(request *http.Request, walletClient pointWith
|
|||||||
GrossPointAmount: grossPoints,
|
GrossPointAmount: grossPoints,
|
||||||
FeePointAmount: feePoints,
|
FeePointAmount: feePoints,
|
||||||
NetPointAmount: netPoints,
|
NetPointAmount: netPoints,
|
||||||
PointsPerUsd: pointWalletPointsPerUSD,
|
PointsPerUsd: pointsPerUSD,
|
||||||
FeeBps: pointWalletWithdrawFeeBPS,
|
FeeBps: feeBPS,
|
||||||
Reason: "point withdrawal application create failed",
|
Reason: "point withdrawal application create failed",
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
})
|
})
|
||||||
@ -311,3 +420,119 @@ func rollbackPointWithdrawalFreeze(request *http.Request, walletClient pointWith
|
|||||||
slog.Error("point_withdrawal_freeze_rollback_failed", "user_id", userID, "gross_point_amount", grossPoints, "freeze_command_id", freezeCommandID, "error", err)
|
slog.Error("point_withdrawal_freeze_rollback_failed", "user_id", userID, "gross_point_amount", grossPoints, "freeze_command_id", freezeCommandID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// transferPointWalletToCoinSeller 只接收目标和 POINT 金额;wallet 事务会重新读取 active 白名单与比例。
|
||||||
|
func (h *Handler) transferPointWalletToCoinSeller(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if !isPointWalletRequest(request) {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pointWallet, ok := h.walletClient.(pointWithdrawalGatewayClient)
|
||||||
|
if !ok || h.userProfileClient == nil || h.userHostClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body pointWalletCoinSellerTransferBody
|
||||||
|
if !httpkit.Decode(writer, request, &body) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commandID := body.commandID()
|
||||||
|
sellerUserID := body.sellerUserID()
|
||||||
|
pointAmount := body.amount()
|
||||||
|
if commandID == "" || sellerUserID <= 0 || pointAmount <= 0 {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := auth.UserIDFromContext(request.Context())
|
||||||
|
profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{Meta: httpkit.UserMeta(request, ""), UserId: userID})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if profileResp.GetUser() == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtimeConfig, err := pointWallet.GetPointWithdrawalConfig(request.Context(), &walletv1.GetPointWithdrawalConfigRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
|
||||||
|
RegionId: profileResp.GetUser().GetRegionId(), NowMs: time.Now().UTC().UnixMilli(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !runtimeConfig.GetFound() || pointAmount < runtimeConfig.GetMinimumPoints() {
|
||||||
|
// 币商和 USDT 两种提现共用已发布政策中的最低门槛,客户端不能通过切换页签绕过。
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "point withdrawal amount is below minimum")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sellerProfileResp, err := h.userHostClient.GetCoinSellerProfile(request.Context(), &userv1.GetCoinSellerProfileRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""), UserId: sellerUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sellerProfile := sellerProfileResp.GetCoinSellerProfile()
|
||||||
|
if sellerProfile == nil || sellerProfile.GetStatus() != "active" || sellerProfile.GetMerchantAssetType() != "COIN_SELLER_COIN" {
|
||||||
|
// Admin 白名单只证明配置存在;转账时仍要检查实时币商身份,防止币商被停用后继续收款。
|
||||||
|
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "coin seller is not active")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
receipt, err := pointWallet.TransferPointToCoinSeller(request.Context(), &walletv1.TransferPointToCoinSellerRequest{
|
||||||
|
CommandId: commandID, AppCode: appcode.FromContext(request.Context()), SourceUserId: userID,
|
||||||
|
SellerUserId: sellerUserID, PointAmount: pointAmount,
|
||||||
|
SourceCountryCode: pointWalletCountryCode(profileResp.GetUser()), Reason: "point wallet withdrawal to coin seller",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
|
"transaction_id": receipt.GetTransactionId(), "seller_user_id": strconv.FormatInt(sellerUserID, 10),
|
||||||
|
"point_amount": receipt.GetPointAmount(), "seller_coin_amount": receipt.GetSellerCoinAmount(),
|
||||||
|
"source_point_balance_after": receipt.GetSourcePointBalanceAfter(), "seller_balance_after": receipt.GetSellerBalanceAfter(),
|
||||||
|
"ratio_point_amount": receipt.GetRatioPointAmount(), "ratio_seller_coin_amount": receipt.GetRatioSellerCoinAmount(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) pointWalletCoinSellerData(request *http.Request, configs []*walletv1.PointWithdrawalCoinSellerConfig) ([]pointWalletCoinSellerData, error) {
|
||||||
|
if len(configs) == 0 {
|
||||||
|
return []pointWalletCoinSellerData{}, nil
|
||||||
|
}
|
||||||
|
userIDs := make([]int64, 0, len(configs))
|
||||||
|
for _, config := range configs {
|
||||||
|
if config != nil && config.GetSellerUserId() > 0 {
|
||||||
|
userIDs = append(userIDs, config.GetSellerUserId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profiles, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{Meta: httpkit.UserMeta(request, ""), UserIds: userIDs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]pointWalletCoinSellerData, 0, len(configs))
|
||||||
|
for _, config := range configs {
|
||||||
|
if config == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
profile := profiles.GetUsers()[config.GetSellerUserId()]
|
||||||
|
// 白名单可在用户删除/禁用后短暂残留;读模型跳过缺失用户,转账入口还会实时校验 coin-seller 身份。
|
||||||
|
if profile == nil || profile.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, pointWalletCoinSellerData{
|
||||||
|
UserID: strconv.FormatInt(profile.GetUserId(), 10), DisplayUserID: profile.GetDisplayUserId(),
|
||||||
|
Nickname: profile.GetUsername(), Avatar: profile.GetAvatar(), SortOrder: config.GetSortOrder(),
|
||||||
|
PointAmount: config.GetPointAmount(), SellerCoinAmount: config.GetSellerCoinAmount(),
|
||||||
|
ServiceCountryCodes: append([]string(nil), config.GetServiceCountryCodes()...),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointWalletCountryCode(profile *userv1.User) string {
|
||||||
|
if profile == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.ToUpper(strings.TrimSpace(profile.GetCountry()))
|
||||||
|
}
|
||||||
|
|||||||
@ -91,6 +91,11 @@ func TestWithdrawPointWalletFreezesWithClientCommandIDAndCreatesApplication(t *t
|
|||||||
writer.last.SalaryAssetType != "POINT" ||
|
writer.last.SalaryAssetType != "POINT" ||
|
||||||
writer.last.WithdrawAmount != "9.50" ||
|
writer.last.WithdrawAmount != "9.50" ||
|
||||||
writer.last.WithdrawAmountMinor != 1_000_000 ||
|
writer.last.WithdrawAmountMinor != 1_000_000 ||
|
||||||
|
writer.last.PointFeeAmount != 50_000 ||
|
||||||
|
writer.last.PointNetAmount != 950_000 ||
|
||||||
|
writer.last.PointsPerUSD != 100_000 ||
|
||||||
|
writer.last.PointFeeBPS != 500 ||
|
||||||
|
writer.last.PointPolicyInstance != "test-policy" ||
|
||||||
writer.last.FreezeCommandID != "client-point-withdraw-1" ||
|
writer.last.FreezeCommandID != "client-point-withdraw-1" ||
|
||||||
writer.last.FreezeTransactionID != "point-freeze-tx" ||
|
writer.last.FreezeTransactionID != "point-freeze-tx" ||
|
||||||
writer.last.WithdrawAddress != "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF" {
|
writer.last.WithdrawAddress != "TQY9pFbZHuR4fUN9WgXPYj5nmB6nQiLQfF" {
|
||||||
@ -112,13 +117,34 @@ func TestWithdrawPointWalletFreezesWithClientCommandIDAndCreatesApplication(t *t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTransferPointWalletToCoinSellerUsesStringIDAndTrustedCountry(t *testing.T) {
|
||||||
|
wallet := &fakePointWalletClient{}
|
||||||
|
handler := New(Config{
|
||||||
|
WalletClient: wallet,
|
||||||
|
UserProfileClient: &fakePointUserProfileClient{country: "SA"},
|
||||||
|
UserHostClient: &fakePointUserHostClient{},
|
||||||
|
})
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := pointWalletRequest(`{"command_id":"point-seller-1","seller_user_id":"9007199254740001","point_amount":1000000}`, "fami")
|
||||||
|
|
||||||
|
httpkit.WithRequestID(http.HandlerFunc(handler.transferPointWalletToCoinSeller)).ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("point seller transfer status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if wallet.lastPointTransfer == nil || wallet.lastPointTransfer.GetSellerUserId() != 9007199254740001 || wallet.lastPointTransfer.GetSourceUserId() != 42001 || wallet.lastPointTransfer.GetSourceCountryCode() != "SA" || wallet.lastPointTransfer.GetAppCode() != "fami" {
|
||||||
|
t.Fatalf("point seller transfer request mismatch: %+v", wallet.lastPointTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type fakePointWalletClient struct {
|
type fakePointWalletClient struct {
|
||||||
client.WalletClient
|
client.WalletClient
|
||||||
balances []*walletv1.AssetBalance
|
balances []*walletv1.AssetBalance
|
||||||
lastBalance *walletv1.GetBalancesRequest
|
lastBalance *walletv1.GetBalancesRequest
|
||||||
lastFreeze *walletv1.FreezePointWithdrawalRequest
|
lastFreeze *walletv1.FreezePointWithdrawalRequest
|
||||||
lastRelease *walletv1.ReleasePointWithdrawalRequest
|
lastRelease *walletv1.ReleasePointWithdrawalRequest
|
||||||
freezeErr error
|
lastPointTransfer *walletv1.TransferPointToCoinSellerRequest
|
||||||
|
freezeErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakePointWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
|
func (f *fakePointWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
|
||||||
@ -151,9 +177,23 @@ func (f *fakePointWalletClient) ReleasePointWithdrawal(_ context.Context, req *w
|
|||||||
return &walletv1.ReleasePointWithdrawalResponse{TransactionId: "point-release-tx"}, nil
|
return &walletv1.ReleasePointWithdrawalResponse{TransactionId: "point-release-tx"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakePointWalletClient) GetPointWithdrawalConfig(_ context.Context, _ *walletv1.GetPointWithdrawalConfigRequest) (*walletv1.GetPointWithdrawalConfigResponse, error) {
|
||||||
|
return &walletv1.GetPointWithdrawalConfigResponse{Found: true, PointsPerUsd: 100_000, FeeBps: 500, MinimumPoints: 1_000_000, PolicyInstanceCode: "test-policy"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePointWalletClient) ListPointWithdrawalCoinSellers(_ context.Context, _ *walletv1.ListPointWithdrawalCoinSellersRequest) (*walletv1.ListPointWithdrawalCoinSellersResponse, error) {
|
||||||
|
return &walletv1.ListPointWithdrawalCoinSellersResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePointWalletClient) TransferPointToCoinSeller(_ context.Context, req *walletv1.TransferPointToCoinSellerRequest) (*walletv1.TransferPointToCoinSellerResponse, error) {
|
||||||
|
f.lastPointTransfer = req
|
||||||
|
return &walletv1.TransferPointToCoinSellerResponse{TransactionId: "point-seller-tx", PointAmount: req.GetPointAmount(), SellerCoinAmount: 920000}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type fakePointUserProfileClient struct {
|
type fakePointUserProfileClient struct {
|
||||||
client.UserProfileClient
|
client.UserProfileClient
|
||||||
address string
|
address string
|
||||||
|
country string
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +201,15 @@ func (f *fakePointUserProfileClient) GetUser(_ context.Context, req *userv1.GetU
|
|||||||
if f.err != nil {
|
if f.err != nil {
|
||||||
return nil, f.err
|
return nil, f.err
|
||||||
}
|
}
|
||||||
return &userv1.GetUserResponse{User: &userv1.User{UserId: req.GetUserId(), WithdrawUsdtTrc20Address: f.address}}, nil
|
return &userv1.GetUserResponse{User: &userv1.User{UserId: req.GetUserId(), Country: f.country, WithdrawUsdtTrc20Address: f.address}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePointUserHostClient struct {
|
||||||
|
client.UserHostClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePointUserHostClient) GetCoinSellerProfile(_ context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error) {
|
||||||
|
return &userv1.GetCoinSellerProfileResponse{CoinSellerProfile: &userv1.CoinSellerProfile{UserId: req.GetUserId(), Status: "active", MerchantAssetType: "COIN_SELLER_COIN"}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakePointUserProfileClient) UpdateUserWithdrawAddress(_ context.Context, req *userv1.UpdateUserWithdrawAddressRequest) (*userv1.UpdateUserWithdrawAddressResponse, error) {
|
func (f *fakePointUserProfileClient) UpdateUserWithdrawAddress(_ context.Context, req *userv1.UpdateUserWithdrawAddressRequest) (*userv1.UpdateUserWithdrawAddressResponse, error) {
|
||||||
@ -190,6 +238,11 @@ func (f *fakePointWithdrawalWriter) CreateApplication(_ context.Context, command
|
|||||||
SalaryAssetType: command.SalaryAssetType,
|
SalaryAssetType: command.SalaryAssetType,
|
||||||
WithdrawAmount: command.WithdrawAmount,
|
WithdrawAmount: command.WithdrawAmount,
|
||||||
WithdrawAmountMinor: command.WithdrawAmountMinor,
|
WithdrawAmountMinor: command.WithdrawAmountMinor,
|
||||||
|
PointFeeAmount: command.PointFeeAmount,
|
||||||
|
PointNetAmount: command.PointNetAmount,
|
||||||
|
PointsPerUSD: command.PointsPerUSD,
|
||||||
|
PointFeeBPS: command.PointFeeBPS,
|
||||||
|
PointPolicyInstance: command.PointPolicyInstance,
|
||||||
WithdrawMethod: command.WithdrawMethod,
|
WithdrawMethod: command.WithdrawMethod,
|
||||||
WithdrawAddress: command.WithdrawAddress,
|
WithdrawAddress: command.WithdrawAddress,
|
||||||
FreezeCommandID: command.FreezeCommandID,
|
FreezeCommandID: command.FreezeCommandID,
|
||||||
|
|||||||
@ -127,6 +127,28 @@ CREATE TABLE IF NOT EXISTS user_follows (
|
|||||||
KEY idx_user_follows_follower_status (app_code, follower_user_id, status, followed_at_ms)
|
KEY idx_user_follows_follower_status (app_code, follower_user_id, status, followed_at_ms)
|
||||||
) 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 user_follow_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
event_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '关注状态变化事件 ID',
|
||||||
|
follower_user_id BIGINT NOT NULL COMMENT '关注人用户 ID',
|
||||||
|
followee_user_id BIGINT NOT NULL COMMENT '被关注人用户 ID',
|
||||||
|
delta TINYINT NOT NULL COMMENT '关注 +1,取消关注 -1',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '事件时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (event_id),
|
||||||
|
KEY idx_user_follow_events_followee_time (app_code, followee_user_id, occurred_at_ms, event_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户关注净增统计事件表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_private_message_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
event_id VARCHAR(128) NOT NULL COMMENT '腾讯 IM 私信事件幂等 ID',
|
||||||
|
sender_user_id BIGINT NOT NULL COMMENT '消息发送用户 ID',
|
||||||
|
target_user_id BIGINT NOT NULL COMMENT '消息接收用户 ID',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '发送时间,UTC epoch ms',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '落库时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, event_id),
|
||||||
|
KEY idx_private_message_target_time (app_code, target_user_id, occurred_at_ms, sender_user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户私信发送事实,仅保存统计字段';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_friend_applications (
|
CREATE TABLE IF NOT EXISTS user_friend_applications (
|
||||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
requester_user_id BIGINT NOT NULL COMMENT '申请人用户 ID',
|
requester_user_id BIGINT NOT NULL COMMENT '申请人用户 ID',
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
USE hyapp_user;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_follow_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
event_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '关注状态变化事件 ID',
|
||||||
|
follower_user_id BIGINT NOT NULL COMMENT '关注人用户 ID',
|
||||||
|
followee_user_id BIGINT NOT NULL COMMENT '被关注人用户 ID',
|
||||||
|
delta TINYINT NOT NULL COMMENT '关注 +1,取消关注 -1',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '事件时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (event_id),
|
||||||
|
KEY idx_user_follow_events_followee_time (app_code, followee_user_id, occurred_at_ms, event_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户关注净增统计事件表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_private_message_events (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
event_id VARCHAR(128) NOT NULL COMMENT '腾讯 IM 私信事件幂等 ID',
|
||||||
|
sender_user_id BIGINT NOT NULL COMMENT '消息发送用户 ID',
|
||||||
|
target_user_id BIGINT NOT NULL COMMENT '消息接收用户 ID',
|
||||||
|
occurred_at_ms BIGINT NOT NULL COMMENT '发送时间,UTC epoch ms',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '落库时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, event_id),
|
||||||
|
KEY idx_private_message_target_time (app_code, target_user_id, occurred_at_ms, sender_user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户私信发送事实,仅保存统计字段';
|
||||||
25
services/user-service/internal/domain/host/stats.go
Normal file
25
services/user-service/internal/domain/host/stats.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package host
|
||||||
|
|
||||||
|
// EngagementStats 聚合 user-service 拥有的 App 在线、有效麦时、私信发送者和粉丝净增事实。
|
||||||
|
type EngagementStats struct {
|
||||||
|
OnlineDurationMS int64
|
||||||
|
ValidMicDurationMS int64
|
||||||
|
ValidMicDays int64
|
||||||
|
PrivateMessageSenders int64
|
||||||
|
NewFollowers int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngagementStatsQuery 使用左闭右开 UTC 毫秒区间,避免跨页/跨日查询重复统计边界事件。
|
||||||
|
type EngagementStatsQuery struct {
|
||||||
|
HostUserID int64
|
||||||
|
StartAtMS int64
|
||||||
|
EndAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateMessageEvent 是腾讯 IM 已发送私信的最小可审计事实,不保存消息正文。
|
||||||
|
type PrivateMessageEvent struct {
|
||||||
|
EventID string
|
||||||
|
SenderUserID int64
|
||||||
|
TargetUserID int64
|
||||||
|
OccurredAtMS int64
|
||||||
|
}
|
||||||
@ -57,6 +57,8 @@ type Repository interface {
|
|||||||
HasActiveManagerCapability(ctx context.Context, userID int64, capability string) (bool, error)
|
HasActiveManagerCapability(ctx context.Context, userID int64, capability string) (bool, error)
|
||||||
ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
|
ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
|
||||||
ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
|
ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
|
||||||
|
GetHostEngagementStats(ctx context.Context, query hostdomain.EngagementStatsQuery) (hostdomain.EngagementStats, error)
|
||||||
|
RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IDGenerator 生成 Host 领域内部事实 ID。
|
// IDGenerator 生成 Host 领域内部事实 ID。
|
||||||
|
|||||||
38
services/user-service/internal/service/host/stats.go
Normal file
38
services/user-service/internal/service/host/stats.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package host
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxHostStatsRange = 366 * 24 * time.Hour
|
||||||
|
|
||||||
|
// GetHostEngagementStats 读取 user-service 拥有的统计事实;收益和送礼人数由 wallet-service 聚合。
|
||||||
|
func (s *Service) GetHostEngagementStats(ctx context.Context, query hostdomain.EngagementStatsQuery) (hostdomain.EngagementStats, error) {
|
||||||
|
if s.repository == nil {
|
||||||
|
return hostdomain.EngagementStats{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||||||
|
}
|
||||||
|
if query.HostUserID <= 0 || query.StartAtMS <= 0 || query.EndAtMS <= query.StartAtMS {
|
||||||
|
return hostdomain.EngagementStats{}, xerr.New(xerr.InvalidArgument, "host stats range is invalid")
|
||||||
|
}
|
||||||
|
if query.EndAtMS-query.StartAtMS > int64(maxHostStatsRange/time.Millisecond) {
|
||||||
|
return hostdomain.EngagementStats{}, xerr.New(xerr.InvalidArgument, "host stats range is too large")
|
||||||
|
}
|
||||||
|
return s.repository.GetHostEngagementStats(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordPrivateMessageEvent 只保存发送者、接收者和事件时间;消息正文不进入统计库。
|
||||||
|
func (s *Service) RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error) {
|
||||||
|
if s.repository == nil {
|
||||||
|
return false, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||||||
|
}
|
||||||
|
event.EventID = strings.TrimSpace(event.EventID)
|
||||||
|
if event.EventID == "" || len(event.EventID) > 128 || event.SenderUserID <= 0 || event.TargetUserID <= 0 || event.OccurredAtMS <= 0 {
|
||||||
|
return false, xerr.New(xerr.InvalidArgument, "private message event is invalid")
|
||||||
|
}
|
||||||
|
return s.repository.RecordPrivateMessageEvent(ctx, event)
|
||||||
|
}
|
||||||
144
services/user-service/internal/storage/mysql/host/stats.go
Normal file
144
services/user-service/internal/storage/mysql/host/stats.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package host
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||||||
|
)
|
||||||
|
|
||||||
|
const validMicDayThresholdMS = int64(time.Hour / time.Millisecond)
|
||||||
|
|
||||||
|
// GetHostEngagementStats 聚合 user-service 自己拥有的事实表,不从当前 Agency 关系反推历史归属。
|
||||||
|
func (r *Repository) GetHostEngagementStats(ctx context.Context, query hostdomain.EngagementStatsQuery) (hostdomain.EngagementStats, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return hostdomain.EngagementStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
stats := hostdomain.EngagementStats{}
|
||||||
|
appCode := appcode.FromContext(ctx)
|
||||||
|
startDate := time.UnixMilli(query.StartAtMS).UTC().Format("2006-01-02")
|
||||||
|
endDate := time.UnixMilli(query.EndAtMS - 1).UTC().Format("2006-01-02")
|
||||||
|
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COALESCE(SUM(mic_online_ms), 0),
|
||||||
|
COALESCE(SUM(CASE WHEN mic_online_ms >= ? THEN 1 ELSE 0 END), 0)
|
||||||
|
FROM user_mic_daily_stats
|
||||||
|
WHERE app_code = ? AND user_id = ? AND stat_date BETWEEN ? AND ?`,
|
||||||
|
validMicDayThresholdMS, appCode, query.HostUserID, startDate, endDate,
|
||||||
|
).Scan(&stats.ValidMicDurationMS, &stats.ValidMicDays); err != nil {
|
||||||
|
return hostdomain.EngagementStats{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(DISTINCT sender_user_id)
|
||||||
|
FROM user_private_message_events
|
||||||
|
WHERE app_code = ? AND target_user_id = ? AND occurred_at_ms >= ? AND occurred_at_ms < ?`,
|
||||||
|
appCode, query.HostUserID, query.StartAtMS, query.EndAtMS,
|
||||||
|
).Scan(&stats.PrivateMessageSenders); err != nil {
|
||||||
|
return hostdomain.EngagementStats{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COALESCE(SUM(delta), 0)
|
||||||
|
FROM user_follow_events
|
||||||
|
WHERE app_code = ? AND followee_user_id = ? AND occurred_at_ms >= ? AND occurred_at_ms < ?`,
|
||||||
|
appCode, query.HostUserID, query.StartAtMS, query.EndAtMS,
|
||||||
|
).Scan(&stats.NewFollowers); err != nil {
|
||||||
|
return hostdomain.EngagementStats{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
onlineMS, err := r.hostOnlineDuration(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return hostdomain.EngagementStats{}, err
|
||||||
|
}
|
||||||
|
stats.OnlineDurationMS = onlineMS
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hostOnlineDuration 按每天“最早会话开始到最后一次心跳”累计;多设备/多 session 不重复相加。
|
||||||
|
func (r *Repository) hostOnlineDuration(ctx context.Context, query hostdomain.EngagementStatsQuery) (int64, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT created_at_ms, last_heartbeat_at_ms
|
||||||
|
FROM auth_sessions
|
||||||
|
WHERE app_code = ? AND user_id = ?
|
||||||
|
AND last_heartbeat_at_ms >= ? AND created_at_ms < ?`,
|
||||||
|
appcode.FromContext(ctx), query.HostUserID, query.StartAtMS, query.EndAtMS,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type daySpan struct{ first, last int64 }
|
||||||
|
spans := map[string]daySpan{}
|
||||||
|
for rows.Next() {
|
||||||
|
var startedAtMS, heartbeatAtMS int64
|
||||||
|
if err := rows.Scan(&startedAtMS, &heartbeatAtMS); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
start := maxInt64(startedAtMS, query.StartAtMS)
|
||||||
|
end := minInt64(heartbeatAtMS, query.EndAtMS-1)
|
||||||
|
if end < start {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for day := utcDayStart(start); day <= utcDayStart(end); day += int64(24 * time.Hour / time.Millisecond) {
|
||||||
|
segmentStart := maxInt64(start, day)
|
||||||
|
segmentEnd := minInt64(end, day+int64(24*time.Hour/time.Millisecond)-1)
|
||||||
|
key := time.UnixMilli(day).UTC().Format("2006-01-02")
|
||||||
|
span, exists := spans[key]
|
||||||
|
if !exists || segmentStart < span.first {
|
||||||
|
span.first = segmentStart
|
||||||
|
}
|
||||||
|
if !exists || segmentEnd > span.last {
|
||||||
|
span.last = segmentEnd
|
||||||
|
}
|
||||||
|
spans[key] = span
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
for _, span := range spans {
|
||||||
|
if span.last >= span.first {
|
||||||
|
total += span.last - span.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordPrivateMessageEvent 用 app_code + event_id 幂等,不保存消息正文或扩展负载。
|
||||||
|
func (r *Repository) RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error) {
|
||||||
|
result, err := r.db.ExecContext(ctx, `
|
||||||
|
INSERT IGNORE INTO user_private_message_events (
|
||||||
|
app_code, event_id, sender_user_id, target_user_id, occurred_at_ms, created_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
appcode.FromContext(ctx), event.EventID, event.SenderUserID, event.TargetUserID, event.OccurredAtMS, time.Now().UTC().UnixMilli(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
rows, err := result.RowsAffected()
|
||||||
|
return rows > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func utcDayStart(value int64) int64 {
|
||||||
|
t := time.UnixMilli(value).UTC()
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC).UnixMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
func minInt64(left, right int64) int64 {
|
||||||
|
if left < right {
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
return right
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxInt64(left, right int64) int64 {
|
||||||
|
if left > right {
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
return right
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package host
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHostEngagementStatsAggregatesOwnedFacts(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectQuery("FROM user_mic_daily_stats").
|
||||||
|
WithArgs(validMicDayThresholdMS, "fami", int64(42), "2026-07-06", "2026-07-12").
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"mic_ms", "valid_days"}).AddRow(int64(5400000), int64(1)))
|
||||||
|
mock.ExpectQuery("FROM user_private_message_events").
|
||||||
|
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"senders"}).AddRow(int64(6)))
|
||||||
|
mock.ExpectQuery("FROM user_follow_events").
|
||||||
|
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"followers"}).AddRow(int64(4)))
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||||
|
SELECT created_at_ms, last_heartbeat_at_ms
|
||||||
|
FROM auth_sessions
|
||||||
|
WHERE app_code = ? AND user_id = ?
|
||||||
|
AND last_heartbeat_at_ms >= ? AND created_at_ms < ?`)).
|
||||||
|
WithArgs("fami", int64(42), int64(1783296000000), int64(1783900800000)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"created_at_ms", "last_heartbeat_at_ms"}).
|
||||||
|
AddRow(int64(1783299600000), int64(1783303200000)).
|
||||||
|
AddRow(int64(1783301400000), int64(1783306800000)))
|
||||||
|
|
||||||
|
repo := New(db)
|
||||||
|
stats, err := repo.GetHostEngagementStats(appcode.WithContext(context.Background(), "fami"), hostdomain.EngagementStatsQuery{
|
||||||
|
HostUserID: 42,
|
||||||
|
StartAtMS: 1783296000000,
|
||||||
|
EndAtMS: 1783900800000,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHostEngagementStats failed: %v", err)
|
||||||
|
}
|
||||||
|
// 两个重叠 session 按当天最早开始到最晚心跳合并,不能相加成重复在线时长。
|
||||||
|
if stats.OnlineDurationMS != 7200000 || stats.ValidMicDurationMS != 5400000 || stats.ValidMicDays != 1 || stats.PrivateMessageSenders != 6 || stats.NewFollowers != 4 {
|
||||||
|
t.Fatalf("unexpected engagement stats: %+v", stats)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("unmet SQL expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -136,6 +136,15 @@ func (r *Repository) FollowUser(ctx context.Context, followerUserID int64, follo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if shouldIncrement {
|
if shouldIncrement {
|
||||||
|
// 新增粉丝按事件净增统计;状态未变化的重复关注不写事件,避免客户端重试重复 +1。
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO user_follow_events (
|
||||||
|
app_code, follower_user_id, followee_user_id, delta, occurred_at_ms
|
||||||
|
) VALUES (?, ?, ?, 1, ?)`,
|
||||||
|
appCode, followerUserID, followeeUserID, nowMs,
|
||||||
|
); err != nil {
|
||||||
|
return userdomain.ProfileStats{}, err
|
||||||
|
}
|
||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
UPDATE user_profile_stats
|
UPDATE user_profile_stats
|
||||||
SET following_count = following_count + 1, updated_at_ms = ?
|
SET following_count = following_count + 1, updated_at_ms = ?
|
||||||
@ -176,6 +185,15 @@ func (r *Repository) UnfollowUser(ctx context.Context, followerUserID int64, fol
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return userdomain.ProfileStats{}, err
|
return userdomain.ProfileStats{}, err
|
||||||
}
|
}
|
||||||
|
// 取消关注写 -1,确保筛选区间内的新增粉丝展示为净增而不是只累计关注动作。
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO user_follow_events (
|
||||||
|
app_code, follower_user_id, followee_user_id, delta, occurred_at_ms
|
||||||
|
) VALUES (?, ?, ?, -1, ?)`,
|
||||||
|
appCode, followerUserID, followeeUserID, nowMs,
|
||||||
|
); err != nil {
|
||||||
|
return userdomain.ProfileStats{}, err
|
||||||
|
}
|
||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
UPDATE user_profile_stats
|
UPDATE user_profile_stats
|
||||||
SET following_count = GREATEST(following_count - 1, 0), updated_at_ms = ?
|
SET following_count = GREATEST(following_count - 1, 0), updated_at_ms = ?
|
||||||
|
|||||||
50
services/user-service/internal/transport/grpc/host_stats.go
Normal file
50
services/user-service/internal/transport/grpc/host_stats.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
userv1 "hyapp.local/api/proto/user/v1"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHostEngagementStats 返回 user-service 拥有的主播互动统计;调用方不能提交聚合值。
|
||||||
|
func (s *Server) GetHostEngagementStats(ctx context.Context, req *userv1.GetHostEngagementStatsRequest) (*userv1.GetHostEngagementStatsResponse, error) {
|
||||||
|
if s.hostSvc == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||||
|
}
|
||||||
|
ctx = contextWithApp(ctx, req.GetMeta())
|
||||||
|
stats, err := s.hostSvc.GetHostEngagementStats(ctx, hostdomain.EngagementStatsQuery{
|
||||||
|
HostUserID: req.GetHostUserId(),
|
||||||
|
StartAtMS: req.GetStartAtMs(),
|
||||||
|
EndAtMS: req.GetEndAtMs(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &userv1.GetHostEngagementStatsResponse{Stats: &userv1.HostEngagementStats{
|
||||||
|
OnlineDurationMs: stats.OnlineDurationMS,
|
||||||
|
ValidMicDurationMs: stats.ValidMicDurationMS,
|
||||||
|
ValidMicDays: stats.ValidMicDays,
|
||||||
|
PrivateMessageSenders: stats.PrivateMessageSenders,
|
||||||
|
NewFollowers: stats.NewFollowers,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordPrivateMessageEvent 只接受 gateway 已鉴权回调产生的最小事件事实。
|
||||||
|
func (s *Server) RecordPrivateMessageEvent(ctx context.Context, req *userv1.RecordPrivateMessageEventRequest) (*userv1.RecordPrivateMessageEventResponse, error) {
|
||||||
|
if s.hostSvc == nil {
|
||||||
|
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||||
|
}
|
||||||
|
ctx = contextWithApp(ctx, req.GetMeta())
|
||||||
|
created, err := s.hostSvc.RecordPrivateMessageEvent(ctx, hostdomain.PrivateMessageEvent{
|
||||||
|
EventID: req.GetEventId(),
|
||||||
|
SenderUserID: req.GetSenderUserId(),
|
||||||
|
TargetUserID: req.GetTargetUserId(),
|
||||||
|
OccurredAtMS: req.GetOccurredAtMs(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &userv1.RecordPrivateMessageEventResponse{Created: created}, nil
|
||||||
|
}
|
||||||
@ -131,6 +131,7 @@ CREATE TABLE IF NOT EXISTS wallet_policy_instances (
|
|||||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
||||||
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
||||||
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
||||||
|
agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '有效付费礼物给 Agency owner 的 POINT 分成比例,ppm',
|
||||||
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
||||||
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
||||||
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
||||||
@ -142,6 +143,41 @@ CREATE TABLE IF NOT EXISTS wallet_policy_instances (
|
|||||||
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表';
|
||||||
|
|
||||||
|
-- 三套 App 共享同一张配置表,但 app_code 是主键的一部分;新 App 只需配置数据,不再复制表和业务代码。
|
||||||
|
-- 未配置任何 active 行时 H5 币商提现入口显示为空,不能用伪造默认币商兜底。
|
||||||
|
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||||
|
seller_user_id BIGINT NOT NULL COMMENT '币商用户 ID',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越小越靠前',
|
||||||
|
point_amount BIGINT NOT NULL COMMENT '兑换比例中的 POINT 分母',
|
||||||
|
seller_coin_amount BIGINT NOT NULL COMMENT '兑换比例中的币商金币分子',
|
||||||
|
service_country_codes JSON NOT NULL COMMENT '服务国家码数组;空数组表示不限国家',
|
||||||
|
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||||
|
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (app_code, seller_user_id),
|
||||||
|
KEY idx_point_withdraw_seller_list (app_code, status, sort_order, seller_user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT 提现币商白名单与兑换比例';
|
||||||
|
|
||||||
|
-- Agency 分成独立流水同时承担归属快照和审计依据;不能按当前成员关系反推历史收益。
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_point_share_entries (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||||
|
transaction_id VARCHAR(96) NOT NULL COMMENT '礼物钱包交易 ID',
|
||||||
|
host_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID 快照',
|
||||||
|
agency_owner_user_id BIGINT NOT NULL COMMENT '送礼时 Agency owner 用户 ID 快照',
|
||||||
|
sender_user_id BIGINT NOT NULL COMMENT '送礼用户 ID',
|
||||||
|
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID,私聊送礼为空',
|
||||||
|
point_delta BIGINT NOT NULL COMMENT 'Agency owner 本次 POINT 分成',
|
||||||
|
ratio_ppm BIGINT NOT NULL COMMENT 'Agency 分成比例快照',
|
||||||
|
policy_instance_code VARCHAR(96) NOT NULL DEFAULT '' COMMENT '收益策略实例快照',
|
||||||
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
PRIMARY KEY (app_code, transaction_id, agency_owner_user_id),
|
||||||
|
KEY idx_agency_point_share_owner_time (app_code, agency_owner_user_id, created_at_ms),
|
||||||
|
KEY idx_agency_point_share_host_time (app_code, host_user_id, created_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Agency POINT 礼物分成流水';
|
||||||
|
|
||||||
-- 主播工资周期钻石账户:只记录政策结算用累计值,不作为用户可消费或可提现的钱包资产。
|
-- 主播工资周期钻石账户:只记录政策结算用累计值,不作为用户可消费或可提现的钱包资产。
|
||||||
-- cycle_key 当前固定为 UTC yyyy-MM;后续日结/半月结不清空该账户,月末清算任务按策略处理。
|
-- cycle_key 当前固定为 UTC yyyy-MM;后续日结/半月结不清空该账户,月末清算任务按策略处理。
|
||||||
CREATE TABLE IF NOT EXISTS host_period_diamond_accounts (
|
CREATE TABLE IF NOT EXISTS host_period_diamond_accounts (
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
package ledger
|
||||||
|
|
||||||
|
type AgencyHostGiftStats struct {
|
||||||
|
GiftIncome int64
|
||||||
|
GiftedHostCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgencyHostGiftStatsQuery struct {
|
||||||
|
HostUserIDs []int64
|
||||||
|
StartAtMS int64
|
||||||
|
EndAtMS int64
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package ledger
|
||||||
|
|
||||||
|
// AgencyPointShareStats 聚合送礼时已固化归属的 Agency POINT 分成。
|
||||||
|
type AgencyPointShareStats struct {
|
||||||
|
ShareIncome int64
|
||||||
|
GiftedHostCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgencyPointShareStatsQuery struct {
|
||||||
|
AgencyOwnerUserID int64
|
||||||
|
StartAtMS int64
|
||||||
|
EndAtMS int64
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package ledger
|
||||||
|
|
||||||
|
// HostRevenueStats 是主播中心按日期展示的 POINT 收益、已兑换数量和去重送礼人数。
|
||||||
|
type HostRevenueStats struct {
|
||||||
|
DiamondEarnings int64
|
||||||
|
DiamondExchanged int64
|
||||||
|
GiftSenders int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostRevenueStatsQuery 使用左闭右开 UTC 毫秒区间;调用方必须限制区间长度。
|
||||||
|
type HostRevenueStatsQuery struct {
|
||||||
|
HostUserID int64
|
||||||
|
StartAtMS int64
|
||||||
|
EndAtMS int64
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package ledger
|
||||||
|
|
||||||
|
// PointWithdrawalCoinSellerConfig 是 POINT 提现可选币商及其服务范围、兑换比例的运行快照。
|
||||||
|
type PointWithdrawalCoinSellerConfig struct {
|
||||||
|
AppCode string
|
||||||
|
SellerUserID int64
|
||||||
|
SortOrder int
|
||||||
|
PointAmount int64
|
||||||
|
SellerCoinAmount int64
|
||||||
|
ServiceCountryCodes []string
|
||||||
|
Status string
|
||||||
|
CreatedAtMS int64
|
||||||
|
UpdatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointToCoinSellerCommand 只接受用户、目标和金额;比例必须由 wallet 数据库的 active 白名单解析。
|
||||||
|
type PointToCoinSellerCommand struct {
|
||||||
|
AppCode string
|
||||||
|
CommandID string
|
||||||
|
SourceUserID int64
|
||||||
|
SellerUserID int64
|
||||||
|
PointAmount int64
|
||||||
|
SourceCountryCode string
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointToCoinSellerReceipt 是 POINT 扣款和币商库存入账的双边稳定回执。
|
||||||
|
type PointToCoinSellerReceipt struct {
|
||||||
|
TransactionID string
|
||||||
|
SourceUserID int64
|
||||||
|
SellerUserID int64
|
||||||
|
SourcePointBalanceAfter int64
|
||||||
|
SellerBalanceAfter int64
|
||||||
|
PointAmount int64
|
||||||
|
SellerCoinAmount int64
|
||||||
|
RatioPointAmount int64
|
||||||
|
RatioSellerCoinAmount int64
|
||||||
|
CreatedAtMS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointWithdrawalRuntimeConfig 是已发布政策中钱包页需要展示并用于冻结的参数。
|
||||||
|
type PointWithdrawalRuntimeConfig struct {
|
||||||
|
Found bool
|
||||||
|
PointsPerUSD int64
|
||||||
|
FeeBPS int64
|
||||||
|
MinimumPoints int64
|
||||||
|
PolicyInstanceCode string
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxAgencyHostGiftStatsRange = 366 * 24 * time.Hour
|
||||||
|
|
||||||
|
func (s *Service) GetAgencyHostGiftStats(ctx context.Context, appCode string, query ledger.AgencyHostGiftStatsQuery) (ledger.AgencyHostGiftStats, error) {
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
if len(query.HostUserIDs) > 1000 || query.StartAtMS <= 0 || query.EndAtMS <= query.StartAtMS {
|
||||||
|
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.InvalidArgument, "agency host gift stats range is invalid")
|
||||||
|
}
|
||||||
|
for _, userID := range query.HostUserIDs {
|
||||||
|
if userID <= 0 {
|
||||||
|
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.InvalidArgument, "host_user_ids is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if query.EndAtMS-query.StartAtMS > int64(maxAgencyHostGiftStatsRange/time.Millisecond) {
|
||||||
|
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.InvalidArgument, "agency host gift stats range is too large")
|
||||||
|
}
|
||||||
|
ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
|
||||||
|
return s.repository.GetAgencyHostGiftStats(ctx, query)
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxAgencyPointShareStatsRange = 366 * 24 * time.Hour
|
||||||
|
|
||||||
|
func (s *Service) GetAgencyPointShareStats(ctx context.Context, appCode string, query ledger.AgencyPointShareStatsQuery) (ledger.AgencyPointShareStats, error) {
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.AgencyPointShareStats{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
if query.AgencyOwnerUserID <= 0 || query.StartAtMS <= 0 || query.EndAtMS <= query.StartAtMS {
|
||||||
|
return ledger.AgencyPointShareStats{}, xerr.New(xerr.InvalidArgument, "agency point share stats range is invalid")
|
||||||
|
}
|
||||||
|
if query.EndAtMS-query.StartAtMS > int64(maxAgencyPointShareStatsRange/time.Millisecond) {
|
||||||
|
return ledger.AgencyPointShareStats{}, xerr.New(xerr.InvalidArgument, "agency point share stats range is too large")
|
||||||
|
}
|
||||||
|
ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
|
||||||
|
return s.repository.GetAgencyPointShareStats(ctx, query)
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxHostRevenueStatsRange = 366 * 24 * time.Hour
|
||||||
|
|
||||||
|
// GetHostRevenueStats 聚合钱包 owner 内的 POINT 收益/兑换和去重送礼人,不让 gateway 扫账本。
|
||||||
|
func (s *Service) GetHostRevenueStats(ctx context.Context, appCode string, query ledger.HostRevenueStatsQuery) (ledger.HostRevenueStats, error) {
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.HostRevenueStats{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
if query.HostUserID <= 0 || query.StartAtMS <= 0 || query.EndAtMS <= query.StartAtMS {
|
||||||
|
return ledger.HostRevenueStats{}, xerr.New(xerr.InvalidArgument, "host revenue stats range is invalid")
|
||||||
|
}
|
||||||
|
if query.EndAtMS-query.StartAtMS > int64(maxHostRevenueStatsRange/time.Millisecond) {
|
||||||
|
return ledger.HostRevenueStats{}, xerr.New(xerr.InvalidArgument, "host revenue stats range is too large")
|
||||||
|
}
|
||||||
|
ctx = appcode.WithContext(ctx, appcode.Normalize(appCode))
|
||||||
|
return s.repository.GetHostRevenueStats(ctx, query)
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package wallet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListPointWithdrawalCoinSellers 返回服务端过滤后的币商白名单;国家由可信用户资料提供而不是客户端表单。
|
||||||
|
func (s *Service) ListPointWithdrawalCoinSellers(ctx context.Context, appCode string, countryCode string, includeDisabled bool) ([]ledger.PointWithdrawalCoinSellerConfig, error) {
|
||||||
|
if strings.TrimSpace(appCode) == "" {
|
||||||
|
return nil, xerr.New(xerr.InvalidArgument, "app_code is required")
|
||||||
|
}
|
||||||
|
if s.repository == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
appCode = appcode.Normalize(appCode)
|
||||||
|
return s.repository.ListPointWithdrawalCoinSellers(appcode.WithContext(ctx, appCode), appCode, strings.ToUpper(strings.TrimSpace(countryCode)), includeDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferPointToCoinSeller 验证客户端允许提交的最小字段;兑换比例由 repository 事务内锁定配置解析。
|
||||||
|
func (s *Service) TransferPointToCoinSeller(ctx context.Context, command ledger.PointToCoinSellerCommand) (ledger.PointToCoinSellerReceipt, error) {
|
||||||
|
if strings.TrimSpace(command.AppCode) == "" || strings.TrimSpace(command.CommandID) == "" || command.SourceUserID <= 0 || command.SellerUserID <= 0 || command.PointAmount <= 0 {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "point coin seller transfer command is incomplete")
|
||||||
|
}
|
||||||
|
command.AppCode = appcode.Normalize(command.AppCode)
|
||||||
|
command.CommandID = strings.TrimSpace(command.CommandID)
|
||||||
|
command.SourceCountryCode = strings.ToUpper(strings.TrimSpace(command.SourceCountryCode))
|
||||||
|
command.Reason = strings.TrimSpace(command.Reason)
|
||||||
|
if len(command.CommandID) > 128 || len(command.SourceCountryCode) > 16 || len(command.Reason) > 512 {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "point coin seller transfer text fields are too long")
|
||||||
|
}
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
return s.repository.TransferPointToCoinSeller(appcode.WithContext(ctx, command.AppCode), command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointWithdrawalConfig 读取已发布政策快照;nowMS 由服务端补齐,避免客户端选择历史政策。
|
||||||
|
func (s *Service) GetPointWithdrawalConfig(ctx context.Context, appCode string, regionID int64, nowMS int64) (ledger.PointWithdrawalRuntimeConfig, error) {
|
||||||
|
if strings.TrimSpace(appCode) == "" || regionID < 0 {
|
||||||
|
return ledger.PointWithdrawalRuntimeConfig{}, xerr.New(xerr.InvalidArgument, "point withdrawal config query is invalid")
|
||||||
|
}
|
||||||
|
if s.repository == nil {
|
||||||
|
return ledger.PointWithdrawalRuntimeConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
if nowMS <= 0 {
|
||||||
|
nowMS = time.Now().UnixMilli()
|
||||||
|
}
|
||||||
|
appCode = appcode.Normalize(appCode)
|
||||||
|
return s.repository.GetPointWithdrawalConfig(appcode.WithContext(ctx, appCode), appCode, regionID, nowMS)
|
||||||
|
}
|
||||||
@ -10,12 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultPointWithdrawalFeeBPS int64 = 500
|
|
||||||
defaultPointWithdrawalPointsPerUSD int64 = 100000
|
defaultPointWithdrawalPointsPerUSD int64 = 100000
|
||||||
minPointWithdrawalGrossPoints int64 = 1000000
|
minPointWithdrawalGrossPoints int64 = 1000000
|
||||||
)
|
)
|
||||||
|
|
||||||
// FreezePointWithdrawal 把 Huwaa 收益积分从 available 冻结到 frozen;它复用钱包单边冻结账本,但不开放旧工资资产。
|
// FreezePointWithdrawal 把已启用 App 的收益积分从 available 冻结到 frozen;gateway 必须先读取已发布政策快照。
|
||||||
func (s *Service) FreezePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
|
func (s *Service) FreezePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
|
||||||
if err := normalizePointWithdrawalCommand(&command, false); err != nil {
|
if err := normalizePointWithdrawalCommand(&command, false); err != nil {
|
||||||
return ledger.PointWithdrawalReceipt{}, err
|
return ledger.PointWithdrawalReceipt{}, err
|
||||||
@ -83,9 +82,13 @@ func normalizePointWithdrawalCommand(command *ledger.PointWithdrawalCommand, req
|
|||||||
return xerr.New(xerr.InvalidArgument, "operator_user_id is required")
|
return xerr.New(xerr.InvalidArgument, "operator_user_id is required")
|
||||||
}
|
}
|
||||||
command.CommandID = strings.TrimSpace(command.CommandID)
|
command.CommandID = strings.TrimSpace(command.CommandID)
|
||||||
|
if strings.TrimSpace(command.AppCode) == "" {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "app_code is required")
|
||||||
|
}
|
||||||
command.AppCode = appcode.Normalize(command.AppCode)
|
command.AppCode = appcode.Normalize(command.AppCode)
|
||||||
if command.AppCode != "huwaa" {
|
if command.AppCode != "huwaa" && command.AppCode != "fami" {
|
||||||
return xerr.New(xerr.InvalidArgument, "point withdrawal is only enabled for huwaa")
|
// Lalu 继续使用旧公会工资提现链路;只有显式迁移到 POINT 钱包的 App 才能调用本接口。
|
||||||
|
return xerr.New(xerr.InvalidArgument, "point withdrawal is not enabled for app")
|
||||||
}
|
}
|
||||||
command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType))
|
command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType))
|
||||||
if !ledger.ValidPointWithdrawalAssetType(command.AssetType) {
|
if !ledger.ValidPointWithdrawalAssetType(command.AssetType) {
|
||||||
@ -97,9 +100,6 @@ func normalizePointWithdrawalCommand(command *ledger.PointWithdrawalCommand, req
|
|||||||
if command.PointsPerUSD <= 0 {
|
if command.PointsPerUSD <= 0 {
|
||||||
command.PointsPerUSD = defaultPointWithdrawalPointsPerUSD
|
command.PointsPerUSD = defaultPointWithdrawalPointsPerUSD
|
||||||
}
|
}
|
||||||
if command.FeeBPS <= 0 {
|
|
||||||
command.FeeBPS = defaultPointWithdrawalFeeBPS
|
|
||||||
}
|
|
||||||
if command.FeeBPS < 0 || command.FeeBPS > 10000 {
|
if command.FeeBPS < 0 || command.FeeBPS > 10000 {
|
||||||
return xerr.New(xerr.InvalidArgument, "point withdrawal fee_bps is invalid")
|
return xerr.New(xerr.InvalidArgument, "point withdrawal fee_bps is invalid")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,9 @@ type HostSalaryStore interface {
|
|||||||
GetActiveHostSalaryPolicy(ctx context.Context, regionID int64, settlementMode string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error)
|
GetActiveHostSalaryPolicy(ctx context.Context, regionID int64, settlementMode string, triggerMode string, nowMs int64) (ledger.HostSalaryPolicy, bool, error)
|
||||||
GetHostSalaryProgress(ctx context.Context, userID int64, cycleKey string) (ledger.HostSalaryProgress, error)
|
GetHostSalaryProgress(ctx context.Context, userID int64, cycleKey string) (ledger.HostSalaryProgress, error)
|
||||||
GetTeamHostSalaryStats(ctx context.Context, query ledger.TeamHostSalaryStatsQuery) ([]ledger.TeamHostSalaryCycleStat, error)
|
GetTeamHostSalaryStats(ctx context.Context, query ledger.TeamHostSalaryStatsQuery) ([]ledger.TeamHostSalaryCycleStat, error)
|
||||||
|
GetHostRevenueStats(ctx context.Context, query ledger.HostRevenueStatsQuery) (ledger.HostRevenueStats, error)
|
||||||
|
GetAgencyPointShareStats(ctx context.Context, query ledger.AgencyPointShareStatsQuery) (ledger.AgencyPointShareStats, error)
|
||||||
|
GetAgencyHostGiftStats(ctx context.Context, query ledger.AgencyHostGiftStatsQuery) (ledger.AgencyHostGiftStats, error)
|
||||||
ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error)
|
ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +69,9 @@ type CoinSellerStore interface {
|
|||||||
ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, appCode string, regionID int64, includeDisabled bool) ([]ledger.CoinSellerSalaryExchangeRateTier, error)
|
ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, appCode string, regionID int64, includeDisabled bool) ([]ledger.CoinSellerSalaryExchangeRateTier, error)
|
||||||
ExchangeSalaryToCoin(ctx context.Context, command ledger.SalaryExchangeCommand) (ledger.SalaryExchangeReceipt, error)
|
ExchangeSalaryToCoin(ctx context.Context, command ledger.SalaryExchangeCommand) (ledger.SalaryExchangeReceipt, error)
|
||||||
TransferSalaryToCoinSeller(ctx context.Context, command ledger.SalaryTransferToCoinSellerCommand) (ledger.SalaryTransferToCoinSellerReceipt, error)
|
TransferSalaryToCoinSeller(ctx context.Context, command ledger.SalaryTransferToCoinSellerCommand) (ledger.SalaryTransferToCoinSellerReceipt, error)
|
||||||
|
ListPointWithdrawalCoinSellers(ctx context.Context, appCode string, countryCode string, includeDisabled bool) ([]ledger.PointWithdrawalCoinSellerConfig, error)
|
||||||
|
TransferPointToCoinSeller(ctx context.Context, command ledger.PointToCoinSellerCommand) (ledger.PointToCoinSellerReceipt, error)
|
||||||
|
GetPointWithdrawalConfig(ctx context.Context, appCode string, regionID int64, nowMS int64) (ledger.PointWithdrawalRuntimeConfig, error)
|
||||||
FreezeSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error)
|
FreezeSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error)
|
||||||
SettleSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error)
|
SettleSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error)
|
||||||
ReleaseSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error)
|
ReleaseSalaryWithdrawal(ctx context.Context, command ledger.SalaryWithdrawalCommand) (ledger.SalaryWithdrawalReceipt, error)
|
||||||
|
|||||||
@ -168,6 +168,44 @@ func TestHuwaaHostGiftPolicyCreditsPointWithoutCoinIncomeAndAllowsSelfGift(t *te
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFamiHostGiftPolicyCreditsAgencyShareWithHistoricalOwnershipSnapshot(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
repository.SetBalanceForApp("fami", 32001, 1000)
|
||||||
|
repository.SetGiftPriceForApp("fami", "fami-rose", "v1", 100, 0, 100)
|
||||||
|
repository.SeedWalletPolicyInstanceWithAgency("fami", "fami-guild-live", 0, "active", 700000, 200000)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
|
||||||
|
receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
||||||
|
AppCode: "fami", CommandID: "cmd-fami-agency-share", RoomID: "room-fami",
|
||||||
|
SenderUserID: 32001, TargetUserID: 32002, TargetIsHost: true, TargetHostRegionID: 12,
|
||||||
|
TargetAgencyOwnerUserID: 32003, GiftID: "fami-rose", GiftCount: 1, PriceVersion: "v1", RegionID: 12, SenderRegionID: 12,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DebitGift with Fami agency policy failed: %v", err)
|
||||||
|
}
|
||||||
|
if receipt.HostPointAdded != 70 || receipt.HostPointBalanceAfter != 70 {
|
||||||
|
t.Fatalf("Fami host point receipt mismatch: %+v", receipt)
|
||||||
|
}
|
||||||
|
assertBalanceForApp(t, svc, "fami", 32002, ledger.AssetPoint, 70)
|
||||||
|
assertBalanceForApp(t, svc, "fami", 32003, ledger.AssetPoint, 20)
|
||||||
|
if got := repository.CountRows("agency_point_share_entries", "transaction_id = ? AND host_user_id = ? AND agency_owner_user_id = ? AND point_delta = ? AND ratio_ppm = ?", receipt.TransactionID, int64(32002), int64(32003), int64(20), int64(200000)); got != 1 {
|
||||||
|
t.Fatalf("Fami agency share snapshot mismatch, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ?", receipt.TransactionID, "AgencyPointCredited"); got != 1 {
|
||||||
|
t.Fatalf("Fami agency share must publish one event, got %d", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
replayed, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
|
||||||
|
AppCode: "fami", CommandID: "cmd-fami-agency-share", RoomID: "room-fami",
|
||||||
|
SenderUserID: 32001, TargetUserID: 32002, TargetIsHost: true, TargetHostRegionID: 12,
|
||||||
|
TargetAgencyOwnerUserID: 32003, GiftID: "fami-rose", GiftCount: 1, PriceVersion: "v1", RegionID: 12, SenderRegionID: 12,
|
||||||
|
})
|
||||||
|
if err != nil || replayed.TransactionID != receipt.TransactionID {
|
||||||
|
t.Fatalf("Fami agency share replay mismatch: receipt=%+v err=%v", replayed, err)
|
||||||
|
}
|
||||||
|
assertBalanceForApp(t, svc, "fami", 32003, ledger.AssetPoint, 20)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHostGiftWithoutActiveHuwaaPolicyKeepsLegacyCoinIncome(t *testing.T) {
|
func TestHostGiftWithoutActiveHuwaaPolicyKeepsLegacyCoinIncome(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
repository.SetBalance(31011, 1000)
|
repository.SetBalance(31011, 1000)
|
||||||
@ -2147,6 +2185,7 @@ func TestPointWithdrawalAppGateWithoutMySQL(t *testing.T) {
|
|||||||
UserID: 23012,
|
UserID: 23012,
|
||||||
AssetType: ledger.AssetPoint,
|
AssetType: ledger.AssetPoint,
|
||||||
GrossPointAmount: 1_000_000,
|
GrossPointAmount: 1_000_000,
|
||||||
|
FeeBPS: 500,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Huwaa POINT withdrawal should reach repository: %v", err)
|
t.Fatalf("Huwaa POINT withdrawal should reach repository: %v", err)
|
||||||
@ -2163,6 +2202,21 @@ func TestPointWithdrawalAppGateWithoutMySQL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFamiPointWithdrawalAllowsAdminConfiguredZeroFee(t *testing.T) {
|
||||||
|
repository := &fakePointGateRepository{}
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
receipt, err := svc.FreezePointWithdrawal(context.Background(), ledger.PointWithdrawalCommand{
|
||||||
|
AppCode: "fami", CommandID: "cmd-fami-zero-fee", UserID: 23013,
|
||||||
|
AssetType: ledger.AssetPoint, GrossPointAmount: 1_000_000, PointsPerUSD: 200_000, FeeBPS: 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("zero-fee Fami point withdrawal failed: %v", err)
|
||||||
|
}
|
||||||
|
if receipt.FeePointAmount != 0 || receipt.NetPointAmount != 1_000_000 || receipt.PointsPerUSD != 200_000 || receipt.FeeBPS != 0 {
|
||||||
|
t.Fatalf("zero-fee policy snapshot mismatch: %+v", receipt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
repository.SetAssetBalanceForApp("huwaa", 22001, ledger.AssetPoint, 2_000_000)
|
repository.SetAssetBalanceForApp("huwaa", 22001, ledger.AssetPoint, 2_000_000)
|
||||||
@ -2187,6 +2241,7 @@ func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
|||||||
UserID: 22001,
|
UserID: 22001,
|
||||||
AssetType: ledger.AssetPoint,
|
AssetType: ledger.AssetPoint,
|
||||||
GrossPointAmount: 1_000_000,
|
GrossPointAmount: 1_000_000,
|
||||||
|
FeeBPS: 500,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("FreezePointWithdrawal failed: %v", err)
|
t.Fatalf("FreezePointWithdrawal failed: %v", err)
|
||||||
@ -2202,6 +2257,7 @@ func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
|||||||
UserID: 22001,
|
UserID: 22001,
|
||||||
AssetType: ledger.AssetPoint,
|
AssetType: ledger.AssetPoint,
|
||||||
GrossPointAmount: 1_000_000,
|
GrossPointAmount: 1_000_000,
|
||||||
|
FeeBPS: 500,
|
||||||
OperatorUserID: 90001,
|
OperatorUserID: 90001,
|
||||||
WithdrawalApplicationID: "77",
|
WithdrawalApplicationID: "77",
|
||||||
})
|
})
|
||||||
@ -2219,6 +2275,7 @@ func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
|||||||
UserID: 22002,
|
UserID: 22002,
|
||||||
AssetType: ledger.AssetPoint,
|
AssetType: ledger.AssetPoint,
|
||||||
GrossPointAmount: 1_000_000,
|
GrossPointAmount: 1_000_000,
|
||||||
|
FeeBPS: 500,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("FreezePointWithdrawal for release failed: %v", err)
|
t.Fatalf("FreezePointWithdrawal for release failed: %v", err)
|
||||||
}
|
}
|
||||||
@ -2228,6 +2285,7 @@ func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
|||||||
UserID: 22002,
|
UserID: 22002,
|
||||||
AssetType: ledger.AssetPoint,
|
AssetType: ledger.AssetPoint,
|
||||||
GrossPointAmount: 1_000_000,
|
GrossPointAmount: 1_000_000,
|
||||||
|
FeeBPS: 500,
|
||||||
WithdrawalApplicationID: "78",
|
WithdrawalApplicationID: "78",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2239,6 +2297,52 @@ func TestPointWithdrawalFreezeSettleReleaseIsHuwaaOnly(t *testing.T) {
|
|||||||
requireWalletBalanceForApp(t, svc, "huwaa", 22002, ledger.AssetPoint, 2_000_000, 0)
|
requireWalletBalanceForApp(t, svc, "huwaa", 22002, ledger.AssetPoint, 2_000_000, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestTransferPointToCoinSellerUsesServerWhitelist 验证 H5 不能自报比例,wallet 会按 App、国家和 active 配置原子扣 POINT/加币商库存。
|
||||||
|
func TestTransferPointToCoinSellerUsesServerWhitelist(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
repository.SetAssetBalanceForApp("fami", 22501, ledger.AssetPoint, 2_000_000)
|
||||||
|
repository.SetPointWithdrawalCoinSellerConfig("fami", 22502, 100_000, 92_000, []string{"SA"}, "active", 1)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
|
||||||
|
sellers, err := svc.ListPointWithdrawalCoinSellers(context.Background(), "fami", "SA", false)
|
||||||
|
if err != nil || len(sellers) != 1 || sellers[0].SellerUserID != 22502 {
|
||||||
|
t.Fatalf("eligible point withdrawal sellers mismatch: sellers=%+v err=%v", sellers, err)
|
||||||
|
}
|
||||||
|
if blocked, err := svc.ListPointWithdrawalCoinSellers(context.Background(), "fami", "TR", false); err != nil || len(blocked) != 0 {
|
||||||
|
t.Fatalf("country-filtered seller must be hidden: sellers=%+v err=%v", blocked, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
command := ledger.PointToCoinSellerCommand{
|
||||||
|
AppCode: "fami", CommandID: "cmd-fami-point-seller-1", SourceUserID: 22501,
|
||||||
|
SellerUserID: 22502, PointAmount: 1_000_000, SourceCountryCode: "SA", Reason: "withdraw to seller",
|
||||||
|
}
|
||||||
|
receipt, err := svc.TransferPointToCoinSeller(context.Background(), command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TransferPointToCoinSeller failed: %v", err)
|
||||||
|
}
|
||||||
|
if receipt.SellerCoinAmount != 920_000 || receipt.RatioPointAmount != 100_000 || receipt.RatioSellerCoinAmount != 92_000 || receipt.SourcePointBalanceAfter != 1_000_000 || receipt.SellerBalanceAfter != 920_000 {
|
||||||
|
t.Fatalf("point seller transfer receipt mismatch: %+v", receipt)
|
||||||
|
}
|
||||||
|
replayed, err := svc.TransferPointToCoinSeller(context.Background(), command)
|
||||||
|
if err != nil || replayed.TransactionID != receipt.TransactionID || replayed.SellerCoinAmount != receipt.SellerCoinAmount {
|
||||||
|
t.Fatalf("point seller transfer replay mismatch: replay=%+v err=%v", replayed, err)
|
||||||
|
}
|
||||||
|
requireWalletBalanceForApp(t, svc, "fami", 22501, ledger.AssetPoint, 1_000_000, 0)
|
||||||
|
requireWalletBalanceForApp(t, svc, "fami", 22502, ledger.AssetCoinSellerCoin, 920_000, 0)
|
||||||
|
if got := repository.CountRows("wallet_transactions", "app_code = ? AND biz_type = ?", "fami", "point_transfer_to_coin_seller"); got != 1 {
|
||||||
|
t.Fatalf("idempotent point seller transfer must write one transaction, got %d", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = svc.TransferPointToCoinSeller(context.Background(), ledger.PointToCoinSellerCommand{
|
||||||
|
AppCode: "fami", CommandID: "cmd-fami-point-seller-country-denied", SourceUserID: 22501,
|
||||||
|
SellerUserID: 22502, PointAmount: 100_000, SourceCountryCode: "TR",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("seller country restriction must reject transfer")
|
||||||
|
}
|
||||||
|
requireWalletBalanceForApp(t, svc, "fami", 22501, ledger.AssetPoint, 1_000_000, 0)
|
||||||
|
}
|
||||||
|
|
||||||
// TestCreditLuckyGiftRewardIsIdempotent 验证幸运礼物返奖只按 draw_id 派一次,并产出余额通知事实。
|
// TestCreditLuckyGiftRewardIsIdempotent 验证幸运礼物返奖只按 draw_id 派一次,并产出余额通知事实。
|
||||||
func TestCreditLuckyGiftRewardIsIdempotent(t *testing.T) {
|
func TestCreditLuckyGiftRewardIsIdempotent(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAgencyHostGiftStats 直接读取送礼交易里的 host_point_added 快照,只统计礼物收益;
|
||||||
|
// 任务奖励、提现释放和 Agency 分成虽然也进入 POINT 账户,但都不能混入 BD 的公会收礼口径。
|
||||||
|
func (r *Repository) GetAgencyHostGiftStats(ctx context.Context, query ledger.AgencyHostGiftStatsQuery) (ledger.AgencyHostGiftStats, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.AgencyHostGiftStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
if len(query.HostUserIDs) == 0 {
|
||||||
|
return ledger.AgencyHostGiftStats{}, nil
|
||||||
|
}
|
||||||
|
placeholders := make([]string, len(query.HostUserIDs))
|
||||||
|
args := make([]any, 0, len(query.HostUserIDs)+5)
|
||||||
|
args = append(args, appcode.FromContext(ctx), bizTypeGiftDebit, bizTypeDirectGiftDebit, query.StartAtMS, query.EndAtMS)
|
||||||
|
for index, userID := range query.HostUserIDs {
|
||||||
|
placeholders[index] = "?"
|
||||||
|
args = append(args, userID)
|
||||||
|
}
|
||||||
|
querySQL := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_point_added')), '0') AS SIGNED)), 0),
|
||||||
|
COUNT(DISTINCT CASE
|
||||||
|
WHEN CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.host_point_added')), '0') AS SIGNED) > 0
|
||||||
|
THEN CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED)
|
||||||
|
END)
|
||||||
|
FROM wallet_transactions
|
||||||
|
WHERE app_code = ? AND biz_type IN (?, ?) AND status = 'succeeded'
|
||||||
|
AND created_at_ms >= ? AND created_at_ms < ?
|
||||||
|
AND CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.target_user_id')) AS SIGNED) IN (%s)`, strings.Join(placeholders, ","))
|
||||||
|
var stats ledger.AgencyHostGiftStats
|
||||||
|
err := r.db.QueryRowContext(ctx, querySQL, args...).Scan(&stats.GiftIncome, &stats.GiftedHostCount)
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// insertAgencyPointShare 把“当时属于哪个 Agency”与实际入账金额固化在同一送礼事务内;
|
||||||
|
// 后续主播转会时统计仍按这条快照归属,不能用当前成员关系重算历史收入。
|
||||||
|
func (r *Repository) insertAgencyPointShare(ctx context.Context, tx *sql.Tx, transactionID string, metadata giftMetadata, nowMS int64) error {
|
||||||
|
if metadata.AgencyPointAdded <= 0 || metadata.TargetAgencyOwnerUserID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO agency_point_share_entries (
|
||||||
|
app_code, transaction_id, host_user_id, agency_owner_user_id, sender_user_id,
|
||||||
|
room_id, point_delta, ratio_ppm, policy_instance_code, created_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
appcode.FromContext(ctx), transactionID, metadata.TargetUserID, metadata.TargetAgencyOwnerUserID,
|
||||||
|
metadata.SenderUserID, metadata.RoomID, metadata.AgencyPointAdded, metadata.AgencyPointRatioPPM,
|
||||||
|
metadata.HostPointPolicyInstanceCode, nowMS,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAgencyPointShareStats 按入账时归属快照统计,不受主播之后转会或被移除影响。
|
||||||
|
func (r *Repository) GetAgencyPointShareStats(ctx context.Context, query ledger.AgencyPointShareStatsQuery) (ledger.AgencyPointShareStats, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.AgencyPointShareStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
var stats ledger.AgencyPointShareStats
|
||||||
|
err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COALESCE(SUM(point_delta), 0), COUNT(DISTINCT host_user_id)
|
||||||
|
FROM agency_point_share_entries
|
||||||
|
WHERE app_code = ? AND agency_owner_user_id = ?
|
||||||
|
AND created_at_ms >= ? AND created_at_ms < ?`,
|
||||||
|
appcode.FromContext(ctx), query.AgencyOwnerUserID, query.StartAtMS, query.EndAtMS,
|
||||||
|
).Scan(&stats.ShareIncome, &stats.GiftedHostCount)
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetAgencyPointShareStatsUsesOwnershipSnapshot(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||||
|
SELECT COALESCE(SUM(point_delta), 0), COUNT(DISTINCT host_user_id)
|
||||||
|
FROM agency_point_share_entries
|
||||||
|
WHERE app_code = ? AND agency_owner_user_id = ?
|
||||||
|
AND created_at_ms >= ? AND created_at_ms < ?`)).
|
||||||
|
WithArgs("fami", int64(73), int64(1000), int64(2000)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"share_income", "gifted_hosts"}).AddRow(int64(2400), int64(3)))
|
||||||
|
|
||||||
|
repo := &Repository{db: db}
|
||||||
|
stats, err := repo.GetAgencyPointShareStats(appcode.WithContext(context.Background(), "fami"), ledger.AgencyPointShareStatsQuery{
|
||||||
|
AgencyOwnerUserID: 73, StartAtMS: 1000, EndAtMS: 2000,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetAgencyPointShareStats failed: %v", err)
|
||||||
|
}
|
||||||
|
if stats.ShareIncome != 2400 || stats.GiftedHostCount != 3 {
|
||||||
|
t.Fatalf("unexpected agency stats: %+v", stats)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("unmet SQL expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ const (
|
|||||||
bizTypeCoinSellerCoinCompensation = "coin_seller_coin_compensation"
|
bizTypeCoinSellerCoinCompensation = "coin_seller_coin_compensation"
|
||||||
bizTypeSalaryExchangeToCoin = "salary_exchange_to_coin"
|
bizTypeSalaryExchangeToCoin = "salary_exchange_to_coin"
|
||||||
bizTypeSalaryTransferToCoinSeller = "salary_transfer_to_coin_seller"
|
bizTypeSalaryTransferToCoinSeller = "salary_transfer_to_coin_seller"
|
||||||
|
bizTypePointTransferToCoinSeller = "point_transfer_to_coin_seller"
|
||||||
bizTypeSalaryWithdrawalFreeze = "salary_withdrawal_freeze"
|
bizTypeSalaryWithdrawalFreeze = "salary_withdrawal_freeze"
|
||||||
bizTypeSalaryWithdrawalSettle = "salary_withdrawal_settle"
|
bizTypeSalaryWithdrawalSettle = "salary_withdrawal_settle"
|
||||||
bizTypeSalaryWithdrawalRelease = "salary_withdrawal_release"
|
bizTypeSalaryWithdrawalRelease = "salary_withdrawal_release"
|
||||||
|
|||||||
@ -112,10 +112,12 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
}
|
}
|
||||||
|
|
||||||
type targetHostPointPolicy struct {
|
type targetHostPointPolicy struct {
|
||||||
policy walletPolicyInstance
|
policy walletPolicyInstance
|
||||||
amount int64
|
amount int64
|
||||||
userID int64
|
userID int64
|
||||||
active bool
|
agencyAmount int64
|
||||||
|
agencyOwnerUserID int64
|
||||||
|
active bool
|
||||||
}
|
}
|
||||||
hostPointPolicies := make(map[string]targetHostPointPolicy, len(command.Targets))
|
hostPointPolicies := make(map[string]targetHostPointPolicy, len(command.Targets))
|
||||||
if chargeSource != giftChargeSourceBag {
|
if chargeSource != giftChargeSourceBag {
|
||||||
@ -139,7 +141,21 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.BatchGiftReceipt{}, err
|
return ledger.BatchGiftReceipt{}, err
|
||||||
}
|
}
|
||||||
hostPointPolicies[target.CommandID] = targetHostPointPolicy{policy: policy, amount: amount, userID: target.TargetUserID, active: true}
|
agencyAmount := int64(0)
|
||||||
|
if target.TargetAgencyOwnerUserID > 0 && policy.AgencyPointRatioPPM > 0 {
|
||||||
|
agencyAmount, err = giftDiamondAmount(chargeAmount, policy.AgencyPointRatioPPM)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.BatchGiftReceipt{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hostPointPolicies[target.CommandID] = targetHostPointPolicy{
|
||||||
|
policy: policy,
|
||||||
|
amount: amount,
|
||||||
|
userID: target.TargetUserID,
|
||||||
|
agencyAmount: agencyAmount,
|
||||||
|
agencyOwnerUserID: target.TargetAgencyOwnerUserID,
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +186,13 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
AssetType: ledger.AssetPoint,
|
AssetType: ledger.AssetPoint,
|
||||||
CreateIfMissing: true,
|
CreateIfMissing: true,
|
||||||
})
|
})
|
||||||
|
if hostPointPolicy.agencyAmount > 0 {
|
||||||
|
lockRequests = append(lockRequests, walletAccountLockRequest{
|
||||||
|
UserID: hostPointPolicy.agencyOwnerUserID,
|
||||||
|
AssetType: ledger.AssetPoint,
|
||||||
|
CreateIfMissing: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
accountStates, err := r.lockAccountsOrdered(ctx, tx, lockRequests, nowMs)
|
accountStates, err := r.lockAccountsOrdered(ctx, tx, lockRequests, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -267,6 +290,18 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
}
|
}
|
||||||
hostPointBalanceAfter = targetPointState.account.AvailableAmount + hostPointPolicy.amount
|
hostPointBalanceAfter = targetPointState.account.AvailableAmount + hostPointPolicy.amount
|
||||||
}
|
}
|
||||||
|
agencyPointBalanceAfter := int64(0)
|
||||||
|
var agencyPointState *walletAccountState
|
||||||
|
if hostPointPolicy.agencyAmount > 0 {
|
||||||
|
agencyPointState = accountStates[walletAccountLockKey(hostPointPolicy.agencyOwnerUserID, ledger.AssetPoint)]
|
||||||
|
if agencyPointState == nil {
|
||||||
|
return ledger.BatchGiftReceipt{}, xerr.New(xerr.Internal, "agency point account is missing")
|
||||||
|
}
|
||||||
|
agencyPointBalanceAfter = agencyPointState.account.AvailableAmount + hostPointPolicy.agencyAmount
|
||||||
|
if agencyPointState == targetPointState {
|
||||||
|
agencyPointBalanceAfter += hostPointPolicy.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
// 批量事务逐目标生成独立账务交易;这里从当前 sender 版本预测本目标完成后的版本,
|
// 批量事务逐目标生成独立账务交易;这里从当前 sender 版本预测本目标完成后的版本,
|
||||||
// 下一轮会读取已推进的 senderState,因此最后一条回执自然代表整批最终版本。
|
// 下一轮会读取已推进的 senderState,因此最后一条回执自然代表整批最终版本。
|
||||||
senderBalanceVersionAfter := giftSenderBalanceVersionAfter(
|
senderBalanceVersionAfter := giftSenderBalanceVersionAfter(
|
||||||
@ -322,6 +357,9 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
HostPointTemplateCode: hostPointPolicy.policy.TemplateCode,
|
HostPointTemplateCode: hostPointPolicy.policy.TemplateCode,
|
||||||
HostPointTemplateVersion: hostPointPolicy.policy.TemplateVersion,
|
HostPointTemplateVersion: hostPointPolicy.policy.TemplateVersion,
|
||||||
HostPointRatioPPM: hostPointPolicy.policy.HostPointRatioPPM,
|
HostPointRatioPPM: hostPointPolicy.policy.HostPointRatioPPM,
|
||||||
|
AgencyPointAdded: hostPointPolicy.agencyAmount,
|
||||||
|
AgencyPointBalanceAfter: agencyPointBalanceAfter,
|
||||||
|
AgencyPointRatioPPM: hostPointPolicy.policy.AgencyPointRatioPPM,
|
||||||
PointsPerUSD: hostPointPolicy.policy.PointsPerUSD,
|
PointsPerUSD: hostPointPolicy.policy.PointsPerUSD,
|
||||||
RoomID: command.RoomID,
|
RoomID: command.RoomID,
|
||||||
RoomRegionID: command.RegionID,
|
RoomRegionID: command.RegionID,
|
||||||
@ -404,6 +442,29 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
return ledger.BatchGiftReceipt{}, err
|
return ledger.BatchGiftReceipt{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if hostPointPolicy.agencyAmount > 0 {
|
||||||
|
agencyPointAccount, err := r.applyTrackedAccountDelta(ctx, tx, agencyPointState, hostPointPolicy.agencyAmount, 0, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.BatchGiftReceipt{}, err
|
||||||
|
}
|
||||||
|
metadata.AgencyPointBalanceAfter = agencyPointAccount.AvailableAmount
|
||||||
|
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||||
|
TransactionID: transactionID,
|
||||||
|
UserID: hostPointPolicy.agencyOwnerUserID,
|
||||||
|
AssetType: ledger.AssetPoint,
|
||||||
|
AvailableDelta: hostPointPolicy.agencyAmount,
|
||||||
|
AvailableAfter: agencyPointAccount.AvailableAmount,
|
||||||
|
FrozenAfter: agencyPointAccount.FrozenAmount,
|
||||||
|
CounterpartyUserID: command.SenderUserID,
|
||||||
|
RoomID: command.RoomID,
|
||||||
|
CreatedAtMS: nowMs,
|
||||||
|
}); err != nil {
|
||||||
|
return ledger.BatchGiftReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.insertAgencyPointShare(ctx, tx, transactionID, metadata, nowMs); err != nil {
|
||||||
|
return ledger.BatchGiftReceipt{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if hostPeriodDiamondAdded > 0 {
|
if hostPeriodDiamondAdded > 0 {
|
||||||
hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, target.CommandID, metadata, nowMs)
|
hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, target.CommandID, metadata, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -439,6 +500,10 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
|
|||||||
events = append(events, balanceChangedEvent(transactionID, target.CommandID, target.TargetUserID, ledger.AssetPoint, hostPointPolicy.amount, 0, metadata.HostPointBalanceAfter, targetPointState.account.FrozenAmount, targetPointState.account.Version, metadata, nowMs, bizTypeGiftDebit))
|
events = append(events, balanceChangedEvent(transactionID, target.CommandID, target.TargetUserID, ledger.AssetPoint, hostPointPolicy.amount, 0, metadata.HostPointBalanceAfter, targetPointState.account.FrozenAmount, targetPointState.account.Version, metadata, nowMs, bizTypeGiftDebit))
|
||||||
events = append(events, hostPointCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
|
events = append(events, hostPointCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
|
||||||
}
|
}
|
||||||
|
if hostPointPolicy.agencyAmount > 0 {
|
||||||
|
events = append(events, balanceChangedEvent(transactionID, target.CommandID, hostPointPolicy.agencyOwnerUserID, ledger.AssetPoint, hostPointPolicy.agencyAmount, 0, metadata.AgencyPointBalanceAfter, agencyPointState.account.FrozenAmount, agencyPointState.account.Version, metadata, nowMs, bizTypeGiftDebit))
|
||||||
|
events = append(events, agencyPointCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
|
||||||
|
}
|
||||||
if hostPeriodDiamondAdded > 0 {
|
if hostPeriodDiamondAdded > 0 {
|
||||||
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
|
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,6 +90,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostPointAdded := int64(0)
|
hostPointAdded := int64(0)
|
||||||
|
agencyPointAdded := int64(0)
|
||||||
hostPointPolicy := walletPolicyInstance{}
|
hostPointPolicy := walletPolicyInstance{}
|
||||||
hostPointPolicyActive := false
|
hostPointPolicyActive := false
|
||||||
hostPointRegionID := command.TargetHostRegionID
|
hostPointRegionID := command.TargetHostRegionID
|
||||||
@ -109,6 +110,13 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
}
|
}
|
||||||
// 命中新政策后不再给主播走旧 COIN 返还,避免同一笔礼物同时产生 COIN 收礼返还和 POINT 收益。
|
// 命中新政策后不再给主播走旧 COIN 返还,避免同一笔礼物同时产生 COIN 收礼返还和 POINT 收益。
|
||||||
giftIncomeCoinAmount = 0
|
giftIncomeCoinAmount = 0
|
||||||
|
if command.TargetAgencyOwnerUserID > 0 && hostPointPolicy.AgencyPointRatioPPM > 0 {
|
||||||
|
// Agency 分成只跟随服务端注入的送礼时归属快照,客户端不能指定;自送礼同样属于有效付费礼物。
|
||||||
|
agencyPointAdded, err = giftDiamondAmount(chargeAmount, hostPointPolicy.AgencyPointRatioPPM)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.Receipt{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hostPeriodDiamondAdded := int64(0)
|
hostPeriodDiamondAdded := int64(0)
|
||||||
@ -137,6 +145,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
var senderState *walletAccountState
|
var senderState *walletAccountState
|
||||||
var targetCoinState *walletAccountState
|
var targetCoinState *walletAccountState
|
||||||
var targetPointState *walletAccountState
|
var targetPointState *walletAccountState
|
||||||
|
var agencyPointState *walletAccountState
|
||||||
if command.RobotGift {
|
if command.RobotGift {
|
||||||
|
|
||||||
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, accountAssetType, true, nowMs)
|
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, accountAssetType, true, nowMs)
|
||||||
@ -164,6 +173,13 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
CreateIfMissing: true,
|
CreateIfMissing: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if agencyPointAdded > 0 {
|
||||||
|
lockRequests = append(lockRequests, walletAccountLockRequest{
|
||||||
|
UserID: command.TargetAgencyOwnerUserID,
|
||||||
|
AssetType: ledger.AssetPoint,
|
||||||
|
CreateIfMissing: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
states, err := r.lockAccountsOrdered(ctx, tx, lockRequests, nowMs)
|
states, err := r.lockAccountsOrdered(ctx, tx, lockRequests, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.Receipt{}, err
|
return ledger.Receipt{}, err
|
||||||
@ -175,6 +191,9 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
if hostPointAdded > 0 {
|
if hostPointAdded > 0 {
|
||||||
targetPointState = states[walletAccountLockKey(command.TargetUserID, ledger.AssetPoint)]
|
targetPointState = states[walletAccountLockKey(command.TargetUserID, ledger.AssetPoint)]
|
||||||
}
|
}
|
||||||
|
if agencyPointAdded > 0 {
|
||||||
|
agencyPointState = states[walletAccountLockKey(command.TargetAgencyOwnerUserID, ledger.AssetPoint)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sender := senderState.account
|
sender := senderState.account
|
||||||
robotCoinTopUp := int64(0)
|
robotCoinTopUp := int64(0)
|
||||||
@ -209,6 +228,16 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
}
|
}
|
||||||
hostPointBalanceAfter = targetPointState.account.AvailableAmount + hostPointAdded
|
hostPointBalanceAfter = targetPointState.account.AvailableAmount + hostPointAdded
|
||||||
}
|
}
|
||||||
|
agencyPointBalanceAfter := int64(0)
|
||||||
|
if agencyPointAdded > 0 {
|
||||||
|
if agencyPointState == nil {
|
||||||
|
return ledger.Receipt{}, xerr.New(xerr.Internal, "agency point account is missing")
|
||||||
|
}
|
||||||
|
agencyPointBalanceAfter = agencyPointState.account.AvailableAmount + agencyPointAdded
|
||||||
|
if agencyPointState == targetPointState {
|
||||||
|
agencyPointBalanceAfter += hostPointAdded
|
||||||
|
}
|
||||||
|
}
|
||||||
transactionID := transactionID(command.AppCode, command.CommandID)
|
transactionID := transactionID(command.AppCode, command.CommandID)
|
||||||
// metadata 在账户更新前写入交易表,因此版本必须按本事务即将执行的 sender 分录精确预测;
|
// metadata 在账户更新前写入交易表,因此版本必须按本事务即将执行的 sender 分录精确预测;
|
||||||
// 事务内实际更新仍由 wallet_accounts.version 乐观锁裁决,失败会整体回滚该预测值。
|
// 事务内实际更新仍由 wallet_accounts.version 乐观锁裁决,失败会整体回滚该预测值。
|
||||||
@ -265,6 +294,9 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
HostPointTemplateCode: hostPointPolicy.TemplateCode,
|
HostPointTemplateCode: hostPointPolicy.TemplateCode,
|
||||||
HostPointTemplateVersion: hostPointPolicy.TemplateVersion,
|
HostPointTemplateVersion: hostPointPolicy.TemplateVersion,
|
||||||
HostPointRatioPPM: hostPointPolicy.HostPointRatioPPM,
|
HostPointRatioPPM: hostPointPolicy.HostPointRatioPPM,
|
||||||
|
AgencyPointAdded: agencyPointAdded,
|
||||||
|
AgencyPointBalanceAfter: agencyPointBalanceAfter,
|
||||||
|
AgencyPointRatioPPM: hostPointPolicy.AgencyPointRatioPPM,
|
||||||
PointsPerUSD: hostPointPolicy.PointsPerUSD,
|
PointsPerUSD: hostPointPolicy.PointsPerUSD,
|
||||||
RoomID: command.RoomID,
|
RoomID: command.RoomID,
|
||||||
RoomRegionID: command.RegionID,
|
RoomRegionID: command.RegionID,
|
||||||
@ -378,6 +410,29 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
return ledger.Receipt{}, err
|
return ledger.Receipt{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if agencyPointAdded > 0 {
|
||||||
|
agencyPointAccount, err := r.applyTrackedAccountDelta(ctx, tx, agencyPointState, agencyPointAdded, 0, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.Receipt{}, err
|
||||||
|
}
|
||||||
|
metadata.AgencyPointBalanceAfter = agencyPointAccount.AvailableAmount
|
||||||
|
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||||
|
TransactionID: transactionID,
|
||||||
|
UserID: command.TargetAgencyOwnerUserID,
|
||||||
|
AssetType: ledger.AssetPoint,
|
||||||
|
AvailableDelta: agencyPointAdded,
|
||||||
|
AvailableAfter: agencyPointAccount.AvailableAmount,
|
||||||
|
FrozenAfter: agencyPointAccount.FrozenAmount,
|
||||||
|
CounterpartyUserID: command.SenderUserID,
|
||||||
|
RoomID: command.RoomID,
|
||||||
|
CreatedAtMS: nowMs,
|
||||||
|
}); err != nil {
|
||||||
|
return ledger.Receipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.insertAgencyPointShare(ctx, tx, transactionID, metadata, nowMs); err != nil {
|
||||||
|
return ledger.Receipt{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if hostPeriodDiamondAdded > 0 {
|
if hostPeriodDiamondAdded > 0 {
|
||||||
|
|
||||||
hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, command.CommandID, metadata, nowMs)
|
hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, command.CommandID, metadata, nowMs)
|
||||||
@ -417,6 +472,10 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
|||||||
events = append(events, balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetPoint, hostPointAdded, 0, metadata.HostPointBalanceAfter, targetPointState.account.FrozenAmount, targetPointState.account.Version, metadata, nowMs, bizType))
|
events = append(events, balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetPoint, hostPointAdded, 0, metadata.HostPointBalanceAfter, targetPointState.account.FrozenAmount, targetPointState.account.Version, metadata, nowMs, bizType))
|
||||||
events = append(events, hostPointCreditedEvent(transactionID, command.CommandID, metadata, nowMs))
|
events = append(events, hostPointCreditedEvent(transactionID, command.CommandID, metadata, nowMs))
|
||||||
}
|
}
|
||||||
|
if agencyPointAdded > 0 {
|
||||||
|
events = append(events, balanceChangedEvent(transactionID, command.CommandID, command.TargetAgencyOwnerUserID, ledger.AssetPoint, agencyPointAdded, 0, metadata.AgencyPointBalanceAfter, agencyPointState.account.FrozenAmount, agencyPointState.account.Version, metadata, nowMs, bizType))
|
||||||
|
events = append(events, agencyPointCreditedEvent(transactionID, command.CommandID, metadata, nowMs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if hostPeriodDiamondAdded > 0 {
|
if hostPeriodDiamondAdded > 0 {
|
||||||
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, command.CommandID, metadata, nowMs))
|
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, command.CommandID, metadata, nowMs))
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHostRevenueStats 聚合 POINT 分录;兑换按 available 侧冻结扣减计数,审核结算不会再次累计。
|
||||||
|
func (r *Repository) GetHostRevenueStats(ctx context.Context, query ledger.HostRevenueStatsQuery) (ledger.HostRevenueStats, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.HostRevenueStats{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
var stats ledger.HostRevenueStats
|
||||||
|
err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN e.available_delta > 0 AND t.biz_type <> ? THEN e.available_delta ELSE 0 END), 0),
|
||||||
|
COALESCE(SUM(CASE WHEN e.available_delta < 0 THEN -e.available_delta ELSE 0 END), 0),
|
||||||
|
COUNT(DISTINCT CASE WHEN e.available_delta > 0 AND e.counterparty_user_id > 0 THEN e.counterparty_user_id END)
|
||||||
|
FROM wallet_entries e
|
||||||
|
INNER JOIN wallet_transactions t
|
||||||
|
ON t.app_code = e.app_code AND t.transaction_id = e.transaction_id
|
||||||
|
WHERE e.app_code = ? AND e.user_id = ? AND e.asset_type = ?
|
||||||
|
AND e.created_at_ms >= ? AND e.created_at_ms < ?`,
|
||||||
|
bizTypeSalaryWithdrawalRelease,
|
||||||
|
appcode.FromContext(ctx), query.HostUserID, ledger.AssetPoint, query.StartAtMS, query.EndAtMS,
|
||||||
|
).Scan(&stats.DiamondEarnings, &stats.DiamondExchanged, &stats.GiftSenders)
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHostRevenueStatsAggregatesPointLedger(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sqlmock: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN e.available_delta > 0 AND t.biz_type <> ? THEN e.available_delta ELSE 0 END), 0),
|
||||||
|
COALESCE(SUM(CASE WHEN e.available_delta < 0 THEN -e.available_delta ELSE 0 END), 0),
|
||||||
|
COUNT(DISTINCT CASE WHEN e.available_delta > 0 AND e.counterparty_user_id > 0 THEN e.counterparty_user_id END)
|
||||||
|
FROM wallet_entries e
|
||||||
|
INNER JOIN wallet_transactions t
|
||||||
|
ON t.app_code = e.app_code AND t.transaction_id = e.transaction_id
|
||||||
|
WHERE e.app_code = ? AND e.user_id = ? AND e.asset_type = ?
|
||||||
|
AND e.created_at_ms >= ? AND e.created_at_ms < ?`)).
|
||||||
|
WithArgs(bizTypeSalaryWithdrawalRelease, "fami", int64(42), ledger.AssetPoint, int64(1000), int64(2000)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"earnings", "exchanged", "senders"}).AddRow(int64(8800), int64(1200), int64(7)))
|
||||||
|
|
||||||
|
repo := &Repository{db: db}
|
||||||
|
stats, err := repo.GetHostRevenueStats(appcode.WithContext(context.Background(), "fami"), ledger.HostRevenueStatsQuery{
|
||||||
|
HostUserID: 42,
|
||||||
|
StartAtMS: 1000,
|
||||||
|
EndAtMS: 2000,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetHostRevenueStats failed: %v", err)
|
||||||
|
}
|
||||||
|
if stats.DiamondEarnings != 8800 || stats.DiamondExchanged != 1200 || stats.GiftSenders != 7 {
|
||||||
|
t.Fatalf("unexpected host revenue stats: %+v", stats)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("unmet SQL expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,6 +56,9 @@ type giftMetadata struct {
|
|||||||
HostPointTemplateCode string `json:"host_point_template_code,omitempty"`
|
HostPointTemplateCode string `json:"host_point_template_code,omitempty"`
|
||||||
HostPointTemplateVersion string `json:"host_point_template_version,omitempty"`
|
HostPointTemplateVersion string `json:"host_point_template_version,omitempty"`
|
||||||
HostPointRatioPPM int64 `json:"host_point_ratio_ppm,omitempty"`
|
HostPointRatioPPM int64 `json:"host_point_ratio_ppm,omitempty"`
|
||||||
|
AgencyPointAdded int64 `json:"agency_point_added,omitempty"`
|
||||||
|
AgencyPointBalanceAfter int64 `json:"agency_point_balance_after,omitempty"`
|
||||||
|
AgencyPointRatioPPM int64 `json:"agency_point_ratio_ppm,omitempty"`
|
||||||
PointsPerUSD int64 `json:"points_per_usd,omitempty"`
|
PointsPerUSD int64 `json:"points_per_usd,omitempty"`
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
RoomRegionID int64 `json:"room_region_id"`
|
RoomRegionID int64 `json:"room_region_id"`
|
||||||
|
|||||||
@ -792,3 +792,37 @@ func hostPointCreditedEvent(transactionID string, commandID string, metadata gif
|
|||||||
CreatedAtMS: nowMs,
|
CreatedAtMS: nowMs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func agencyPointCreditedEvent(transactionID string, commandID string, metadata giftMetadata, nowMs int64) walletOutboxEvent {
|
||||||
|
return walletOutboxEvent{
|
||||||
|
EventID: eventID(transactionID, "AgencyPointCredited", metadata.TargetAgencyOwnerUserID, ledger.AssetPoint),
|
||||||
|
EventType: "AgencyPointCredited",
|
||||||
|
TransactionID: transactionID,
|
||||||
|
CommandID: commandID,
|
||||||
|
UserID: metadata.TargetAgencyOwnerUserID,
|
||||||
|
AssetType: ledger.AssetPoint,
|
||||||
|
AvailableDelta: metadata.AgencyPointAdded,
|
||||||
|
FrozenDelta: 0,
|
||||||
|
Payload: map[string]any{
|
||||||
|
"transaction_id": transactionID,
|
||||||
|
"command_id": commandID,
|
||||||
|
"host_user_id": metadata.TargetUserID,
|
||||||
|
"agency_owner_user_id": metadata.TargetAgencyOwnerUserID,
|
||||||
|
"sender_user_id": metadata.SenderUserID,
|
||||||
|
"room_id": metadata.RoomID,
|
||||||
|
"asset_type": ledger.AssetPoint,
|
||||||
|
"point_delta": metadata.AgencyPointAdded,
|
||||||
|
"point_balance_after": metadata.AgencyPointBalanceAfter,
|
||||||
|
"gift_charge_asset_type": metadata.ChargeAssetType,
|
||||||
|
"gift_charge_amount": metadata.ChargeAmount,
|
||||||
|
"gift_id": metadata.GiftID,
|
||||||
|
"gift_count": metadata.GiftCount,
|
||||||
|
"policy_instance_code": metadata.HostPointPolicyInstanceCode,
|
||||||
|
"policy_template_code": metadata.HostPointTemplateCode,
|
||||||
|
"policy_template_version": metadata.HostPointTemplateVersion,
|
||||||
|
"agency_point_ratio_ppm": metadata.AgencyPointRatioPPM,
|
||||||
|
"created_at_ms": nowMs,
|
||||||
|
},
|
||||||
|
CreatedAtMS: nowMs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,268 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pointWithdrawalMinimumPoints int64 = 1_000_000
|
||||||
|
|
||||||
|
type pointToCoinSellerMetadata struct {
|
||||||
|
AppCode string `json:"app_code"`
|
||||||
|
SourceUserID int64 `json:"source_user_id"`
|
||||||
|
SellerUserID int64 `json:"seller_user_id"`
|
||||||
|
SourceCountryCode string `json:"source_country_code"`
|
||||||
|
SourceAssetType string `json:"source_asset_type"`
|
||||||
|
SellerAssetType string `json:"seller_asset_type"`
|
||||||
|
PointAmount int64 `json:"point_amount"`
|
||||||
|
SellerCoinAmount int64 `json:"seller_coin_amount"`
|
||||||
|
RatioPointAmount int64 `json:"ratio_point_amount"`
|
||||||
|
RatioSellerCoinAmount int64 `json:"ratio_seller_coin_amount"`
|
||||||
|
SourcePointBalanceAfter int64 `json:"source_point_balance_after"`
|
||||||
|
SellerBalanceAfter int64 `json:"seller_balance_after"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPointWithdrawalCoinSellers 返回当前 App 的白名单;国家过滤在 wallet 内执行,gateway 不能绕过服务范围。
|
||||||
|
func (r *Repository) ListPointWithdrawalCoinSellers(ctx context.Context, appCode string, countryCode string, includeDisabled bool) ([]ledger.PointWithdrawalCoinSellerConfig, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
ctx = contextWithCommandApp(ctx, appCode)
|
||||||
|
where := "app_code = ?"
|
||||||
|
args := []any{appcode.FromContext(ctx)}
|
||||||
|
if !includeDisabled {
|
||||||
|
where += " AND status = 'active'"
|
||||||
|
}
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, seller_user_id, sort_order, point_amount, seller_coin_amount,
|
||||||
|
service_country_codes, status, created_at_ms, updated_at_ms
|
||||||
|
FROM point_withdrawal_coin_seller_configs
|
||||||
|
WHERE `+where+`
|
||||||
|
ORDER BY sort_order ASC, seller_user_id ASC`, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
||||||
|
items := make([]ledger.PointWithdrawalCoinSellerConfig, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item ledger.PointWithdrawalCoinSellerConfig
|
||||||
|
var countriesJSON []byte
|
||||||
|
if err := rows.Scan(&item.AppCode, &item.SellerUserID, &item.SortOrder, &item.PointAmount, &item.SellerCoinAmount, &countriesJSON, &item.Status, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(countriesJSON, &item.ServiceCountryCodes); err != nil {
|
||||||
|
return nil, xerr.New(xerr.Internal, "point withdrawal seller country config is invalid")
|
||||||
|
}
|
||||||
|
item.ServiceCountryCodes = normalizeCountryCodes(item.ServiceCountryCodes)
|
||||||
|
if item.PointAmount <= 0 || item.SellerCoinAmount <= 0 {
|
||||||
|
return nil, xerr.New(xerr.Internal, "point withdrawal seller ratio is invalid")
|
||||||
|
}
|
||||||
|
if countryCode != "" && !countryAllowed(item.ServiceCountryCodes, countryCode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferPointToCoinSeller 在一个事务中锁定白名单配置和双方账户;任何比例更新只影响新 command_id。
|
||||||
|
func (r *Repository) TransferPointToCoinSeller(ctx context.Context, command ledger.PointToCoinSellerCommand) (ledger.PointToCoinSellerReceipt, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, 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.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
requestHash := stableHash(fmt.Sprintf("point_transfer_coin_seller|%s|%d|%d|%d|%s|%s",
|
||||||
|
command.AppCode, command.SourceUserID, command.SellerUserID, command.PointAmount,
|
||||||
|
strings.ToUpper(strings.TrimSpace(command.SourceCountryCode)), strings.TrimSpace(command.Reason)))
|
||||||
|
if txRow, exists, lookupErr := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypePointTransferToCoinSeller, xerr.IdempotencyConflict); lookupErr != nil || exists {
|
||||||
|
if lookupErr != nil || !exists {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, lookupErr
|
||||||
|
}
|
||||||
|
return r.receiptForPointToCoinSellerTransaction(ctx, tx, txRow.TransactionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := r.lockPointWithdrawalCoinSellerConfig(ctx, tx, command.SellerUserID)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
countryCode := strings.ToUpper(strings.TrimSpace(command.SourceCountryCode))
|
||||||
|
if !countryAllowed(config.ServiceCountryCodes, countryCode) {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.PermissionDenied, "coin seller does not serve user country")
|
||||||
|
}
|
||||||
|
numerator, err := checkedMul(command.PointAmount, config.SellerCoinAmount)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
sellerCoinAmount := numerator / config.PointAmount
|
||||||
|
if sellerCoinAmount <= 0 {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "point amount is below exchange precision")
|
||||||
|
}
|
||||||
|
|
||||||
|
nowMS := time.Now().UnixMilli()
|
||||||
|
source, err := r.lockAccount(ctx, tx, command.SourceUserID, ledger.AssetPoint, false, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if source.AvailableAmount < command.PointAmount {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient POINT balance")
|
||||||
|
}
|
||||||
|
seller, err := r.lockAccount(ctx, tx, command.SellerUserID, ledger.AssetCoinSellerCoin, true, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
sourceAfter := source.AvailableAmount - command.PointAmount
|
||||||
|
sellerAfter, err := checkedAdd(seller.AvailableAmount, sellerCoinAmount)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
transactionID := transactionID(command.AppCode, command.CommandID)
|
||||||
|
metadata := pointToCoinSellerMetadata{
|
||||||
|
AppCode: command.AppCode, SourceUserID: command.SourceUserID, SellerUserID: command.SellerUserID,
|
||||||
|
SourceCountryCode: countryCode, SourceAssetType: ledger.AssetPoint, SellerAssetType: ledger.AssetCoinSellerCoin,
|
||||||
|
PointAmount: command.PointAmount, SellerCoinAmount: sellerCoinAmount,
|
||||||
|
RatioPointAmount: config.PointAmount, RatioSellerCoinAmount: config.SellerCoinAmount,
|
||||||
|
SourcePointBalanceAfter: sourceAfter, SellerBalanceAfter: sellerAfter, Reason: command.Reason, CreatedAtMS: nowMS,
|
||||||
|
}
|
||||||
|
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypePointTransferToCoinSeller, requestHash, fmt.Sprintf("point_coin_seller:%d:%d", command.SourceUserID, command.SellerUserID), metadata, nowMS); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.applyAccountDelta(ctx, tx, source, -command.PointAmount, 0, nowMS); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.insertEntry(ctx, tx, walletEntry{TransactionID: transactionID, UserID: command.SourceUserID, AssetType: ledger.AssetPoint, AvailableDelta: -command.PointAmount, AvailableAfter: sourceAfter, FrozenAfter: source.FrozenAmount, CounterpartyUserID: command.SellerUserID, CreatedAtMS: nowMS}); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.applyAccountDelta(ctx, tx, seller, sellerCoinAmount, 0, nowMS); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.insertEntry(ctx, tx, walletEntry{TransactionID: transactionID, UserID: command.SellerUserID, AssetType: ledger.AssetCoinSellerCoin, AvailableDelta: sellerCoinAmount, AvailableAfter: sellerAfter, FrozenAfter: seller.FrozenAmount, CounterpartyUserID: command.SourceUserID, CreatedAtMS: nowMS}); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
|
||||||
|
balanceChangedEvent(transactionID, command.CommandID, command.SourceUserID, ledger.AssetPoint, -command.PointAmount, 0, sourceAfter, source.FrozenAmount, source.Version+1, metadata, nowMS, bizTypePointTransferToCoinSeller),
|
||||||
|
balanceChangedEvent(transactionID, command.CommandID, command.SellerUserID, ledger.AssetCoinSellerCoin, sellerCoinAmount, 0, sellerAfter, seller.FrozenAmount, seller.Version+1, metadata, nowMS, bizTypePointTransferToCoinSeller),
|
||||||
|
}); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
return receiptFromPointToCoinSellerMetadata(transactionID, metadata), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointWithdrawalConfig 只读取已发布钱包政策;不存在时显式 found=false,gateway 不回退到猜测参数。
|
||||||
|
func (r *Repository) GetPointWithdrawalConfig(ctx context.Context, appCode string, regionID int64, nowMS int64) (ledger.PointWithdrawalRuntimeConfig, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return ledger.PointWithdrawalRuntimeConfig{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
ctx = contextWithCommandApp(ctx, appCode)
|
||||||
|
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
|
||||||
|
if err != nil {
|
||||||
|
return ledger.PointWithdrawalRuntimeConfig{}, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
policy, found, err := r.resolveActiveWalletPolicy(ctx, tx, appcode.FromContext(ctx), regionID, nowMS)
|
||||||
|
if err != nil || !found {
|
||||||
|
return ledger.PointWithdrawalRuntimeConfig{Found: false}, err
|
||||||
|
}
|
||||||
|
return ledger.PointWithdrawalRuntimeConfig{Found: true, PointsPerUSD: policy.PointsPerUSD, FeeBPS: policy.WithdrawFeeBPS, MinimumPoints: pointWithdrawalMinimumPoints, PolicyInstanceCode: policy.InstanceCode}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) lockPointWithdrawalCoinSellerConfig(ctx context.Context, tx *sql.Tx, sellerUserID int64) (ledger.PointWithdrawalCoinSellerConfig, error) {
|
||||||
|
var item ledger.PointWithdrawalCoinSellerConfig
|
||||||
|
var countriesJSON []byte
|
||||||
|
err := tx.QueryRowContext(ctx, `
|
||||||
|
SELECT app_code, seller_user_id, sort_order, point_amount, seller_coin_amount,
|
||||||
|
service_country_codes, status, created_at_ms, updated_at_ms
|
||||||
|
FROM point_withdrawal_coin_seller_configs
|
||||||
|
WHERE app_code = ? AND seller_user_id = ? AND status = 'active'
|
||||||
|
FOR UPDATE`, appcode.FromContext(ctx), sellerUserID).Scan(
|
||||||
|
&item.AppCode, &item.SellerUserID, &item.SortOrder, &item.PointAmount, &item.SellerCoinAmount,
|
||||||
|
&countriesJSON, &item.Status, &item.CreatedAtMS, &item.UpdatedAtMS,
|
||||||
|
)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return item, xerr.New(xerr.NotFound, "point withdrawal coin seller is not available")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(countriesJSON, &item.ServiceCountryCodes); err != nil || item.PointAmount <= 0 || item.SellerCoinAmount <= 0 {
|
||||||
|
return item, xerr.New(xerr.Internal, "point withdrawal coin seller config is invalid")
|
||||||
|
}
|
||||||
|
item.ServiceCountryCodes = normalizeCountryCodes(item.ServiceCountryCodes)
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) receiptForPointToCoinSellerTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.PointToCoinSellerReceipt, error) {
|
||||||
|
var metadataJSON string
|
||||||
|
if err := tx.QueryRowContext(ctx, `SELECT COALESCE(CAST(metadata_json AS CHAR), '{}') FROM wallet_transactions WHERE app_code = ? AND transaction_id = ?`, appcode.FromContext(ctx), transactionID).Scan(&metadataJSON); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
var metadata pointToCoinSellerMetadata
|
||||||
|
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
|
||||||
|
return ledger.PointToCoinSellerReceipt{}, err
|
||||||
|
}
|
||||||
|
return receiptFromPointToCoinSellerMetadata(transactionID, metadata), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiptFromPointToCoinSellerMetadata(transactionID string, metadata pointToCoinSellerMetadata) ledger.PointToCoinSellerReceipt {
|
||||||
|
return ledger.PointToCoinSellerReceipt{
|
||||||
|
TransactionID: transactionID, SourceUserID: metadata.SourceUserID, SellerUserID: metadata.SellerUserID,
|
||||||
|
SourcePointBalanceAfter: metadata.SourcePointBalanceAfter, SellerBalanceAfter: metadata.SellerBalanceAfter,
|
||||||
|
PointAmount: metadata.PointAmount, SellerCoinAmount: metadata.SellerCoinAmount,
|
||||||
|
RatioPointAmount: metadata.RatioPointAmount, RatioSellerCoinAmount: metadata.RatioSellerCoinAmount, CreatedAtMS: metadata.CreatedAtMS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCountryCodes(values []string) []string {
|
||||||
|
seen := make(map[string]struct{}, len(values))
|
||||||
|
out := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.ToUpper(strings.TrimSpace(value))
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[value]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func countryAllowed(configured []string, countryCode string) bool {
|
||||||
|
if len(configured) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
||||||
|
if countryCode == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, configuredCode := range configured {
|
||||||
|
if strings.EqualFold(configuredCode, countryCode) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@ -12,14 +12,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type walletPolicyInstance struct {
|
type walletPolicyInstance struct {
|
||||||
InstanceCode string
|
InstanceCode string
|
||||||
TemplateCode string
|
TemplateCode string
|
||||||
TemplateVersion string
|
TemplateVersion string
|
||||||
RegionID int64
|
RegionID int64
|
||||||
HostPointAsset string
|
HostPointAsset string
|
||||||
HostPointRatioPPM int64
|
HostPointRatioPPM int64
|
||||||
PointsPerUSD int64
|
AgencyPointRatioPPM int64
|
||||||
WithdrawFeeBPS int64
|
PointsPerUSD int64
|
||||||
|
WithdrawFeeBPS int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) resolveActiveWalletPolicy(ctx context.Context, tx *sql.Tx, appCode string, regionID int64, nowMS int64) (walletPolicyInstance, bool, error) {
|
func (r *Repository) resolveActiveWalletPolicy(ctx context.Context, tx *sql.Tx, appCode string, regionID int64, nowMS int64) (walletPolicyInstance, bool, error) {
|
||||||
@ -29,7 +30,7 @@ func (r *Repository) resolveActiveWalletPolicy(ctx context.Context, tx *sql.Tx,
|
|||||||
}
|
}
|
||||||
row := tx.QueryRowContext(ctx, `
|
row := tx.QueryRowContext(ctx, `
|
||||||
SELECT instance_code, template_code, template_version, region_id, host_point_asset_type,
|
SELECT instance_code, template_code, template_version, region_id, host_point_asset_type,
|
||||||
host_point_ratio_ppm, points_per_usd, withdraw_fee_bps
|
host_point_ratio_ppm, agency_point_ratio_ppm, points_per_usd, withdraw_fee_bps
|
||||||
FROM wallet_policy_instances
|
FROM wallet_policy_instances
|
||||||
WHERE app_code = ?
|
WHERE app_code = ?
|
||||||
AND region_id IN (?, 0)
|
AND region_id IN (?, 0)
|
||||||
@ -41,7 +42,7 @@ func (r *Repository) resolveActiveWalletPolicy(ctx context.Context, tx *sql.Tx,
|
|||||||
appCode, regionID, nowMS, nowMS, regionID,
|
appCode, regionID, nowMS, nowMS, regionID,
|
||||||
)
|
)
|
||||||
var item walletPolicyInstance
|
var item walletPolicyInstance
|
||||||
if err := row.Scan(&item.InstanceCode, &item.TemplateCode, &item.TemplateVersion, &item.RegionID, &item.HostPointAsset, &item.HostPointRatioPPM, &item.PointsPerUSD, &item.WithdrawFeeBPS); err != nil {
|
if err := row.Scan(&item.InstanceCode, &item.TemplateCode, &item.TemplateVersion, &item.RegionID, &item.HostPointAsset, &item.HostPointRatioPPM, &item.AgencyPointRatioPPM, &item.PointsPerUSD, &item.WithdrawFeeBPS); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return walletPolicyInstance{}, false, nil
|
return walletPolicyInstance{}, false, nil
|
||||||
}
|
}
|
||||||
@ -51,7 +52,7 @@ func (r *Repository) resolveActiveWalletPolicy(ctx context.Context, tx *sql.Tx,
|
|||||||
if item.HostPointAsset == "" {
|
if item.HostPointAsset == "" {
|
||||||
item.HostPointAsset = ledger.AssetPoint
|
item.HostPointAsset = ledger.AssetPoint
|
||||||
}
|
}
|
||||||
if item.HostPointAsset != ledger.AssetPoint || item.HostPointRatioPPM <= 0 || item.HostPointRatioPPM > 1_000_000 || item.PointsPerUSD <= 0 || item.WithdrawFeeBPS < 0 || item.WithdrawFeeBPS > 10_000 {
|
if item.HostPointAsset != ledger.AssetPoint || item.HostPointRatioPPM <= 0 || item.HostPointRatioPPM > 1_000_000 || item.AgencyPointRatioPPM < 0 || item.AgencyPointRatioPPM > 1_000_000 || item.HostPointRatioPPM+item.AgencyPointRatioPPM > 1_000_000 || item.PointsPerUSD <= 0 || item.WithdrawFeeBPS < 0 || item.WithdrawFeeBPS > 10_000 {
|
||||||
return walletPolicyInstance{}, false, xerr.New(xerr.InvalidArgument, "wallet policy instance is invalid")
|
return walletPolicyInstance{}, false, xerr.New(xerr.InvalidArgument, "wallet policy instance is invalid")
|
||||||
}
|
}
|
||||||
return item, true, nil
|
return item, true, nil
|
||||||
|
|||||||
@ -71,6 +71,10 @@ func Open(ctx context.Context, dsn string) (*Repository, error) {
|
|||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ensurePointWithdrawalCoinSellerSchema(ctx, db); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Repository{db: db}, nil
|
return &Repository{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -207,6 +207,7 @@ func ensureWalletPolicyInstanceSchema(ctx context.Context, db *sql.DB) error {
|
|||||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
|
||||||
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '主播收益积分资产',
|
||||||
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
host_point_ratio_ppm BIGINT NOT NULL DEFAULT 700000 COMMENT '有效付费礼物转 POINT 比例,ppm',
|
||||||
|
agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '有效付费礼物给 Agency owner 的 POINT 分成比例,ppm',
|
||||||
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
|
||||||
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
|
||||||
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
rule_json JSON NOT NULL COMMENT '完整政策快照',
|
||||||
@ -217,6 +218,50 @@ func ensureWalletPolicyInstanceSchema(ctx context.Context, db *sql.DB) error {
|
|||||||
PRIMARY KEY (app_code, instance_code, region_id),
|
PRIMARY KEY (app_code, instance_code, region_id),
|
||||||
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表'`)
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧收益政策实例表'`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `ALTER TABLE wallet_policy_instances ADD COLUMN agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '有效付费礼物给 Agency owner 的 POINT 分成比例,ppm' AFTER host_point_ratio_ppm`); err != nil && !isDuplicateColumnError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS agency_point_share_entries (
|
||||||
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||||||
|
transaction_id VARCHAR(96) NOT NULL,
|
||||||
|
host_user_id BIGINT NOT NULL,
|
||||||
|
agency_owner_user_id BIGINT NOT NULL,
|
||||||
|
sender_user_id BIGINT NOT NULL,
|
||||||
|
room_id VARCHAR(96) NOT NULL DEFAULT '',
|
||||||
|
point_delta BIGINT NOT NULL,
|
||||||
|
ratio_ppm BIGINT NOT NULL,
|
||||||
|
policy_instance_code VARCHAR(96) NOT NULL DEFAULT '',
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (app_code, transaction_id, agency_owner_user_id),
|
||||||
|
KEY idx_agency_point_share_owner_time (app_code, agency_owner_user_id, created_at_ms),
|
||||||
|
KEY idx_agency_point_share_host_time (app_code, host_user_id, created_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Agency POINT 礼物分成流水'`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensurePointWithdrawalCoinSellerSchema 创建跨 Fami/Huwaa/Lalu 共用、按 app_code 隔离的提现币商配置。
|
||||||
|
// 表放在 wallet 数据库,保证兑换时读取比例和写账本处于同一可信服务边界,H5 不能自报比例。
|
||||||
|
func ensurePointWithdrawalCoinSellerSchema(ctx context.Context, db *sql.DB) error {
|
||||||
|
_, err := db.ExecContext(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
|
||||||
|
app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
|
||||||
|
seller_user_id BIGINT NOT NULL COMMENT '币商用户 ID',
|
||||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序,越小越靠前',
|
||||||
|
point_amount BIGINT NOT NULL COMMENT '兑换比例中的 POINT 分母',
|
||||||
|
seller_coin_amount BIGINT NOT NULL COMMENT '兑换比例中的币商金币分子',
|
||||||
|
service_country_codes JSON NOT NULL COMMENT '服务国家码数组;空数组表示不限国家',
|
||||||
|
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
|
||||||
|
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (app_code, seller_user_id),
|
||||||
|
KEY idx_point_withdraw_seller_list (app_code, status, sort_order, seller_user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT 提现币商白名单与兑换比例'`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ package mysqltest
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -14,6 +15,31 @@ import (
|
|||||||
mysqlstorage "hyapp/services/wallet-service/internal/storage/mysql"
|
mysqlstorage "hyapp/services/wallet-service/internal/storage/mysql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetPointWithdrawalCoinSellerConfig seeds one app-scoped whitelist row for real ledger tests.
|
||||||
|
func (r *Repository) SetPointWithdrawalCoinSellerConfig(appCode string, sellerUserID int64, pointAmount int64, sellerCoinAmount int64, countryCodes []string, status string, sortOrder int) {
|
||||||
|
r.t.Helper()
|
||||||
|
countriesJSON, err := json.Marshal(countryCodes)
|
||||||
|
if err != nil {
|
||||||
|
r.t.Fatalf("marshal point withdrawal countries failed: %v", err)
|
||||||
|
}
|
||||||
|
if status == "" {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
nowMS := time.Now().UnixMilli()
|
||||||
|
if _, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
|
INSERT INTO point_withdrawal_coin_seller_configs (
|
||||||
|
app_code, seller_user_id, sort_order, point_amount, seller_coin_amount, service_country_codes,
|
||||||
|
status, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
sort_order = VALUES(sort_order), point_amount = VALUES(point_amount), seller_coin_amount = VALUES(seller_coin_amount),
|
||||||
|
service_country_codes = VALUES(service_country_codes), status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`,
|
||||||
|
appCode, sellerUserID, sortOrder, pointAmount, sellerCoinAmount, countriesJSON, status, nowMS, nowMS,
|
||||||
|
); err != nil {
|
||||||
|
r.t.Fatalf("seed point withdrawal coin seller config failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Repository wraps the production MySQL wallet repository with test seed helpers.
|
// Repository wraps the production MySQL wallet repository with test seed helpers.
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
*mysqlstorage.Repository
|
*mysqlstorage.Repository
|
||||||
@ -371,22 +397,28 @@ func (r *Repository) SetAssetBalanceForApp(appCode string, userID int64, assetTy
|
|||||||
|
|
||||||
// SeedWalletPolicyInstance writes the wallet runtime policy row consumed by gift settlement tests.
|
// SeedWalletPolicyInstance writes the wallet runtime policy row consumed by gift settlement tests.
|
||||||
func (r *Repository) SeedWalletPolicyInstance(appCode string, instanceCode string, regionID int64, status string, hostPointRatioPPM int64) {
|
func (r *Repository) SeedWalletPolicyInstance(appCode string, instanceCode string, regionID int64, status string, hostPointRatioPPM int64) {
|
||||||
|
r.SeedWalletPolicyInstanceWithAgency(appCode, instanceCode, regionID, status, hostPointRatioPPM, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedWalletPolicyInstanceWithAgency 让账务测试显式声明 Agency 比例,避免普通 policy fixture 意外改变旧用例语义。
|
||||||
|
func (r *Repository) SeedWalletPolicyInstanceWithAgency(appCode string, instanceCode string, regionID int64, status string, hostPointRatioPPM int64, agencyPointRatioPPM int64) {
|
||||||
r.t.Helper()
|
r.t.Helper()
|
||||||
|
|
||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
INSERT INTO wallet_policy_instances (
|
INSERT INTO wallet_policy_instances (
|
||||||
app_code, instance_code, template_code, template_version, region_id, status,
|
app_code, instance_code, template_code, template_version, region_id, status,
|
||||||
effective_from_ms, effective_to_ms, host_point_asset_type, host_point_ratio_ppm,
|
effective_from_ms, effective_to_ms, host_point_asset_type, host_point_ratio_ppm, agency_point_ratio_ppm,
|
||||||
points_per_usd, withdraw_fee_bps, rule_json, published_by_admin_id,
|
points_per_usd, withdraw_fee_bps, rule_json, published_by_admin_id,
|
||||||
published_at_ms, created_at_ms, updated_at_ms
|
published_at_ms, created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, 'v1', ?, ?, 0, 0, 'POINT', ?, 100000, 500, JSON_OBJECT(), 90001, ?, ?, ?)
|
) VALUES (?, ?, ?, 'v1', ?, ?, 0, 0, 'POINT', ?, ?, 100000, 500, JSON_OBJECT(), 90001, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
status = VALUES(status),
|
status = VALUES(status),
|
||||||
host_point_ratio_ppm = VALUES(host_point_ratio_ppm),
|
host_point_ratio_ppm = VALUES(host_point_ratio_ppm),
|
||||||
|
agency_point_ratio_ppm = VALUES(agency_point_ratio_ppm),
|
||||||
published_at_ms = VALUES(published_at_ms),
|
published_at_ms = VALUES(published_at_ms),
|
||||||
updated_at_ms = VALUES(updated_at_ms)
|
updated_at_ms = VALUES(updated_at_ms)
|
||||||
`, strings.TrimSpace(appCode), strings.TrimSpace(instanceCode), strings.TrimSpace(instanceCode)+"_template", regionID, strings.TrimSpace(status), hostPointRatioPPM, nowMs, nowMs, nowMs)
|
`, strings.TrimSpace(appCode), strings.TrimSpace(instanceCode), strings.TrimSpace(instanceCode)+"_template", regionID, strings.TrimSpace(status), hostPointRatioPPM, agencyPointRatioPPM, nowMs, nowMs, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.t.Fatalf("seed wallet policy instance failed: %v", err)
|
r.t.Fatalf("seed wallet policy instance failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) GetAgencyHostGiftStats(ctx context.Context, req *walletv1.GetAgencyHostGiftStatsRequest) (*walletv1.GetAgencyHostGiftStatsResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
|
stats, err := s.svc.GetAgencyHostGiftStats(ctx, req.GetAppCode(), ledger.AgencyHostGiftStatsQuery{
|
||||||
|
HostUserIDs: req.GetHostUserIds(), StartAtMS: req.GetStartAtMs(), EndAtMS: req.GetEndAtMs(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.GetAgencyHostGiftStatsResponse{Stats: &walletv1.AgencyHostGiftStats{
|
||||||
|
GiftIncome: stats.GiftIncome, GiftedHostCount: stats.GiftedHostCount,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) GetAgencyPointShareStats(ctx context.Context, req *walletv1.GetAgencyPointShareStatsRequest) (*walletv1.GetAgencyPointShareStatsResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
|
stats, err := s.svc.GetAgencyPointShareStats(ctx, req.GetAppCode(), ledger.AgencyPointShareStatsQuery{
|
||||||
|
AgencyOwnerUserID: req.GetAgencyOwnerUserId(),
|
||||||
|
StartAtMS: req.GetStartAtMs(),
|
||||||
|
EndAtMS: req.GetEndAtMs(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.GetAgencyPointShareStatsResponse{Stats: &walletv1.AgencyPointShareStats{
|
||||||
|
ShareIncome: stats.ShareIncome,
|
||||||
|
GiftedHostCount: stats.GiftedHostCount,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHostRevenueStats 返回钱包账本内的 POINT 收益、兑换和去重送礼用户。
|
||||||
|
func (s *Server) GetHostRevenueStats(ctx context.Context, req *walletv1.GetHostRevenueStatsRequest) (*walletv1.GetHostRevenueStatsResponse, error) {
|
||||||
|
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||||
|
stats, err := s.svc.GetHostRevenueStats(ctx, req.GetAppCode(), ledger.HostRevenueStatsQuery{
|
||||||
|
HostUserID: req.GetHostUserId(),
|
||||||
|
StartAtMS: req.GetStartAtMs(),
|
||||||
|
EndAtMS: req.GetEndAtMs(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.GetHostRevenueStatsResponse{Stats: &walletv1.HostRevenueStats{
|
||||||
|
DiamondEarnings: stats.DiamondEarnings,
|
||||||
|
DiamondExchanged: stats.DiamondExchanged,
|
||||||
|
GiftSenders: stats.GiftSenders,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
|
"hyapp/pkg/xerr"
|
||||||
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListPointWithdrawalCoinSellers 返回 wallet 侧已执行 app/status/国家过滤的可信白名单。
|
||||||
|
func (s *Server) ListPointWithdrawalCoinSellers(ctx context.Context, req *walletv1.ListPointWithdrawalCoinSellersRequest) (*walletv1.ListPointWithdrawalCoinSellersResponse, error) {
|
||||||
|
items, err := s.svc.ListPointWithdrawalCoinSellers(ctx, req.GetAppCode(), req.GetCountryCode(), req.GetIncludeDisabled())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
response := &walletv1.ListPointWithdrawalCoinSellersResponse{Sellers: make([]*walletv1.PointWithdrawalCoinSellerConfig, 0, len(items))}
|
||||||
|
for _, item := range items {
|
||||||
|
response.Sellers = append(response.Sellers, &walletv1.PointWithdrawalCoinSellerConfig{
|
||||||
|
AppCode: item.AppCode, SellerUserId: item.SellerUserID, SortOrder: int32(item.SortOrder),
|
||||||
|
PointAmount: item.PointAmount, SellerCoinAmount: item.SellerCoinAmount,
|
||||||
|
ServiceCountryCodes: append([]string(nil), item.ServiceCountryCodes...), Status: item.Status,
|
||||||
|
CreatedAtMs: item.CreatedAtMS, UpdatedAtMs: item.UpdatedAtMS,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferPointToCoinSeller 不接受兑换比例字段,确保所有调用方都只能使用数据库白名单快照。
|
||||||
|
func (s *Server) TransferPointToCoinSeller(ctx context.Context, req *walletv1.TransferPointToCoinSellerRequest) (*walletv1.TransferPointToCoinSellerResponse, error) {
|
||||||
|
receipt, err := s.svc.TransferPointToCoinSeller(ctx, ledger.PointToCoinSellerCommand{
|
||||||
|
AppCode: req.GetAppCode(), CommandID: req.GetCommandId(), SourceUserID: req.GetSourceUserId(),
|
||||||
|
SellerUserID: req.GetSellerUserId(), PointAmount: req.GetPointAmount(),
|
||||||
|
SourceCountryCode: req.GetSourceCountryCode(), Reason: req.GetReason(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.TransferPointToCoinSellerResponse{
|
||||||
|
TransactionId: receipt.TransactionID, SourcePointBalanceAfter: receipt.SourcePointBalanceAfter,
|
||||||
|
SellerBalanceAfter: receipt.SellerBalanceAfter, PointAmount: receipt.PointAmount,
|
||||||
|
SellerCoinAmount: receipt.SellerCoinAmount, RatioPointAmount: receipt.RatioPointAmount,
|
||||||
|
RatioSellerCoinAmount: receipt.RatioSellerCoinAmount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointWithdrawalConfig 暴露政策编译后的最小运行参数,H5 不解析 Admin rule_json。
|
||||||
|
func (s *Server) GetPointWithdrawalConfig(ctx context.Context, req *walletv1.GetPointWithdrawalConfigRequest) (*walletv1.GetPointWithdrawalConfigResponse, error) {
|
||||||
|
config, err := s.svc.GetPointWithdrawalConfig(ctx, req.GetAppCode(), req.GetRegionId(), req.GetNowMs())
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.GetPointWithdrawalConfigResponse{
|
||||||
|
Found: config.Found, PointsPerUsd: config.PointsPerUSD, FeeBps: int32(config.FeeBPS),
|
||||||
|
MinimumPoints: config.MinimumPoints, PolicyInstanceCode: config.PolicyInstanceCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user