674 lines
21 KiB
Go
674 lines
21 KiB
Go
package payment
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/middleware"
|
||
"hyapp-admin-server/internal/model"
|
||
"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"
|
||
)
|
||
|
||
const (
|
||
financeViewPermission = "finance:view"
|
||
legacyMoneyViewPermission = "money:view"
|
||
temporaryPaymentLinkCreatePermission = "payment-temporary-link:create"
|
||
temporaryRechargeAudience = "temporary"
|
||
)
|
||
|
||
type temporaryPaymentLinkCreateRequest struct {
|
||
AppCode string `json:"appCode"`
|
||
RegionID int64 `json:"regionId"`
|
||
USDMinorAmount int64 `json:"usdMinorAmount"`
|
||
ProviderCode string `json:"providerCode"`
|
||
PaymentMethodID int64 `json:"paymentMethodId"`
|
||
ReturnURL string `json:"returnUrl"`
|
||
Language string `json:"language"`
|
||
}
|
||
|
||
type moneyScopeItemDTO struct {
|
||
AppCode string `json:"appCode"`
|
||
RegionID int64 `json:"regionId"`
|
||
}
|
||
|
||
type moneyAppDTO struct {
|
||
AppID int64 `json:"appId"`
|
||
AppCode string `json:"appCode"`
|
||
AppName string `json:"appName"`
|
||
PackageName string `json:"packageName"`
|
||
Platform string `json:"platform"`
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
type moneyRegionDTO struct {
|
||
AppCode string `json:"appCode"`
|
||
RegionID int64 `json:"regionId"`
|
||
RegionCode string `json:"regionCode"`
|
||
Name string `json:"name"`
|
||
Status string `json:"status"`
|
||
SortOrder int `json:"sortOrder"`
|
||
Countries []string `json:"countries"`
|
||
}
|
||
|
||
type moneyCountryDTO struct {
|
||
AppCode string `json:"appCode"`
|
||
CountryID int64 `json:"countryId"`
|
||
CountryCode string `json:"countryCode"`
|
||
CountryName string `json:"countryName"`
|
||
CountryDisplayName string `json:"countryDisplayName"`
|
||
Flag string `json:"flag"`
|
||
Enabled bool `json:"enabled"`
|
||
RegionID int64 `json:"regionId"`
|
||
SortOrder int `json:"sortOrder"`
|
||
}
|
||
|
||
type moneyPerformanceItemDTO struct {
|
||
AppCode string `json:"appCode"`
|
||
RegionID int64 `json:"regionId"`
|
||
OperatorUserID uint `json:"operatorUserId"`
|
||
OperatorName string `json:"operatorName"`
|
||
OperatorAccount string `json:"operatorAccount"`
|
||
PaidLinkCount int64 `json:"paidLinkCount"`
|
||
PaidUSDMinor int64 `json:"paidUsdMinor"`
|
||
LastPaidAtMS int64 `json:"lastPaidAtMs"`
|
||
}
|
||
|
||
type paidTemporaryOrder struct {
|
||
AppCode string
|
||
OrderID string
|
||
USDMinorAmount int64
|
||
Status string
|
||
CountryCode string
|
||
ProviderCode string
|
||
PaymentMethodID int64
|
||
PayURL string
|
||
CreatedAtMS int64
|
||
UpdatedAtMS int64
|
||
}
|
||
|
||
func (h *Handler) GetMoneyScope(c *gin.Context) {
|
||
access, ok := h.moneyAccess(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if h.userDB == nil {
|
||
response.ServerError(c, "user mysql is not configured")
|
||
return
|
||
}
|
||
apps, regions, countries, err := h.loadMoneyMasterData(c.Request.Context(), access)
|
||
if err != nil {
|
||
response.ServerError(c, "获取财务范围失败")
|
||
return
|
||
}
|
||
response.OK(c, gin.H{
|
||
"all": access.All,
|
||
"scopes": moneyScopeDTOsFromModel(access.Scopes),
|
||
"apps": apps,
|
||
"regions": regions,
|
||
"countries": countries,
|
||
})
|
||
}
|
||
|
||
func (h *Handler) GetMoneyPerformance(c *gin.Context) {
|
||
access, ok := h.moneyAccess(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if h.walletDB == nil || h.store == nil {
|
||
response.OK(c, gin.H{"items": []moneyPerformanceItemDTO{}, "summary": moneyPerformanceSummary(nil)})
|
||
return
|
||
}
|
||
appCodes, err := h.performanceAppCodes(c.Request.Context(), access, firstQuery(c, "app_code", "appCode"))
|
||
if err != nil {
|
||
response.ServerError(c, "获取绩效 App 失败")
|
||
return
|
||
}
|
||
regionID := queryInt64(c, "region_id", "regionId")
|
||
operatorUserID := queryUint(c, "operator_user_id", "operatorUserId")
|
||
startAtMS := queryInt64(c, "start_at_ms", "startAtMs")
|
||
endAtMS := queryInt64(c, "end_at_ms", "endAtMs")
|
||
|
||
aggregates := map[string]*moneyPerformanceItemDTO{}
|
||
userIDs := []uint{}
|
||
for _, appCode := range appCodes {
|
||
orders, err := h.listPaidTemporaryOrders(c.Request.Context(), appCode, startAtMS, endAtMS)
|
||
if err != nil {
|
||
response.ServerError(c, "获取支付绩效失败")
|
||
return
|
||
}
|
||
orderIDs := make([]string, 0, len(orders))
|
||
for _, order := range orders {
|
||
orderIDs = append(orderIDs, order.OrderID)
|
||
}
|
||
owners, err := h.store.TemporaryPaymentLinkOwners(appCode, orderIDs)
|
||
if err != nil {
|
||
response.ServerError(c, "获取支付链接归属失败")
|
||
return
|
||
}
|
||
for _, order := range orders {
|
||
owner, ok := owners[order.OrderID]
|
||
if !ok || !access.All && !access.Allows(owner.AppCode, owner.RegionID) {
|
||
continue
|
||
}
|
||
if regionID > 0 && owner.RegionID != regionID {
|
||
continue
|
||
}
|
||
if operatorUserID > 0 && owner.AdminUserID != operatorUserID {
|
||
continue
|
||
}
|
||
key := fmt.Sprintf("%s:%d:%d", owner.AppCode, owner.RegionID, owner.AdminUserID)
|
||
item := aggregates[key]
|
||
if item == nil {
|
||
item = &moneyPerformanceItemDTO{
|
||
AppCode: owner.AppCode,
|
||
RegionID: owner.RegionID,
|
||
OperatorUserID: owner.AdminUserID,
|
||
}
|
||
aggregates[key] = item
|
||
userIDs = append(userIDs, owner.AdminUserID)
|
||
}
|
||
item.PaidLinkCount++
|
||
item.PaidUSDMinor += order.USDMinorAmount
|
||
if order.UpdatedAtMS > item.LastPaidAtMS {
|
||
item.LastPaidAtMS = order.UpdatedAtMS
|
||
}
|
||
}
|
||
}
|
||
|
||
users, err := h.store.FindUsersByIDs(userIDs)
|
||
if err != nil {
|
||
response.ServerError(c, "获取运营用户失败")
|
||
return
|
||
}
|
||
items := make([]moneyPerformanceItemDTO, 0, len(aggregates))
|
||
for _, item := range aggregates {
|
||
if user, ok := users[item.OperatorUserID]; ok {
|
||
item.OperatorName = user.Name
|
||
item.OperatorAccount = user.Username
|
||
}
|
||
items = append(items, *item)
|
||
}
|
||
sort.Slice(items, func(i, j int) bool {
|
||
if items[i].PaidUSDMinor == items[j].PaidUSDMinor {
|
||
return items[i].LastPaidAtMS > items[j].LastPaidAtMS
|
||
}
|
||
return items[i].PaidUSDMinor > items[j].PaidUSDMinor
|
||
})
|
||
response.OK(c, gin.H{"items": items, "summary": moneyPerformanceSummary(items)})
|
||
}
|
||
|
||
func (h *Handler) CreateTemporaryPaymentLink(c *gin.Context) {
|
||
if h.store == nil {
|
||
response.ServerError(c, "admin store is not configured")
|
||
return
|
||
}
|
||
var request temporaryPaymentLinkCreateRequest
|
||
if err := c.ShouldBindJSON(&request); err != nil {
|
||
response.BadRequest(c, "支付链接参数不正确")
|
||
return
|
||
}
|
||
appCode := appctx.Normalize(request.AppCode)
|
||
if appCode == "" || request.RegionID <= 0 || request.USDMinorAmount <= 0 || request.PaymentMethodID <= 0 {
|
||
response.BadRequest(c, "支付链接参数不完整")
|
||
return
|
||
}
|
||
access, ok := h.moneyAccess(c)
|
||
if !ok {
|
||
return
|
||
}
|
||
if !access.Allows(appCode, request.RegionID) {
|
||
response.Forbidden(c, "没有该 App 区域的财务范围")
|
||
return
|
||
}
|
||
method, err := h.findTemporaryPaymentMethod(c.Request.Context(), appCode, request.ProviderCode, request.PaymentMethodID)
|
||
if err != nil {
|
||
if errors.Is(err, errTemporaryPaymentMethodInvalid) {
|
||
response.BadRequest(c, "支付方式不正确")
|
||
return
|
||
}
|
||
writeWalletError(c, err, "获取支付方式失败")
|
||
return
|
||
}
|
||
if ok, err := h.countryBelongsToRegion(c.Request.Context(), appCode, request.RegionID, method.GetCountryCode()); err != nil {
|
||
response.ServerError(c, "校验区域国家失败")
|
||
return
|
||
} else if !ok {
|
||
response.BadRequest(c, "支付方式国家不属于所选区域")
|
||
return
|
||
}
|
||
language := strings.TrimSpace(request.Language)
|
||
if language == "" {
|
||
language = "en"
|
||
}
|
||
resp, err := h.wallet.CreateTemporaryRechargeOrder(c.Request.Context(), &walletv1.CreateTemporaryRechargeOrderRequest{
|
||
RequestId: middleware.CurrentRequestID(c),
|
||
AppCode: appCode,
|
||
CommandId: temporaryPaymentCommandID(shared.ActorFromContext(c).UserID, middleware.CurrentRequestID(c)),
|
||
CoinAmount: 0,
|
||
UsdMinorAmount: request.USDMinorAmount,
|
||
ProviderCode: method.GetProviderCode(),
|
||
PaymentMethodId: method.GetMethodId(),
|
||
ReturnUrl: strings.TrimSpace(request.ReturnURL),
|
||
ClientIp: c.ClientIP(),
|
||
Language: language,
|
||
PayerName: shared.ActorFromContext(c).Username,
|
||
PayerAccount: shared.ActorFromContext(c).Username,
|
||
})
|
||
if err != nil {
|
||
writeWalletError(c, err, "创建支付链接失败")
|
||
return
|
||
}
|
||
order := resp.GetOrder()
|
||
if order == nil || strings.TrimSpace(order.GetOrderId()) == "" {
|
||
response.ServerError(c, "创建支付链接失败")
|
||
return
|
||
}
|
||
if err := h.store.RecordTemporaryPaymentLinkOwner(model.TemporaryPaymentLinkOwner{
|
||
AppCode: appCode,
|
||
OrderID: order.GetOrderId(),
|
||
AdminUserID: shared.ActorFromContext(c).UserID,
|
||
RegionID: request.RegionID,
|
||
}); err != nil {
|
||
response.ServerError(c, "记录支付链接归属失败")
|
||
return
|
||
}
|
||
item := temporaryPaymentLinkFromProto(order)
|
||
items, err := h.enrichAndFilterTemporaryLinks(appCode, []temporaryPaymentLinkDTO{item}, access, 0, 0)
|
||
if err != nil {
|
||
response.ServerError(c, "获取支付链接归属失败")
|
||
return
|
||
}
|
||
if len(items) > 0 {
|
||
item = items[0]
|
||
}
|
||
shared.OperationLogWithResourceID(c, h.audit, "create-temporary-payment-link", "external_recharge_orders", order.GetOrderId(), "success", fmt.Sprintf("%s:%d", appCode, request.RegionID))
|
||
response.Created(c, item)
|
||
}
|
||
|
||
func (h *Handler) moneyAccess(c *gin.Context) (repository.MoneyAccess, bool) {
|
||
if h.store == nil {
|
||
return repository.MoneyAccess{All: true}, true
|
||
}
|
||
access, err := h.store.MoneyAccessForUser(shared.ActorFromContext(c).UserID)
|
||
if err != nil {
|
||
response.ServerError(c, "获取财务范围失败")
|
||
return repository.MoneyAccess{}, false
|
||
}
|
||
return access, true
|
||
}
|
||
|
||
func moneyAccessAllowsApp(access repository.MoneyAccess, appCode string) bool {
|
||
if access.All {
|
||
return true
|
||
}
|
||
appCode = appctx.Normalize(appCode)
|
||
for _, scope := range access.Scopes {
|
||
if appctx.Normalize(scope.AppCode) == appCode {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (h *Handler) enrichAndFilterTemporaryLinks(appCode string, items []temporaryPaymentLinkDTO, access repository.MoneyAccess, regionID int64, operatorUserID uint) ([]temporaryPaymentLinkDTO, error) {
|
||
orderIDs := make([]string, 0, len(items))
|
||
for _, item := range items {
|
||
orderIDs = append(orderIDs, item.OrderID)
|
||
}
|
||
owners, err := h.store.TemporaryPaymentLinkOwners(appCode, orderIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
userIDs := []uint{}
|
||
for _, owner := range owners {
|
||
userIDs = append(userIDs, owner.AdminUserID)
|
||
}
|
||
users, err := h.store.FindUsersByIDs(userIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
out := make([]temporaryPaymentLinkDTO, 0, len(items))
|
||
for _, item := range items {
|
||
owner, ok := owners[item.OrderID]
|
||
if !ok {
|
||
if access.All && regionID == 0 && operatorUserID == 0 {
|
||
out = append(out, item)
|
||
}
|
||
continue
|
||
}
|
||
if !access.All && !access.Allows(owner.AppCode, owner.RegionID) {
|
||
continue
|
||
}
|
||
if regionID > 0 && owner.RegionID != regionID {
|
||
continue
|
||
}
|
||
if operatorUserID > 0 && owner.AdminUserID != operatorUserID {
|
||
continue
|
||
}
|
||
item.RegionID = owner.RegionID
|
||
item.OwnerUserID = owner.AdminUserID
|
||
if user, ok := users[owner.AdminUserID]; ok {
|
||
item.OwnerUsername = user.Username
|
||
item.OwnerName = user.Name
|
||
}
|
||
out = append(out, item)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
var errTemporaryPaymentMethodInvalid = errors.New("temporary payment method invalid")
|
||
|
||
func (h *Handler) findTemporaryPaymentMethod(ctx context.Context, appCode string, providerCode string, paymentMethodID int64) (*walletv1.ThirdPartyPaymentMethod, error) {
|
||
resp, err := h.wallet.ListThirdPartyPaymentChannels(ctx, &walletv1.ListThirdPartyPaymentChannelsRequest{
|
||
RequestId: strconv.FormatInt(time.Now().UnixNano(), 10),
|
||
AppCode: appCode,
|
||
ProviderCode: strings.TrimSpace(providerCode),
|
||
Status: "active",
|
||
IncludeDisabledMethods: true,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, channel := range resp.GetChannels() {
|
||
for _, method := range channel.GetMethods() {
|
||
if method.GetMethodId() != paymentMethodID {
|
||
continue
|
||
}
|
||
if strings.TrimSpace(providerCode) != "" && method.GetProviderCode() != strings.TrimSpace(providerCode) {
|
||
return nil, errTemporaryPaymentMethodInvalid
|
||
}
|
||
if method.GetStatus() != "active" || strings.TrimSpace(method.GetCountryCode()) == "" {
|
||
return nil, errTemporaryPaymentMethodInvalid
|
||
}
|
||
return method, nil
|
||
}
|
||
}
|
||
return nil, errTemporaryPaymentMethodInvalid
|
||
}
|
||
|
||
func (h *Handler) countryBelongsToRegion(ctx context.Context, appCode string, regionID int64, countryCode string) (bool, error) {
|
||
appCode = appctx.Normalize(appCode)
|
||
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
||
if countryCode == "" || countryCode == "GLOBAL" {
|
||
return false, nil
|
||
}
|
||
for _, source := range h.moneyRegionSources {
|
||
// legacy App 的区域-国家关系和区域目录同源;source 明确命中时直接返回,避免 Yumi/Aslan 回落到 hyapp_user 本地表。
|
||
ok, handled, err := source.CountryBelongsToRegion(ctx, appCode, regionID, countryCode)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if handled {
|
||
return ok, nil
|
||
}
|
||
}
|
||
if h.userDB == nil {
|
||
return false, errors.New("user mysql is not configured")
|
||
}
|
||
var count int
|
||
err := h.userDB.QueryRowContext(ctx, `
|
||
SELECT COUNT(*)
|
||
FROM region_countries
|
||
WHERE app_code = ? AND region_id = ? AND country_code = ? AND status = 'active'
|
||
`, appCode, regionID, countryCode).Scan(&count)
|
||
return count > 0, err
|
||
}
|
||
|
||
func (h *Handler) loadMoneyMasterData(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, []moneyRegionDTO, []moneyCountryDTO, error) {
|
||
apps, err := h.listMoneyApps(ctx, access)
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
appCodes := make([]string, 0, len(apps))
|
||
for _, app := range apps {
|
||
appCodes = append(appCodes, app.AppCode)
|
||
}
|
||
regions, err := h.listMoneyRegions(ctx, access, appCodes)
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
countries, err := h.listMoneyCountries(ctx, access, appCodes)
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
// Yumi/Aslan 这类 legacy App 的区域事实来自线上 likei Mongo;这里合并目录数据,但财务授权仍只保存 app_code + region_id。
|
||
sourceApps, sourceRegions, sourceCountries, err := h.listMoneyRegionSources(ctx, access)
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
apps = appendUniqueMoneyApps(apps, sourceApps)
|
||
regions = appendUniqueMoneyRegions(regions, sourceRegions)
|
||
countries = appendUniqueMoneyCountries(countries, sourceCountries)
|
||
return apps, regions, countries, nil
|
||
}
|
||
|
||
func (h *Handler) listMoneyApps(ctx context.Context, access repository.MoneyAccess) ([]moneyAppDTO, error) {
|
||
where, args := scopedAppWhere(access)
|
||
rows, err := h.userDB.QueryContext(ctx, `
|
||
SELECT app_id, app_code, app_name, package_name, platform, status
|
||
FROM apps
|
||
WHERE status = 'active'`+where+`
|
||
ORDER BY app_name ASC, app_code ASC`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []moneyAppDTO{}
|
||
for rows.Next() {
|
||
var item moneyAppDTO
|
||
if err := rows.Scan(&item.AppID, &item.AppCode, &item.AppName, &item.PackageName, &item.Platform, &item.Status); err != nil {
|
||
return nil, err
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return items, rows.Err()
|
||
}
|
||
|
||
func (h *Handler) listMoneyRegions(ctx context.Context, access repository.MoneyAccess, appCodes []string) ([]moneyRegionDTO, error) {
|
||
if len(appCodes) == 0 {
|
||
return []moneyRegionDTO{}, nil
|
||
}
|
||
where, args := inClause("r.app_code", appCodes)
|
||
rows, err := h.userDB.QueryContext(ctx, `
|
||
SELECT r.app_code, r.region_id, r.region_code, r.name, r.status, r.sort_order,
|
||
COALESCE(GROUP_CONCAT(rc.country_code ORDER BY rc.country_code SEPARATOR ','), '')
|
||
FROM regions r
|
||
LEFT JOIN region_countries rc
|
||
ON rc.app_code = r.app_code AND rc.region_id = r.region_id AND rc.status = 'active'
|
||
WHERE r.status = 'active' AND `+where+`
|
||
GROUP BY r.app_code, r.region_id, r.region_code, r.name, r.status, r.sort_order
|
||
ORDER BY r.app_code ASC, r.sort_order ASC, r.name ASC`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []moneyRegionDTO{}
|
||
for rows.Next() {
|
||
var item moneyRegionDTO
|
||
var countries string
|
||
if err := rows.Scan(&item.AppCode, &item.RegionID, &item.RegionCode, &item.Name, &item.Status, &item.SortOrder, &countries); err != nil {
|
||
return nil, err
|
||
}
|
||
if !access.All && !access.Allows(item.AppCode, item.RegionID) {
|
||
continue
|
||
}
|
||
item.Countries = splitCodes(countries)
|
||
items = append(items, item)
|
||
}
|
||
return items, rows.Err()
|
||
}
|
||
|
||
func (h *Handler) listMoneyCountries(ctx context.Context, access repository.MoneyAccess, appCodes []string) ([]moneyCountryDTO, error) {
|
||
if len(appCodes) == 0 {
|
||
return []moneyCountryDTO{}, nil
|
||
}
|
||
where, args := inClause("c.app_code", appCodes)
|
||
rows, err := h.userDB.QueryContext(ctx, `
|
||
SELECT c.app_code, c.country_id, c.country_code, c.country_name, c.country_display_name, c.flag,
|
||
c.enabled, COALESCE(rc.region_id, 0), c.sort_order
|
||
FROM countries c
|
||
LEFT JOIN region_countries rc
|
||
ON rc.app_code = c.app_code AND rc.country_code = c.country_code AND rc.status = 'active'
|
||
WHERE c.enabled = TRUE AND `+where+`
|
||
ORDER BY c.app_code ASC, c.sort_order ASC, c.country_name ASC`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
items := []moneyCountryDTO{}
|
||
for rows.Next() {
|
||
var item moneyCountryDTO
|
||
if err := rows.Scan(&item.AppCode, &item.CountryID, &item.CountryCode, &item.CountryName, &item.CountryDisplayName, &item.Flag, &item.Enabled, &item.RegionID, &item.SortOrder); err != nil {
|
||
return nil, err
|
||
}
|
||
if !access.All && !access.Allows(item.AppCode, item.RegionID) {
|
||
continue
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return items, rows.Err()
|
||
}
|
||
|
||
func (h *Handler) performanceAppCodes(ctx context.Context, access repository.MoneyAccess, queryAppCode string) ([]string, error) {
|
||
if appCode := appctx.Normalize(queryAppCode); strings.TrimSpace(queryAppCode) != "" {
|
||
if !moneyAccessAllowsApp(access, appCode) {
|
||
return nil, nil
|
||
}
|
||
return []string{appCode}, nil
|
||
}
|
||
if !access.All {
|
||
return access.AppCodes(), nil
|
||
}
|
||
rows, err := h.walletDB.QueryContext(ctx, `
|
||
SELECT DISTINCT app_code
|
||
FROM external_recharge_orders
|
||
WHERE audience_type = ?
|
||
ORDER BY app_code ASC`, temporaryRechargeAudience)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
appCodes := []string{}
|
||
for rows.Next() {
|
||
var appCode string
|
||
if err := rows.Scan(&appCode); err != nil {
|
||
return nil, err
|
||
}
|
||
appCodes = append(appCodes, appctx.Normalize(appCode))
|
||
}
|
||
return appCodes, rows.Err()
|
||
}
|
||
|
||
func (h *Handler) listPaidTemporaryOrders(ctx context.Context, appCode string, startAtMS int64, endAtMS int64) ([]paidTemporaryOrder, error) {
|
||
where := `WHERE app_code = ? AND audience_type = ? AND status IN ('paid', 'credited')`
|
||
args := []any{appCode, temporaryRechargeAudience}
|
||
if startAtMS > 0 {
|
||
where += ` AND updated_at_ms >= ?`
|
||
args = append(args, startAtMS)
|
||
}
|
||
if endAtMS > 0 {
|
||
where += ` AND updated_at_ms <= ?`
|
||
args = append(args, endAtMS)
|
||
}
|
||
rows, err := h.walletDB.QueryContext(ctx, `
|
||
SELECT app_code, order_id, usd_minor_amount, status, country_code, provider_code, payment_method_id, pay_url, created_at_ms, updated_at_ms
|
||
FROM external_recharge_orders
|
||
`+where+`
|
||
ORDER BY updated_at_ms DESC, order_id DESC`, args...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
orders := []paidTemporaryOrder{}
|
||
for rows.Next() {
|
||
var order paidTemporaryOrder
|
||
if err := rows.Scan(&order.AppCode, &order.OrderID, &order.USDMinorAmount, &order.Status, &order.CountryCode, &order.ProviderCode, &order.PaymentMethodID, &order.PayURL, &order.CreatedAtMS, &order.UpdatedAtMS); err != nil {
|
||
return nil, err
|
||
}
|
||
orders = append(orders, order)
|
||
}
|
||
return orders, rows.Err()
|
||
}
|
||
|
||
func moneyPerformanceSummary(items []moneyPerformanceItemDTO) gin.H {
|
||
var paidLinks int64
|
||
var paidUSDMinor int64
|
||
for _, item := range items {
|
||
paidLinks += item.PaidLinkCount
|
||
paidUSDMinor += item.PaidUSDMinor
|
||
}
|
||
return gin.H{
|
||
"paidLinkCount": paidLinks,
|
||
"paidUsdMinor": paidUSDMinor,
|
||
"operatorCount": len(items),
|
||
}
|
||
}
|
||
|
||
func moneyScopeDTOsFromModel(scopes []model.UserMoneyScope) []moneyScopeItemDTO {
|
||
out := make([]moneyScopeItemDTO, 0, len(scopes))
|
||
for _, scope := range scopes {
|
||
out = append(out, moneyScopeItemDTO{AppCode: scope.AppCode, RegionID: scope.RegionID})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func scopedAppWhere(access repository.MoneyAccess) (string, []any) {
|
||
if access.All {
|
||
return "", nil
|
||
}
|
||
where, args := inClause("app_code", access.AppCodes())
|
||
return " AND " + where, args
|
||
}
|
||
|
||
func inClause(column string, values []string) (string, []any) {
|
||
if len(values) == 0 {
|
||
return "1 = 0", nil
|
||
}
|
||
placeholders := make([]string, 0, len(values))
|
||
args := make([]any, 0, len(values))
|
||
for _, value := range values {
|
||
placeholders = append(placeholders, "?")
|
||
args = append(args, appctx.Normalize(value))
|
||
}
|
||
return column + " IN (" + strings.Join(placeholders, ",") + ")", args
|
||
}
|
||
|
||
func splitCodes(value string) []string {
|
||
parts := strings.Split(value, ",")
|
||
out := make([]string, 0, len(parts))
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if part != "" {
|
||
out = append(out, part)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func queryUint(c *gin.Context, keys ...string) uint {
|
||
value := strings.TrimSpace(firstQuery(c, keys...))
|
||
if value == "" {
|
||
return 0
|
||
}
|
||
parsed, err := strconv.ParseUint(value, 10, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return uint(parsed)
|
||
}
|
||
|
||
func temporaryPaymentCommandID(userID uint, requestID string) string {
|
||
return fmt.Sprintf("admin-temporary:%d:%s", userID, strings.TrimSpace(requestID))
|
||
}
|