增加创建时间

This commit is contained in:
zhx 2026-07-13 11:10:02 +08:00
parent 18d1a1c7cc
commit b9603832fc
7 changed files with 76 additions and 11 deletions

View File

@ -646,6 +646,14 @@ func (CoinSellerRechargeOrder) TableName() string {
return "admin_coin_seller_recharge_orders"
}
// BusinessTimeMS 是财务归档和统计切日使用的订单业务时间;历史订单未保存 order_date_ms 时回退到真实录入时间。
func (order CoinSellerRechargeOrder) BusinessTimeMS() int64 {
if order.OrderDateMS > 0 {
return order.OrderDateMS
}
return order.CreatedAtMS
}
type UserWithdrawalApplication struct {
ID uint `gorm:"primaryKey" json:"id"`
AppCode string `gorm:"size:32;index:idx_admin_withdrawal_app_status_time;not null" json:"appCode"`

View File

@ -293,7 +293,8 @@ func (s *DashboardService) RecordFinanceCoinSellerRechargeOrder(ctx context.Cont
"target_region_id": order.TargetRegionID,
"usd_minor": order.USDMinorAmount,
"coin_amount": order.CoinAmount,
"created_at_ms": order.CreatedAtMS,
// 统计切日必须使用运营选择的订单业务时间;历史数据通过模型方法回退到录入时间。
"created_at_ms": order.BusinessTimeMS(),
}
return s.statisticsPOST(ctx, "/internal/v1/statistics/social/coin-seller-recharge-orders", payload)
}

View File

