财务模块
This commit is contained in:
parent
a1e7514af3
commit
0480304483
File diff suppressed because it is too large
Load Diff
@ -1037,6 +1037,17 @@ message RechargeBill {
|
||||
int64 exchange_usd_minor_amount = 17;
|
||||
int64 created_at_ms = 18;
|
||||
int64 provider_amount_minor = 19;
|
||||
// provider_code 标识账单实际支付渠道:google_play/mifapay/v5pay/usdt_trc20。
|
||||
string provider_code = 20;
|
||||
// user_paid_* 是用户实付事实:外部订单来自三方下单快照,Google 订单来自 Play Orders API。
|
||||
string user_paid_currency_code = 21;
|
||||
int64 user_paid_amount_micro = 22;
|
||||
// provider_fee/tax/net 是三方扣款事实(实付币种微单位);0 表示渠道未提供该数据。
|
||||
int64 provider_fee_micro = 23;
|
||||
int64 provider_tax_micro = 24;
|
||||
int64 provider_net_micro = 25;
|
||||
// paid_synced_at_ms 仅对 Google 账单有意义:Orders API 实付明细的同步时间,0 表示未同步。
|
||||
int64 paid_synced_at_ms = 26;
|
||||
}
|
||||
|
||||
message ListRechargeBillsRequest {
|
||||
@ -1059,6 +1070,51 @@ message ListRechargeBillsResponse {
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message GetRechargeBillSummaryRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
int64 user_id = 3;
|
||||
int64 seller_user_id = 4;
|
||||
int64 region_id = 5;
|
||||
string recharge_type = 6;
|
||||
string status = 7;
|
||||
string keyword = 8;
|
||||
int64 start_at_ms = 9;
|
||||
int64 end_at_ms = 10;
|
||||
}
|
||||
|
||||
message RechargeBillSummaryBucket {
|
||||
int64 bill_count = 1;
|
||||
int64 coin_amount = 2;
|
||||
int64 usd_minor_amount = 3;
|
||||
}
|
||||
|
||||
message GetRechargeBillSummaryResponse {
|
||||
RechargeBillSummaryBucket total = 1;
|
||||
RechargeBillSummaryBucket google_play = 2;
|
||||
RechargeBillSummaryBucket third_party = 3;
|
||||
}
|
||||
|
||||
message RefreshGooglePaymentPricesRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
repeated string transaction_ids = 3;
|
||||
}
|
||||
|
||||
message GooglePaymentPrice {
|
||||
string transaction_id = 1;
|
||||
string currency_code = 2;
|
||||
int64 paid_amount_micro = 3;
|
||||
int64 tax_micro = 4;
|
||||
int64 net_micro = 5;
|
||||
int64 synced_at_ms = 6;
|
||||
string error = 7;
|
||||
}
|
||||
|
||||
message RefreshGooglePaymentPricesResponse {
|
||||
repeated GooglePaymentPrice prices = 1;
|
||||
}
|
||||
|
||||
// WalletFeatureFlags 是我的页和钱包首页使用的操作开关,来源属于 wallet-service。
|
||||
message WalletFeatureFlags {
|
||||
bool recharge_enabled = 1;
|
||||
@ -2163,6 +2219,8 @@ service WalletService {
|
||||
rpc ListResourceShopPurchaseOrders(ListResourceShopPurchaseOrdersRequest) returns (ListResourceShopPurchaseOrdersResponse);
|
||||
rpc PurchaseResourceShopItem(PurchaseResourceShopItemRequest) returns (PurchaseResourceShopItemResponse);
|
||||
rpc ListRechargeBills(ListRechargeBillsRequest) returns (ListRechargeBillsResponse);
|
||||
rpc GetRechargeBillSummary(GetRechargeBillSummaryRequest) returns (GetRechargeBillSummaryResponse);
|
||||
rpc RefreshGooglePaymentPrices(RefreshGooglePaymentPricesRequest) returns (RefreshGooglePaymentPricesResponse);
|
||||
rpc GetWalletOverview(GetWalletOverviewRequest) returns (GetWalletOverviewResponse);
|
||||
rpc GetWalletValueSummary(GetWalletValueSummaryRequest) returns (GetWalletValueSummaryResponse);
|
||||
rpc GetUserGiftWall(GetUserGiftWallRequest) returns (GetUserGiftWallResponse);
|
||||
|
||||
@ -248,6 +248,8 @@ const (
|
||||
WalletService_ListResourceShopPurchaseOrders_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceShopPurchaseOrders"
|
||||
WalletService_PurchaseResourceShopItem_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseResourceShopItem"
|
||||
WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills"
|
||||
WalletService_GetRechargeBillSummary_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRechargeBillSummary"
|
||||
WalletService_RefreshGooglePaymentPrices_FullMethodName = "/hyapp.wallet.v1.WalletService/RefreshGooglePaymentPrices"
|
||||
WalletService_GetWalletOverview_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletOverview"
|
||||
WalletService_GetWalletValueSummary_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletValueSummary"
|
||||
WalletService_GetUserGiftWall_FullMethodName = "/hyapp.wallet.v1.WalletService/GetUserGiftWall"
|
||||
@ -350,6 +352,8 @@ type WalletServiceClient interface {
|
||||
ListResourceShopPurchaseOrders(ctx context.Context, in *ListResourceShopPurchaseOrdersRequest, opts ...grpc.CallOption) (*ListResourceShopPurchaseOrdersResponse, error)
|
||||
PurchaseResourceShopItem(ctx context.Context, in *PurchaseResourceShopItemRequest, opts ...grpc.CallOption) (*PurchaseResourceShopItemResponse, error)
|
||||
ListRechargeBills(ctx context.Context, in *ListRechargeBillsRequest, opts ...grpc.CallOption) (*ListRechargeBillsResponse, error)
|
||||
GetRechargeBillSummary(ctx context.Context, in *GetRechargeBillSummaryRequest, opts ...grpc.CallOption) (*GetRechargeBillSummaryResponse, error)
|
||||
RefreshGooglePaymentPrices(ctx context.Context, in *RefreshGooglePaymentPricesRequest, opts ...grpc.CallOption) (*RefreshGooglePaymentPricesResponse, error)
|
||||
GetWalletOverview(ctx context.Context, in *GetWalletOverviewRequest, opts ...grpc.CallOption) (*GetWalletOverviewResponse, error)
|
||||
GetWalletValueSummary(ctx context.Context, in *GetWalletValueSummaryRequest, opts ...grpc.CallOption) (*GetWalletValueSummaryResponse, error)
|
||||
GetUserGiftWall(ctx context.Context, in *GetUserGiftWallRequest, opts ...grpc.CallOption) (*GetUserGiftWallResponse, error)
|
||||
@ -877,6 +881,26 @@ func (c *walletServiceClient) ListRechargeBills(ctx context.Context, in *ListRec
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GetRechargeBillSummary(ctx context.Context, in *GetRechargeBillSummaryRequest, opts ...grpc.CallOption) (*GetRechargeBillSummaryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetRechargeBillSummaryResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_GetRechargeBillSummary_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) RefreshGooglePaymentPrices(ctx context.Context, in *RefreshGooglePaymentPricesRequest, opts ...grpc.CallOption) (*RefreshGooglePaymentPricesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(RefreshGooglePaymentPricesResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_RefreshGooglePaymentPrices_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GetWalletOverview(ctx context.Context, in *GetWalletOverviewRequest, opts ...grpc.CallOption) (*GetWalletOverviewResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetWalletOverviewResponse)
|
||||
@ -1400,6 +1424,8 @@ type WalletServiceServer interface {
|
||||
ListResourceShopPurchaseOrders(context.Context, *ListResourceShopPurchaseOrdersRequest) (*ListResourceShopPurchaseOrdersResponse, error)
|
||||
PurchaseResourceShopItem(context.Context, *PurchaseResourceShopItemRequest) (*PurchaseResourceShopItemResponse, error)
|
||||
ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error)
|
||||
GetRechargeBillSummary(context.Context, *GetRechargeBillSummaryRequest) (*GetRechargeBillSummaryResponse, error)
|
||||
RefreshGooglePaymentPrices(context.Context, *RefreshGooglePaymentPricesRequest) (*RefreshGooglePaymentPricesResponse, error)
|
||||
GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error)
|
||||
GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error)
|
||||
GetUserGiftWall(context.Context, *GetUserGiftWallRequest) (*GetUserGiftWallResponse, error)
|
||||
@ -1598,6 +1624,12 @@ func (UnimplementedWalletServiceServer) PurchaseResourceShopItem(context.Context
|
||||
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRechargeBillSummary(context.Context, *GetRechargeBillSummaryRequest) (*GetRechargeBillSummaryResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRechargeBillSummary not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) RefreshGooglePaymentPrices(context.Context, *RefreshGooglePaymentPricesRequest) (*RefreshGooglePaymentPricesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RefreshGooglePaymentPrices not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetWalletOverview not implemented")
|
||||
}
|
||||
@ -2606,6 +2638,42 @@ func _WalletService_ListRechargeBills_Handler(srv interface{}, ctx context.Conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GetRechargeBillSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetRechargeBillSummaryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).GetRechargeBillSummary(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_GetRechargeBillSummary_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).GetRechargeBillSummary(ctx, req.(*GetRechargeBillSummaryRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_RefreshGooglePaymentPrices_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RefreshGooglePaymentPricesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).RefreshGooglePaymentPrices(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_RefreshGooglePaymentPrices_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).RefreshGooglePaymentPrices(ctx, req.(*RefreshGooglePaymentPricesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GetWalletOverview_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetWalletOverviewRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -3647,6 +3715,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListRechargeBills",
|
||||
Handler: _WalletService_ListRechargeBills_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRechargeBillSummary",
|
||||
Handler: _WalletService_GetRechargeBillSummary_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RefreshGooglePaymentPrices",
|
||||
Handler: _WalletService_RefreshGooglePaymentPrices_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetWalletOverview",
|
||||
Handler: _WalletService_GetWalletOverview_Handler,
|
||||
|
||||
12
scripts/mysql/055_google_payment_paid_details.sql
Normal file
12
scripts/mysql/055_google_payment_paid_details.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- 财务系统 APP 充值详情:payment_orders 补 Google Play Orders API 实付明细列。
|
||||
-- 与 wallet-service 启动迁移 ensureGooglePaymentPaidDetailsSchema 等价,供手工升级线上库使用。
|
||||
|
||||
ALTER TABLE payment_orders
|
||||
ADD COLUMN paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '用户实付币种,来自 Google Orders API' AFTER amount_micro,
|
||||
ADD COLUMN paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '用户实付总额微单位(实付币种)' AFTER paid_currency_code,
|
||||
ADD COLUMN paid_tax_micro BIGINT NOT NULL DEFAULT 0 COMMENT '订单税额微单位(实付币种)' AFTER paid_amount_micro,
|
||||
ADD COLUMN paid_net_micro BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 扣费后开发者净收入微单位(实付币种)' AFTER paid_tax_micro,
|
||||
ADD COLUMN paid_synced_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实付明细同步时间,UTC epoch ms;0 表示未同步' AFTER paid_net_micro;
|
||||
|
||||
ALTER TABLE payment_orders
|
||||
ADD INDEX idx_payment_orders_wallet_tx (app_code, wallet_transaction_id);
|
||||
@ -41,6 +41,8 @@ type Client interface {
|
||||
SettleSalaryWithdrawal(ctx context.Context, req *walletv1.SettleSalaryWithdrawalRequest) (*walletv1.SettleSalaryWithdrawalResponse, error)
|
||||
ReleaseSalaryWithdrawal(ctx context.Context, req *walletv1.ReleaseSalaryWithdrawalRequest) (*walletv1.ReleaseSalaryWithdrawalResponse, error)
|
||||
ListRechargeBills(ctx context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error)
|
||||
GetRechargeBillSummary(ctx context.Context, req *walletv1.GetRechargeBillSummaryRequest) (*walletv1.GetRechargeBillSummaryResponse, error)
|
||||
RefreshGooglePaymentPrices(ctx context.Context, req *walletv1.RefreshGooglePaymentPricesRequest) (*walletv1.RefreshGooglePaymentPricesResponse, error)
|
||||
ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error)
|
||||
CreateTemporaryRechargeOrder(ctx context.Context, req *walletv1.CreateTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
|
||||
GetTemporaryRechargeOrder(ctx context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error)
|
||||
@ -194,6 +196,14 @@ func (c *GRPCClient) ListRechargeBills(ctx context.Context, req *walletv1.ListRe
|
||||
return c.client.ListRechargeBills(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetRechargeBillSummary(ctx context.Context, req *walletv1.GetRechargeBillSummaryRequest) (*walletv1.GetRechargeBillSummaryResponse, error) {
|
||||
return c.client.GetRechargeBillSummary(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) RefreshGooglePaymentPrices(ctx context.Context, req *walletv1.RefreshGooglePaymentPricesRequest) (*walletv1.RefreshGooglePaymentPricesResponse, error) {
|
||||
return c.client.RefreshGooglePaymentPrices(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListThirdPartyPaymentChannels(ctx context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) {
|
||||
return c.client.ListThirdPartyPaymentChannels(ctx, req)
|
||||
}
|
||||
|
||||
@ -28,6 +28,13 @@ type rechargeBillDTO struct {
|
||||
ExchangeUSDMinorAmount int64 `json:"exchangeUsdMinorAmount"`
|
||||
ProviderAmountMinor int64 `json:"providerAmountMinor,omitempty"`
|
||||
CreatedAtMS int64 `json:"createdAtMs"`
|
||||
ProviderCode string `json:"providerCode"`
|
||||
UserPaidCurrencyCode string `json:"userPaidCurrencyCode"`
|
||||
UserPaidAmountMicro int64 `json:"userPaidAmountMicro"`
|
||||
ProviderFeeMicro int64 `json:"providerFeeMicro"`
|
||||
ProviderTaxMicro int64 `json:"providerTaxMicro"`
|
||||
ProviderNetMicro int64 `json:"providerNetMicro"`
|
||||
PaidSyncedAtMS int64 `json:"paidSyncedAtMs"`
|
||||
User rechargeBillUserDTO `json:"user"`
|
||||
Seller rechargeBillUserDTO `json:"seller"`
|
||||
}
|
||||
@ -168,6 +175,13 @@ func rechargeBillFromProto(item *walletv1.RechargeBill) rechargeBillDTO {
|
||||
ExchangeUSDMinorAmount: item.GetExchangeUsdMinorAmount(),
|
||||
ProviderAmountMinor: item.GetProviderAmountMinor(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(),
|
||||
ProviderCode: item.GetProviderCode(),
|
||||
UserPaidCurrencyCode: item.GetUserPaidCurrencyCode(),
|
||||
UserPaidAmountMicro: item.GetUserPaidAmountMicro(),
|
||||
ProviderFeeMicro: item.GetProviderFeeMicro(),
|
||||
ProviderTaxMicro: item.GetProviderTaxMicro(),
|
||||
ProviderNetMicro: item.GetProviderNetMicro(),
|
||||
PaidSyncedAtMS: item.GetPaidSyncedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
196
server/admin/internal/modules/payment/recharge_bill_stats.go
Normal file
196
server/admin/internal/modules/payment/recharge_bill_stats.go
Normal file
@ -0,0 +1,196 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/repository"
|
||||
"hyapp-admin-server/internal/response"
|
||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type rechargeBillSummaryBucketDTO struct {
|
||||
BillCount int64 `json:"billCount"`
|
||||
CoinAmount int64 `json:"coinAmount"`
|
||||
USDMinorAmount int64 `json:"usdMinorAmount"`
|
||||
}
|
||||
|
||||
type rechargeBillSummaryDTO struct {
|
||||
Total rechargeBillSummaryBucketDTO `json:"total"`
|
||||
GooglePlay rechargeBillSummaryBucketDTO `json:"googlePlay"`
|
||||
ThirdParty rechargeBillSummaryBucketDTO `json:"thirdParty"`
|
||||
}
|
||||
|
||||
type rechargeBillRegionDTO struct {
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionCode string `json:"regionCode"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type googleRechargePaidRefreshRequest struct {
|
||||
TransactionIDs []string `json:"transactionIds"`
|
||||
}
|
||||
|
||||
type googleRechargePaidDTO struct {
|
||||
TransactionID string `json:"transactionId"`
|
||||
CurrencyCode string `json:"currencyCode"`
|
||||
PaidAmountMicro int64 `json:"paidAmountMicro"`
|
||||
TaxMicro int64 `json:"taxMicro"`
|
||||
NetMicro int64 `json:"netMicro"`
|
||||
SyncedAtMS int64 `json:"syncedAtMs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// GetRechargeBillSummary 返回与账单列表同一筛选口径的充值总和、Google 充值与三方充值聚合。
|
||||
func (h *Handler) GetRechargeBillSummary(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
appCode := appctx.FromContext(c.Request.Context())
|
||||
userFilter := shared.UserIdentityFilterFromQuery(c, "")
|
||||
userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
sellerFilter := shared.UserIdentityFilterFromQuery(c, "seller")
|
||||
sellerUserID, sellerMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, sellerFilter, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if (!userFilter.IsEmpty() && !userMatched) || (!sellerFilter.IsEmpty() && !sellerMatched) {
|
||||
response.OK(c, rechargeBillSummaryDTO{})
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.GetRechargeBillSummary(c.Request.Context(), &walletv1.GetRechargeBillSummaryRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appCode,
|
||||
UserId: userID,
|
||||
SellerUserId: sellerUserID,
|
||||
RegionId: queryInt64(c, "region_id", "regionId"),
|
||||
RechargeType: strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType")),
|
||||
Status: options.Status,
|
||||
Keyword: options.Keyword,
|
||||
StartAtMs: queryInt64(c, "start_at_ms", "startAtMs"),
|
||||
EndAtMs: queryInt64(c, "end_at_ms", "endAtMs"),
|
||||
})
|
||||
if err != nil {
|
||||
writeWalletError(c, err, "获取充值汇总失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, rechargeBillSummaryDTO{
|
||||
Total: rechargeBillSummaryBucketFromProto(resp.GetTotal()),
|
||||
GooglePlay: rechargeBillSummaryBucketFromProto(resp.GetGooglePlay()),
|
||||
ThirdParty: rechargeBillSummaryBucketFromProto(resp.GetThirdParty()),
|
||||
})
|
||||
}
|
||||
|
||||
func rechargeBillSummaryBucketFromProto(bucket *walletv1.RechargeBillSummaryBucket) rechargeBillSummaryBucketDTO {
|
||||
if bucket == nil {
|
||||
return rechargeBillSummaryBucketDTO{}
|
||||
}
|
||||
return rechargeBillSummaryBucketDTO{
|
||||
BillCount: bucket.GetBillCount(),
|
||||
CoinAmount: bucket.GetCoinAmount(),
|
||||
USDMinorAmount: bucket.GetUsdMinorAmount(),
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshGoogleRechargePaidDetails 对指定账单触发 Google Orders API 实付明细同步,供财务补齐历史订单实付。
|
||||
func (h *Handler) RefreshGoogleRechargePaidDetails(c *gin.Context) {
|
||||
var request googleRechargePaidRefreshRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil || len(request.TransactionIDs) == 0 {
|
||||
response.BadRequest(c, "交易号参数不正确")
|
||||
return
|
||||
}
|
||||
if len(request.TransactionIDs) > 100 {
|
||||
response.BadRequest(c, "单次最多刷新 100 笔账单")
|
||||
return
|
||||
}
|
||||
appCode := appctx.FromContext(c.Request.Context())
|
||||
resp, err := h.wallet.RefreshGooglePaymentPrices(c.Request.Context(), &walletv1.RefreshGooglePaymentPricesRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appCode,
|
||||
TransactionIds: request.TransactionIDs,
|
||||
})
|
||||
if err != nil {
|
||||
writeWalletError(c, err, "查询谷歌实付明细失败")
|
||||
return
|
||||
}
|
||||
items := make([]googleRechargePaidDTO, 0, len(resp.GetPrices()))
|
||||
for _, price := range resp.GetPrices() {
|
||||
items = append(items, googleRechargePaidDTO{
|
||||
TransactionID: price.GetTransactionId(),
|
||||
CurrencyCode: price.GetCurrencyCode(),
|
||||
PaidAmountMicro: price.GetPaidAmountMicro(),
|
||||
TaxMicro: price.GetTaxMicro(),
|
||||
NetMicro: price.GetNetMicro(),
|
||||
SyncedAtMS: price.GetSyncedAtMs(),
|
||||
Error: price.GetError(),
|
||||
})
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "refresh-google-recharge-paid", "payment_orders", "success", appCode)
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
// ListRechargeBillRegions 返回当前 App 的区域目录,用于充值详情按区域筛选;不受财务范围授权限制。
|
||||
func (h *Handler) ListRechargeBillRegions(c *gin.Context) {
|
||||
appCode := appctx.FromContext(c.Request.Context())
|
||||
regions, err := h.listRechargeBillRegions(c.Request.Context(), appCode)
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取区域列表失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": regions, "total": len(regions)})
|
||||
}
|
||||
|
||||
func (h *Handler) listRechargeBillRegions(ctx context.Context, appCode string) ([]rechargeBillRegionDTO, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
items := []rechargeBillRegionDTO{}
|
||||
seen := map[int64]struct{}{}
|
||||
if h.userDB != nil {
|
||||
rows, err := h.userDB.QueryContext(ctx, `
|
||||
SELECT region_id, region_code, name
|
||||
FROM regions
|
||||
WHERE app_code = ? AND status = 'active'
|
||||
ORDER BY sort_order ASC, name ASC`, appCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var item rechargeBillRegionDTO
|
||||
if err := rows.Scan(&item.RegionID, &item.RegionCode, &item.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen[item.RegionID] = struct{}{}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Yumi/Aslan 这类 legacy App 的区域目录在线上 likei Mongo;这里只取目录展示,账单筛选仍以 wallet 的 target_region_id 为准。
|
||||
for _, source := range h.moneyRegionSources {
|
||||
_, sourceRegions, _, err := source.ListMoneyMasterData(ctx, repository.MoneyAccess{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, region := range sourceRegions {
|
||||
if appctx.Normalize(region.AppCode) != appCode {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[region.RegionID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[region.RegionID] = struct{}{}
|
||||
items = append(items, rechargeBillRegionDTO{RegionID: region.RegionID, RegionCode: region.RegionCode, Name: region.Name})
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@ -12,6 +12,9 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
}
|
||||
|
||||
protected.GET("/admin/payment/recharge-bills", middleware.RequirePermission("payment-bill:view"), h.ListRechargeBills)
|
||||
protected.GET("/admin/payment/recharge-bills/summary", middleware.RequirePermission("payment-bill:view"), h.GetRechargeBillSummary)
|
||||
protected.POST("/admin/payment/recharge-bills/google-paid/refresh", middleware.RequirePermission("payment-bill:view"), h.RefreshGoogleRechargePaidDetails)
|
||||
protected.GET("/admin/payment/recharge-regions", middleware.RequirePermission("payment-bill:view"), h.ListRechargeBillRegions)
|
||||
protected.GET("/admin/payment/third-party-channels", middleware.RequirePermission("payment-third-party:view"), h.ListThirdPartyPaymentChannels)
|
||||
protected.GET("/admin/finance/scope", middleware.RequirePermission(financeViewPermission), h.GetMoneyScope)
|
||||
protected.GET("/admin/money/scope", middleware.RequireAnyPermission(financeViewPermission, legacyMoneyViewPermission), h.GetMoneyScope)
|
||||
|
||||
@ -836,6 +836,11 @@ CREATE TABLE IF NOT EXISTS payment_orders (
|
||||
coin_amount BIGINT NOT NULL COMMENT '到账金币数量',
|
||||
currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '商品币种编码',
|
||||
amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '商品金额微单位',
|
||||
paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '用户实付币种,来自 Google Orders API',
|
||||
paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '用户实付总额微单位(实付币种)',
|
||||
paid_tax_micro BIGINT NOT NULL DEFAULT 0 COMMENT '订单税额微单位(实付币种)',
|
||||
paid_net_micro BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 扣费后开发者净收入微单位(实付币种)',
|
||||
paid_synced_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实付明细同步时间,UTC epoch ms;0 表示未同步',
|
||||
provider_payload JSON NULL COMMENT 'Google Play 校验返回快照',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
@ -843,7 +848,8 @@ CREATE TABLE IF NOT EXISTS payment_orders (
|
||||
UNIQUE KEY uk_payment_orders_token (app_code, provider, purchase_token_hash),
|
||||
UNIQUE KEY uk_payment_orders_command (app_code, command_id),
|
||||
KEY idx_payment_orders_user_time (app_code, user_id, created_at_ms),
|
||||
KEY idx_payment_orders_provider_order (app_code, provider, provider_order_id)
|
||||
KEY idx_payment_orders_provider_order (app_code, provider, provider_order_id),
|
||||
KEY idx_payment_orders_wallet_tx (app_code, wallet_transaction_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单与 provider 审计表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS coin_seller_stock_records (
|
||||
|
||||
@ -131,6 +131,46 @@ func (c *Client) GetProductPurchase(ctx context.Context, packageName string, pur
|
||||
return purchase, nil
|
||||
}
|
||||
|
||||
// GetOrder 拉取 Play Developer API Orders 资源,取回用户实付币种、实付总额、税额和开发者净收入。
|
||||
// 需要服务账号在 Play Console 拥有“查看财务数据”权限,否则 Google 返回 403。
|
||||
func (c *Client) GetOrder(ctx context.Context, packageName string, orderID string) (ledger.GooglePlayOrder, error) {
|
||||
if c == nil {
|
||||
return ledger.GooglePlayOrder{}, xerr.New(xerr.Unavailable, "google play client is not configured")
|
||||
}
|
||||
packageName = c.normalizePackageName(packageName)
|
||||
orderID = strings.TrimSpace(orderID)
|
||||
if packageName == "" || orderID == "" {
|
||||
return ledger.GooglePlayOrder{}, xerr.New(xerr.InvalidArgument, "google play order request is incomplete")
|
||||
}
|
||||
endpoint := fmt.Sprintf("%s/androidpublisher/v3/applications/%s/orders/%s",
|
||||
c.apiBaseURL,
|
||||
url.PathEscape(packageName),
|
||||
url.PathEscape(orderID),
|
||||
)
|
||||
body, err := c.doJSON(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return ledger.GooglePlayOrder{}, err
|
||||
}
|
||||
var parsed orderResource
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return ledger.GooglePlayOrder{}, err
|
||||
}
|
||||
order := ledger.GooglePlayOrder{
|
||||
OrderID: parsed.OrderID,
|
||||
State: parsed.State,
|
||||
BuyerCountry: parsed.BuyerAddress.BuyerCountry,
|
||||
CurrencyCode: parsed.Total.CurrencyCode,
|
||||
TotalAmountMicro: parsed.Total.micro(),
|
||||
TaxAmountMicro: parsed.Tax.micro(),
|
||||
NetAmountMicro: parsed.DeveloperRevenueInBuyerCurrency.micro(),
|
||||
RawJSON: string(body),
|
||||
}
|
||||
if order.CurrencyCode == "" {
|
||||
order.CurrencyCode = parsed.Tax.CurrencyCode
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (c *Client) ConsumeProduct(ctx context.Context, packageName string, productID string, purchaseToken string) error {
|
||||
if c == nil {
|
||||
return xerr.New(xerr.Unavailable, "google play client is not configured")
|
||||
@ -279,3 +319,28 @@ type productLineItem struct {
|
||||
type productOfferDetails struct {
|
||||
ConsumptionState string `json:"consumptionState"`
|
||||
}
|
||||
|
||||
type orderResource struct {
|
||||
OrderID string `json:"orderId"`
|
||||
State string `json:"state"`
|
||||
BuyerAddress buyerAddress `json:"buyerAddress"`
|
||||
Total moneyAmount `json:"total"`
|
||||
Tax moneyAmount `json:"tax"`
|
||||
DeveloperRevenueInBuyerCurrency moneyAmount `json:"developerRevenueInBuyerCurrency"`
|
||||
}
|
||||
|
||||
type buyerAddress struct {
|
||||
BuyerCountry string `json:"buyerCountry"`
|
||||
}
|
||||
|
||||
// moneyAmount 是 google.type.Money 的 JSON 形态;units 是字符串形式的整数金额,nanos 是 10^-9 小数部分。
|
||||
type moneyAmount struct {
|
||||
CurrencyCode string `json:"currencyCode"`
|
||||
Units json.Number `json:"units"`
|
||||
Nanos int64 `json:"nanos"`
|
||||
}
|
||||
|
||||
func (m moneyAmount) micro() int64 {
|
||||
units, _ := m.Units.Int64()
|
||||
return units*1_000_000 + m.Nanos/1_000
|
||||
}
|
||||
|
||||
@ -84,6 +84,37 @@ type GooglePlayPurchase struct {
|
||||
RawJSON string
|
||||
}
|
||||
|
||||
// GooglePlayOrder 是 Play Developer API Orders 接口返回的订单实付快照;金额统一收敛为实付币种微单位。
|
||||
type GooglePlayOrder struct {
|
||||
OrderID string
|
||||
State string
|
||||
BuyerCountry string
|
||||
CurrencyCode string
|
||||
TotalAmountMicro int64
|
||||
TaxAmountMicro int64
|
||||
// NetAmountMicro 是 Google 扣除服务费和代缴税后的开发者净收入(实付币种);接口未返回时为 0。
|
||||
NetAmountMicro int64
|
||||
RawJSON string
|
||||
}
|
||||
|
||||
// GooglePaymentPaidDetails 是写回 payment_orders 的实付明细命令。
|
||||
type GooglePaymentPaidDetails struct {
|
||||
CurrencyCode string
|
||||
PaidAmountMicro int64
|
||||
TaxAmountMicro int64
|
||||
NetAmountMicro int64
|
||||
SyncedAtMS int64
|
||||
}
|
||||
|
||||
// GooglePaymentOrderRef 是按钱包交易反查 Google 支付订单、发起 Orders API 查询所需的最小定位信息。
|
||||
type GooglePaymentOrderRef struct {
|
||||
PaymentOrderID string
|
||||
TransactionID string
|
||||
PackageName string
|
||||
ProviderOrderID string
|
||||
PaidSyncedAtMS int64
|
||||
}
|
||||
|
||||
// GooglePaymentReceipt 是 Google 支付确认接口返回给 App 的稳定入账回执。
|
||||
type GooglePaymentReceipt struct {
|
||||
PaymentOrderID string
|
||||
|
||||
@ -25,6 +25,31 @@ type RechargeBill struct {
|
||||
ExchangeUSDMinorAmount int64
|
||||
ProviderAmountMinor int64
|
||||
CreatedAtMS int64
|
||||
// ProviderCode 标识实际支付渠道:google_play/mifapay/v5pay/usdt_trc20。
|
||||
ProviderCode string
|
||||
// UserPaid* 是用户实付事实:外部订单来自三方下单/回调快照,Google 订单来自 Play Orders API。
|
||||
UserPaidCurrencyCode string
|
||||
UserPaidAmountMicro int64
|
||||
// ProviderFee/Tax/Net 是三方扣款事实(实付币种微单位);0 表示渠道未提供。
|
||||
ProviderFeeMicro int64
|
||||
ProviderTaxMicro int64
|
||||
ProviderNetMicro int64
|
||||
// PaidSyncedAtMS 仅对 Google 账单有意义:Orders API 实付明细的同步时间,0 表示未同步。
|
||||
PaidSyncedAtMS int64
|
||||
}
|
||||
|
||||
// RechargeBillSummaryBucket 是充值账单的聚合桶:笔数、金币总数与美元最小单位总额。
|
||||
type RechargeBillSummaryBucket struct {
|
||||
BillCount int64
|
||||
CoinAmount int64
|
||||
USDMinorAmount int64
|
||||
}
|
||||
|
||||
// RechargeBillSummary 拆分财务系统关心的三个口径:总和、Google 充值与三方充值。
|
||||
type RechargeBillSummary struct {
|
||||
Total RechargeBillSummaryBucket
|
||||
GooglePlay RechargeBillSummaryBucket
|
||||
ThirdParty RechargeBillSummaryBucket
|
||||
}
|
||||
|
||||
// ListRechargeBillsQuery 是后台查询所有充值渠道账单的筛选条件。
|
||||
|
||||
@ -13,10 +13,11 @@ type ActivityBadgeClient interface {
|
||||
ConsumeAchievementEvent(ctx context.Context, req *activityv1.ConsumeAchievementEventRequest, opts ...grpc.CallOption) (*activityv1.ConsumeAchievementEventResponse, error)
|
||||
}
|
||||
|
||||
// GooglePlayClient 隔离 Google Play Developer API,service 只依赖购买校验和消耗能力。
|
||||
// GooglePlayClient 隔离 Google Play Developer API,service 只依赖购买校验、消耗和订单实付查询能力。
|
||||
type GooglePlayClient interface {
|
||||
GetProductPurchase(ctx context.Context, packageName string, purchaseToken string) (ledger.GooglePlayPurchase, error)
|
||||
ConsumeProduct(ctx context.Context, packageName string, productID string, purchaseToken string) error
|
||||
GetOrder(ctx context.Context, packageName string, orderID string) (ledger.GooglePlayOrder, error)
|
||||
}
|
||||
|
||||
// MifaPayCreateOrderRequest 是 wallet-service 传给 MiFaPay client 的已校验下单快照。
|
||||
|
||||
@ -94,6 +94,9 @@ type GameLedgerStore interface {
|
||||
// RechargeStore 管理内购充值档位、Google 支付和充值账单。
|
||||
type RechargeStore interface {
|
||||
ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error)
|
||||
SummarizeRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) (ledger.RechargeBillSummary, error)
|
||||
ListGooglePaymentOrderRefs(ctx context.Context, appCode string, transactionIDs []string) (map[string]ledger.GooglePaymentOrderRef, error)
|
||||
UpdateGooglePaymentPaidDetails(ctx context.Context, appCode string, paymentOrderID string, details ledger.GooglePaymentPaidDetails) error
|
||||
ListRechargeProducts(ctx context.Context, userID int64, regionID int64, platform string, audienceType string) ([]ledger.RechargeProduct, []string, error)
|
||||
ListAdminRechargeProducts(ctx context.Context, query ledger.ListRechargeProductsQuery) ([]ledger.RechargeProduct, int64, error)
|
||||
GetRechargeProduct(ctx context.Context, appCode string, productID int64) (ledger.RechargeProduct, error)
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/wallet-service/internal/domain/ledger"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ListRechargeBills 返回充值账单只读列表;所有充值来源必须先在 wallet-service 落充值记录。
|
||||
@ -19,6 +20,97 @@ func (s *Service) ListRechargeBills(ctx context.Context, query ledger.ListRechar
|
||||
return s.repository.ListRechargeBills(ctx, query)
|
||||
}
|
||||
|
||||
// GetRechargeBillSummary 按与账单列表一致的筛选口径聚合充值总和、Google 充值与三方充值。
|
||||
func (s *Service) GetRechargeBillSummary(ctx context.Context, query ledger.ListRechargeBillsQuery) (ledger.RechargeBillSummary, error) {
|
||||
if s.repository == nil {
|
||||
return ledger.RechargeBillSummary{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
query.AppCode = appcode.Normalize(query.AppCode)
|
||||
ctx = appcode.WithContext(ctx, query.AppCode)
|
||||
return s.repository.SummarizeRechargeBills(ctx, query)
|
||||
}
|
||||
|
||||
// GooglePaymentPriceResult 是单笔 Google 账单实付明细刷新结果;Err 非空表示该笔刷新失败但不影响其他账单。
|
||||
type GooglePaymentPriceResult struct {
|
||||
TransactionID string
|
||||
CurrencyCode string
|
||||
PaidAmountMicro int64
|
||||
TaxAmountMicro int64
|
||||
NetAmountMicro int64
|
||||
SyncedAtMS int64
|
||||
Err string
|
||||
}
|
||||
|
||||
const maxGooglePaymentPriceRefreshBatch = 100
|
||||
|
||||
// RefreshGooglePaymentPrices 用 Play Orders API 拉取账单的用户实付币种、总额、税额和净收入并写回 payment_orders。
|
||||
// 服务账号需要 Play Console 的“查看财务数据”权限;单笔失败只标记该笔的 Err,整批继续。
|
||||
func (s *Service) RefreshGooglePaymentPrices(ctx context.Context, appCode string, transactionIDs []string) ([]GooglePaymentPriceResult, error) {
|
||||
if s.repository == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||
}
|
||||
if s.googlePlay == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "google play client is not configured")
|
||||
}
|
||||
if len(transactionIDs) == 0 {
|
||||
return []GooglePaymentPriceResult{}, nil
|
||||
}
|
||||
if len(transactionIDs) > maxGooglePaymentPriceRefreshBatch {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "too many transaction_ids")
|
||||
}
|
||||
appCode = appcode.Normalize(appCode)
|
||||
ctx = appcode.WithContext(ctx, appCode)
|
||||
refs, err := s.repository.ListGooglePaymentOrderRefs(ctx, appCode, transactionIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := make([]GooglePaymentPriceResult, 0, len(transactionIDs))
|
||||
for _, transactionID := range transactionIDs {
|
||||
transactionID = strings.TrimSpace(transactionID)
|
||||
if transactionID == "" {
|
||||
continue
|
||||
}
|
||||
ref, ok := refs[transactionID]
|
||||
if !ok {
|
||||
results = append(results, GooglePaymentPriceResult{TransactionID: transactionID, Err: "not a google payment bill"})
|
||||
continue
|
||||
}
|
||||
result := s.refreshGooglePaymentPrice(ctx, appCode, ref)
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *Service) refreshGooglePaymentPrice(ctx context.Context, appCode string, ref ledger.GooglePaymentOrderRef) GooglePaymentPriceResult {
|
||||
result := GooglePaymentPriceResult{TransactionID: ref.TransactionID}
|
||||
if strings.TrimSpace(ref.ProviderOrderID) == "" {
|
||||
result.Err = "google order id is missing"
|
||||
return result
|
||||
}
|
||||
order, err := s.googlePlay.GetOrder(ctx, ref.PackageName, ref.ProviderOrderID)
|
||||
if err != nil {
|
||||
result.Err = err.Error()
|
||||
return result
|
||||
}
|
||||
details := ledger.GooglePaymentPaidDetails{
|
||||
CurrencyCode: order.CurrencyCode,
|
||||
PaidAmountMicro: order.TotalAmountMicro,
|
||||
TaxAmountMicro: order.TaxAmountMicro,
|
||||
NetAmountMicro: order.NetAmountMicro,
|
||||
SyncedAtMS: s.now().UnixMilli(),
|
||||
}
|
||||
if err := s.repository.UpdateGooglePaymentPaidDetails(ctx, appCode, ref.PaymentOrderID, details); err != nil {
|
||||
result.Err = err.Error()
|
||||
return result
|
||||
}
|
||||
result.CurrencyCode = details.CurrencyCode
|
||||
result.PaidAmountMicro = details.PaidAmountMicro
|
||||
result.TaxAmountMicro = details.TaxAmountMicro
|
||||
result.NetAmountMicro = details.NetAmountMicro
|
||||
result.SyncedAtMS = details.SyncedAtMS
|
||||
return result
|
||||
}
|
||||
|
||||
// ListRechargeProducts 返回区域化充值档位;region_id 必须由 gateway 从 user-service 资料解析。
|
||||
func (s *Service) ListRechargeProducts(ctx context.Context, userID int64, regionID int64, platform string, audienceType string) ([]ledger.RechargeProduct, []string, error) {
|
||||
if userID <= 0 || regionID <= 0 {
|
||||
@ -172,6 +264,8 @@ func (s *Service) ConfirmGooglePayment(ctx context.Context, command ledger.Googl
|
||||
if err != nil {
|
||||
return ledger.GooglePaymentReceipt{}, err
|
||||
}
|
||||
// 入账已完成,实付明细只影响财务展示:异步拉 Orders API,失败静默,后台可手动刷新补数。
|
||||
s.scheduleGooglePaymentPriceRefresh(command.AppCode, receipt.PaymentOrderID, command.PackageName, purchase.OrderID)
|
||||
if receipt.ConsumeState == ledger.PaymentConsumeStateConsumed {
|
||||
return receipt, nil
|
||||
}
|
||||
@ -189,6 +283,30 @@ func (s *Service) ConfirmGooglePayment(ctx context.Context, command ledger.Googl
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// scheduleGooglePaymentPriceRefresh 在支付确认返回后异步同步实付明细,不给支付主链路增加延迟或失败面。
|
||||
func (s *Service) scheduleGooglePaymentPriceRefresh(appCode string, paymentOrderID string, packageName string, orderID string) {
|
||||
if s.googlePlay == nil || s.repository == nil ||
|
||||
strings.TrimSpace(paymentOrderID) == "" || strings.TrimSpace(orderID) == "" {
|
||||
return
|
||||
}
|
||||
syncedAt := s.now().UnixMilli()
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(appcode.WithContext(context.Background(), appCode), 10*time.Second)
|
||||
defer cancel()
|
||||
order, err := s.googlePlay.GetOrder(ctx, packageName, orderID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = s.repository.UpdateGooglePaymentPaidDetails(ctx, appCode, paymentOrderID, ledger.GooglePaymentPaidDetails{
|
||||
CurrencyCode: order.CurrencyCode,
|
||||
PaidAmountMicro: order.TotalAmountMicro,
|
||||
TaxAmountMicro: order.TaxAmountMicro,
|
||||
NetAmountMicro: order.NetAmountMicro,
|
||||
SyncedAtMS: syncedAt,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) googleRechargeProductForPayment(ctx context.Context, command ledger.GooglePaymentCommand) (ledger.RechargeProduct, error) {
|
||||
if command.ProductID > 0 {
|
||||
return s.repository.GetRechargeProduct(ctx, command.AppCode, command.ProductID)
|
||||
|
||||
@ -6192,6 +6192,11 @@ func (f *fakeGooglePlayClient) ConsumeProduct(_ context.Context, _ string, produ
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeGooglePlayClient) GetOrder(_ context.Context, _ string, _ string) (ledger.GooglePlayOrder, error) {
|
||||
// 实付明细刷新是入账后的异步补数动作;fake 直接失败,让确认链路测试不产生额外副作用。
|
||||
return ledger.GooglePlayOrder{}, xerr.New(xerr.Unavailable, "orders api is not faked")
|
||||
}
|
||||
|
||||
type fakeMifaPayClient struct {
|
||||
createReq walletservice.MifaPayCreateOrderRequest
|
||||
createResp walletservice.MifaPayCreateOrderResponse
|
||||
|
||||
@ -299,6 +299,76 @@ func (r *Repository) receiptForGooglePaymentOrder(ctx context.Context, tx *sql.T
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
// ListGooglePaymentOrderRefs 按钱包交易 ID 批量反查 Google 支付订单定位信息;只返回命中的交易。
|
||||
func (r *Repository) ListGooglePaymentOrderRefs(ctx context.Context, appCode string, transactionIDs []string) (map[string]ledger.GooglePaymentOrderRef, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
ctx = contextWithCommandApp(ctx, appCode)
|
||||
cleaned := make([]string, 0, len(transactionIDs))
|
||||
for _, transactionID := range transactionIDs {
|
||||
if transactionID = strings.TrimSpace(transactionID); transactionID != "" {
|
||||
cleaned = append(cleaned, transactionID)
|
||||
}
|
||||
}
|
||||
if len(cleaned) == 0 {
|
||||
return map[string]ledger.GooglePaymentOrderRef{}, nil
|
||||
}
|
||||
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(cleaned)), ",")
|
||||
args := make([]any, 0, len(cleaned)+2)
|
||||
args = append(args, appcode.FromContext(ctx), ledger.PaymentProviderGooglePlay)
|
||||
for _, transactionID := range cleaned {
|
||||
args = append(args, transactionID)
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT payment_order_id, wallet_transaction_id, package_name, provider_order_id, paid_synced_at_ms
|
||||
FROM payment_orders
|
||||
WHERE app_code = ? AND provider = ? AND wallet_transaction_id IN (`+placeholders+`)`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
refs := make(map[string]ledger.GooglePaymentOrderRef, len(cleaned))
|
||||
for rows.Next() {
|
||||
var ref ledger.GooglePaymentOrderRef
|
||||
if err := rows.Scan(&ref.PaymentOrderID, &ref.TransactionID, &ref.PackageName, &ref.ProviderOrderID, &ref.PaidSyncedAtMS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refs[ref.TransactionID] = ref
|
||||
}
|
||||
return refs, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateGooglePaymentPaidDetails 把 Orders API 返回的实付明细写回 payment_orders;同步时间必须由调用方给出。
|
||||
func (r *Repository) UpdateGooglePaymentPaidDetails(ctx context.Context, appCode string, paymentOrderID string, details ledger.GooglePaymentPaidDetails) error {
|
||||
if r == nil || r.db == nil {
|
||||
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
ctx = contextWithCommandApp(ctx, appCode)
|
||||
paymentOrderID = strings.TrimSpace(paymentOrderID)
|
||||
if paymentOrderID == "" {
|
||||
return xerr.New(xerr.InvalidArgument, "payment_order_id is required")
|
||||
}
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
UPDATE payment_orders
|
||||
SET paid_currency_code = ?, paid_amount_micro = ?, paid_tax_micro = ?, paid_net_micro = ?, paid_synced_at_ms = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND payment_order_id = ? AND provider = ?`,
|
||||
strings.ToUpper(strings.TrimSpace(details.CurrencyCode)),
|
||||
details.PaidAmountMicro,
|
||||
details.TaxAmountMicro,
|
||||
details.NetAmountMicro,
|
||||
details.SyncedAtMS,
|
||||
time.Now().UnixMilli(),
|
||||
appcode.FromContext(ctx),
|
||||
paymentOrderID,
|
||||
ledger.PaymentProviderGooglePlay,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
type googlePaymentMetadata struct {
|
||||
AppCode string `json:"app_code"`
|
||||
PaymentOrderID string `json:"payment_order_id"`
|
||||
|
||||
@ -2,6 +2,9 @@ package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hyapp/pkg/appcode"
|
||||
@ -23,16 +26,21 @@ func (r *Repository) ListRechargeBills(ctx context.Context, query ledger.ListRec
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 充值事实仍以 wallet_recharge_records 为主表;external_recharge_orders 只补 H5 三方订单的实付金额展示,使用 wallet_transaction_id 索引命中,避免列表查询回扫订单表。
|
||||
// 充值事实仍以 wallet_recharge_records 为主表;external_recharge_orders 补 H5 三方订单的实付币种和金额,
|
||||
// payment_orders 补 Google 订单经 Orders API 同步的实付明细,两个 LEFT JOIN 都走 wallet_transaction_id 索引。
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT rr.app_code, rr.transaction_id, wt.command_id, wt.biz_type, wt.status, wt.external_ref,
|
||||
rr.user_id, rr.seller_user_id, rr.seller_region_id, rr.target_region_id,
|
||||
rr.policy_id, rr.policy_version, rr.currency_code,
|
||||
rr.coin_amount, rr.usd_minor_amount, rr.exchange_coin_amount, rr.exchange_usd_minor_amount,
|
||||
COALESCE(eo.provider_amount_minor, 0), rr.created_at_ms
|
||||
COALESCE(eo.provider_amount_minor, 0), rr.created_at_ms,
|
||||
COALESCE(eo.provider_code, ''), COALESCE(eo.currency_code, ''), COALESCE(CAST(eo.provider_payload AS CHAR), ''),
|
||||
COALESCE(po.paid_currency_code, ''), COALESCE(po.paid_amount_micro, 0), COALESCE(po.paid_tax_micro, 0),
|
||||
COALESCE(po.paid_net_micro, 0), COALESCE(po.paid_synced_at_ms, 0)
|
||||
FROM wallet_recharge_records rr
|
||||
JOIN wallet_transactions wt ON wt.app_code = rr.app_code AND wt.transaction_id = rr.transaction_id
|
||||
LEFT JOIN external_recharge_orders eo ON eo.app_code = rr.app_code AND eo.wallet_transaction_id = rr.transaction_id
|
||||
LEFT JOIN payment_orders po ON po.app_code = rr.app_code AND po.wallet_transaction_id = rr.transaction_id
|
||||
`+where+`
|
||||
ORDER BY rr.created_at_ms DESC, rr.transaction_id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
@ -46,6 +54,9 @@ func (r *Repository) ListRechargeBills(ctx context.Context, query ledger.ListRec
|
||||
items := make([]ledger.RechargeBill, 0, query.PageSize)
|
||||
for rows.Next() {
|
||||
var item ledger.RechargeBill
|
||||
var externalProviderCode, externalCurrencyCode, externalPayloadJSON string
|
||||
var paidCurrencyCode string
|
||||
var paidAmountMicro, paidTaxMicro, paidNetMicro, paidSyncedAtMS int64
|
||||
if err := rows.Scan(
|
||||
&item.AppCode,
|
||||
&item.TransactionID,
|
||||
@ -66,9 +77,19 @@ func (r *Repository) ListRechargeBills(ctx context.Context, query ledger.ListRec
|
||||
&item.ExchangeUSDMinorAmount,
|
||||
&item.ProviderAmountMinor,
|
||||
&item.CreatedAtMS,
|
||||
&externalProviderCode,
|
||||
&externalCurrencyCode,
|
||||
&externalPayloadJSON,
|
||||
&paidCurrencyCode,
|
||||
&paidAmountMicro,
|
||||
&paidTaxMicro,
|
||||
&paidNetMicro,
|
||||
&paidSyncedAtMS,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
fillRechargeBillPaidFacts(&item, externalProviderCode, externalCurrencyCode, externalPayloadJSON,
|
||||
paidCurrencyCode, paidAmountMicro, paidTaxMicro, paidNetMicro, paidSyncedAtMS)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
@ -77,6 +98,115 @@ func (r *Repository) ListRechargeBills(ctx context.Context, query ledger.ListRec
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// fillRechargeBillPaidFacts 把三方订单快照和 Google 实付明细收敛成账单统一的实付字段。
|
||||
func fillRechargeBillPaidFacts(item *ledger.RechargeBill, externalProviderCode string, externalCurrencyCode string, externalPayloadJSON string,
|
||||
paidCurrencyCode string, paidAmountMicro int64, paidTaxMicro int64, paidNetMicro int64, paidSyncedAtMS int64) {
|
||||
if item.RechargeType == bizTypeGooglePlayRecharge {
|
||||
item.ProviderCode = "google_play"
|
||||
item.UserPaidCurrencyCode = paidCurrencyCode
|
||||
item.UserPaidAmountMicro = paidAmountMicro
|
||||
item.ProviderTaxMicro = paidTaxMicro
|
||||
item.ProviderNetMicro = paidNetMicro
|
||||
item.PaidSyncedAtMS = paidSyncedAtMS
|
||||
// Google 不单独返回服务费,只能由 实付总额 - 净收入 - 税 推出;任一缺失则保持 0。
|
||||
if paidAmountMicro > 0 && paidNetMicro > 0 {
|
||||
if fee := paidAmountMicro - paidNetMicro - paidTaxMicro; fee > 0 {
|
||||
item.ProviderFeeMicro = fee
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
item.ProviderCode = externalProviderCode
|
||||
item.UserPaidCurrencyCode = externalCurrencyCode
|
||||
if item.ProviderAmountMinor > 0 {
|
||||
item.UserPaidAmountMicro = item.ProviderAmountMinor * 10_000
|
||||
}
|
||||
// V5Pay 的回调和查单快照带 fee/tax/accAmount;MiFaPay 与 USDT 没有该数据,解析不到就保持 0。
|
||||
fee, tax, net := externalRechargeSettlementFromPayload(externalPayloadJSON)
|
||||
item.ProviderFeeMicro = fee
|
||||
item.ProviderTaxMicro = tax
|
||||
item.ProviderNetMicro = net
|
||||
}
|
||||
|
||||
// externalRechargeSettlementFromPayload 从 provider_payload 快照解析三方结算字段(实付币种微单位)。
|
||||
func externalRechargeSettlementFromPayload(payloadJSON string) (feeMicro int64, taxMicro int64, netMicro int64) {
|
||||
payloadJSON = strings.TrimSpace(payloadJSON)
|
||||
if payloadJSON == "" || !strings.Contains(payloadJSON, "accAmount") && !strings.Contains(payloadJSON, "\"fee\"") {
|
||||
return 0, 0, 0
|
||||
}
|
||||
var payload struct {
|
||||
Fee string `json:"fee"`
|
||||
Tax string `json:"tax"`
|
||||
AccAmount string `json:"accAmount"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
return decimalAmountToMicro(payload.Fee), decimalAmountToMicro(payload.Tax), decimalAmountToMicro(payload.AccAmount)
|
||||
}
|
||||
|
||||
// decimalAmountToMicro 把三方返回的十进制金额字符串(如 "10.50")转成微单位;非法或负数一律按 0 处理。
|
||||
func decimalAmountToMicro(value string) int64 {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
parsed, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int64(math.Round(parsed * 1_000_000))
|
||||
}
|
||||
|
||||
// SummarizeRechargeBills 按与列表一致的筛选口径聚合充值账单,拆分 Google 与三方两个渠道桶。
|
||||
func (r *Repository) SummarizeRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) (ledger.RechargeBillSummary, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return ledger.RechargeBillSummary{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
ctx = contextWithCommandApp(ctx, query.AppCode)
|
||||
query = normalizeRechargeBillsQuery(query)
|
||||
query.AppCode = appcode.FromContext(ctx)
|
||||
|
||||
where, args := rechargeBillsWhereSQL(query)
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT wt.biz_type, COUNT(*), COALESCE(SUM(rr.coin_amount), 0), COALESCE(SUM(rr.usd_minor_amount), 0)
|
||||
FROM wallet_recharge_records rr
|
||||
JOIN wallet_transactions wt ON wt.app_code = rr.app_code AND wt.transaction_id = rr.transaction_id
|
||||
`+where+`
|
||||
GROUP BY wt.biz_type`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return ledger.RechargeBillSummary{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var summary ledger.RechargeBillSummary
|
||||
for rows.Next() {
|
||||
var bizType string
|
||||
var bucket ledger.RechargeBillSummaryBucket
|
||||
if err := rows.Scan(&bizType, &bucket.BillCount, &bucket.CoinAmount, &bucket.USDMinorAmount); err != nil {
|
||||
return ledger.RechargeBillSummary{}, err
|
||||
}
|
||||
summary.Total.BillCount += bucket.BillCount
|
||||
summary.Total.CoinAmount += bucket.CoinAmount
|
||||
summary.Total.USDMinorAmount += bucket.USDMinorAmount
|
||||
if bizType == bizTypeGooglePlayRecharge {
|
||||
summary.GooglePlay.BillCount += bucket.BillCount
|
||||
summary.GooglePlay.CoinAmount += bucket.CoinAmount
|
||||
summary.GooglePlay.USDMinorAmount += bucket.USDMinorAmount
|
||||
} else {
|
||||
summary.ThirdParty.BillCount += bucket.BillCount
|
||||
summary.ThirdParty.CoinAmount += bucket.CoinAmount
|
||||
summary.ThirdParty.USDMinorAmount += bucket.USDMinorAmount
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return ledger.RechargeBillSummary{}, err
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func (r *Repository) countRechargeBillRows(ctx context.Context, where string, args ...any) (int64, error) {
|
||||
var total int64
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
|
||||
@ -57,6 +57,10 @@ func Open(ctx context.Context, dsn string) (*Repository, error) {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureGooglePaymentPaidDetailsSchema(ctx, db); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Repository{db: db}, nil
|
||||
}
|
||||
|
||||
@ -158,6 +158,29 @@ func ensureExternalRechargeSchema(ctx context.Context, db *sql.DB) error {
|
||||
return seedThirdPartyPaymentDefaults(ctx, db)
|
||||
}
|
||||
|
||||
// ensureGooglePaymentPaidDetailsSchema 给 payment_orders 补 Google Orders API 实付明细列;
|
||||
// 财务列表按 wallet_transaction_id 反查 Google 支付订单,需要独立索引避免回表扫描。
|
||||
func ensureGooglePaymentPaidDetailsSchema(ctx context.Context, db *sql.DB) error {
|
||||
alters := []string{
|
||||
`ALTER TABLE payment_orders ADD COLUMN paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '用户实付币种,来自 Google Orders API' AFTER amount_micro`,
|
||||
`ALTER TABLE payment_orders ADD COLUMN paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '用户实付总额微单位(实付币种)' AFTER paid_currency_code`,
|
||||
`ALTER TABLE payment_orders ADD COLUMN paid_tax_micro BIGINT NOT NULL DEFAULT 0 COMMENT '订单税额微单位(实付币种)' AFTER paid_amount_micro`,
|
||||
`ALTER TABLE payment_orders ADD COLUMN paid_net_micro BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 扣费后开发者净收入微单位(实付币种)' AFTER paid_tax_micro`,
|
||||
`ALTER TABLE payment_orders ADD COLUMN paid_synced_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实付明细同步时间,UTC epoch ms;0 表示未同步' AFTER paid_net_micro`,
|
||||
}
|
||||
for _, stmt := range alters {
|
||||
if _, err := db.ExecContext(ctx, stmt); err != nil && !isDuplicateColumnError(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
ALTER TABLE payment_orders
|
||||
ADD INDEX idx_payment_orders_wallet_tx (app_code, wallet_transaction_id)`); err != nil && !isDuplicateKeyNameError(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureGiftDiamondRatioSchema(ctx context.Context, db *sql.DB) error {
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
|
||||
|
||||
@ -36,6 +36,60 @@ func (s *Server) ListRechargeBills(ctx context.Context, req *walletv1.ListRechar
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetRechargeBillSummary 暴露充值账单聚合查询,筛选口径与列表完全一致。
|
||||
func (s *Server) GetRechargeBillSummary(ctx context.Context, req *walletv1.GetRechargeBillSummaryRequest) (*walletv1.GetRechargeBillSummaryResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||
summary, err := s.svc.GetRechargeBillSummary(ctx, ledger.ListRechargeBillsQuery{
|
||||
AppCode: req.GetAppCode(),
|
||||
UserID: req.GetUserId(),
|
||||
SellerUserID: req.GetSellerUserId(),
|
||||
RegionID: req.GetRegionId(),
|
||||
RechargeType: req.GetRechargeType(),
|
||||
Status: req.GetStatus(),
|
||||
Keyword: req.GetKeyword(),
|
||||
StartAtMS: req.GetStartAtMs(),
|
||||
EndAtMS: req.GetEndAtMs(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &walletv1.GetRechargeBillSummaryResponse{
|
||||
Total: rechargeBillSummaryBucketToProto(summary.Total),
|
||||
GooglePlay: rechargeBillSummaryBucketToProto(summary.GooglePlay),
|
||||
ThirdParty: rechargeBillSummaryBucketToProto(summary.ThirdParty),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshGooglePaymentPrices 触发 Google Orders API 实付明细同步;单笔失败不阻断整批。
|
||||
func (s *Server) RefreshGooglePaymentPrices(ctx context.Context, req *walletv1.RefreshGooglePaymentPricesRequest) (*walletv1.RefreshGooglePaymentPricesResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||
results, err := s.svc.RefreshGooglePaymentPrices(ctx, req.GetAppCode(), req.GetTransactionIds())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
resp := &walletv1.RefreshGooglePaymentPricesResponse{Prices: make([]*walletv1.GooglePaymentPrice, 0, len(results))}
|
||||
for _, result := range results {
|
||||
resp.Prices = append(resp.Prices, &walletv1.GooglePaymentPrice{
|
||||
TransactionId: result.TransactionID,
|
||||
CurrencyCode: result.CurrencyCode,
|
||||
PaidAmountMicro: result.PaidAmountMicro,
|
||||
TaxMicro: result.TaxAmountMicro,
|
||||
NetMicro: result.NetAmountMicro,
|
||||
SyncedAtMs: result.SyncedAtMS,
|
||||
Error: result.Err,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func rechargeBillSummaryBucketToProto(bucket ledger.RechargeBillSummaryBucket) *walletv1.RechargeBillSummaryBucket {
|
||||
return &walletv1.RechargeBillSummaryBucket{
|
||||
BillCount: bucket.BillCount,
|
||||
CoinAmount: bucket.CoinAmount,
|
||||
UsdMinorAmount: bucket.USDMinorAmount,
|
||||
}
|
||||
}
|
||||
|
||||
func rechargeBillToProto(item ledger.RechargeBill) *walletv1.RechargeBill {
|
||||
return &walletv1.RechargeBill{
|
||||
AppCode: item.AppCode,
|
||||
@ -57,5 +111,12 @@ func rechargeBillToProto(item ledger.RechargeBill) *walletv1.RechargeBill {
|
||||
ExchangeUsdMinorAmount: item.ExchangeUSDMinorAmount,
|
||||
ProviderAmountMinor: item.ProviderAmountMinor,
|
||||
CreatedAtMs: item.CreatedAtMS,
|
||||
ProviderCode: item.ProviderCode,
|
||||
UserPaidCurrencyCode: item.UserPaidCurrencyCode,
|
||||
UserPaidAmountMicro: item.UserPaidAmountMicro,
|
||||
ProviderFeeMicro: item.ProviderFeeMicro,
|
||||
ProviderTaxMicro: item.ProviderTaxMicro,
|
||||
ProviderNetMicro: item.ProviderNetMicro,
|
||||
PaidSyncedAtMs: item.PaidSyncedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user