197 lines
6.8 KiB
Go
197 lines
6.8 KiB
Go
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
|
||
}
|