fix mobile task rules and feature availability

This commit is contained in:
richjohnrambo
2026-10-18 20:53:33 +08:00
parent b79b781f15
commit 5a0fe67027
11 changed files with 144 additions and 130 deletions
+14 -8
View File
@@ -803,7 +803,10 @@ func (s *Service) ensureWithdrawalEnabled(ctx context.Context, tenantID int64) e
return err return err
} }
var config map[string]any var config map[string]any
if json.Unmarshal(raw, &config) != nil || !findBool(config, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL") { if json.Unmarshal(raw, &config) != nil {
config = map[string]any{}
}
if enabled, found := findBoolValue(config, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"); found && !enabled {
return store.ErrWithdrawalDisabled return store.ErrWithdrawalDisabled
} }
return nil return nil
@@ -1142,25 +1145,28 @@ func allDigits(value string) bool {
return value != "" return value != ""
} }
func findBool(config map[string]any, keys ...string) bool { func findBoolValue(config map[string]any, keys ...string) (bool, bool) {
for _, key := range keys { for _, key := range keys {
for candidate, value := range config { for candidate, value := range config {
if strings.EqualFold(candidate, key) { if strings.EqualFold(candidate, key) {
switch typed := value.(type) { switch typed := value.(type) {
case bool: case bool:
return typed return typed, true
case float64: case float64:
return typed == 1 return typed == 1, true
case string: case string:
return typed == "1" || strings.EqualFold(typed, "true") || strings.EqualFold(typed, "on") return typed == "1" || strings.EqualFold(typed, "true") || strings.EqualFold(typed, "on"), true
} }
return false, true
} }
} }
} }
for _, value := range config { for _, value := range config {
if nested, ok := value.(map[string]any); ok && findBool(nested, keys...) { if nested, ok := value.(map[string]any); ok {
return true if result, found := findBoolValue(nested, keys...); found {
return result, true
}
} }
} }
return false return false, false
} }
+9 -2
View File
@@ -118,11 +118,18 @@ func TestQuoteCalculatesRateFeeAndLimits(t *testing.T) {
} }
} }
func TestQuoteFailsClosed(t *testing.T) { func TestQuoteDefaultsToEnabledWithoutWithdrawalSwitch(t *testing.T) {
service := &Service{repository: &fakeRepository{config: json.RawMessage(`{}`)}, ready: true, now: time.Now} service := &Service{repository: &fakeRepository{config: json.RawMessage(`{}`)}, ready: true, now: time.Now}
if err := service.ensureWithdrawalEnabled(context.Background(), 0); err != nil {
t.Fatalf("missing switch should default to enabled, got %v", err)
}
}
func TestQuoteHonorsExplicitDisabledWithdrawalSwitch(t *testing.T) {
service := &Service{repository: &fakeRepository{config: json.RawMessage(`{"features":{"withdrawal_enabled":false}}`)}, ready: true, now: time.Now}
_, err := service.Quote(context.Background(), 0, 1, QuoteInput{PaymentAccountID: 1, Amount: 100, Currency: "CNY"}) _, err := service.Quote(context.Background(), 0, 1, QuoteInput{PaymentAccountID: 1, Amount: 100, Currency: "CNY"})
if !errors.Is(err, store.ErrWithdrawalDisabled) { if !errors.Is(err, store.ErrWithdrawalDisabled) {
t.Fatalf("expected disabled error, got %v", err) t.Fatalf("expected explicitly disabled withdrawal, got %v", err)
} }
} }
+25 -15
View File
@@ -328,30 +328,40 @@ func maskedUsername(value string) string {
func parseFeatures(raw json.RawMessage) Features { func parseFeatures(raw json.RawMessage) Features {
var config map[string]any var config map[string]any
if json.Unmarshal(raw, &config) != nil { if json.Unmarshal(raw, &config) != nil {
return Features{} config = map[string]any{}
} }
return Features{ return Features{
TaskEnabled: findBool(config, "task_enabled", "taskEnabled", "TASK"), TaskEnabled: findBoolDefault(config, true, "task_enabled", "taskEnabled", "TASK"),
RechargeEnabled: findBool(config, "recharge_enabled", "rechargeEnabled", "RECHARGE"), RechargeEnabled: findBoolDefault(config, true, "recharge_enabled", "rechargeEnabled", "RECHARGE"),
WithdrawalEnabled: findBool(config, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"), WithdrawalEnabled: findBoolDefault(config, true, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"),
ManualMallEnabled: findBool(config, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"), ManualMallEnabled: findBoolDefault(config, true, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"),
} }
} }
func findBool(config map[string]any, keys ...string) bool { func findBoolDefault(config map[string]any, defaultValue bool, keys ...string) bool {
for _, key := range keys { value, found := findBoolValue(config, keys...)
if value, ok := config[key]; ok { if !found {
return boolValue(value) return defaultValue
}
} }
if nested, ok := config["features"].(map[string]any); ok { return value
for _, key := range keys { }
if value, found := nested[key]; found {
return boolValue(value) func findBoolValue(config map[string]any, keys ...string) (bool, bool) {
for _, key := range keys {
for candidate, value := range config {
if strings.EqualFold(candidate, key) {
return boolValue(value), true
} }
} }
} }
return false for _, value := range config {
if nested, ok := value.(map[string]any); ok {
if result, found := findBoolValue(nested, keys...); found {
return result, true
}
}
}
return false, false
} }
func boolValue(value any) bool { func boolValue(value any) bool {
+12
View File
@@ -77,6 +77,18 @@ func TestParseFeaturesSupportsNestedAndLegacyKeys(t *testing.T) {
} }
} }
func TestParseFeaturesDefaultsMissingSwitchesToEnabled(t *testing.T) {
features := parseFeatures(json.RawMessage(`{}`))
if !features.TaskEnabled || !features.RechargeEnabled || !features.WithdrawalEnabled || !features.ManualMallEnabled {
t.Fatalf("missing switches should default to enabled: %+v", features)
}
features = parseFeatures(json.RawMessage(`{"features":{"task_enabled":false,"withdrawal_enabled":false}}`))
if features.TaskEnabled || features.WithdrawalEnabled || !features.RechargeEnabled || !features.ManualMallEnabled {
t.Fatalf("explicitly disabled switches must remain disabled: %+v", features)
}
}
func TestBannersSanitizeTargets(t *testing.T) { func TestBannersSanitizeTargets(t *testing.T) {
service := New(fakeRepository{banners: []store.Banner{ service := New(fakeRepository{banners: []store.Banner{
{ID: 1, TargetType: 2, TargetValue: "/receive"}, {ID: 1, TargetType: 2, TargetValue: "/receive"},
+1 -1
View File
@@ -205,7 +205,7 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
if wallet.Status != 1 { if wallet.Status != 1 {
return nil, ErrAccountRestricted return nil, ErrAccountRestricted
} }
if member.AutoDispatch == 1 && (wallet.AvailableAmount <= 0 || wallet.AvailableAmount < input.MinimumAmount) { if wallet.AvailableAmount < input.MinimumAmount {
return nil, ErrInsufficientBalance return nil, ErrInsufficientBalance
} }
var pending int64 var pending int64
+11 -49
View File
@@ -18,10 +18,11 @@ const (
defaultPageSize = 20 defaultPageSize = 20
maxPageSize = 100 maxPageSize = 100
defaultDeadlineMinute = 15 defaultDeadlineMinute = 15
minimumClaimAmount = 29900
businessDisplayStart = "09:00:00+08:00"
businessDisplayEnd = "21:00:00+08:00"
) )
var shanghai = time.FixedZone("Asia/Shanghai", 8*60*60)
type Repository interface { type Repository interface {
CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error)
TaskHalls(context.Context, int64, int64) ([]store.TaskHall, error) TaskHalls(context.Context, int64, int64) ([]store.TaskHall, error)
@@ -158,8 +159,6 @@ type taskPolicy struct {
AutoDelay int AutoDelay int
AutoMinAmount int64 AutoMinAmount int64
AutoMaxAmount int64 AutoMaxAmount int64
Start string
End string
Deadline time.Duration Deadline time.Duration
} }
@@ -194,20 +193,18 @@ func (s *Service) Eligibility(ctx context.Context, tenantID, memberID, hallID in
} }
result := &Eligibility{Eligible: true, AvailableAmount: row.AvailableAmount, MinimumAmount: policy.MinimumAmount, result := &Eligibility{Eligible: true, AvailableAmount: row.AvailableAmount, MinimumAmount: policy.MinimumAmount,
TodayCount: row.TodayCount, DailyLimit: row.DailyLimit, PendingOrderID: row.PendingOrderID, TodayCount: row.TodayCount, DailyLimit: row.DailyLimit, PendingOrderID: row.PendingOrderID,
BusinessStartAt: policy.Start + "+08:00", BusinessEndAt: policy.End + "+08:00"} BusinessStartAt: businessDisplayStart, BusinessEndAt: businessDisplayEnd}
switch { switch {
case !policy.Enabled: case !policy.Enabled:
result.reject("TASK_DISABLED", "任务功能未开启") result.reject("TASK_DISABLED", "任务功能未开启")
case !withinBusinessHours(s.now().In(shanghai), policy.Start, policy.End):
result.reject("OUTSIDE_BUSINESS_HOURS", "当前不在营业时间")
case row.MemberStatus != 1: case row.MemberStatus != 1:
result.reject("ACCOUNT_RESTRICTED", "账户状态受限") result.reject("ACCOUNT_RESTRICTED", "账户状态受限")
case !row.HallEligible: case !row.HallEligible:
result.reject("LEVEL_NOT_ELIGIBLE", "当前等级不能进入该任务大厅") result.reject("LEVEL_NOT_ELIGIBLE", "当前等级不能进入该任务大厅")
case row.WalletStatus != 1: case row.WalletStatus != 1:
result.reject("ACCOUNT_RESTRICTED", "账户状态受限") result.reject("ACCOUNT_RESTRICTED", "账户状态受限")
case row.AutoDispatch == 1 && (row.AvailableAmount <= 0 || row.AvailableAmount < policy.MinimumAmount): case row.AvailableAmount < policy.MinimumAmount:
result.reject("INSUFFICIENT_BALANCE", "可用余额不足") result.reject("INSUFFICIENT_BALANCE", "可用余额不足299元")
case row.DailyLimit > 0 && row.TodayCount >= row.DailyLimit: case row.DailyLimit > 0 && row.TodayCount >= row.DailyLimit:
result.reject("DAILY_LIMIT_REACHED", "已达当日领取上限") result.reject("DAILY_LIMIT_REACHED", "已达当日领取上限")
case row.PendingOrderID > 0: case row.PendingOrderID > 0:
@@ -233,9 +230,6 @@ func (s *Service) Claim(ctx context.Context, tenantID, memberID, hallID int64) (
if !policy.Enabled { if !policy.Enabled {
return nil, ErrTaskDisabled return nil, ErrTaskDisabled
} }
if !withinBusinessHours(s.now().In(shanghai), policy.Start, policy.End) {
return nil, ErrOutsideBusinessHour
}
now := s.now().UTC() now := s.now().UTC()
row, err := s.repository.ClaimTask(ctx, store.ClaimTaskInput{TenantID: tenantID, MemberID: memberID, row, err := s.repository.ClaimTask(ctx, store.ClaimTaskInput{TenantID: tenantID, MemberID: memberID,
HallID: hallID, MinimumAmount: policy.MinimumAmount, AutoDelay: policy.AutoDelay, HallID: hallID, MinimumAmount: policy.MinimumAmount, AutoDelay: policy.AutoDelay,
@@ -379,14 +373,14 @@ func (s *Service) policy(ctx context.Context, tenantID int64) (taskPolicy, error
if minutes <= 0 || minutes > 24*60 { if minutes <= 0 || minutes > 24*60 {
minutes = defaultDeadlineMinute minutes = defaultDeadlineMinute
} }
start := clockValue(findValue(config, "task_business_start_at", "business_start_at", "taskBusinessStartAt"), "00:00:00") enabledValue := findValue(config, "task_enabled", "taskEnabled", "TASK")
end := clockValue(findValue(config, "task_business_end_at", "business_end_at", "taskBusinessEndAt"), "23:59:59") enabled := enabledValue == nil || boolValue(enabledValue)
return taskPolicy{Enabled: boolValue(findValue(config, "task_enabled", "taskEnabled", "TASK")), return taskPolicy{Enabled: enabled,
MinimumAmount: max64(0, int64Value(findValue(config, "task_minimum_amount", "minimum_balance_amount", "taskMinimumAmount"))), MinimumAmount: minimumClaimAmount,
AutoDelay: max(0, intValue(findValue(config, "auto_order_delay_seconds", "autoOrderDelaySeconds"))), AutoDelay: max(0, intValue(findValue(config, "auto_order_delay_seconds", "autoOrderDelaySeconds"))),
AutoMinAmount: max64(0, int64Value(findValue(config, "auto_order_min_amount", "autoOrderMinAmount"))), AutoMinAmount: max64(0, int64Value(findValue(config, "auto_order_min_amount", "autoOrderMinAmount"))),
AutoMaxAmount: max64(0, int64Value(findValue(config, "auto_order_max_amount", "autoOrderMaxAmount"))), AutoMaxAmount: max64(0, int64Value(findValue(config, "auto_order_max_amount", "autoOrderMaxAmount"))),
Start: start, End: end, Deadline: time.Duration(minutes) * time.Minute}, nil Deadline: time.Duration(minutes) * time.Minute}, nil
} }
func orderFromStore(row store.TaskOrder, detail bool) Order { func orderFromStore(row store.TaskOrder, detail bool) Order {
@@ -506,38 +500,6 @@ func submissionStatusText(status int) string {
return names[status] return names[status]
} }
func withinBusinessHours(now time.Time, start, end string) bool {
current := now.Hour()*3600 + now.Minute()*60 + now.Second()
startSecond := parseClock(start)
endSecond := parseClock(end)
if startSecond <= endSecond {
return current >= startSecond && current <= endSecond
}
return current >= startSecond || current <= endSecond
}
func parseClock(value string) int {
parsed, err := time.Parse("15:04:05", value)
if err != nil {
return 0
}
return parsed.Hour()*3600 + parsed.Minute()*60 + parsed.Second()
}
func clockValue(value any, fallback string) string {
raw, ok := value.(string)
if !ok {
return fallback
}
raw = strings.TrimSpace(raw)
for _, layout := range []string{"15:04:05", "15:04"} {
if parsed, err := time.Parse(layout, raw); err == nil {
return parsed.Format("15:04:05")
}
}
return fallback
}
func findValue(config map[string]any, keys ...string) any { func findValue(config map[string]any, keys ...string) any {
for _, key := range keys { for _, key := range keys {
for candidate, value := range config { for candidate, value := range config {
+36 -28
View File
@@ -62,58 +62,66 @@ func (f *fakeRepository) OrderLedgers(context.Context, int64, int64, []string) (
return []map[string]any{}, nil return []map[string]any{}, nil
} }
func TestEligibilityAppliesPublishedPolicy(t *testing.T) { func TestEligibilityRequiresFixedMinimumBalance(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":true},"task_minimum_amount":50000,"task_business_start_at":"09:00","task_business_end_at":"22:00"}`), repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":true},"task_minimum_amount":0}`),
eligibility: &store.TaskEligibility{MemberStatus: 1, AutoDispatch: 1, HallEligible: true, AvailableAmount: 49999, WalletStatus: 1, DailyLimit: 5, TodayCount: 2}} eligibility: &store.TaskEligibility{MemberStatus: 1, AutoDispatch: 1, HallEligible: true, AvailableAmount: 29899, WalletStatus: 1, DailyLimit: 5, TodayCount: 2}}
service := New(repository) service := New(repository)
service.now = func() time.Time { return time.Date(2026, 10, 9, 10, 0, 0, 0, shanghai) }
result, err := service.Eligibility(context.Background(), 0, 10, 1) result, err := service.Eligibility(context.Background(), 0, 10, 1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if result.Eligible || result.Reason != "INSUFFICIENT_BALANCE" || result.MinimumAmount != 50000 { if result.Eligible || result.Reason != "INSUFFICIENT_BALANCE" || result.MinimumAmount != 29900 {
t.Fatalf("unexpected eligibility: %#v", result) t.Fatalf("unexpected eligibility: %#v", result)
} }
} }
func TestEligibilityAllowsManualDispatchBelowMinimumBalance(t *testing.T) { func TestEligibilityAlsoRequiresMinimumForManualDispatch(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":true},"task_minimum_amount":50000,"task_business_start_at":"09:00","task_business_end_at":"22:00"}`), repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":true}}`),
eligibility: &store.TaskEligibility{MemberStatus: 1, AutoDispatch: 2, HallEligible: true, AvailableAmount: 0, WalletStatus: 1, DailyLimit: 5}} eligibility: &store.TaskEligibility{MemberStatus: 1, AutoDispatch: 2, HallEligible: true, AvailableAmount: 29899, WalletStatus: 1, DailyLimit: 5}}
service := New(repository) service := New(repository)
service.now = func() time.Time { return time.Date(2026, 10, 9, 10, 0, 0, 0, shanghai) }
result, err := service.Eligibility(context.Background(), 0, 10, 1)
if err != nil {
t.Fatal(err)
}
if !result.Eligible {
t.Fatalf("manual dispatch should ignore the automatic minimum balance gate: %#v", result)
}
}
func TestEligibilityRejectsAutoDispatchWithZeroBalanceWhenMinimumIsZero(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":true},"task_minimum_amount":0,"task_business_start_at":"09:00","task_business_end_at":"22:00"}`),
eligibility: &store.TaskEligibility{MemberStatus: 1, AutoDispatch: 1, HallEligible: true, AvailableAmount: 0, WalletStatus: 1, DailyLimit: 5}}
service := New(repository)
service.now = func() time.Time { return time.Date(2026, 10, 9, 10, 0, 0, 0, shanghai) }
result, err := service.Eligibility(context.Background(), 0, 10, 1) result, err := service.Eligibility(context.Background(), 0, 10, 1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if result.Eligible || result.Reason != "INSUFFICIENT_BALANCE" { if result.Eligible || result.Reason != "INSUFFICIENT_BALANCE" {
t.Fatalf("zero balance must not allow automatic claiming: %#v", result) t.Fatalf("manual dispatch must use the same minimum balance gate: %#v", result)
} }
} }
func TestClaimFailsClosedWithoutTaskSwitch(t *testing.T) { func TestEligibilityDoesNotEnforceDisplayedBusinessHours(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":true},"task_business_start_at":"09:00","task_business_end_at":"21:00"}`),
eligibility: &store.TaskEligibility{MemberStatus: 1, AutoDispatch: 1, HallEligible: true, AvailableAmount: 29900, WalletStatus: 1, DailyLimit: 5}}
service := New(repository)
service.now = func() time.Time { return time.Date(2026, 10, 9, 23, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*60*60)) }
result, err := service.Eligibility(context.Background(), 0, 10, 1)
if err != nil {
t.Fatal(err)
}
if !result.Eligible || result.BusinessStartAt != "09:00:00+08:00" || result.BusinessEndAt != "21:00:00+08:00" {
t.Fatalf("business hours are display-only: %#v", result)
}
}
func TestClaimDefaultsToEnabledWithoutTaskSwitch(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{}`)} repository := &fakeRepository{config: json.RawMessage(`{}`)}
service := New(repository) service := New(repository)
service.now = func() time.Time { return time.Date(2026, 10, 9, 2, 0, 0, 0, time.UTC) }
if _, err := service.Claim(context.Background(), 0, 10, 1); err != nil {
t.Fatalf("missing switch should default to enabled, got %v", err)
}
}
func TestClaimHonorsExplicitDisabledTaskSwitch(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{"features":{"task_enabled":false}}`)}
service := New(repository)
_, err := service.Claim(context.Background(), 0, 10, 1) _, err := service.Claim(context.Background(), 0, 10, 1)
if !errors.Is(err, ErrTaskDisabled) { if !errors.Is(err, ErrTaskDisabled) {
t.Fatalf("expected task disabled, got %v", err) t.Fatalf("expected explicitly disabled task, got %v", err)
} }
} }
@@ -128,7 +136,7 @@ func TestClaimBuildsMatchingOrder(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if result.Status != "matching" || repository.claimed.HallID != 2 || if result.Status != "matching" || repository.claimed.HallID != 2 ||
!repository.claimed.DeadlineAt.Equal(now.Add(20*time.Minute)) { repository.claimed.MinimumAmount != 29900 || !repository.claimed.DeadlineAt.Equal(now.Add(20*time.Minute)) {
t.Fatalf("unexpected claim: result=%#v input=%#v", result, repository.claimed) t.Fatalf("unexpected claim: result=%#v input=%#v", result, repository.claimed)
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ const reasonMessages: Record<string, string> = {
RATE_LIMITED: '操作过于频繁,请稍后再试', RATE_LIMITED: '操作过于频繁,请稍后再试',
TASK_DISABLED: '领取功能暂未开放', TASK_DISABLED: '领取功能暂未开放',
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内', OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
INSUFFICIENT_BALANCE: '可用余额不足', INSUFFICIENT_BALANCE: '可用余额不足299元,暂时无法领取',
DAILY_LIMIT_REACHED: '今日领取次数已用完', DAILY_LIMIT_REACHED: '今日领取次数已用完',
PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理', PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理',
NO_TASK_AVAILABLE: '当前暂无可领取任务', NO_TASK_AVAILABLE: '当前暂无可领取任务',
+18 -7
View File
@@ -3760,10 +3760,18 @@ body {
.m-rank-winner > strong, .m-rank-winner > strong,
.m-rank-winner > em { .m-rank-winner > em {
max-width: 100%; max-width: 100%;
font-style: normal;
}
.m-rank-winner > strong {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-style: normal; }
.m-rank-winner > em {
overflow: visible;
font-size: clamp(11px, 3vw, 14px);
line-height: 1.2;
overflow-wrap: anywhere;
} }
.m-rank-winner > small { .m-rank-winner > small {
margin-top: 4px; margin-top: 4px;
@@ -3781,7 +3789,7 @@ body {
.m-rank-table header, .m-rank-table header,
.m-rank-row { .m-rank-row {
display: grid; display: grid;
grid-template-columns: 48px minmax(0, 1.5fr) minmax(82px, 1fr) 70px; grid-template-columns: 48px minmax(0, 1fr) max-content 70px;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
} }
@@ -3823,6 +3831,7 @@ body {
font-style: normal; font-style: normal;
} }
.m-rank-row strong { .m-rank-row strong {
white-space: nowrap;
font-size: 17px; font-size: 17px;
font-weight: 500; font-weight: 500;
} }
@@ -3849,7 +3858,7 @@ body {
.m-home-rank-list a { .m-home-rank-list a {
min-width: 0; min-width: 0;
display: grid; display: grid;
grid-template-columns: 30px 34px minmax(0, 1fr) 72px 82px; grid-template-columns: 30px 34px minmax(0, 1fr) max-content 82px;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
color: var(--m-ink); color: var(--m-ink);
@@ -3895,14 +3904,16 @@ body {
.m-home-rank-list strong, .m-home-rank-list strong,
.m-home-rank-list em { .m-home-rank-list em {
min-width: 0; min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.m-home-rank-list strong { .m-home-rank-list strong {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500; font-weight: 500;
} }
.m-home-rank-list em { .m-home-rank-list em {
overflow: visible;
font-size: clamp(12px, 3.2vw, 15px);
font-style: normal; font-style: normal;
text-align: right; text-align: right;
font-weight: 600; font-weight: 600;
@@ -4039,14 +4050,14 @@ body {
} }
.m-rank-table header, .m-rank-table header,
.m-rank-row { .m-rank-row {
grid-template-columns: 38px minmax(0, 1.4fr) 72px 58px; grid-template-columns: 38px minmax(0, 1fr) max-content 58px;
gap: 5px; gap: 5px;
} }
.m-rank-table { .m-rank-table {
padding-inline: 8px; padding-inline: 8px;
} }
.m-home-rank-list a { .m-home-rank-list a {
grid-template-columns: 26px 32px minmax(0, 1fr) 54px 72px; grid-template-columns: 26px 32px minmax(0, 1fr) max-content 68px;
gap: 5px; gap: 5px;
} }
.m-home-rank-list small { .m-home-rank-list small {
+16 -18
View File
@@ -22,21 +22,22 @@
</div> </div>
</section> </section>
<div v-if="halls.length && !eligibleHalls.length && eligibility?.eligible === false" class="m-claim2-warning"> <div v-if="balanceInsufficient" class="m-claim2-warning">
<n-icon :component="AlertCircleOutline" /> <n-icon :component="AlertCircleOutline" />
<span>{{ eligibility.message || '暂不满足领取条件' }}</span> <span>可用余额不足299元</span>
<button v-if="eligibility.pending_order_id" type="button" @click="openPendingOrder">查看任务</button>
</div> </div>
<section class="m-claim2-rules"> <section class="m-claim2-rules">
<h2><span></span>活动规则<span></span></h2> <h2><span></span>活动规则<span></span></h2>
<ol> <ol>
<li>平台运营时间每天 {{ businessHours }}。</li> <li>平台运营时间每天上午9:00-晚上21:00点!</li>
<li>每一次储入请先咨询客服获取商家卡号,如有疑问请联系在线客服。</li> <li>每一次储入必须咨询客户获取商家卡号!如有不理解可联系在线客服!</li>
<li>领取后系统将自动分配任务,请按订单要求完成任务内容。</li> <li>领取后系统将自动分配任务,请按订单要求完成任务内容!</li>
<li>领取时间根据会员等级不同,系统随机匹配订单。</li> <li>领取订单的时间是根据会员等级的不同!系统随机匹配订单!</li>
<li>账户可用余额需达到 {{ money(eligibility?.minimum_amount) }} 才可领取。</li> <li>会员账户必须要拥有299启动资金才可进行领取!</li>
<li>订单需要在规定时间内完成,请勿超时提交。</li> <li>订单需要在规定时间内完成!请勿超时完成商品!</li>
<li>平台硬性条件提现必须信用分值满足100分,请广大会员珍惜账户信用分值!信用分下降必须联系客服处理恢复100分才可正常提现,信用分提升金额具体根据会员等级规定,详细咨询平台客服。</li>
<li>非本人充值资金需要提供支付人身份证信息验证充值资金,避免出现非必要的资金纠纷。</li>
</ol> </ol>
</section> </section>
</div> </div>
@@ -132,11 +133,12 @@
const eligibleHalls = computed(() => const eligibleHalls = computed(() =>
halls.value.filter((hall) => eligibilityByHall.value[hall.id]?.eligible) halls.value.filter((hall) => eligibilityByHall.value[hall.id]?.eligible)
); );
const businessHours = computed(() => { const balanceInsufficient = computed(() =>
const start = String(eligibility.value?.business_start_at || '09:00').slice(0, 5); Object.values(eligibilityByHall.value).some((item: any) =>
const end = String(eligibility.value?.business_end_at || '21:00').slice(0, 5); Number(item?.minimum_amount || 0) > 0 &&
return `${start}-${end}`; Number(item?.available_amount || 0) < Number(item.minimum_amount)
}); )
);
const isAutoMatching = computed(() => order.value?.dispatch_mode !== 'manual'); const isAutoMatching = computed(() => order.value?.dispatch_mode !== 'manual');
const matchingHint = computed(() => { const matchingHint = computed(() => {
if (order.value?.dispatch_mode === 'manual') return '订单已领取,正在等待后台匹配...'; if (order.value?.dispatch_mode === 'manual') return '订单已领取,正在等待后台匹配...';
@@ -348,10 +350,6 @@
closeResult(); closeResult();
router.push('/app/orders'); router.push('/app/orders');
} }
function openPendingOrder() {
const orderID = Number(eligibility.value?.pending_order_id || 0);
router.push(orderID > 0 ? `/app/orders/${orderID}` : '/app/orders');
}
function retryClaim() { function retryClaim() {
closeResult(); closeResult();
void claim(); void claim();
+1 -1
View File
@@ -183,7 +183,7 @@
enabled: home.value?.features?.withdrawal_enabled === true, enabled: home.value?.features?.withdrawal_enabled === true,
}, },
{ {
label: '商城', label: '手工',
path: '/app/mall', path: '/app/mall',
icon: mobileAssets.shortcutMall, icon: mobileAssets.shortcutMall,
enabled: home.value?.features?.manual_mall_enabled !== false, enabled: home.value?.features?.manual_mall_enabled !== false,