@ -107,6 +107,11 @@ func (s *Service) CreateOrder(ctx context.Context, actor shared.Actor, req creat
if err != nil {
return nil, err
}
nowMS := s.now().UnixMilli()
input.OrderDateMS, err = normalizeCreateOrderBusinessTime(input.OrderDateMS, nowMS)
if err != nil {
return nil, err
}
// 金币数只由当前 APP 汇率配置生成;即使调用方绕过前端直接提交 coinAmount也不会进入请求结构或账务结果。
target, err := s.resolveCoinSellerTarget(ctx, requestID, input.AppCode, input.TargetUserID)
if err != nil {
@ -128,7 +133,6 @@ func (s *Service) CreateOrder(ctx context.Context, actor shared.Actor, req creat
if err := verifyReceiptMatchesInput(input, receipt); err != nil {
return nil, err
}
nowMS := s.now().UnixMilli()
verifiedAtMS := receipt.VerifiedAtMS
if verifiedAtMS <= 0 {
verifiedAtMS = nowMS
@ -935,6 +939,19 @@ func normalizeOrderDateMS(orderDate string, orderDateMS int64) (int64, error) {
return 0, errors.New("订单日期不正确")
}
func normalizeCreateOrderBusinessTime(orderDateMS int64, nowMS int64) (int64, error) {
if nowMS <= 0 {
return 0, errors.New("当前时间不正确")
}
if orderDateMS <= 0 {
return nowMS, nil
}
if orderDateMS > nowMS {
return 0, errors.New("订单时间不能晚于当前时间")
}
return orderDateMS, nil
}
func requiresLegacyWriter(appCode string) bool {
switch appctx.Normalize(appCode) {
case "aslan", "yumi":

View File

@ -102,6 +102,19 @@ func TestNormalizeReceiptVerificationCarriesProviderCurrencyAmount(t *testing.T)
}
}
func TestNormalizeCreateOrderBusinessTimeDefaultsToNowAndRejectsFuture(t *testing.T) {
const nowMS = int64(1_783_900_000_000)
if got, err := normalizeCreateOrderBusinessTime(0, nowMS); err != nil || got != nowMS {
t.Fatalf("missing order time should default to now: got=%d err=%v", got, err)
}
if got, err := normalizeCreateOrderBusinessTime(nowMS-86_400_000, nowMS); err != nil || got != nowMS-86_400_000 {
t.Fatalf("past business time should be preserved: got=%d err=%v", got, err)
}
if _, err := normalizeCreateOrderBusinessTime(nowMS+1, nowMS); err == nil || !strings.Contains(err.Error(), "不能晚于") {
t.Fatalf("future business time should be rejected, got %v", err)
}
}
func TestNormalizeCreateOrderLeavesCoinAmountForServerQuote(t *testing.T) {
input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
AppCode: "lalu",

View File

@ -530,11 +530,11 @@ func financeCoinSellerRechargeOrderBill(order model.CoinSellerRechargeOrder) rec
CoinAmount: order.CoinAmount,
USDMinorAmount: order.USDMinorAmount,
ProviderAmountMinor: providerAmountMinor,
CreatedAtMS: order.CreatedAtMS,
CreatedAtMS: order.BusinessTimeMS(),
ProviderCode: order.ProviderCode,
UserPaidCurrencyCode: currency,
UserPaidAmountMicro: userPaidAmountMicro,
PaidSyncedAtMS: order.CreatedAtMS,
PaidSyncedAtMS: order.BusinessTimeMS(),
}
}

View File

@ -0,0 +1,23 @@
package payment
import (
"testing"
"hyapp-admin-server/internal/model"
)
func TestFinanceCoinSellerRechargeOrderBillUsesBusinessTime(t *testing.T) {
order := model.CoinSellerRechargeOrder{
ID: 7,
AppCode: "lalu",
TargetUserID: 10001,
USDMinorAmount: 15000,
CoinAmount: 12000000,
OrderDateMS: 1_783_708_200_000,
CreatedAtMS: 1_783_881_000_000,
}
bill := financeCoinSellerRechargeOrderBill(order)
if bill.CreatedAtMS != order.OrderDateMS || bill.PaidSyncedAtMS != order.OrderDateMS {
t.Fatalf("finance bill must use selected business time: %+v", bill)
}
}

View File

@ -16,6 +16,9 @@ var (
ErrCoinSellerRechargeOrderGranted = errors.New("coin seller recharge order already granted")
)
// coinSellerRechargeBusinessTimeSQL 让新订单按运营选择的业务时间归档,同时保持历史 order_date_ms=0 数据仍按录入时间查询。
const coinSellerRechargeBusinessTimeSQL = "CASE WHEN order_date_ms > 0 THEN order_date_ms ELSE created_at_ms END"
type CoinSellerRechargeOrderListOptions struct {
Page int
PageSize int
@ -121,7 +124,7 @@ func (s *Store) ListCoinSellerRechargeOrders(options CoinSellerRechargeOrderList
}
var orders []model.CoinSellerRechargeOrder
err := query.
Order("created_at_ms DESC, id DESC").
Order(coinSellerRechargeBusinessTimeSQL + " DESC, id DESC").
Limit(pageSize).
Offset((page - 1) * pageSize).
Find(&orders).Error
@ -375,10 +378,10 @@ func applyCoinSellerRechargeOrderFilters(query *gorm.DB, options CoinSellerRecha
query = query.Where("operator_user_id = ?", options.OperatorID)
}
if options.CreatedFromMS > 0 {
query = query.Where("created_at_ms >= ?", options.CreatedFromMS)
query = query.Where(coinSellerRechargeBusinessTimeSQL+" >= ?", options.CreatedFromMS)
}
if options.CreatedToMS > 0 {
query = query.Where("created_at_ms < ?", options.CreatedToMS)
query = query.Where(coinSellerRechargeBusinessTimeSQL+" < ?", options.CreatedToMS)
}
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
like := "%" + keyword + "%"
@ -406,10 +409,10 @@ func applyCoinSellerRechargeOrderStatsFilters(query *gorm.DB, options CoinSeller
query = query.Where("target_country_id = ?", options.CountryID)
}
if options.CreatedFromMS > 0 {
query = query.Where("created_at_ms >= ?", options.CreatedFromMS)
query = query.Where(coinSellerRechargeBusinessTimeSQL+" >= ?", options.CreatedFromMS)
}
if options.CreatedToMS > 0 {
query = query.Where("created_at_ms < ?", options.CreatedToMS)
query = query.Where(coinSellerRechargeBusinessTimeSQL+" < ?", options.CreatedToMS)
}
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
like := "%" + keyword + "%"
@ -434,9 +437,9 @@ func (s *Store) scanCoinSellerRechargeOrderDailyStats(options CoinSellerRecharge
}
var rows []row
tzOffsetMS := int64(options.TzOffsetMinutes) * 60_000
// 财务筛选按 created_at_ms 取数;日趋势按前端传入的统计时区偏移切日,避免北京时间财务页跨日错桶
// 财务筛选和趋势都按订单业务时间取数;历史记录自动回退录入时间,并继续按前端统计时区偏移切日
if err := query.Select(`
DATE_FORMAT(FROM_UNIXTIME((created_at_ms + ?) / 1000), '%Y-%m-%d') AS date,
DATE_FORMAT(FROM_UNIXTIME((`+coinSellerRechargeBusinessTimeSQL+` + ?) / 1000), '%Y-%m-%d') AS date,
COUNT(*) AS count,
COALESCE(SUM(usd_minor_amount), 0) AS usd_minor,
COALESCE(SUM(coin_amount), 0) AS coin_amount`, tzOffsetMS).