feat: add scheduled automatic task matching
This commit is contained in:
+77
-15
@@ -2,10 +2,12 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
@@ -49,6 +51,7 @@ type ClaimTaskInput struct {
|
||||
MemberID int64
|
||||
HallID int64
|
||||
MinimumAmount int64
|
||||
AutoDelay int
|
||||
DeadlineAt time.Time
|
||||
OrderNo string
|
||||
}
|
||||
@@ -212,8 +215,12 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
|
||||
if pending > 0 {
|
||||
return nil, ErrPendingOrderExists
|
||||
}
|
||||
var dailyLimit int
|
||||
err = tx.GetContext(ctx, &dailyLimit, `SELECT COALESCE(v.daily_order_limit,0) FROM hg_app_member_level l
|
||||
var levelRule struct {
|
||||
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
|
||||
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) {
|
||||
@@ -222,26 +229,53 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dailyLimit > 0 {
|
||||
if levelRule.DailyLimit > 0 {
|
||||
var todayCount int
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if todayCount >= dailyLimit {
|
||||
if todayCount >= levelRule.DailyLimit {
|
||||
return nil, ErrDailyLimitReached
|
||||
}
|
||||
}
|
||||
dispatchMode, message := "manual", "等待后台匹配订单"
|
||||
matchAt := time.Now().UTC()
|
||||
deadlineAt := input.DeadlineAt.UTC()
|
||||
delaySeconds := 0
|
||||
if member.AutoDispatch == 1 {
|
||||
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{
|
||||
"source": "member_auto_claim",
|
||||
"dispatch_mode": dispatchMode,
|
||||
"hall_id": input.HallID,
|
||||
"deadline_at": input.DeadlineAt.UTC().Format(time.RFC3339),
|
||||
"member_level_id": member.LevelID,
|
||||
"source": "member_auto_claim",
|
||||
"dispatch_mode": dispatchMode,
|
||||
"hall_id": input.HallID,
|
||||
"deadline_at": deadlineAt.Format(time.RFC3339),
|
||||
"scheduled_match_at": matchAt.Format(time.RFC3339),
|
||||
"dispatch_delay_seconds": delaySeconds,
|
||||
"member_level_id": member.LevelID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -267,7 +301,7 @@ func (s *Store) ClaimTask(ctx context.Context, input ClaimTaskInput) (*TaskOrder
|
||||
}
|
||||
return &TaskOrder{ID: orderID, OrderNo: input.OrderNo, Currency: "CNY",
|
||||
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
|
||||
}
|
||||
|
||||
@@ -298,7 +332,16 @@ func (s *Store) AutoMatchTaskOrder(ctx context.Context, tenantID, memberID, orde
|
||||
if order.Status != orderStatusMatching {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -357,10 +400,6 @@ func (s *Store) AutoMatchTaskOrder(ctx context.Context, tenantID, memberID, orde
|
||||
return err
|
||||
}
|
||||
|
||||
snapshot := map[string]any{}
|
||||
if len(order.RuleSnapshot) > 0 {
|
||||
_ = json.Unmarshal(order.RuleSnapshot, &snapshot)
|
||||
}
|
||||
snapshot["match_source"] = "system_auto_match"
|
||||
snapshot["product_id"] = product.ID
|
||||
snapshot["product_no"] = product.ProductNo
|
||||
@@ -409,6 +448,29 @@ func (s *Store) AutoMatchTaskOrder(ctx context.Context, tenantID, memberID, orde
|
||||
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) {
|
||||
where := ` WHERE o.tenant_id=? AND o.member_id=? AND o.order_type=2`
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user