feat: add scheduled automatic task matching
This commit is contained in:
+73
-11
@@ -2,10 +2,12 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
cryptorand "crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
@@ -49,6 +51,7 @@ type ClaimTaskInput struct {
|
|||||||
MemberID int64
|
MemberID int64
|
||||||
HallID int64
|
HallID int64
|
||||||
MinimumAmount int64
|
MinimumAmount int64
|
||||||
|
AutoDelay int
|
||||||
DeadlineAt time.Time
|
DeadlineAt time.Time
|
||||||
OrderNo string
|
OrderNo string
|
||||||
}
|
}
|
||||||
@@ -212,8 +215,12 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
|
|||||||
if pending > 0 {
|
if pending > 0 {
|
||||||
return nil, ErrPendingOrderExists
|
return nil, ErrPendingOrderExists
|
||||||
}
|
}
|
||||||
var dailyLimit int
|
var levelRule struct {
|
||||||
err = tx.GetContext(ctx, &dailyLimit, `SELECT COALESCE(v.daily_order_limit,0) FROM hg_app_member_level l
|
DailyLimit int `db:"daily_order_limit"`
|
||||||
|
RuleJSON json.RawMessage `db:"rule_json"`
|
||||||
|
}
|
||||||
|
err = tx.GetContext(ctx, &levelRule, `SELECT COALESCE(v.daily_order_limit,0) AS daily_order_limit,
|
||||||
|
COALESCE(v.rule_json,JSON_OBJECT()) AS rule_json FROM hg_app_member_level l
|
||||||
LEFT JOIN hg_app_member_level_version v ON v.tenant_id=l.tenant_id AND v.id=l.current_version_id AND v.publish_status=3
|
LEFT JOIN hg_app_member_level_version v ON v.tenant_id=l.tenant_id AND v.id=l.current_version_id AND v.publish_status=3
|
||||||
WHERE l.tenant_id=? AND l.id=? AND l.status=1 AND l.deleted_at IS NULL LIMIT 1`, input.TenantID, member.LevelID)
|
WHERE l.tenant_id=? AND l.id=? AND l.status=1 AND l.deleted_at IS NULL LIMIT 1`, input.TenantID, member.LevelID)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -222,25 +229,52 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if dailyLimit > 0 {
|
if levelRule.DailyLimit > 0 {
|
||||||
var todayCount int
|
var todayCount int
|
||||||
if err := tx.GetContext(ctx, &todayCount, `SELECT COUNT(*) FROM hg_trade_order WHERE tenant_id=? AND member_id=?
|
if err := tx.GetContext(ctx, &todayCount, `SELECT COUNT(*) FROM hg_trade_order WHERE tenant_id=? AND member_id=?
|
||||||
AND order_type=2 AND created_at>=UTC_DATE() AND created_at<UTC_DATE()+INTERVAL 1 DAY`, input.TenantID, input.MemberID); err != nil {
|
AND order_type=2 AND created_at>=UTC_DATE() AND created_at<UTC_DATE()+INTERVAL 1 DAY`, input.TenantID, input.MemberID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if todayCount >= dailyLimit {
|
if todayCount >= levelRule.DailyLimit {
|
||||||
return nil, ErrDailyLimitReached
|
return nil, ErrDailyLimitReached
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dispatchMode, message := "manual", "等待后台匹配订单"
|
dispatchMode, message := "manual", "等待后台匹配订单"
|
||||||
|
matchAt := time.Now().UTC()
|
||||||
|
deadlineAt := input.DeadlineAt.UTC()
|
||||||
|
delaySeconds := 0
|
||||||
if member.AutoDispatch == 1 {
|
if member.AutoDispatch == 1 {
|
||||||
dispatchMode, message = "auto", "正在自动匹配订单"
|
dispatchMode, message = "auto", "正在自动匹配订单"
|
||||||
|
delayMin, delayMax := input.AutoDelay, input.AutoDelay
|
||||||
|
var levelRules map[string]any
|
||||||
|
if json.Unmarshal(levelRule.RuleJSON, &levelRules) == nil {
|
||||||
|
if value, ok := jsonInt(levelRules["auto_order_delay_min_seconds"]); ok {
|
||||||
|
delayMin = value
|
||||||
|
}
|
||||||
|
if value, ok := jsonInt(levelRules["auto_order_delay_max_seconds"]); ok {
|
||||||
|
delayMax = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if delayMin < 0 {
|
||||||
|
delayMin = 0
|
||||||
|
}
|
||||||
|
if delayMax < delayMin {
|
||||||
|
delayMax = delayMin
|
||||||
|
}
|
||||||
|
delaySeconds = randomDelaySeconds(delayMin, delayMax)
|
||||||
|
matchAt = matchAt.Add(time.Duration(delaySeconds) * time.Second)
|
||||||
|
completionWindow := input.DeadlineAt.UTC().Sub(time.Now().UTC())
|
||||||
|
if completionWindow > 0 {
|
||||||
|
deadlineAt = matchAt.Add(completionWindow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rules, err := json.Marshal(map[string]any{
|
rules, err := json.Marshal(map[string]any{
|
||||||
"source": "member_auto_claim",
|
"source": "member_auto_claim",
|
||||||
"dispatch_mode": dispatchMode,
|
"dispatch_mode": dispatchMode,
|
||||||
"hall_id": input.HallID,
|
"hall_id": input.HallID,
|
||||||
"deadline_at": input.DeadlineAt.UTC().Format(time.RFC3339),
|
"deadline_at": deadlineAt.Format(time.RFC3339),
|
||||||
|
"scheduled_match_at": matchAt.Format(time.RFC3339),
|
||||||
|
"dispatch_delay_seconds": delaySeconds,
|
||||||
"member_level_id": member.LevelID,
|
"member_level_id": member.LevelID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -267,7 +301,7 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
|
|||||||
}
|
}
|
||||||
return &TaskOrder{ID: orderID, OrderNo: input.OrderNo, Currency: "CNY",
|
return &TaskOrder{ID: orderID, OrderNo: input.OrderNo, Currency: "CNY",
|
||||||
MemberLevelID: member.LevelID, RuleSnapshot: rules, Message: message,
|
MemberLevelID: member.LevelID, RuleSnapshot: rules, Message: message,
|
||||||
OrderStatus: orderStatusMatching, FreezeStatus: 1, DeadlineAt: input.DeadlineAt.UTC().Format(time.RFC3339),
|
OrderStatus: orderStatusMatching, FreezeStatus: 1, DeadlineAt: deadlineAt.Format(time.RFC3339),
|
||||||
CreatedAt: time.Now().UTC().Format(time.RFC3339)}, nil
|
CreatedAt: time.Now().UTC().Format(time.RFC3339)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +332,16 @@ func (s *Store) AutoMatchTaskOrder(ctx context.Context, tenantID, memberID, orde
|
|||||||
if order.Status != orderStatusMatching {
|
if order.Status != orderStatusMatching {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
if order.AutoDispatch != 1 || delaySeconds > 0 && time.Now().UTC().Before(order.CreatedAt.UTC().Add(time.Duration(delaySeconds)*time.Second)) {
|
matchAt := order.CreatedAt.UTC().Add(time.Duration(delaySeconds) * time.Second)
|
||||||
|
snapshot := map[string]any{}
|
||||||
|
if len(order.RuleSnapshot) > 0 && json.Unmarshal(order.RuleSnapshot, &snapshot) == nil {
|
||||||
|
if raw, ok := snapshot["scheduled_match_at"].(string); ok {
|
||||||
|
if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil {
|
||||||
|
matchAt = parsed.UTC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if order.AutoDispatch != 1 || time.Now().UTC().Before(matchAt) {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,10 +400,6 @@ func (s *Store) AutoMatchTaskOrder(ctx context.Context, tenantID, memberID, orde
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot := map[string]any{}
|
|
||||||
if len(order.RuleSnapshot) > 0 {
|
|
||||||
_ = json.Unmarshal(order.RuleSnapshot, &snapshot)
|
|
||||||
}
|
|
||||||
snapshot["match_source"] = "system_auto_match"
|
snapshot["match_source"] = "system_auto_match"
|
||||||
snapshot["product_id"] = product.ID
|
snapshot["product_id"] = product.ID
|
||||||
snapshot["product_no"] = product.ProductNo
|
snapshot["product_no"] = product.ProductNo
|
||||||
@@ -409,6 +448,29 @@ func (s *Store) AutoMatchTaskOrder(ctx context.Context, tenantID, memberID, orde
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func jsonInt(value any) (int, bool) {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return int(typed), true
|
||||||
|
case json.Number:
|
||||||
|
parsed, err := typed.Int64()
|
||||||
|
return int(parsed), err == nil
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomDelaySeconds(minimum, maximum int) int {
|
||||||
|
if maximum <= minimum {
|
||||||
|
return minimum
|
||||||
|
}
|
||||||
|
value, err := cryptorand.Int(cryptorand.Reader, big.NewInt(int64(maximum-minimum+1)))
|
||||||
|
if err != nil {
|
||||||
|
return minimum
|
||||||
|
}
|
||||||
|
return minimum + int(value.Int64())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) TaskOrders(ctx context.Context, tenantID, memberID int64, status, offset, limit int) (*TaskOrderPage, error) {
|
func (s *Store) TaskOrders(ctx context.Context, tenantID, memberID int64, status, offset, limit int) (*TaskOrderPage, error) {
|
||||||
where := ` WHERE o.tenant_id=? AND o.member_id=? AND o.order_type=2`
|
where := ` WHERE o.tenant_id=? AND o.member_id=? AND o.order_type=2`
|
||||||
args := []any{tenantID, memberID}
|
args := []any{tenantID, memberID}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestRandomDelaySeconds(t *testing.T) {
|
||||||
|
for index := 0; index < 100; index++ {
|
||||||
|
value := randomDelaySeconds(900, 1200)
|
||||||
|
if value < 900 || value > 1200 {
|
||||||
|
t.Fatalf("delay out of range: %d", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value := randomDelaySeconds(30, 30); value != 30 {
|
||||||
|
t.Fatalf("fixed delay = %d, want 30", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,8 @@ type Order struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
StatusText string `json:"status_text"`
|
StatusText string `json:"status_text"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
ScheduledMatchAt string `json:"scheduled_match_at,omitempty"`
|
||||||
|
DispatchDelay int `json:"dispatch_delay_seconds,omitempty"`
|
||||||
DeadlineAt string `json:"deadline_at"`
|
DeadlineAt string `json:"deadline_at"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
@@ -235,14 +237,39 @@ func (s *Service) Claim(ctx context.Context, tenantID, memberID, hallID int64) (
|
|||||||
}
|
}
|
||||||
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, DeadlineAt: now.Add(policy.Deadline), OrderNo: newNumber("TO", now)})
|
HallID: hallID, MinimumAmount: policy.MinimumAmount, AutoDelay: policy.AutoDelay,
|
||||||
|
DeadlineAt: now.Add(policy.Deadline), OrderNo: newNumber("TO", now)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := orderFromStore(*row, false)
|
result := orderFromStore(*row, false)
|
||||||
|
s.scheduleAutoMatch(tenantID, memberID, row.ID, row.RuleSnapshot, policy)
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) scheduleAutoMatch(tenantID, memberID, orderID int64, raw json.RawMessage, policy taskPolicy) {
|
||||||
|
var snapshot map[string]any
|
||||||
|
if json.Unmarshal(raw, &snapshot) != nil || snapshot["dispatch_mode"] != "auto" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
matchAt, err := time.Parse(time.RFC3339, fmt.Sprint(snapshot["scheduled_match_at"]))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delay := time.Until(matchAt)
|
||||||
|
if delay < 0 {
|
||||||
|
delay = 0
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
timer := time.NewTimer(delay)
|
||||||
|
defer timer.Stop()
|
||||||
|
<-timer.C
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = s.repository.AutoMatchTaskOrder(ctx, tenantID, memberID, orderID, policy.AutoDelay, policy.AutoMinAmount, policy.AutoMaxAmount)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Orders(ctx context.Context, tenantID, memberID int64, page, pageSize int, status string) (*OrderPage, error) {
|
func (s *Service) Orders(ctx context.Context, tenantID, memberID int64, page, pageSize int, status string) (*OrderPage, error) {
|
||||||
if page == 0 {
|
if page == 0 {
|
||||||
page = 1
|
page = 1
|
||||||
@@ -368,6 +395,11 @@ func orderFromStore(row store.TaskOrder, detail bool) Order {
|
|||||||
UnitPriceAmount: row.UnitPriceAmount, TotalAmount: row.TotalAmount, CommissionAmount: row.CommissionAmount,
|
UnitPriceAmount: row.UnitPriceAmount, TotalAmount: row.TotalAmount, CommissionAmount: row.CommissionAmount,
|
||||||
Currency: row.Currency, Status: status, StatusText: orderStatusText(row.OrderStatus), Message: row.Message,
|
Currency: row.Currency, Status: status, StatusText: orderStatusText(row.OrderStatus), Message: row.Message,
|
||||||
DeadlineAt: row.DeadlineAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
DeadlineAt: row.DeadlineAt, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||||
|
var snapshot map[string]any
|
||||||
|
if json.Unmarshal(row.RuleSnapshot, &snapshot) == nil {
|
||||||
|
result.ScheduledMatchAt, _ = snapshot["scheduled_match_at"].(string)
|
||||||
|
result.DispatchDelay = intValue(snapshot["dispatch_delay_seconds"])
|
||||||
|
}
|
||||||
if !detail {
|
if !detail {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<span>✓</span>{{ step }}
|
<span>✓</span>{{ step }}
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<button class="m-result-secondary" @click="openOrders">后台匹配,去记录查看</button>
|
<button class="m-result-secondary" @click="openOrders">稍后去记录查看</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -118,9 +118,11 @@
|
|||||||
const resultState = ref<ResultState>('matching');
|
const resultState = ref<ResultState>('matching');
|
||||||
const failureMessage = ref('当前任务库存未分配成功,请稍后重试');
|
const failureMessage = ref('当前任务库存未分配成功,请稍后重试');
|
||||||
const matchProgress = ref(18);
|
const matchProgress = ref(18);
|
||||||
|
const matchRemainingSeconds = ref(0);
|
||||||
|
const matchTotalSeconds = ref(0);
|
||||||
const pollFinished = ref(false);
|
const pollFinished = ref(false);
|
||||||
let pollTimer: number | undefined;
|
let pollTimer: number | undefined;
|
||||||
let progressTimer: number | undefined;
|
let countdownTimer: number | undefined;
|
||||||
let pollCount = 0;
|
let pollCount = 0;
|
||||||
|
|
||||||
const statCards = computed(() => [
|
const statCards = computed(() => [
|
||||||
@@ -139,7 +141,10 @@
|
|||||||
});
|
});
|
||||||
const matchStep = computed(() => (matchProgress.value < 35 ? 0 : matchProgress.value < 60 ? 1 : matchProgress.value < 85 ? 2 : 3));
|
const matchStep = computed(() => (matchProgress.value < 35 ? 0 : matchProgress.value < 60 ? 1 : matchProgress.value < 85 ? 2 : 3));
|
||||||
const matchSteps = ['正在匹配任务', '获取商品信息', '确认任务内容', '发放任务结果'];
|
const matchSteps = ['正在匹配任务', '获取商品信息', '确认任务内容', '发放任务结果'];
|
||||||
const matchingHint = computed(() => pollFinished.value ? '任务仍在匹配,可稍后前往记录查看' : '系统正在为您匹配任务,请稍等...');
|
const matchingHint = computed(() => {
|
||||||
|
if (matchRemainingSeconds.value > 0) return `系统正在派单... ${formatCountdown(matchRemainingSeconds.value)}`;
|
||||||
|
return pollFinished.value ? '任务仍在匹配,可稍后前往记录查看' : '系统正在为您匹配任务,请稍等...';
|
||||||
|
});
|
||||||
const resultIcon = computed(() => resultState.value === 'success' ? mobileAssets.claimSuccess : resultState.value === 'failed' ? mobileAssets.claimFailed : mobileAssets.claimCancelled);
|
const resultIcon = computed(() => resultState.value === 'success' ? mobileAssets.claimSuccess : resultState.value === 'failed' ? mobileAssets.claimFailed : mobileAssets.claimCancelled);
|
||||||
const resultTitle = computed(() => resultState.value === 'success' ? '任务获取成功' : resultState.value === 'failed' ? '领取失败' : '已取消任务');
|
const resultTitle = computed(() => resultState.value === 'success' ? '任务获取成功' : resultState.value === 'failed' ? '领取失败' : '已取消任务');
|
||||||
const resultDescription = computed(() => resultState.value === 'success' ? '恭喜你成功领取任务' : resultState.value === 'failed' ? '本次任务领取未成功,请稍后再试' : '本次任务已取消,可重新领取');
|
const resultDescription = computed(() => resultState.value === 'success' ? '恭喜你成功领取任务' : resultState.value === 'failed' ? '本次任务领取未成功,请稍后再试' : '本次任务已取消,可重新领取');
|
||||||
@@ -158,7 +163,10 @@
|
|||||||
]);
|
]);
|
||||||
halls.value = hallData || [];
|
halls.value = hallData || [];
|
||||||
stats.value = statData;
|
stats.value = statData;
|
||||||
if (halls.value.length) await loadEligibilities();
|
if (halls.value.length) {
|
||||||
|
await loadEligibilities();
|
||||||
|
await resumeMatchingOrder();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = errorText(err);
|
error.value = errorText(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -177,6 +185,21 @@
|
|||||||
eligibility.value = entries.find(([, value]) => value?.eligible)?.[1] || entries[0]?.[1];
|
eligibility.value = entries.find(([, value]) => value?.eligible)?.[1] || entries[0]?.[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resumeMatchingOrder() {
|
||||||
|
const pendingOrderID = Object.values(eligibilityByHall.value)
|
||||||
|
.map((item: any) => Number(item?.pending_order_id || 0))
|
||||||
|
.find((id) => id > 0);
|
||||||
|
if (!pendingOrderID) return;
|
||||||
|
try {
|
||||||
|
const pendingOrder = await request<any>({ url: `/v1/task-orders/${pendingOrderID}` });
|
||||||
|
if (pendingOrder?.status !== 'matching') return;
|
||||||
|
openMatching();
|
||||||
|
handleOrder(pendingOrder);
|
||||||
|
} catch {
|
||||||
|
// 保留页面上的“存在未完成订单”入口,避免详情请求失败阻断领取页加载。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function claim() {
|
async function claim() {
|
||||||
if (claiming.value || !halls.value.length) return;
|
if (claiming.value || !halls.value.length) return;
|
||||||
claiming.value = true;
|
claiming.value = true;
|
||||||
@@ -224,15 +247,15 @@
|
|||||||
resultState.value = 'matching';
|
resultState.value = 'matching';
|
||||||
showResult.value = true;
|
showResult.value = true;
|
||||||
matchProgress.value = 18;
|
matchProgress.value = 18;
|
||||||
|
matchRemainingSeconds.value = 0;
|
||||||
|
matchTotalSeconds.value = 0;
|
||||||
pollCount = 0;
|
pollCount = 0;
|
||||||
progressTimer = window.setInterval(() => {
|
|
||||||
matchProgress.value = Math.min(88, matchProgress.value + Math.ceil(Math.random() * 5));
|
|
||||||
}, 700);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOrder(value: any) {
|
function handleOrder(value: any) {
|
||||||
order.value = value;
|
order.value = value;
|
||||||
if (value?.status === 'matching') {
|
if (value?.status === 'matching') {
|
||||||
|
startCountdown(value);
|
||||||
schedulePoll();
|
schedulePoll();
|
||||||
} else if (['pending', 'processing', 'completed'].includes(value?.status)) {
|
} else if (['pending', 'processing', 'completed'].includes(value?.status)) {
|
||||||
matchProgress.value = 100;
|
matchProgress.value = 100;
|
||||||
@@ -250,6 +273,11 @@
|
|||||||
|
|
||||||
function schedulePoll() {
|
function schedulePoll() {
|
||||||
if (!order.value?.id) return;
|
if (!order.value?.id) return;
|
||||||
|
if (pollTimer) window.clearTimeout(pollTimer);
|
||||||
|
const scheduledAt = Date.parse(String(order.value?.scheduled_match_at || ''));
|
||||||
|
const wait = Number.isFinite(scheduledAt) && scheduledAt > Date.now()
|
||||||
|
? scheduledAt - Date.now() + 300
|
||||||
|
: 1200;
|
||||||
pollTimer = window.setTimeout(async () => {
|
pollTimer = window.setTimeout(async () => {
|
||||||
pollCount += 1;
|
pollCount += 1;
|
||||||
try {
|
try {
|
||||||
@@ -264,14 +292,43 @@
|
|||||||
pollFinished.value = true;
|
pollFinished.value = true;
|
||||||
stopPolling();
|
stopPolling();
|
||||||
}
|
}
|
||||||
}, 1200);
|
}, wait);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCountdown(value: any) {
|
||||||
|
if (countdownTimer) window.clearInterval(countdownTimer);
|
||||||
|
const scheduledAt = Date.parse(String(value?.scheduled_match_at || ''));
|
||||||
|
if (!Number.isFinite(scheduledAt)) return;
|
||||||
|
matchTotalSeconds.value = Math.max(1, Number(value?.dispatch_delay_seconds || 0));
|
||||||
|
const update = () => {
|
||||||
|
matchRemainingSeconds.value = Math.max(0, Math.ceil((scheduledAt - Date.now()) / 1000));
|
||||||
|
const elapsed = Math.max(0, matchTotalSeconds.value - matchRemainingSeconds.value);
|
||||||
|
matchProgress.value = Math.min(88, Math.round(18 + elapsed / matchTotalSeconds.value * 70));
|
||||||
|
if (matchRemainingSeconds.value === 0 && countdownTimer) {
|
||||||
|
window.clearInterval(countdownTimer);
|
||||||
|
countdownTimer = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
update();
|
||||||
|
if (matchRemainingSeconds.value > 0) countdownTimer = window.setInterval(update, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCountdown(totalSeconds: number) {
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
return hours > 0
|
||||||
|
? [hours, minutes, seconds].map((item) => String(item).padStart(2, '0')).join(':')
|
||||||
|
: [minutes, seconds].map((item) => String(item).padStart(2, '0')).join(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPolling(clearProgress = true) {
|
function stopPolling(clearProgress = true) {
|
||||||
if (pollTimer) window.clearTimeout(pollTimer);
|
if (pollTimer) window.clearTimeout(pollTimer);
|
||||||
if (clearProgress && progressTimer) window.clearInterval(progressTimer);
|
if (clearProgress && countdownTimer) window.clearInterval(countdownTimer);
|
||||||
pollTimer = undefined;
|
pollTimer = undefined;
|
||||||
if (clearProgress) progressTimer = undefined;
|
if (clearProgress) {
|
||||||
|
countdownTimer = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeResult() {
|
function closeResult() {
|
||||||
|
|||||||
Reference in New Issue
Block a user