增加全表导出
This commit is contained in:
parent
2a6053da56
commit
4c980ddeca
@ -94,6 +94,7 @@ var catalog = map[Code]Spec{
|
|||||||
CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "payment reference already used"),
|
CoinSellerPaymentRefDuplicated: spec(codes.AlreadyExists, httpStatusConflict, CoinSellerPaymentRefDuplicated, "payment reference already used"),
|
||||||
IdempotencyConflict: spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "request payload does not match previous command"),
|
IdempotencyConflict: spec(codes.AlreadyExists, httpStatusConflict, IdempotencyConflict, "request payload does not match previous command"),
|
||||||
PointWithdrawalLimitReached: spec(codes.ResourceExhausted, 429, PointWithdrawalLimitReached, "withdrawal limit reached"),
|
PointWithdrawalLimitReached: spec(codes.ResourceExhausted, 429, PointWithdrawalLimitReached, "withdrawal limit reached"),
|
||||||
|
PointWithdrawalDateNotAllowed: spec(codes.FailedPrecondition, httpStatusConflict, PointWithdrawalDateNotAllowed, "withdrawal date is not allowed"),
|
||||||
VIPLevelNotFound: spec(codes.NotFound, httpStatusNotFound, VIPLevelNotFound, "not found"),
|
VIPLevelNotFound: spec(codes.NotFound, httpStatusNotFound, VIPLevelNotFound, "not found"),
|
||||||
VIPLevelDisabled: spec(codes.FailedPrecondition, httpStatusConflict, VIPLevelDisabled, "vip level is disabled"),
|
VIPLevelDisabled: spec(codes.FailedPrecondition, httpStatusConflict, VIPLevelDisabled, "vip level is disabled"),
|
||||||
VIPDowngradeNotAllowed: spec(codes.FailedPrecondition, httpStatusConflict, VIPDowngradeNotAllowed, "vip downgrade is not allowed"),
|
VIPDowngradeNotAllowed: spec(codes.FailedPrecondition, httpStatusConflict, VIPDowngradeNotAllowed, "vip downgrade is not allowed"),
|
||||||
|
|||||||
@ -103,6 +103,8 @@ const (
|
|||||||
IdempotencyConflict Code = "IDEMPOTENCY_CONFLICT"
|
IdempotencyConflict Code = "IDEMPOTENCY_CONFLICT"
|
||||||
// PointWithdrawalLimitReached 表示用户已达到月工资政策配置的 UTC 日/周/月提现次数上限。
|
// PointWithdrawalLimitReached 表示用户已达到月工资政策配置的 UTC 日/周/月提现次数上限。
|
||||||
PointWithdrawalLimitReached Code = "POINT_WITHDRAWAL_LIMIT_REACHED"
|
PointWithdrawalLimitReached Code = "POINT_WITHDRAWAL_LIMIT_REACHED"
|
||||||
|
// PointWithdrawalDateNotAllowed 表示当前 UTC 日不在工资政策允许的平台提现日期内。
|
||||||
|
PointWithdrawalDateNotAllowed Code = "POINT_WITHDRAWAL_DATE_NOT_ALLOWED"
|
||||||
// VIPLevelNotFound 表示目标 VIP 等级不存在。
|
// VIPLevelNotFound 表示目标 VIP 等级不存在。
|
||||||
VIPLevelNotFound Code = "VIP_LEVEL_NOT_FOUND"
|
VIPLevelNotFound Code = "VIP_LEVEL_NOT_FOUND"
|
||||||
// VIPLevelDisabled 表示目标 VIP 等级当前不可购买。
|
// VIPLevelDisabled 表示目标 VIP 等级当前不可购买。
|
||||||
|
|||||||
@ -143,6 +143,14 @@ func TestCatalogMappings(t *testing.T) {
|
|||||||
publicCode: string(PointWithdrawalLimitReached),
|
publicCode: string(PointWithdrawalLimitReached),
|
||||||
publicMessage: "withdrawal limit reached",
|
publicMessage: "withdrawal limit reached",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "point withdrawal date gate keeps actionable conflict",
|
||||||
|
code: PointWithdrawalDateNotAllowed,
|
||||||
|
grpcCode: codes.FailedPrecondition,
|
||||||
|
httpStatus: httpStatusConflict,
|
||||||
|
publicCode: string(PointWithdrawalDateNotAllowed),
|
||||||
|
publicMessage: "withdrawal date is not allowed",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "country cooldown keeps actionable app message",
|
name: "country cooldown keeps actionable app message",
|
||||||
code: CountryChangeCooldown,
|
code: CountryChangeCooldown,
|
||||||
|
|||||||
@ -450,6 +450,8 @@ type HostAgencySalaryPolicy struct {
|
|||||||
CoinSellerWithdrawalLimitCount int64 `gorm:"column:coin_seller_withdrawal_limit_count;not null;default:0" json:"coinSellerWithdrawalLimitCount"`
|
CoinSellerWithdrawalLimitCount int64 `gorm:"column:coin_seller_withdrawal_limit_count;not null;default:0" json:"coinSellerWithdrawalLimitCount"`
|
||||||
PlatformWithdrawalLimitPeriod string `gorm:"column:platform_withdrawal_limit_period;size:16;not null;default:month" json:"platformWithdrawalLimitPeriod"`
|
PlatformWithdrawalLimitPeriod string `gorm:"column:platform_withdrawal_limit_period;size:16;not null;default:month" json:"platformWithdrawalLimitPeriod"`
|
||||||
PlatformWithdrawalLimitCount int64 `gorm:"column:platform_withdrawal_limit_count;not null;default:0" json:"platformWithdrawalLimitCount"`
|
PlatformWithdrawalLimitCount int64 `gorm:"column:platform_withdrawal_limit_count;not null;default:0" json:"platformWithdrawalLimitCount"`
|
||||||
|
// PlatformWithdrawalAllowedDays 使用排序去重后的逗号串保存 UTC 月内日;空串兼容为每天可提现。
|
||||||
|
PlatformWithdrawalAllowedDays string `gorm:"column:platform_withdrawal_allowed_days;size:96;not null;default:''" json:"platformWithdrawalAllowedDays"`
|
||||||
// Effective* 仅保留兼容既有表结构;后台不再配置或展示,工资结算只按 CycleKey 绑定版本。
|
// Effective* 仅保留兼容既有表结构;后台不再配置或展示,工资结算只按 CycleKey 绑定版本。
|
||||||
EffectiveFromMS int64 `gorm:"column:effective_from_ms;index:idx_admin_host_agency_salary_policy_region,not null;default:0" json:"effectiveFromMs"`
|
EffectiveFromMS int64 `gorm:"column:effective_from_ms;index:idx_admin_host_agency_salary_policy_region,not null;default:0" json:"effectiveFromMs"`
|
||||||
EffectiveToMS int64 `gorm:"column:effective_to_ms;not null;default:0" json:"effectiveToMs"`
|
EffectiveToMS int64 `gorm:"column:effective_to_ms;not null;default:0" json:"effectiveToMs"`
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type policyRequest struct {
|
|||||||
CoinSellerWithdrawalLimitCount int64 `json:"coin_seller_withdrawal_limit_count"`
|
CoinSellerWithdrawalLimitCount int64 `json:"coin_seller_withdrawal_limit_count"`
|
||||||
PlatformWithdrawalLimitPeriod string `json:"platform_withdrawal_limit_period"`
|
PlatformWithdrawalLimitPeriod string `json:"platform_withdrawal_limit_period"`
|
||||||
PlatformWithdrawalLimitCount int64 `json:"platform_withdrawal_limit_count"`
|
PlatformWithdrawalLimitCount int64 `json:"platform_withdrawal_limit_count"`
|
||||||
|
PlatformWithdrawalAllowedDays []int32 `json:"platform_withdrawal_allowed_days"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Levels []levelRequest `json:"levels"`
|
Levels []levelRequest `json:"levels"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ type policyDTO struct {
|
|||||||
CoinSellerWithdrawalLimitCount int64 `json:"coin_seller_withdrawal_limit_count"`
|
CoinSellerWithdrawalLimitCount int64 `json:"coin_seller_withdrawal_limit_count"`
|
||||||
PlatformWithdrawalLimitPeriod string `json:"platform_withdrawal_limit_period"`
|
PlatformWithdrawalLimitPeriod string `json:"platform_withdrawal_limit_period"`
|
||||||
PlatformWithdrawalLimitCount int64 `json:"platform_withdrawal_limit_count"`
|
PlatformWithdrawalLimitCount int64 `json:"platform_withdrawal_limit_count"`
|
||||||
|
PlatformWithdrawalAllowedDays []int32 `json:"platform_withdrawal_allowed_days"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
CreatedByAdminID uint `json:"created_by_admin_id"`
|
CreatedByAdminID uint `json:"created_by_admin_id"`
|
||||||
UpdatedByAdminID uint `json:"updated_by_admin_id"`
|
UpdatedByAdminID uint `json:"updated_by_admin_id"`
|
||||||
@ -76,6 +77,7 @@ func policyFromModel(item model.HostAgencySalaryPolicy) policyDTO {
|
|||||||
CoinSellerWithdrawalLimitCount: item.CoinSellerWithdrawalLimitCount,
|
CoinSellerWithdrawalLimitCount: item.CoinSellerWithdrawalLimitCount,
|
||||||
PlatformWithdrawalLimitPeriod: firstNonBlank(item.PlatformWithdrawalLimitPeriod, withdrawalLimitPeriodMonth),
|
PlatformWithdrawalLimitPeriod: firstNonBlank(item.PlatformWithdrawalLimitPeriod, withdrawalLimitPeriodMonth),
|
||||||
PlatformWithdrawalLimitCount: item.PlatformWithdrawalLimitCount,
|
PlatformWithdrawalLimitCount: item.PlatformWithdrawalLimitCount,
|
||||||
|
PlatformWithdrawalAllowedDays: parseWithdrawalDays(item.PlatformWithdrawalAllowedDays),
|
||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
CreatedByAdminID: item.CreatedByAdminID,
|
CreatedByAdminID: item.CreatedByAdminID,
|
||||||
UpdatedByAdminID: item.UpdatedByAdminID,
|
UpdatedByAdminID: item.UpdatedByAdminID,
|
||||||
|
|||||||
@ -219,6 +219,7 @@ func (s *Service) Update(ctx context.Context, appCode string, actorID uint, id u
|
|||||||
item.CoinSellerWithdrawalLimitCount = updated.CoinSellerWithdrawalLimitCount
|
item.CoinSellerWithdrawalLimitCount = updated.CoinSellerWithdrawalLimitCount
|
||||||
item.PlatformWithdrawalLimitPeriod = updated.PlatformWithdrawalLimitPeriod
|
item.PlatformWithdrawalLimitPeriod = updated.PlatformWithdrawalLimitPeriod
|
||||||
item.PlatformWithdrawalLimitCount = updated.PlatformWithdrawalLimitCount
|
item.PlatformWithdrawalLimitCount = updated.PlatformWithdrawalLimitCount
|
||||||
|
item.PlatformWithdrawalAllowedDays = updated.PlatformWithdrawalAllowedDays
|
||||||
item.Description = updated.Description
|
item.Description = updated.Description
|
||||||
item.UpdatedByAdminID = actorID
|
item.UpdatedByAdminID = actorID
|
||||||
// 编辑后运行快照可能已经落后于 admin 配置,因此保留上次发布时间但把状态重新标成 draft。
|
// 编辑后运行快照可能已经落后于 admin 配置,因此保留上次发布时间但把状态重新标成 draft。
|
||||||
@ -382,6 +383,7 @@ type runtimeHostSalaryPolicySnapshot struct {
|
|||||||
CoinSellerWithdrawalLimitCount int64
|
CoinSellerWithdrawalLimitCount int64
|
||||||
PlatformWithdrawalLimitPeriod string
|
PlatformWithdrawalLimitPeriod string
|
||||||
PlatformWithdrawalLimitCount int64
|
PlatformWithdrawalLimitCount int64
|
||||||
|
PlatformWithdrawalAllowedDays string
|
||||||
}
|
}
|
||||||
|
|
||||||
type runtimeHostSalaryLevelSnapshot struct {
|
type runtimeHostSalaryLevelSnapshot struct {
|
||||||
@ -402,15 +404,16 @@ func ensureRuntimeHostSalaryPolicySnapshot(ctx context.Context, tx *sql.Tx, item
|
|||||||
app_code, policy_id, cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
app_code, policy_id, cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
||||||
gift_coin_to_diamond_ratio, residual_diamond_to_usd_rate,
|
gift_coin_to_diamond_ratio, residual_diamond_to_usd_rate,
|
||||||
coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
|
coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
|
||||||
platform_withdrawal_limit_period, platform_withdrawal_limit_count,
|
platform_withdrawal_limit_period, platform_withdrawal_limit_count, platform_withdrawal_allowed_days,
|
||||||
created_at_ms, updated_at_ms
|
created_at_ms, updated_at_ms
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE policy_id = VALUES(policy_id)`,
|
ON DUPLICATE KEY UPDATE policy_id = VALUES(policy_id)`,
|
||||||
item.AppCode, item.ID, item.CycleKey, policyVersion, item.Name, item.RegionID, item.Status,
|
item.AppCode, item.ID, item.CycleKey, policyVersion, item.Name, item.RegionID, item.Status,
|
||||||
item.SettlementMode, firstNonBlank(item.SettlementTriggerMode, settlementTriggerAutomatic),
|
item.SettlementMode, firstNonBlank(item.SettlementTriggerMode, settlementTriggerAutomatic),
|
||||||
item.GiftCoinToDiamondRatio, item.ResidualDiamondToUSDRate,
|
item.GiftCoinToDiamondRatio, item.ResidualDiamondToUSDRate,
|
||||||
firstNonBlank(item.CoinSellerWithdrawalLimitPeriod, withdrawalLimitPeriodMonth), item.CoinSellerWithdrawalLimitCount,
|
firstNonBlank(item.CoinSellerWithdrawalLimitPeriod, withdrawalLimitPeriodMonth), item.CoinSellerWithdrawalLimitCount,
|
||||||
firstNonBlank(item.PlatformWithdrawalLimitPeriod, withdrawalLimitPeriodMonth), item.PlatformWithdrawalLimitCount,
|
firstNonBlank(item.PlatformWithdrawalLimitPeriod, withdrawalLimitPeriodMonth), item.PlatformWithdrawalLimitCount,
|
||||||
|
item.PlatformWithdrawalAllowedDays,
|
||||||
nonZeroMS(item.CreatedAtMS, publishedAtMS), publishedAtMS,
|
nonZeroMS(item.CreatedAtMS, publishedAtMS), publishedAtMS,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -421,7 +424,7 @@ func ensureRuntimeHostSalaryPolicySnapshot(ctx context.Context, tx *sql.Tx, item
|
|||||||
SELECT cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
SELECT cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode,
|
||||||
CAST(gift_coin_to_diamond_ratio AS CHAR), CAST(residual_diamond_to_usd_rate AS CHAR),
|
CAST(gift_coin_to_diamond_ratio AS CHAR), CAST(residual_diamond_to_usd_rate AS CHAR),
|
||||||
coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
|
coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
|
||||||
platform_withdrawal_limit_period, platform_withdrawal_limit_count
|
platform_withdrawal_limit_period, platform_withdrawal_limit_count, platform_withdrawal_allowed_days
|
||||||
FROM host_agency_salary_policies
|
FROM host_agency_salary_policies
|
||||||
WHERE app_code = ? AND policy_id = ?
|
WHERE app_code = ? AND policy_id = ?
|
||||||
FOR UPDATE`, item.AppCode, item.ID).Scan(
|
FOR UPDATE`, item.AppCode, item.ID).Scan(
|
||||||
@ -430,6 +433,7 @@ func ensureRuntimeHostSalaryPolicySnapshot(ctx context.Context, tx *sql.Tx, item
|
|||||||
&existing.GiftCoinToDiamondRatio, &existing.ResidualDiamondToUSDRate,
|
&existing.GiftCoinToDiamondRatio, &existing.ResidualDiamondToUSDRate,
|
||||||
&existing.CoinSellerWithdrawalLimitPeriod, &existing.CoinSellerWithdrawalLimitCount,
|
&existing.CoinSellerWithdrawalLimitPeriod, &existing.CoinSellerWithdrawalLimitCount,
|
||||||
&existing.PlatformWithdrawalLimitPeriod, &existing.PlatformWithdrawalLimitCount,
|
&existing.PlatformWithdrawalLimitPeriod, &existing.PlatformWithdrawalLimitCount,
|
||||||
|
&existing.PlatformWithdrawalAllowedDays,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -483,7 +487,8 @@ func runtimeHostSalaryPolicyMatches(existing runtimeHostSalaryPolicySnapshot, it
|
|||||||
existing.CoinSellerWithdrawalLimitPeriod == firstNonBlank(item.CoinSellerWithdrawalLimitPeriod, withdrawalLimitPeriodMonth) &&
|
existing.CoinSellerWithdrawalLimitPeriod == firstNonBlank(item.CoinSellerWithdrawalLimitPeriod, withdrawalLimitPeriodMonth) &&
|
||||||
existing.CoinSellerWithdrawalLimitCount == item.CoinSellerWithdrawalLimitCount &&
|
existing.CoinSellerWithdrawalLimitCount == item.CoinSellerWithdrawalLimitCount &&
|
||||||
existing.PlatformWithdrawalLimitPeriod == firstNonBlank(item.PlatformWithdrawalLimitPeriod, withdrawalLimitPeriodMonth) &&
|
existing.PlatformWithdrawalLimitPeriod == firstNonBlank(item.PlatformWithdrawalLimitPeriod, withdrawalLimitPeriodMonth) &&
|
||||||
existing.PlatformWithdrawalLimitCount == item.PlatformWithdrawalLimitCount
|
existing.PlatformWithdrawalLimitCount == item.PlatformWithdrawalLimitCount &&
|
||||||
|
existing.PlatformWithdrawalAllowedDays == item.PlatformWithdrawalAllowedDays
|
||||||
}
|
}
|
||||||
|
|
||||||
func runtimeHostSalaryLevelsFromModel(levels []model.HostAgencySalaryLevel) ([]runtimeHostSalaryLevelSnapshot, error) {
|
func runtimeHostSalaryLevelsFromModel(levels []model.HostAgencySalaryLevel) ([]runtimeHostSalaryLevelSnapshot, error) {
|
||||||
@ -645,6 +650,10 @@ func policyModelFromRequest(appCode string, actorID uint, req policyRequest) (mo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return model.HostAgencySalaryPolicy{}, err
|
return model.HostAgencySalaryPolicy{}, err
|
||||||
}
|
}
|
||||||
|
platformAllowedDays, err := normalizeWithdrawalDays(req.PlatformWithdrawalAllowedDays)
|
||||||
|
if err != nil {
|
||||||
|
return model.HostAgencySalaryPolicy{}, err
|
||||||
|
}
|
||||||
description := strings.TrimSpace(req.Description)
|
description := strings.TrimSpace(req.Description)
|
||||||
if len([]rune(description)) > 255 {
|
if len([]rune(description)) > 255 {
|
||||||
return model.HostAgencySalaryPolicy{}, errors.New("备注不能超过 255 个字符")
|
return model.HostAgencySalaryPolicy{}, errors.New("备注不能超过 255 个字符")
|
||||||
@ -670,6 +679,7 @@ func policyModelFromRequest(appCode string, actorID uint, req policyRequest) (mo
|
|||||||
CoinSellerWithdrawalLimitCount: coinSellerLimitCount,
|
CoinSellerWithdrawalLimitCount: coinSellerLimitCount,
|
||||||
PlatformWithdrawalLimitPeriod: platformLimitPeriod,
|
PlatformWithdrawalLimitPeriod: platformLimitPeriod,
|
||||||
PlatformWithdrawalLimitCount: platformLimitCount,
|
PlatformWithdrawalLimitCount: platformLimitCount,
|
||||||
|
PlatformWithdrawalAllowedDays: platformAllowedDays,
|
||||||
Description: description,
|
Description: description,
|
||||||
CreatedByAdminID: actorID,
|
CreatedByAdminID: actorID,
|
||||||
UpdatedByAdminID: actorID,
|
UpdatedByAdminID: actorID,
|
||||||
@ -692,6 +702,48 @@ func normalizeWithdrawalLimit(period string, count int64, field string) (string,
|
|||||||
return period, count, nil
|
return period, count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeWithdrawalDays 把后台多选日规整为不可变快照中的稳定逗号串;空集合表示不限制日期。
|
||||||
|
func normalizeWithdrawalDays(days []int32) (string, error) {
|
||||||
|
if len(days) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
unique := make(map[int32]struct{}, len(days))
|
||||||
|
for _, day := range days {
|
||||||
|
if day < 1 || day > 31 {
|
||||||
|
return "", errors.New("平台每月可提现日期必须在 1 到 31 号之间")
|
||||||
|
}
|
||||||
|
unique[day] = struct{}{}
|
||||||
|
}
|
||||||
|
normalized := make([]int, 0, len(unique))
|
||||||
|
for day := range unique {
|
||||||
|
normalized = append(normalized, int(day))
|
||||||
|
}
|
||||||
|
sort.Ints(normalized)
|
||||||
|
parts := make([]string, 0, len(normalized))
|
||||||
|
for _, day := range normalized {
|
||||||
|
parts = append(parts, strconv.Itoa(day))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ","), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWithdrawalDays(raw string) []int32 {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return []int32{}
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
days := make([]int32, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
|
day, err := strconv.Atoi(strings.TrimSpace(part))
|
||||||
|
if err != nil || day < 1 || day > 31 {
|
||||||
|
// 数据库异常值不回传给表单,发布和 Wallet 运行侧仍会拒绝不合法快照。
|
||||||
|
return []int32{}
|
||||||
|
}
|
||||||
|
days = append(days, int32(day))
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
func levelModelsFromRequest(requests []levelRequest) ([]model.HostAgencySalaryLevel, error) {
|
func levelModelsFromRequest(requests []levelRequest) ([]model.HostAgencySalaryLevel, error) {
|
||||||
if len(requests) == 0 {
|
if len(requests) == 0 {
|
||||||
return nil, errors.New("至少需要配置一个等级")
|
return nil, errors.New("至少需要配置一个等级")
|
||||||
|
|||||||
@ -26,6 +26,7 @@ func TestPolicyModelFromRequestNormalizesAndSortsLevels(t *testing.T) {
|
|||||||
CoinSellerWithdrawalLimitCount: 3,
|
CoinSellerWithdrawalLimitCount: 3,
|
||||||
PlatformWithdrawalLimitPeriod: "day",
|
PlatformWithdrawalLimitPeriod: "day",
|
||||||
PlatformWithdrawalLimitCount: 1,
|
PlatformWithdrawalLimitCount: 1,
|
||||||
|
PlatformWithdrawalAllowedDays: []int32{30, 15, 30},
|
||||||
Levels: []levelRequest{
|
Levels: []levelRequest{
|
||||||
{Level: 2, RequiredDiamonds: 700000, HostSalaryUSD: "3", HostCoinReward: 198000, AgencySalaryUSD: "0.8"},
|
{Level: 2, RequiredDiamonds: 700000, HostSalaryUSD: "3", HostCoinReward: 198000, AgencySalaryUSD: "0.8"},
|
||||||
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"},
|
||||||
@ -44,7 +45,7 @@ func TestPolicyModelFromRequestNormalizesAndSortsLevels(t *testing.T) {
|
|||||||
if item.GiftCoinToDiamondRatio != "1.000000" || item.ResidualDiamondToUSDRate != "0.000001000000" {
|
if item.GiftCoinToDiamondRatio != "1.000000" || item.ResidualDiamondToUSDRate != "0.000001000000" {
|
||||||
t.Fatalf("ratio fields mismatch: %+v", item)
|
t.Fatalf("ratio fields mismatch: %+v", item)
|
||||||
}
|
}
|
||||||
if item.CoinSellerWithdrawalLimitPeriod != "week" || item.CoinSellerWithdrawalLimitCount != 3 || item.PlatformWithdrawalLimitPeriod != "day" || item.PlatformWithdrawalLimitCount != 1 {
|
if item.CoinSellerWithdrawalLimitPeriod != "week" || item.CoinSellerWithdrawalLimitCount != 3 || item.PlatformWithdrawalLimitPeriod != "day" || item.PlatformWithdrawalLimitCount != 1 || item.PlatformWithdrawalAllowedDays != "15,30" {
|
||||||
t.Fatalf("withdrawal limit fields mismatch: %+v", item)
|
t.Fatalf("withdrawal limit fields mismatch: %+v", item)
|
||||||
}
|
}
|
||||||
if len(item.Levels) != 2 || item.Levels[0].LevelNo != 1 || item.Levels[1].LevelNo != 2 {
|
if len(item.Levels) != 2 || item.Levels[0].LevelNo != 1 || item.Levels[1].LevelNo != 2 {
|
||||||
@ -72,6 +73,14 @@ func TestPolicyModelFromRequestRejectsInvalidWithdrawalLimit(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("negative withdrawal limit count must fail")
|
t.Fatal("negative withdrawal limit count must fail")
|
||||||
}
|
}
|
||||||
|
_, err = policyModelFromRequest("lalu", 1, policyRequest{
|
||||||
|
Name: "Invalid withdrawal day", RegionID: 101, CycleKey: "2026-06", SettlementMode: settlementModeDaily,
|
||||||
|
PlatformWithdrawalLimitPeriod: "month", PlatformWithdrawalAllowedDays: []int32{15, 32},
|
||||||
|
Levels: []levelRequest{{Level: 1, RequiredDiamonds: 350000, HostSalaryUSD: "1.5", HostCoinReward: 90000, AgencySalaryUSD: "0.5"}},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("platform withdrawal day outside 1..31 must fail")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolicyModelFromRequestRejectsNonIncreasingLevels(t *testing.T) {
|
func TestPolicyModelFromRequestRejectsNonIncreasingLevels(t *testing.T) {
|
||||||
@ -201,13 +210,14 @@ func TestEnsureRuntimeHostSalaryPolicySnapshotAcceptsIdenticalExistingSnapshot(t
|
|||||||
GiftCoinToDiamondRatio: "1.000000", ResidualDiamondToUSDRate: "0.000000000000",
|
GiftCoinToDiamondRatio: "1.000000", ResidualDiamondToUSDRate: "0.000000000000",
|
||||||
CoinSellerWithdrawalLimitPeriod: "week", CoinSellerWithdrawalLimitCount: 2,
|
CoinSellerWithdrawalLimitPeriod: "week", CoinSellerWithdrawalLimitCount: 2,
|
||||||
PlatformWithdrawalLimitPeriod: "day", PlatformWithdrawalLimitCount: 1,
|
PlatformWithdrawalLimitPeriod: "day", PlatformWithdrawalLimitCount: 1,
|
||||||
Levels: []model.HostAgencySalaryLevel{{LevelNo: 1, RequiredDiamonds: 100, HostSalaryUSD: "1.50", HostCoinReward: 20, AgencySalaryUSD: "0.50", Status: policyStatusActive, SortOrder: 1}},
|
PlatformWithdrawalAllowedDays: "15,30",
|
||||||
|
Levels: []model.HostAgencySalaryLevel{{LevelNo: 1, RequiredDiamonds: 100, HostSalaryUSD: "1.50", HostCoinReward: 20, AgencySalaryUSD: "0.50", Status: policyStatusActive, SortOrder: 1}},
|
||||||
}
|
}
|
||||||
mock.ExpectExec("INSERT INTO host_agency_salary_policies").WillReturnResult(sqlmock.NewResult(0, 0))
|
mock.ExpectExec("INSERT INTO host_agency_salary_policies").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
mock.ExpectQuery("SELECT cycle_key, policy_version").WithArgs("fami", uint(81)).WillReturnRows(sqlmock.NewRows([]string{
|
mock.ExpectQuery("SELECT cycle_key, policy_version").WithArgs("fami", uint(81)).WillReturnRows(sqlmock.NewRows([]string{
|
||||||
"cycle_key", "policy_version", "name", "region_id", "status", "settlement_mode", "settlement_trigger_mode",
|
"cycle_key", "policy_version", "name", "region_id", "status", "settlement_mode", "settlement_trigger_mode",
|
||||||
"gift_ratio", "residual_rate", "coin_period", "coin_count", "platform_period", "platform_count",
|
"gift_ratio", "residual_rate", "coin_period", "coin_count", "platform_period", "platform_count", "platform_allowed_days",
|
||||||
}).AddRow("2026-08", 81, "August policy", 25, "active", "daily", "automatic", "1.000000", "0.000000000000", "week", 2, "day", 1))
|
}).AddRow("2026-08", 81, "August policy", 25, "active", "daily", "automatic", "1.000000", "0.000000000000", "week", 2, "day", 1, "15,30"))
|
||||||
mock.ExpectQuery("SELECT level_no, required_diamonds").WithArgs("fami", uint(81)).WillReturnRows(sqlmock.NewRows([]string{
|
mock.ExpectQuery("SELECT level_no, required_diamonds").WithArgs("fami", uint(81)).WillReturnRows(sqlmock.NewRows([]string{
|
||||||
"level_no", "required_diamonds", "host_salary_usd_minor", "host_coin_reward", "agency_salary_usd_minor", "status", "sort_order",
|
"level_no", "required_diamonds", "host_salary_usd_minor", "host_coin_reward", "agency_salary_usd_minor", "status", "sort_order",
|
||||||
}).AddRow(1, 100, 150, 20, 50, "active", 1))
|
}).AddRow(1, 100, 150, 20, 50, "active", 1))
|
||||||
@ -227,13 +237,15 @@ func TestRuntimeHostSalaryPolicySnapshotRejectsDifferentContent(t *testing.T) {
|
|||||||
SettlementMode: settlementModeDaily, SettlementTriggerMode: settlementTriggerAutomatic,
|
SettlementMode: settlementModeDaily, SettlementTriggerMode: settlementTriggerAutomatic,
|
||||||
GiftCoinToDiamondRatio: "1", ResidualDiamondToUSDRate: "0",
|
GiftCoinToDiamondRatio: "1", ResidualDiamondToUSDRate: "0",
|
||||||
CoinSellerWithdrawalLimitPeriod: "month", PlatformWithdrawalLimitPeriod: "month",
|
CoinSellerWithdrawalLimitPeriod: "month", PlatformWithdrawalLimitPeriod: "month",
|
||||||
|
PlatformWithdrawalAllowedDays: "15,30",
|
||||||
}
|
}
|
||||||
existing := runtimeHostSalaryPolicySnapshot{
|
existing := runtimeHostSalaryPolicySnapshot{
|
||||||
Name: "August policy", RegionID: 25, CycleKey: "2026-08", PolicyVersion: 81, Status: policyStatusActive,
|
Name: "August policy", RegionID: 25, CycleKey: "2026-08", PolicyVersion: 81, Status: policyStatusActive,
|
||||||
SettlementMode: settlementModeDaily, SettlementTriggerMode: settlementTriggerAutomatic,
|
SettlementMode: settlementModeDaily, SettlementTriggerMode: settlementTriggerAutomatic,
|
||||||
GiftCoinToDiamondRatio: "1.000000", ResidualDiamondToUSDRate: "0.000000000000",
|
GiftCoinToDiamondRatio: "1.000000", ResidualDiamondToUSDRate: "0.000000000000",
|
||||||
CoinSellerWithdrawalLimitPeriod: "month", PlatformWithdrawalLimitPeriod: "month",
|
CoinSellerWithdrawalLimitPeriod: "month", PlatformWithdrawalLimitPeriod: "month",
|
||||||
PlatformWithdrawalLimitCount: 2,
|
PlatformWithdrawalLimitCount: 2,
|
||||||
|
PlatformWithdrawalAllowedDays: "15,30",
|
||||||
}
|
}
|
||||||
if runtimeHostSalaryPolicyMatches(existing, item, 81) {
|
if runtimeHostSalaryPolicyMatches(existing, item, 81) {
|
||||||
t.Fatal("different withdrawal limit content must never be accepted as an idempotent snapshot")
|
t.Fatal("different withdrawal limit content must never be accepted as an idempotent snapshot")
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
"hyapp-admin-server/internal/config"
|
"hyapp-admin-server/internal/config"
|
||||||
|
"hyapp-admin-server/internal/repository"
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||||
@ -50,6 +51,15 @@ func (s *MongoRechargeBillSource) financePaidCoinSellerFilter(ctx context.Contex
|
|||||||
"orderId": bson.M{"$type": "string", "$ne": ""},
|
"orderId": bson.M{"$type": "string", "$ne": ""},
|
||||||
"factory.factoryCode": bson.M{"$type": "string", "$ne": ""},
|
"factory.factoryCode": bson.M{"$type": "string", "$ne": ""},
|
||||||
}
|
}
|
||||||
|
// Legacy 支付厂商码带下划线;按已确认的两个历史写法等值匹配,避免正则扫描,也保证厂商筛选不会混入其他三方。
|
||||||
|
if rechargeSource, ok := financeCoinSellerRechargeSource(query.RechargeType); ok {
|
||||||
|
switch rechargeSource {
|
||||||
|
case repository.CoinSellerRechargeSourceMiFaPay:
|
||||||
|
filter["factory.factoryCode"] = bson.M{"$in": bson.A{"MIFA_PAY", "MIFAPAY"}}
|
||||||
|
case repository.CoinSellerRechargeSourceV5Pay:
|
||||||
|
filter["factory.factoryCode"] = bson.M{"$in": bson.A{"V5_PAY", "V5PAY"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
conditions := bson.A{bson.M{"$or": legacyFreightCommodityMatchConditions()}}
|
conditions := bson.A{bson.M{"$or": legacyFreightCommodityMatchConditions()}}
|
||||||
if query.TargetUserID > 0 {
|
if query.TargetUserID > 0 {
|
||||||
filter["acceptUserId"] = query.TargetUserID
|
filter["acceptUserId"] = query.TargetUserID
|
||||||
@ -332,8 +342,8 @@ func legacyPaidCoinSellerRechargeBillDTO(sourceConfig config.FinanceBillSourceCo
|
|||||||
return rechargeBillDTO{
|
return rechargeBillDTO{
|
||||||
AppCode: sourceConfig.AppCode,
|
AppCode: sourceConfig.AppCode,
|
||||||
TransactionID: legacyDocumentID(document.ID),
|
TransactionID: legacyDocumentID(document.ID),
|
||||||
// Legacy H5 FREIGHT_GOLD 是支付渠道成功单,归入币商-三方;运营手工选择的 USDT 只来自 admin 订单台账。
|
// Legacy H5 以支付单 factoryCode 还原具体厂商;未知历史值保留“币商充值-三方”聚合标签。
|
||||||
RechargeType: financeCoinSellerThirdPartyType,
|
RechargeType: financeCoinSellerBillType(factoryCode),
|
||||||
Status: "succeeded",
|
Status: "succeeded",
|
||||||
ExternalRef: strings.TrimSpace(document.OrderID),
|
ExternalRef: strings.TrimSpace(document.OrderID),
|
||||||
SellerUserID: document.AcceptUserID,
|
SellerUserID: document.AcceptUserID,
|
||||||
|
|||||||
@ -76,7 +76,7 @@ func TestLegacyPaidCoinSellerRechargeBillDTOUsesOriginalBillAmounts(t *testing.T
|
|||||||
LegacyBillCountry: "EG",
|
LegacyBillCountry: "EG",
|
||||||
}
|
}
|
||||||
dto := legacyPaidCoinSellerRechargeBillDTO(aslanBillSourceConfig(), document)
|
dto := legacyPaidCoinSellerRechargeBillDTO(aslanBillSourceConfig(), document)
|
||||||
if dto.RechargeType != financeCoinSellerThirdPartyType || dto.Status != "succeeded" {
|
if dto.RechargeType != financeCoinSellerMiFaPayType || dto.Status != "succeeded" {
|
||||||
t.Fatalf("coin seller bill identity mismatch: %+v", dto)
|
t.Fatalf("coin seller bill identity mismatch: %+v", dto)
|
||||||
}
|
}
|
||||||
if dto.ExternalRef != document.OrderID || dto.SellerUserID != document.AcceptUserID {
|
if dto.ExternalRef != document.OrderID || dto.SellerUserID != document.AcceptUserID {
|
||||||
|
|||||||
@ -41,6 +41,8 @@ const (
|
|||||||
financeRechargeMergeMaxPrefetch = 5000
|
financeRechargeMergeMaxPrefetch = 5000
|
||||||
financeCoinSellerRechargeType = "coin_seller"
|
financeCoinSellerRechargeType = "coin_seller"
|
||||||
financeCoinSellerThirdPartyType = "coin_seller_third_party"
|
financeCoinSellerThirdPartyType = "coin_seller_third_party"
|
||||||
|
financeCoinSellerMiFaPayType = "coin_seller_mifapay"
|
||||||
|
financeCoinSellerV5PayType = "coin_seller_v5pay"
|
||||||
financeCoinSellerUSDTType = "coin_seller_usdt"
|
financeCoinSellerUSDTType = "coin_seller_usdt"
|
||||||
|
|
||||||
// 2^53 以内的 int64 在 float64 里精确可表示,不会被浏览器 JSON 圆整;
|
// 2^53 以内的 int64 在 float64 里精确可表示,不会被浏览器 JSON 圆整;
|
||||||
@ -56,6 +58,10 @@ func financeCoinSellerRechargeSource(rechargeType string) (string, bool) {
|
|||||||
return "", true
|
return "", true
|
||||||
case financeCoinSellerThirdPartyType:
|
case financeCoinSellerThirdPartyType:
|
||||||
return repository.CoinSellerRechargeSourceThirdParty, true
|
return repository.CoinSellerRechargeSourceThirdParty, true
|
||||||
|
case financeCoinSellerMiFaPayType:
|
||||||
|
return repository.CoinSellerRechargeSourceMiFaPay, true
|
||||||
|
case financeCoinSellerV5PayType:
|
||||||
|
return repository.CoinSellerRechargeSourceV5Pay, true
|
||||||
case financeCoinSellerUSDTType:
|
case financeCoinSellerUSDTType:
|
||||||
return repository.CoinSellerRechargeSourceUSDT, true
|
return repository.CoinSellerRechargeSourceUSDT, true
|
||||||
default:
|
default:
|
||||||
@ -69,10 +75,19 @@ func isFinanceCoinSellerRechargeType(rechargeType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func financeCoinSellerBillType(providerCode string) string {
|
func financeCoinSellerBillType(providerCode string) string {
|
||||||
if strings.EqualFold(strings.TrimSpace(providerCode), "usdt") {
|
// Legacy Mongo 使用 MIFA_PAY/V5_PAY,新台账和 wallet 使用 mifapay/v5pay;去掉分隔符后才能得到同一财务厂商口径。
|
||||||
|
normalized := strings.NewReplacer("_", "", "-", "", " ", "").Replace(strings.ToLower(strings.TrimSpace(providerCode)))
|
||||||
|
switch normalized {
|
||||||
|
case "mifapay":
|
||||||
|
return financeCoinSellerMiFaPayType
|
||||||
|
case "v5pay":
|
||||||
|
return financeCoinSellerV5PayType
|
||||||
|
case "usdt", "usdttrc20":
|
||||||
return financeCoinSellerUSDTType
|
return financeCoinSellerUSDTType
|
||||||
|
default:
|
||||||
|
// 未知历史三方不能被误标为任一厂商;保留聚合标签,仍可从 ProviderCode 审计原值。
|
||||||
|
return financeCoinSellerThirdPartyType
|
||||||
}
|
}
|
||||||
return financeCoinSellerThirdPartyType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeFinanceCoinSellerRegionID 把请求里可能被前端 float64 圆整过的 legacy 区域 ID
|
// normalizeFinanceCoinSellerRegionID 把请求里可能被前端 float64 圆整过的 legacy 区域 ID
|
||||||
@ -619,8 +634,8 @@ func (h *Handler) listH5CoinSellerRechargeBills(ctx context.Context, appCode str
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
item.TransactionID = firstNonEmptyPaymentString(walletTransactionID, item.ExternalRef)
|
item.TransactionID = firstNonEmptyPaymentString(walletTransactionID, item.ExternalRef)
|
||||||
// H5 币商身份订单通过支付渠道完成,属于币商-三方;只有运营台账 provider_code=usdt 才进入币商-USDT。
|
// H5 订单直接使用支付事实里的 provider_code,避免厂商筛选后明细仍退化成笼统“三方”。
|
||||||
item.RechargeType = financeCoinSellerThirdPartyType
|
item.RechargeType = financeCoinSellerBillType(item.ProviderCode)
|
||||||
item.Status = "succeeded"
|
item.Status = "succeeded"
|
||||||
item.TargetRegionID = item.SellerRegionID
|
item.TargetRegionID = item.SellerRegionID
|
||||||
item.PolicyID = productID
|
item.PolicyID = productID
|
||||||
@ -682,6 +697,17 @@ func h5CoinSellerRechargeWhere(appCode string, query financeCoinSellerRechargeQu
|
|||||||
"wt.status = 'succeeded'",
|
"wt.status = 'succeeded'",
|
||||||
}
|
}
|
||||||
args := []any{appCode}
|
args := []any{appCode}
|
||||||
|
// wallet 表已有 (app_code,status,provider_code,updated_at_ms) 索引;把厂商条件下推到 SQL 才能保证统计、总数和分页一致。
|
||||||
|
if rechargeSource, ok := financeCoinSellerRechargeSource(query.RechargeType); ok {
|
||||||
|
switch rechargeSource {
|
||||||
|
case repository.CoinSellerRechargeSourceThirdParty:
|
||||||
|
where = append(where, "eo.provider_code IN (?, ?)")
|
||||||
|
args = append(args, repository.CoinSellerRechargeSourceMiFaPay, repository.CoinSellerRechargeSourceV5Pay)
|
||||||
|
case repository.CoinSellerRechargeSourceMiFaPay, repository.CoinSellerRechargeSourceV5Pay:
|
||||||
|
where = append(where, "eo.provider_code = ?")
|
||||||
|
args = append(args, rechargeSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
if query.TargetUserID > 0 {
|
if query.TargetUserID > 0 {
|
||||||
where = append(where, "eo.target_user_id = ?")
|
where = append(where, "eo.target_user_id = ?")
|
||||||
args = append(args, query.TargetUserID)
|
args = append(args, query.TargetUserID)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package payment
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/model"
|
"hyapp-admin-server/internal/model"
|
||||||
@ -26,8 +27,16 @@ func TestFinanceCoinSellerRechargeOrderBillUsesBusinessTime(t *testing.T) {
|
|||||||
t.Fatalf("USDT admin order must expose the refined recharge source: %+v", bill)
|
t.Fatalf("USDT admin order must expose the refined recharge source: %+v", bill)
|
||||||
}
|
}
|
||||||
order.ProviderCode = "mifapay"
|
order.ProviderCode = "mifapay"
|
||||||
if thirdPartyBill := financeCoinSellerRechargeOrderBill(order); thirdPartyBill.RechargeType != financeCoinSellerThirdPartyType {
|
if mifapayBill := financeCoinSellerRechargeOrderBill(order); mifapayBill.RechargeType != financeCoinSellerMiFaPayType {
|
||||||
t.Fatalf("provider admin order must expose the refined recharge source: %+v", thirdPartyBill)
|
t.Fatalf("MiFaPay admin order must expose the provider source: %+v", mifapayBill)
|
||||||
|
}
|
||||||
|
order.ProviderCode = "v5pay"
|
||||||
|
if v5payBill := financeCoinSellerRechargeOrderBill(order); v5payBill.RechargeType != financeCoinSellerV5PayType {
|
||||||
|
t.Fatalf("V5Pay admin order must expose the provider source: %+v", v5payBill)
|
||||||
|
}
|
||||||
|
order.ProviderCode = "unknown_legacy_provider"
|
||||||
|
if legacyBill := financeCoinSellerRechargeOrderBill(order); legacyBill.RechargeType != financeCoinSellerThirdPartyType {
|
||||||
|
t.Fatalf("unknown legacy provider must keep the aggregate third-party source: %+v", legacyBill)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,11 +49,28 @@ func TestFinanceCoinSellerRechargeSourceKeepsParentCompatibility(t *testing.T) {
|
|||||||
if source, ok := financeCoinSellerRechargeSource(financeCoinSellerThirdPartyType); !ok || source != "third_party" {
|
if source, ok := financeCoinSellerRechargeSource(financeCoinSellerThirdPartyType); !ok || source != "third_party" {
|
||||||
t.Fatalf("third-party source mismatch: source=%q ok=%v", source, ok)
|
t.Fatalf("third-party source mismatch: source=%q ok=%v", source, ok)
|
||||||
}
|
}
|
||||||
|
if source, ok := financeCoinSellerRechargeSource(financeCoinSellerMiFaPayType); !ok || source != "mifapay" {
|
||||||
|
t.Fatalf("MiFaPay source mismatch: source=%q ok=%v", source, ok)
|
||||||
|
}
|
||||||
|
if source, ok := financeCoinSellerRechargeSource(financeCoinSellerV5PayType); !ok || source != "v5pay" {
|
||||||
|
t.Fatalf("V5Pay source mismatch: source=%q ok=%v", source, ok)
|
||||||
|
}
|
||||||
if source, ok := financeCoinSellerRechargeSource(financeCoinSellerUSDTType); !ok || source != "usdt" {
|
if source, ok := financeCoinSellerRechargeSource(financeCoinSellerUSDTType); !ok || source != "usdt" {
|
||||||
t.Fatalf("USDT source mismatch: source=%q ok=%v", source, ok)
|
t.Fatalf("USDT source mismatch: source=%q ok=%v", source, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestH5CoinSellerRechargeWhereScopesProviderBeforePagination(t *testing.T) {
|
||||||
|
where, args := h5CoinSellerRechargeWhere("aslan", financeCoinSellerRechargeQuery{RechargeType: financeCoinSellerV5PayType})
|
||||||
|
if !strings.Contains(where, "eo.provider_code = ?") || len(args) != 2 || args[1] != "v5pay" {
|
||||||
|
t.Fatalf("V5Pay filter must be pushed into the indexed SQL query: where=%q args=%v", where, args)
|
||||||
|
}
|
||||||
|
where, args = h5CoinSellerRechargeWhere("aslan", financeCoinSellerRechargeQuery{RechargeType: financeCoinSellerThirdPartyType})
|
||||||
|
if !strings.Contains(where, "eo.provider_code IN (?, ?)") || len(args) != 3 || args[1] != "mifapay" || args[2] != "v5pay" {
|
||||||
|
t.Fatalf("third-party aggregate must contain both providers: where=%q args=%v", where, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeFinanceCoinSellerRegionIDRestoresRoundedLegacyID(t *testing.T) {
|
func TestNormalizeFinanceCoinSellerRegionIDRestoresRoundedLegacyID(t *testing.T) {
|
||||||
handler := New(&mockPaymentWallet{}, nil, nil, nil, nil, WithMoneyRegionSources(&staticMoneyRegionSource{
|
handler := New(&mockPaymentWallet{}, nil, nil, nil, nil, WithMoneyRegionSources(&staticMoneyRegionSource{
|
||||||
regions: []moneyRegionDTO{{AppCode: "aslan", RegionID: 2049040141349486594, RegionCode: "IN", Name: "印度"}},
|
regions: []moneyRegionDTO{{AppCode: "aslan", RegionID: 2049040141349486594, RegionCode: "IN", Name: "印度"}},
|
||||||
|
|||||||
@ -283,6 +283,10 @@ func rechargeSourceLabel(rechargeType string) string {
|
|||||||
return "币商充值"
|
return "币商充值"
|
||||||
case financeCoinSellerThirdPartyType:
|
case financeCoinSellerThirdPartyType:
|
||||||
return "币商充值-三方"
|
return "币商充值-三方"
|
||||||
|
case financeCoinSellerMiFaPayType:
|
||||||
|
return "币商充值-MiFaPay"
|
||||||
|
case financeCoinSellerV5PayType:
|
||||||
|
return "币商充值-V5Pay"
|
||||||
case financeCoinSellerUSDTType:
|
case financeCoinSellerUSDTType:
|
||||||
return "币商充值-USDT"
|
return "币商充值-USDT"
|
||||||
case "coin_seller_stock_purchase":
|
case "coin_seller_stock_purchase":
|
||||||
|
|||||||
@ -19,6 +19,8 @@ var (
|
|||||||
const (
|
const (
|
||||||
// RechargeSource 只描述运营录入币商充值时的付款大类;provider_code 仍保留具体支付商,不能用展示分组覆盖审计事实。
|
// RechargeSource 只描述运营录入币商充值时的付款大类;provider_code 仍保留具体支付商,不能用展示分组覆盖审计事实。
|
||||||
CoinSellerRechargeSourceThirdParty = "third_party"
|
CoinSellerRechargeSourceThirdParty = "third_party"
|
||||||
|
CoinSellerRechargeSourceMiFaPay = "mifapay"
|
||||||
|
CoinSellerRechargeSourceV5Pay = "v5pay"
|
||||||
CoinSellerRechargeSourceUSDT = "usdt"
|
CoinSellerRechargeSourceUSDT = "usdt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -432,11 +434,15 @@ func applyCoinSellerRechargeOrderStatsFilters(query *gorm.DB, options CoinSeller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// applyCoinSellerRechargeSourceFilter 把财务展示分组还原到台账的权威 provider_code。
|
// applyCoinSellerRechargeSourceFilter 把财务展示分组还原到台账的权威 provider_code。
|
||||||
// 当前创建入口只允许 mifapay/v5pay/usdt,使用等值或 IN 条件可以继续利用 provider_code 索引,避免为了筛选扫描并在 Go 内丢弃整页数据。
|
// 当前创建入口只允许 mifapay/v5pay/usdt;聚合三方使用小集合 IN,厂商和 USDT 使用等值条件,均可继续利用 provider_code 索引。
|
||||||
func applyCoinSellerRechargeSourceFilter(query *gorm.DB, rechargeSource string) *gorm.DB {
|
func applyCoinSellerRechargeSourceFilter(query *gorm.DB, rechargeSource string) *gorm.DB {
|
||||||
switch strings.ToLower(strings.TrimSpace(rechargeSource)) {
|
switch strings.ToLower(strings.TrimSpace(rechargeSource)) {
|
||||||
case CoinSellerRechargeSourceThirdParty:
|
case CoinSellerRechargeSourceThirdParty:
|
||||||
return query.Where("provider_code IN ?", []string{"mifapay", "v5pay"})
|
return query.Where("provider_code IN ?", []string{"mifapay", "v5pay"})
|
||||||
|
case CoinSellerRechargeSourceMiFaPay:
|
||||||
|
return query.Where("provider_code = ?", "mifapay")
|
||||||
|
case CoinSellerRechargeSourceV5Pay:
|
||||||
|
return query.Where("provider_code = ?", "v5pay")
|
||||||
case CoinSellerRechargeSourceUSDT:
|
case CoinSellerRechargeSourceUSDT:
|
||||||
return query.Where("provider_code = ?", "usdt")
|
return query.Where("provider_code = ?", "usdt")
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 工资政策是按 App/月份维护的小型配置表;日期使用最长 1,2,...,31 的规范逗号串,空串兼容为每天可提现。
|
||||||
|
SET @ddl := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_host_agency_salary_policies'
|
||||||
|
AND COLUMN_NAME = 'platform_withdrawal_allowed_days') = 0,
|
||||||
|
'ALTER TABLE admin_host_agency_salary_policies ADD COLUMN platform_withdrawal_allowed_days VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''用户找平台提现允许的 UTC 月内日,逗号分隔,空表示每天'' AFTER platform_withdrawal_limit_count, ALGORITHM=INPLACE, LOCK=NONE',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -302,6 +302,7 @@ CREATE TABLE IF NOT EXISTS host_agency_salary_policies (
|
|||||||
coin_seller_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找币商提现次数,0 表示不限',
|
coin_seller_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找币商提现次数,0 表示不限',
|
||||||
platform_withdrawal_limit_period VARCHAR(16) NOT NULL DEFAULT 'month' COMMENT '用户找平台提现限制周期:day/week/month',
|
platform_withdrawal_limit_period VARCHAR(16) NOT NULL DEFAULT 'month' COMMENT '用户找平台提现限制周期:day/week/month',
|
||||||
platform_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找平台提现次数,0 表示不限',
|
platform_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找平台提现次数,0 表示不限',
|
||||||
|
platform_withdrawal_allowed_days VARCHAR(96) NOT NULL DEFAULT '' COMMENT '用户找平台提现允许的 UTC 月内日,逗号分隔,空表示每天',
|
||||||
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间,UTC epoch ms',
|
effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间,UTC epoch ms',
|
||||||
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间,0 表示长期有效',
|
effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间,0 表示长期有效',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 运行快照表是小型政策表;空值保持历史政策“每天可提现”,无需扫描或改写提现、交易和计数流水。
|
||||||
|
SET @ddl := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'host_agency_salary_policies'
|
||||||
|
AND COLUMN_NAME = 'platform_withdrawal_allowed_days') = 0,
|
||||||
|
'ALTER TABLE host_agency_salary_policies ADD COLUMN platform_withdrawal_allowed_days VARCHAR(96) NOT NULL DEFAULT '''' COMMENT ''用户找平台提现允许的 UTC 月内日,逗号分隔,空表示每天'' AFTER platform_withdrawal_limit_count, ALGORITHM=INPLACE, LOCK=NONE',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -43,9 +43,11 @@ type HostSalaryPolicy struct {
|
|||||||
CoinSellerWithdrawalLimitCount int64
|
CoinSellerWithdrawalLimitCount int64
|
||||||
PlatformWithdrawalLimitPeriod string
|
PlatformWithdrawalLimitPeriod string
|
||||||
PlatformWithdrawalLimitCount int64
|
PlatformWithdrawalLimitCount int64
|
||||||
EffectiveFromMs int64
|
// PlatformWithdrawalAllowedDays 是 UTC 月内日的规范逗号串;空串表示不限制提现日期。
|
||||||
EffectiveToMs int64
|
PlatformWithdrawalAllowedDays string
|
||||||
Levels []HostSalaryPolicyLevel
|
EffectiveFromMs int64
|
||||||
|
EffectiveToMs int64
|
||||||
|
Levels []HostSalaryPolicyLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostSalaryPolicyLevel 使用累计值表达工资和奖励,结算时只发放“当前累计 - 已发累计”的差额。
|
// HostSalaryPolicyLevel 使用累计值表达工资和奖励,结算时只发放“当前累计 - 已发累计”的差额。
|
||||||
|
|||||||
@ -2334,7 +2334,11 @@ func TestCreditTaskRewardPointIsHuwaaOnly(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedPointWithdrawalSalaryPolicy(repository *mysqltest.Repository, appCode string, policyID uint64, regionID int64, coinSellerPeriod string, coinSellerCount int64, platformPeriod string, platformCount int64) {
|
func seedPointWithdrawalSalaryPolicy(repository *mysqltest.Repository, appCode string, policyID uint64, regionID int64, coinSellerPeriod string, coinSellerCount int64, platformPeriod string, platformCount int64, platformAllowedDays ...string) {
|
||||||
|
allowedDays := ""
|
||||||
|
if len(platformAllowedDays) > 0 {
|
||||||
|
allowedDays = platformAllowedDays[0]
|
||||||
|
}
|
||||||
repository.SetHostSalaryPolicyForApp(appCode, ledger.HostSalaryPolicy{
|
repository.SetHostSalaryPolicyForApp(appCode, ledger.HostSalaryPolicy{
|
||||||
PolicyID: policyID, PolicyVersion: policyID, CycleKey: time.Now().UTC().Format("2006-01"),
|
PolicyID: policyID, PolicyVersion: policyID, CycleKey: time.Now().UTC().Format("2006-01"),
|
||||||
Name: "point withdrawal limit policy", RegionID: regionID, Status: "active",
|
Name: "point withdrawal limit policy", RegionID: regionID, Status: "active",
|
||||||
@ -2342,9 +2346,46 @@ func seedPointWithdrawalSalaryPolicy(repository *mysqltest.Repository, appCode s
|
|||||||
GiftCoinToDiamondRatio: "1", ResidualDiamondToUSDRate: "0",
|
GiftCoinToDiamondRatio: "1", ResidualDiamondToUSDRate: "0",
|
||||||
CoinSellerWithdrawalLimitPeriod: coinSellerPeriod, CoinSellerWithdrawalLimitCount: coinSellerCount,
|
CoinSellerWithdrawalLimitPeriod: coinSellerPeriod, CoinSellerWithdrawalLimitCount: coinSellerCount,
|
||||||
PlatformWithdrawalLimitPeriod: platformPeriod, PlatformWithdrawalLimitCount: platformCount,
|
PlatformWithdrawalLimitPeriod: platformPeriod, PlatformWithdrawalLimitCount: platformCount,
|
||||||
|
PlatformWithdrawalAllowedDays: allowedDays,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPointWithdrawalPlatformAllowedDaysUseExactUTCDate(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
repository.SetAssetBalanceForApp("fami", 22008, ledger.AssetPoint, 2_000_000)
|
||||||
|
today := time.Now().UTC()
|
||||||
|
allowedDay := today.Format("2")
|
||||||
|
disallowedDay := "1"
|
||||||
|
if today.Day() == 1 {
|
||||||
|
disallowedDay = "2"
|
||||||
|
}
|
||||||
|
seedPointWithdrawalSalaryPolicy(repository, "fami", 228, 25, "month", 0, "month", 0, allowedDay)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
if _, err := svc.FreezePointWithdrawal(context.Background(), ledger.PointWithdrawalCommand{
|
||||||
|
AppCode: "fami", CommandID: "point-day-allowed", UserID: 22008, AssetType: ledger.AssetPoint,
|
||||||
|
GrossPointAmount: 1_000_000, RegionID: 25, WithdrawalRef: "point-day-allowed-ref",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("configured UTC withdrawal day must allow platform freeze: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.SetAssetBalanceForApp("fami", 22009, ledger.AssetPoint, 2_000_000)
|
||||||
|
seedPointWithdrawalSalaryPolicy(repository, "fami", 229, 26, "month", 0, "month", 0, disallowedDay)
|
||||||
|
_, err := svc.FreezePointWithdrawal(context.Background(), ledger.PointWithdrawalCommand{
|
||||||
|
AppCode: "fami", CommandID: "point-day-blocked", UserID: 22009, AssetType: ledger.AssetPoint,
|
||||||
|
GrossPointAmount: 1_000_000, RegionID: 26, WithdrawalRef: "point-day-blocked-ref",
|
||||||
|
})
|
||||||
|
if !xerr.IsCode(err, xerr.PointWithdrawalDateNotAllowed) {
|
||||||
|
t.Fatalf("date outside configured UTC days must be rejected, got %v", err)
|
||||||
|
}
|
||||||
|
metadata := xerr.MetadataOf(err)
|
||||||
|
if metadata["allowed_days"] != disallowedDay || metadata["current_day"] != allowedDay || metadata["timezone"] != "UTC" {
|
||||||
|
t.Fatalf("withdrawal date metadata mismatch: %+v", metadata)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_transactions", "app_code = ? AND command_id = ?", "fami", "point-day-blocked"); got != 0 {
|
||||||
|
t.Fatalf("rejected date must not create a wallet transaction, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPointWithdrawalAppGateWithoutMySQL(t *testing.T) {
|
func TestPointWithdrawalAppGateWithoutMySQL(t *testing.T) {
|
||||||
repository := &fakePointGateRepository{}
|
repository := &fakePointGateRepository{}
|
||||||
svc := walletservice.New(repository)
|
svc := walletservice.New(repository)
|
||||||
|
|||||||
@ -484,7 +484,7 @@ func (r *Repository) queryBoundHostSalaryPolicy(ctx context.Context, q hostSalar
|
|||||||
CAST(p.gift_coin_to_diamond_ratio AS CHAR),
|
CAST(p.gift_coin_to_diamond_ratio AS CHAR),
|
||||||
CAST(p.residual_diamond_to_usd_rate AS CHAR),
|
CAST(p.residual_diamond_to_usd_rate AS CHAR),
|
||||||
p.coin_seller_withdrawal_limit_period, p.coin_seller_withdrawal_limit_count,
|
p.coin_seller_withdrawal_limit_period, p.coin_seller_withdrawal_limit_count,
|
||||||
p.platform_withdrawal_limit_period, p.platform_withdrawal_limit_count,
|
p.platform_withdrawal_limit_period, p.platform_withdrawal_limit_count, p.platform_withdrawal_allowed_days,
|
||||||
p.effective_from_ms, p.effective_to_ms
|
p.effective_from_ms, p.effective_to_ms
|
||||||
FROM host_salary_policy_cycle_bindings binding
|
FROM host_salary_policy_cycle_bindings binding
|
||||||
JOIN host_agency_salary_policies p
|
JOIN host_agency_salary_policies p
|
||||||
@ -504,6 +504,7 @@ func (r *Repository) queryBoundHostSalaryPolicy(ctx context.Context, q hostSalar
|
|||||||
&policy.SettlementMode, &policy.SettlementTriggerMode, &policy.GiftCoinToDiamondRatio, &policy.ResidualDiamondToUSDRate,
|
&policy.SettlementMode, &policy.SettlementTriggerMode, &policy.GiftCoinToDiamondRatio, &policy.ResidualDiamondToUSDRate,
|
||||||
&policy.CoinSellerWithdrawalLimitPeriod, &policy.CoinSellerWithdrawalLimitCount,
|
&policy.CoinSellerWithdrawalLimitPeriod, &policy.CoinSellerWithdrawalLimitCount,
|
||||||
&policy.PlatformWithdrawalLimitPeriod, &policy.PlatformWithdrawalLimitCount,
|
&policy.PlatformWithdrawalLimitPeriod, &policy.PlatformWithdrawalLimitCount,
|
||||||
|
&policy.PlatformWithdrawalAllowedDays,
|
||||||
&policy.EffectiveFromMs, &policy.EffectiveToMs,
|
&policy.EffectiveFromMs, &policy.EffectiveToMs,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ type pointWithdrawalPolicyLimit struct {
|
|||||||
Period string
|
Period string
|
||||||
Limit int64
|
Limit int64
|
||||||
PeriodKey string
|
PeriodKey string
|
||||||
|
AllowedDays string
|
||||||
}
|
}
|
||||||
|
|
||||||
// consumePointWithdrawalLimit 在真实账变事务内解析当前 UTC 月绑定政策并锁定一条计数主键。
|
// consumePointWithdrawalLimit 在真实账变事务内解析当前 UTC 月绑定政策并锁定一条计数主键。
|
||||||
@ -43,6 +44,19 @@ func (r *Repository) consumePointWithdrawalLimit(ctx context.Context, tx *sql.Tx
|
|||||||
// 平台先冻结再建申请;没有稳定引用就无法在建单失败时精确撤销这一次 reservation。
|
// 平台先冻结再建申请;没有稳定引用就无法在建单失败时精确撤销这一次 reservation。
|
||||||
return xerr.New(xerr.InvalidArgument, "withdrawal_ref is required for platform withdrawal")
|
return xerr.New(xerr.InvalidArgument, "withdrawal_ref is required for platform withdrawal")
|
||||||
}
|
}
|
||||||
|
if channel == pointWithdrawalChannelPlatform {
|
||||||
|
allowed, err := platformWithdrawalAllowedOnUTCDate(limit.AllowedDays, time.UnixMilli(nowMS).UTC())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !allowed {
|
||||||
|
return xerr.NewWithMetadata(xerr.PointWithdrawalDateNotAllowed, "point withdrawal date is not allowed", map[string]string{
|
||||||
|
"allowed_days": limit.AllowedDays,
|
||||||
|
"current_day": strconv.Itoa(time.UnixMilli(nowMS).UTC().Day()),
|
||||||
|
"timezone": "UTC",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
appCode := appcode.FromContext(ctx)
|
appCode := appcode.FromContext(ctx)
|
||||||
if limit.Limit > 0 {
|
if limit.Limit > 0 {
|
||||||
@ -119,6 +133,7 @@ func (r *Repository) resolvePointWithdrawalPolicyLimit(ctx context.Context, tx *
|
|||||||
limit.Period, limit.Limit = policy.CoinSellerWithdrawalLimitPeriod, policy.CoinSellerWithdrawalLimitCount
|
limit.Period, limit.Limit = policy.CoinSellerWithdrawalLimitPeriod, policy.CoinSellerWithdrawalLimitCount
|
||||||
case pointWithdrawalChannelPlatform:
|
case pointWithdrawalChannelPlatform:
|
||||||
limit.Period, limit.Limit = policy.PlatformWithdrawalLimitPeriod, policy.PlatformWithdrawalLimitCount
|
limit.Period, limit.Limit = policy.PlatformWithdrawalLimitPeriod, policy.PlatformWithdrawalLimitCount
|
||||||
|
limit.AllowedDays = strings.TrimSpace(policy.PlatformWithdrawalAllowedDays)
|
||||||
default:
|
default:
|
||||||
return pointWithdrawalPolicyLimit{}, xerr.New(xerr.InvalidArgument, "point withdrawal channel is invalid")
|
return pointWithdrawalPolicyLimit{}, xerr.New(xerr.InvalidArgument, "point withdrawal channel is invalid")
|
||||||
}
|
}
|
||||||
@ -144,6 +159,32 @@ func (r *Repository) resolvePointWithdrawalPolicyLimit(ctx context.Context, tx *
|
|||||||
return limit, nil
|
return limit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// platformWithdrawalAllowedOnUTCDate 精确匹配 UTC 日历日;例如 30 号在二月不存在时不会自动改成月底。
|
||||||
|
// 空串兼容迁移前政策为每天可提现,异常快照则返回内部错误,不能静默放开限制。
|
||||||
|
func platformWithdrawalAllowedOnUTCDate(raw string, now time.Time) (bool, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
currentDay := now.UTC().Day()
|
||||||
|
seen := make(map[int]struct{})
|
||||||
|
allowed := false
|
||||||
|
for _, part := range strings.Split(raw, ",") {
|
||||||
|
day, err := strconv.Atoi(strings.TrimSpace(part))
|
||||||
|
if err != nil || day < 1 || day > 31 {
|
||||||
|
return false, xerr.New(xerr.Internal, "point withdrawal allowed days policy is invalid")
|
||||||
|
}
|
||||||
|
if _, duplicate := seen[day]; duplicate {
|
||||||
|
return false, xerr.New(xerr.Internal, "point withdrawal allowed days policy is invalid")
|
||||||
|
}
|
||||||
|
seen[day] = struct{}{}
|
||||||
|
if day == currentDay {
|
||||||
|
allowed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowed, nil
|
||||||
|
}
|
||||||
|
|
||||||
// releasePointWithdrawalLimitReservation 只服务 gateway 建单失败补偿。
|
// releasePointWithdrawalLimitReservation 只服务 gateway 建单失败补偿。
|
||||||
// application_id 非空的人工驳回不会调用这里,因此驳回申请仍占用原限制周期的一次次数。
|
// application_id 非空的人工驳回不会调用这里,因此驳回申请仍占用原限制周期的一次次数。
|
||||||
func (r *Repository) releasePointWithdrawalLimitReservation(ctx context.Context, tx *sql.Tx, userID int64, withdrawalRef string, rollbackCommandID string, nowMS int64) error {
|
func (r *Repository) releasePointWithdrawalLimitReservation(ctx context.Context, tx *sql.Tx, userID int64, withdrawalRef string, rollbackCommandID string, nowMS int64) error {
|
||||||
|
|||||||
@ -87,6 +87,11 @@ func ensureHostSalarySettlementSchema(ctx context.Context, db *sql.DB) error {
|
|||||||
ADD COLUMN platform_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找平台提现次数,0 表示不限' AFTER platform_withdrawal_limit_period`); err != nil && !isDuplicateColumnError(err) {
|
ADD COLUMN platform_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找平台提现次数,0 表示不限' AFTER platform_withdrawal_limit_period`); err != nil && !isDuplicateColumnError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `
|
||||||
|
ALTER TABLE host_agency_salary_policies
|
||||||
|
ADD COLUMN platform_withdrawal_allowed_days VARCHAR(96) NOT NULL DEFAULT '' COMMENT '用户找平台提现允许的 UTC 月内日,逗号分隔,空表示每天' AFTER platform_withdrawal_limit_count`); err != nil && !isDuplicateColumnError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := db.ExecContext(ctx, `
|
if _, err := db.ExecContext(ctx, `
|
||||||
CREATE TABLE IF NOT EXISTS point_withdrawal_policy_counters (
|
CREATE TABLE IF NOT EXISTS point_withdrawal_policy_counters (
|
||||||
app_code VARCHAR(32) NOT NULL,
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
|||||||
@ -967,8 +967,9 @@ func (r *Repository) SetHostSalaryPolicyForApp(appCode string, policy ledger.Hos
|
|||||||
INSERT INTO host_agency_salary_policies (
|
INSERT INTO host_agency_salary_policies (
|
||||||
app_code, policy_id, cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode, gift_coin_to_diamond_ratio,
|
app_code, policy_id, cycle_key, policy_version, name, region_id, status, settlement_mode, settlement_trigger_mode, gift_coin_to_diamond_ratio,
|
||||||
residual_diamond_to_usd_rate, coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
|
residual_diamond_to_usd_rate, coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
|
||||||
platform_withdrawal_limit_period, platform_withdrawal_limit_count, effective_from_ms, effective_to_ms, created_at_ms, updated_at_ms
|
platform_withdrawal_limit_period, platform_withdrawal_limit_count, platform_withdrawal_allowed_days,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
effective_from_ms, effective_to_ms, created_at_ms, updated_at_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
cycle_key = VALUES(cycle_key),
|
cycle_key = VALUES(cycle_key),
|
||||||
policy_version = VALUES(policy_version),
|
policy_version = VALUES(policy_version),
|
||||||
@ -983,12 +984,13 @@ func (r *Repository) SetHostSalaryPolicyForApp(appCode string, policy ledger.Hos
|
|||||||
coin_seller_withdrawal_limit_count = VALUES(coin_seller_withdrawal_limit_count),
|
coin_seller_withdrawal_limit_count = VALUES(coin_seller_withdrawal_limit_count),
|
||||||
platform_withdrawal_limit_period = VALUES(platform_withdrawal_limit_period),
|
platform_withdrawal_limit_period = VALUES(platform_withdrawal_limit_period),
|
||||||
platform_withdrawal_limit_count = VALUES(platform_withdrawal_limit_count),
|
platform_withdrawal_limit_count = VALUES(platform_withdrawal_limit_count),
|
||||||
|
platform_withdrawal_allowed_days = VALUES(platform_withdrawal_allowed_days),
|
||||||
effective_from_ms = VALUES(effective_from_ms),
|
effective_from_ms = VALUES(effective_from_ms),
|
||||||
effective_to_ms = VALUES(effective_to_ms),
|
effective_to_ms = VALUES(effective_to_ms),
|
||||||
updated_at_ms = VALUES(updated_at_ms)
|
updated_at_ms = VALUES(updated_at_ms)
|
||||||
`, appCode, policy.PolicyID, policy.CycleKey, policy.PolicyVersion, policy.Name, policy.RegionID, policy.Status, policy.SettlementMode, policy.SettlementTriggerMode, policy.GiftCoinToDiamondRatio, policy.ResidualDiamondToUSDRate,
|
`, appCode, policy.PolicyID, policy.CycleKey, policy.PolicyVersion, policy.Name, policy.RegionID, policy.Status, policy.SettlementMode, policy.SettlementTriggerMode, policy.GiftCoinToDiamondRatio, policy.ResidualDiamondToUSDRate,
|
||||||
policy.CoinSellerWithdrawalLimitPeriod, policy.CoinSellerWithdrawalLimitCount,
|
policy.CoinSellerWithdrawalLimitPeriod, policy.CoinSellerWithdrawalLimitCount,
|
||||||
policy.PlatformWithdrawalLimitPeriod, policy.PlatformWithdrawalLimitCount,
|
policy.PlatformWithdrawalLimitPeriod, policy.PlatformWithdrawalLimitCount, policy.PlatformWithdrawalAllowedDays,
|
||||||
policy.EffectiveFromMs, policy.EffectiveToMs, nowMs, nowMs)
|
policy.EffectiveFromMs, policy.EffectiveToMs, nowMs, nowMs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.t.Fatalf("seed host salary policy failed: %v", err)
|
r.t.Fatalf("seed host salary policy failed: %v", err)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user