package douyin import ( "bindbox-game/internal/pkg/logger" "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" "bindbox-game/internal/service/game" "bindbox-game/internal/service/sysconfig" "context" "encoding/json" "fmt" "io" "math" "net/http" "net/url" "strconv" "strings" "sync" "sync/atomic" "time" "unicode" "go.uber.org/zap" "golang.org/x/sync/singleflight" "bindbox-game/internal/service/user" ) // 系统配置键 const ( ConfigKeyDouyinCookie = "douyin_cookie" ConfigKeyDouyinInterval = "douyin_sync_interval_minutes" ) type Service interface { // FetchAndSyncOrders 从抖店 API 获取订单并同步到本地 (按绑定用户同步) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) // SyncAllOrders 批量同步所有订单变更 (基于更新时间,不分状态) // useProxy: 是否使用代理服务器访问抖音API SyncAllOrders(ctx context.Context, duration time.Duration, useProxy bool) (*SyncResult, error) // ListOrders 获取本地抖店订单列表 ListOrders(ctx context.Context, page, pageSize int, status *int) ([]*model.DouyinOrders, int64, error) // GetConfig 获取抖店配置 GetConfig(ctx context.Context) (*DouyinConfig, error) // SaveConfig 保存抖店配置 SaveConfig(ctx context.Context, cookie string, intervalMinutes int) error // SyncOrder 同步单个订单到本地,可传入建议关联的用户ID和商品ID SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64, productID string) (isNew bool, isMatched bool) // GrantMinesweeperQualifications 自动补发扫雷资格 GrantMinesweeperQualifications(ctx context.Context) error // GrantLivestreamPrizes 自动发放直播间奖品 GrantLivestreamPrizes(ctx context.Context) error // SyncRefundStatus 同步退款状态 SyncRefundStatus(ctx context.Context) error } type DouyinConfig struct { Cookie string `json:"cookie"` IntervalMinutes int `json:"interval_minutes"` } type SyncResult struct { TotalFetched int `json:"total_fetched"` NewOrders int `json:"new_orders"` MatchedUsers int `json:"matched_users"` Orders []*model.DouyinOrders `json:"orders"` // 新增:返回详情以供后续处理 DebugInfo string `json:"debug_info"` } type service struct { logger logger.CustomLogger repo mysql.Repo readDB *dao.Query writeDB *dao.Query syscfg sysconfig.Service ticketSvc game.TicketService userSvc user.Service rewardDispatcher *RewardDispatcher sfGroup singleflight.Group lastSyncTime time.Time syncLock sync.Mutex } func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService, userSvc user.Service, titleSvc TitleAssigner) Service { // 创建奖励发放器 var dispatcher *RewardDispatcher if titleSvc != nil { dispatcher = NewRewardDispatcher(ticketSvc, userSvc, titleSvc) } return &service{ logger: l, repo: repo, readDB: dao.Use(repo.GetDbR()), writeDB: dao.Use(repo.GetDbW()), syscfg: syscfg, ticketSvc: ticketSvc, userSvc: userSvc, rewardDispatcher: dispatcher, } } // GetConfig 获取抖店配置 func (s *service) GetConfig(ctx context.Context) (*DouyinConfig, error) { cfg := &DouyinConfig{IntervalMinutes: 5} if c, err := s.syscfg.GetByKey(ctx, ConfigKeyDouyinCookie); err == nil && c != nil { cfg.Cookie = c.ConfigValue } if c, err := s.syscfg.GetByKey(ctx, ConfigKeyDouyinInterval); err == nil && c != nil { if v, e := strconv.Atoi(c.ConfigValue); e == nil && v > 0 { cfg.IntervalMinutes = v } } return cfg, nil } // SaveConfig 保存抖店配置 func (s *service) SaveConfig(ctx context.Context, cookie string, intervalMinutes int) error { if _, err := s.syscfg.UpsertByKey(ctx, ConfigKeyDouyinCookie, cookie, "抖店Cookie"); err != nil { return err } if intervalMinutes < 1 { intervalMinutes = 5 } if _, err := s.syscfg.UpsertByKey(ctx, ConfigKeyDouyinInterval, strconv.Itoa(intervalMinutes), "抖店订单同步间隔(分钟)"); err != nil { return err } return nil } // ListOrders 获取本地抖店订单列表 func (s *service) ListOrders(ctx context.Context, page, pageSize int, status *int) ([]*model.DouyinOrders, int64, error) { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } db := s.repo.GetDbR().WithContext(ctx).Model(&model.DouyinOrders{}) if status != nil { db = db.Where("order_status = ?", *status) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, 0, err } var orders []*model.DouyinOrders if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&orders).Error; err != nil { return nil, 0, err } return orders, total, nil } // FetchAndSyncOrders 遍历所有已绑定抖音号的用户并同步其订单 func (s *service) FetchAndSyncOrders(ctx context.Context) (*SyncResult, error) { cfg, err := s.GetConfig(ctx) if err != nil { return nil, fmt.Errorf("获取配置失败: %w", err) } if cfg.Cookie == "" { return nil, fmt.Errorf("抖店 Cookie 未配置") } // 1. 获取所有绑定了抖音号的用户 var users []model.Users if err := s.repo.GetDbR().WithContext(ctx).Where("douyin_user_id != ''").Find(&users).Error; err != nil { return nil, fmt.Errorf("获取绑定用户失败: %w", err) } result := &SyncResult{} fmt.Printf("[DEBUG] 开始全量同步,共 %d 个绑定用户\n", len(users)) // 2. 遍历用户,按 buyer 抓取订单 for _, u := range users { fmt.Printf("[DEBUG] 正在同步用户 ID: %d (昵称: %s, 抖音号: %s) 的订单...\n", u.ID, u.Nickname, u.DouyinUserID) orders, err := s.fetchDouyinOrdersByBuyer(cfg.Cookie, u.DouyinUserID) if err != nil { fmt.Printf("[DEBUG] 抓取用户 %s 订单失败: %v\n", u.DouyinUserID, err) continue } result.TotalFetched += len(orders) // 3. 同步 for _, order := range orders { // 同步订单(传入建议关联的用户 ID) isNew, matched := s.SyncOrder(ctx, &order, u.ID, "") if isNew { result.NewOrders++ } if matched { result.MatchedUsers++ } } } result.DebugInfo += fmt.Sprintf("\n同步完成: 总抓取 %d, 新订单 %d, 匹配用户 %d", result.TotalFetched, result.NewOrders, result.MatchedUsers) return result, nil } // removed SyncShopOrders // 抖店 API 响应结构 type douyinOrderResponse struct { Code int `json:"code"` St int `json:"st"` // 抖店实际返回的是 st 而非 code Msg string `json:"msg"` Data []DouyinOrderItem `json:"data"` // data 直接是数组 } type DouyinOrderItem struct { ShopOrderID string `json:"shop_order_id"` OrderStatus int `json:"order_status"` UserID string `json:"user_id"` ActualReceiveAmount any `json:"actual_receive_amount"` ActualPayAmount any `json:"actual_pay_amount"` PayTypeDesc string `json:"pay_type_desc"` Remark string `json:"remark"` UserNickname string `json:"user_nickname"` ProductCount int64 `json:"product_count"` // 抖店返回的商品数量 ProductItemList []DouyinProductItem `json:"product_item"` // 商品详情列表 SkuOrderList []SkuOrderItem `json:"sku_order_list"` } type DouyinProductItem struct { ProductID string `json:"product_id"` ProductName string `json:"product_name"` ComboNum int64 `json:"combo_num"` TotalProductCount int64 `json:"total_product_count"` } type SkuOrderItem struct { ProductID string `json:"product_id"` ProductName string `json:"product_name"` SkuID string `json:"sku_id"` } // fetchDouyinOrdersByBuyer 调用抖店 API 按 Buyer ID 获取订单 (保持向后兼容) func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]DouyinOrderItem, error) { params := url.Values{} params.Set("page", "0") params.Set("pageSize", "100") params.Set("buyer", buyer) params.Set("order_by", "create_time") params.Set("order", "desc") params.Set("tab", "all") params.Set("appid", "1") params.Set("_bid", "ffa_order") params.Set("aid", "4272") return s.fetchDouyinOrders(cookie, params, true) // 按用户同步使用代理 } // fetchDouyinOrders 通用的抖店订单抓取方法 func (s *service) fetchDouyinOrders(cookie string, params url.Values, useProxy bool) ([]DouyinOrderItem, error) { baseUrl := "https://fxg.jinritemai.com/api/order/searchlist" fullUrl := baseUrl + "?" + params.Encode() // 配置代理服务器:巨量代理IP (可选) var proxyURL *url.URL if useProxy { proxyURL, _ = url.Parse("http://t13319619426654:ln8aj9nl@s432.kdltps.com:15818") } var lastErr error // 重试 3 次 for i := 0; i < 3; i++ { req, err := http.NewRequest("GET", fullUrl, nil) if err != nil { return nil, err } // 设置请求头 req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") req.Header.Set("Accept", "application/json, text/plain, */*") req.Header.Set("Cookie", cookie) req.Header.Set("Referer", "https://fxg.jinritemai.com/ffa/morder/order/list") // 禁用连接复用,防止代理断开导致 EOF req.Close = true // 根据 useProxy 参数决定是否使用代理 var transport *http.Transport if useProxy && proxyURL != nil { transport = &http.Transport{ Proxy: http.ProxyURL(proxyURL), DisableKeepAlives: true, // 禁用 Keep-Alive } } else { transport = &http.Transport{ DisableKeepAlives: true, // 禁用 Keep-Alive } } client := &http.Client{ Timeout: 60 * time.Second, Transport: transport, } resp, err := client.Do(req) if err != nil { lastErr = err s.logger.Warn("[抖店API] 请求失败,准备重试", zap.Int("retry", i+1), zap.Bool("use_proxy", useProxy), zap.Error(err)) time.Sleep(1 * time.Second) continue } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { lastErr = err s.logger.Warn("[抖店API] 读取响应失败,准备重试", zap.Int("retry", i+1), zap.Error(err)) time.Sleep(1 * time.Second) continue } var respData douyinOrderResponse if err := json.Unmarshal(body, &respData); err != nil { s.logger.Error("[抖店API] 解析响应失败", zap.String("response", string(body[:min(len(body), 5000)]))) return nil, fmt.Errorf("解析响应失败: %w", err) } // 临时调试日志:打印第一笔订单的金额字段 if len(respData.Data) > 0 { fmt.Printf("[DEBUG] 抖店订单 0 金额测试: RawBody(500)=%s\n", string(body[:min(len(body), 500)])) } if respData.St != 0 && respData.Code != 0 { return nil, fmt.Errorf("API 返回错误: %s (ST:%d CODE:%d)", respData.Msg, respData.St, respData.Code) } return respData.Data, nil } return nil, fmt.Errorf("请求失败(重试3次): %w", lastErr) } // SyncOrder 同步单个订单到本地 func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64, productID string) (isNew bool, isMatched bool) { db := s.repo.GetDbW().WithContext(ctx) // 解析金额工具函数 parseMoney := func(val any) int64 { if val == nil { return 0 } // JSON 数字会被解析为 float64 if f, ok := val.(float64); ok { // 如果是数值类型,但带有小数部分(如 138.4),通常是元单位 if f != math.Trunc(f) { return int64(f*100 + 0.5) } // 如果是整数,保持原样(分) return int64(f) } s := fmt.Sprintf("%v", val) s = strings.TrimSpace(s) if s == "" { return 0 } // 只保留数字和点号 (处理 "¥158.40" 这种情况) var sb strings.Builder for _, r := range s { if unicode.IsDigit(r) || r == '.' { sb.WriteRune(r) } } cleanStr := sb.String() if f, err := strconv.ParseFloat(cleanStr, 64); err == nil { // 字符串一律按元转分处理 (兼容旧逻辑) return int64(f*100 + 0.5) } return 0 } var order model.DouyinOrders err := db.Where("shop_order_id = ?", item.ShopOrderID).First(&order).Error if err == nil { // 订单已存在 isNew = false // 只有当订单还没关联用户,且提供了建议用户时,才做关联 if (order.LocalUserID == "" || order.LocalUserID == "0") && suggestUserID > 0 { order.LocalUserID = strconv.FormatInt(suggestUserID, 10) db.Model(&order).Update("local_user_id", order.LocalUserID) fmt.Printf("[DEBUG] 抖店辅助关联成功: %s -> User %d\n", item.ShopOrderID, suggestUserID) } // 更新状态与金额 (确保之前因解析失败导致的 0 金额被修复) db.Model(&order).Updates(map[string]any{ "order_status": item.OrderStatus, "remark": item.Remark, "actual_receive_amount": parseMoney(item.ActualReceiveAmount), "actual_pay_amount": parseMoney(item.ActualPayAmount), }) // 重要:同步内存状态 order.OrderStatus = int32(item.OrderStatus) order.Remark = item.Remark } else { // 订单不存在,创建新记录 isNew = true localUserIDStr := "0" if suggestUserID > 0 { localUserIDStr = strconv.FormatInt(suggestUserID, 10) } fmt.Printf("[DEBUG] 抖店新订单: %s, UserID: %s, Recommend: %s\n", item.ShopOrderID, item.UserID, localUserIDStr) amount := parseMoney(item.ActualReceiveAmount) payAmount := parseMoney(item.ActualPayAmount) // 计算商品数量:如果指定了 productID,则只统计该商品的数量;否则使用总数量 pCount := item.ProductCount if productID != "" && len(item.ProductItemList) > 0 { var matchedCount int64 for _, pi := range item.ProductItemList { if pi.ProductID == productID { // 有些情况下 TotalProductCount 准确,有些 ComboNum 准确 // 用户反馈的 JSON 中 ComboNum=2, TotalProductCount=2 // 优先使用 ComboNum if pi.ComboNum > 0 { matchedCount += pi.ComboNum } else { matchedCount += pi.TotalProductCount } } } if matchedCount > 0 { pCount = matchedCount } } // 如果没指定 productID,尝试自动填补 if productID == "" && len(item.ProductItemList) > 0 { if len(item.ProductItemList) == 1 { // 只有一个商品时,自动使用该商品ID productID = item.ProductItemList[0].ProductID } else { // 多个商品时,使用第一个商品ID(记录主商品) productID = item.ProductItemList[0].ProductID // 记录日志,方便后续分析多商品订单 fmt.Printf("[WARN] 订单 %s 包含多个商品(%d个),使用第一个商品ID: %s\n", item.ShopOrderID, len(item.ProductItemList), productID) } } rawData, _ := json.Marshal(item) order = model.DouyinOrders{ ShopOrderID: item.ShopOrderID, DouyinProductID: productID, // 写入商品ID ProductCount: int32(pCount), // 写入计算后的商品数量 OrderStatus: int32(item.OrderStatus), DouyinUserID: item.UserID, ActualReceiveAmount: amount, ActualPayAmount: payAmount, PayTypeDesc: item.PayTypeDesc, Remark: item.Remark, UserNickname: item.UserNickname, RawData: string(rawData), RewardGranted: 0, LocalUserID: localUserIDStr, } if err := db.Create(&order).Error; err != nil { return false, false } } // 如果还没关联用户(比如之前全量抓取的),尝试用抖店的 UID (long string) 匹配 if (order.LocalUserID == "" || order.LocalUserID == "0") && item.UserID != "" { var user model.Users if err := s.repo.GetDbR().Where("douyin_user_id = ?", item.UserID).First(&user).Error; err == nil { order.LocalUserID = strconv.FormatInt(user.ID, 10) db.Model(&order).Update("local_user_id", order.LocalUserID) fmt.Printf("[DEBUG] 通过抖店 UID 匹配成功: User %d\n", user.ID) } } // ---- 统一处理:发放奖励 ---- isMatched = order.LocalUserID != "" && order.LocalUserID != "0" // 订单完成且未发放奖励时,根据产品ID查询奖励规则并发放 if isMatched && order.RewardGranted == 0 && order.OrderStatus == 2 && order.DouyinProductID != "" { // 检查黑名单 var blacklistCount int64 if err := db.Table("douyin_blacklist").Where("douyin_user_id = ?", item.UserID).Count(&blacklistCount).Error; err == nil && blacklistCount > 0 { fmt.Printf("[DEBUG] 用户 %s 在黑名单中,跳过发奖\n", item.UserID) return isNew, isMatched } localUserID, _ := strconv.ParseInt(order.LocalUserID, 10, 64) if localUserID <= 0 { return isNew, isMatched } // 查询该商品的所有奖励规则 (status=1 表示启用) var rewards []model.DouyinProductRewards if err := db.Where("product_id = ? AND status = 1", order.DouyinProductID).Find(&rewards).Error; err != nil { fmt.Printf("[DEBUG] 查询奖励规则失败: %v\n", err) return isNew, isMatched } if len(rewards) == 0 { fmt.Printf("[DEBUG] 订单 %s 未找到奖励规则,跳过发奖\n", item.ShopOrderID) return isNew, isMatched } // 遍历所有规则发放奖励 allSuccess := true hasFlipCard := false grantedCount := 0 for _, reward := range rewards { // 翻牌游戏不自动发放,等待用户手动翻牌 if s.rewardDispatcher != nil && s.rewardDispatcher.IsFlipCardReward(reward) { fmt.Printf("[DEBUG] 订单 %s 配置为翻牌游戏,跳过自动发放 (规则ID: %d)\n", item.ShopOrderID, reward.ID) hasFlipCard = true continue } if s.rewardDispatcher == nil { fmt.Printf("[DEBUG] 订单 %s 奖励发放器未初始化,跳过 (规则ID: %d)\n", item.ShopOrderID, reward.ID) continue } fmt.Printf("[DEBUG] 准备发放奖励: User: %d, ProductID: %s, Type: %s, Quantity: %d, RuleID: %d\n", localUserID, order.DouyinProductID, reward.RewardType, reward.Quantity, reward.ID) err := s.rewardDispatcher.GrantReward(ctx, localUserID, reward, int(order.ProductCount), "douyin_order", order.ID) if err != nil { fmt.Printf("[DEBUG] 订单 %s 发放奖励失败 (规则ID: %d): %v\n", item.ShopOrderID, reward.ID, err) allSuccess = false } else { grantedCount++ fmt.Printf("[DEBUG] 订单 %s 发放奖励成功: %s × %d (规则ID: %d)\n", item.ShopOrderID, reward.RewardType, int(reward.Quantity)*int(order.ProductCount), reward.ID) } } // 标记奖励已发放 // - 如果只有翻牌游戏,不标记(让翻牌页面可以查询) // - 如果有其他奖励成功发放,标记为已发放(防止重复发放) if allSuccess && grantedCount > 0 { db.Model(&order).Update("reward_granted", order.ProductCount) order.RewardGranted = order.ProductCount } else if hasFlipCard && grantedCount == 0 { // 纯翻牌游戏,不标记 fmt.Printf("[DEBUG] 订单 %s 仅配置翻牌游戏,等待手动翻牌\n", item.ShopOrderID) } } return isNew, isMatched } // min 返回两个整数的最小值 func min(a, b int) int { if a < b { return a } return b } // SyncAllOrders 批量同步所有订单变更 (基于更新时间,不分状态) func (s *service) SyncAllOrders(ctx context.Context, duration time.Duration, useProxy bool) (*SyncResult, error) { // 使用 singleflight 合并并发请求 v, err, _ := s.sfGroup.Do("SyncAllOrders", func() (interface{}, error) { // 1. 检查限流 (5秒内不重复同步) s.syncLock.Lock() if time.Since(s.lastSyncTime) < 5*time.Second { s.syncLock.Unlock() // 触发限流,直接返回空结果(调用方会使用数据库旧数据) return &SyncResult{ DebugInfo: "Sync throttled (within 5s)", }, nil } s.syncLock.Unlock() // 2. 执行真正的同步逻辑 start := time.Now() cfg, err := s.GetConfig(ctx) if err != nil { return nil, fmt.Errorf("获取配置失败: %w", err) } if cfg.Cookie == "" { return nil, fmt.Errorf("抖店 Cookie 未配置") } // 临时:强制使用用户提供的最新 Cookie if len(cfg.Cookie) < 100 { cfg.Cookie = "passport_csrf_token=afcc4debfeacce6454979bb9465999dc; passport_csrf_token_default=afcc4debfeacce6454979bb9465999dc; is_staff_user=false; zsgw_business_data=%7B%22uuid%22%3A%22fa769974-ba17-4daf-94cb-3162ba299c40%22%2C%22platform%22%3A%22pc%22%2C%22source%22%3A%22seo.fxg.jinritemai.com%22%7D; s_v_web_id=verify_mjqlw6yx_mNQjOEnB_oXBo_4Etb_AVQ9_7tQGH9WORNRy; SHOP_ID=47668214; PIGEON_CID=3501298428676440; x-web-secsdk-uid=663d5a20-e75c-4789-bc98-839744bf70bc; Hm_lvt_b6520b076191ab4b36812da4c90f7a5e=1766891015,1766979339,1767628404,1768381245; HMACCOUNT=95F3EBE1C47ED196; ttcid=7962a054674f4dd7bf895af73ae3f34142; passport_mfa_token=CjfZetGovLzEQb6MwoEpMQnvCSomMC9o0P776kEFy77vhrRCAdFvvrnTSpTXY2aib8hCdU5w3tQvGkoKPAAAAAAAAAAAAABP88E%2FGYNOqYg7lJ6fcoAzlVHbNi0bqTR%2Fru8noACGHR%2BtNjtq%2FnW9rBK32mcHCC5TzRDW8YYOGPax0WwgAiIBA3WMQyg%3D; source=seo.fxg.jinritemai.com; gfkadpd=4272,23756; csrf_session_id=b7b4150c5eeefaede4ef5e71473e9dc1; Hm_lpvt_b6520b076191ab4b36812da4c90f7a5e=1768381314; ttwid=1%7CAwu3-vdDBhOP12XdEzmCJlbyX3Qt_5RcioPVgjBIDps%7C1768381315%7Ca763fd05ed6fa274ed997007385cc0090896c597cfac0b812c962faf34f04897; tt_scid=f4YqIWnO3OdWrfVz0YVnJmYahx-qu9o9j.VZC2op7nwrQRodgrSh1ka0Ow3g5nyKd42a; odin_tt=bcf942ae72bd6b4b8f357955b71cc21199b6aec5e9acee4ce64f80704f08ea1cbaaa6e70f444f6a09712806aa424f4d0cce236e77b0bfa2991aa8a23dab27e1e; passport_auth_status=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; passport_auth_status_ss=b3b3a865e0bd3857e6a28ea5a6854830%2C228cf6630632c26472c096506639ed6e; uid_tt=4dfa662033e2e4eefe629ad8815f076f; uid_tt_ss=4dfa662033e2e4eefe629ad8815f076f; sid_tt=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid=4cc6aa2f1a6e338ec72d663a0b611d3c; sessionid_ss=4cc6aa2f1a6e338ec72d663a0b611d3c; PHPSESSID=a1b2fd062c1346e5c6f94bac3073cd7d; PHPSESSID_SS=a1b2fd062c1346e5c6f94bac3073cd7d; ucas_c0=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ucas_c0_ss=CkEKBTEuMC4wEJOIgezc9NazaRjmJiD61rDnqc2DBCiwITCb1oDYuM3aB0Cpt53LBkip69nNBlC_vL6Ekt3t1GdYbhIU2LuS6yHmC8_SKu9Jok5ToGxfQIg; ecom_gray_shop_id=156231010; sid_guard=4cc6aa2f1a6e338ec72d663a0b611d3c%7C1768381360%7C5184000%7CSun%2C+15-Mar-2026+09%3A02%3A40+GMT; session_tlb_tag=sttt%7C4%7CTMaqLxpuM47HLWY6C2EdPP________-x3_oZvMYjz8-Uw3dAm6JiPFDhS1ih9XTV79AgAO_5cvo%3D; sid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; ssid_ucp_v1=1.0.0-KGRmNzNkZjM2YjUwZDk2M2M0MjQ5MGE2NzNkNGZkZjNhZWFhYmJkMmIKGQib1oDYuM3aBxCwt53LBhiwISAMOAZA9AcaAmxmIiA0Y2M2YWEyZjFhNmUzMzhlYzcyZDY2M2EwYjYxMWQzYw; COMPASS_LUOPAN_DT=session_7595137429020049706; BUYIN_SASID=SID2_7595138116287152420" } startTime := time.Now().Add(-duration) queryParams := url.Values{ "page": {"0"}, "pageSize": {"50"}, "order_by": {"update_time"}, "order": {"desc"}, "appid": {"1"}, "_bid": {"ffa_order"}, "aid": {"4272"}, "tab": {"all"}, // 全量状态 "update_time_start": {strconv.FormatInt(startTime.Unix(), 10)}, } fetchStart := time.Now() orders, err := s.fetchDouyinOrders(cfg.Cookie, queryParams, useProxy) fetchDuration := time.Since(fetchStart) if err != nil { fmt.Printf("[SyncAll] 抓取失败,耗时: %v, Err: %v\n", fetchDuration, err) return nil, fmt.Errorf("抓取增量订单失败: %w", err) } fmt.Printf("[SyncAll] 抓取成功,耗时: %v, 订单数: %d\n", fetchDuration, len(orders)) result := &SyncResult{ TotalFetched: len(orders), DebugInfo: fmt.Sprintf("UpdateSince: %s, Fetched: %d", startTime.Format("15:04:05"), len(orders)), } processStart := time.Now() // 并发处理订单 var wg sync.WaitGroup // 限制并发数为 10,防止数据库连接耗尽 sem := make(chan struct{}, 10) var newOrdersCount int64 var matchedUsersCount int64 for _, order := range orders { wg.Add(1) go func(o DouyinOrderItem) { defer wg.Done() sem <- struct{}{} // 获取信号量 defer func() { <-sem }() isNew, matched := s.SyncOrder(ctx, &o, 0, "") if isNew { atomic.AddInt64(&newOrdersCount, 1) } if matched { atomic.AddInt64(&matchedUsersCount, 1) } }(order) } wg.Wait() result.NewOrders = int(newOrdersCount) result.MatchedUsers = int(matchedUsersCount) processDuration := time.Since(processStart) totalDuration := time.Since(start) fmt.Printf("[SyncAll] 处理完成,DB耗时: %v, 总耗时: %v\n", processDuration, totalDuration) // 3. 更新同步时间 s.syncLock.Lock() s.lastSyncTime = time.Now() s.syncLock.Unlock() return result, nil }) if err != nil { return nil, err } return v.(*SyncResult), nil }