package admin import ( "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/service/douyin" "context" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" ) // ---------- 抖店配置 API ---------- type getDouyinConfigResponse struct { Cookie string `json:"cookie"` IntervalMinutes int `json:"interval_minutes"` Proxy string `json:"proxy"` } func (h *handler) GetDouyinConfig() core.HandlerFunc { return func(ctx core.Context) { cfg, err := h.douyinSvc.GetConfig(ctx.RequestContext()) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(getDouyinConfigResponse{ Cookie: cfg.Cookie, IntervalMinutes: cfg.IntervalMinutes, Proxy: cfg.Proxy, }) } } type saveDouyinConfigRequest struct { Cookie string `json:"cookie"` IntervalMinutes int `json:"interval_minutes"` Proxy string `json:"proxy"` } func (h *handler) SaveDouyinConfig() core.HandlerFunc { return func(ctx core.Context) { req := new(saveDouyinConfigRequest) if err := ctx.ShouldBindJSON(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if err := h.douyinSvc.SaveConfig(ctx.RequestContext(), req.Cookie, req.Proxy, req.IntervalMinutes); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(pcSimpleMessage{Message: "保存成功"}) } } // ---------- 抖店订单列表 API ---------- type listDouyinOrdersRequest struct { Page int `form:"page"` PageSize int `form:"page_size"` Status *int `form:"status"` Match string `form:"match_status"` ShopOrderID string `form:"shop_order_id"` DouyinUserID string `form:"douyin_user_id"` } type douyinOrderItem struct { ID int64 `json:"id"` ShopOrderID string `json:"shop_order_id"` OrderStatus int32 `json:"order_status"` OrderStatusText string `json:"order_status_text"` DouyinUserID string `json:"douyin_user_id"` LocalUserID int64 `json:"local_user_id"` LocalUserNickname string `json:"local_user_nickname"` ActualReceiveAmount string `json:"actual_receive_amount"` ActualPayAmount string `json:"actual_pay_amount"` PayTypeDesc string `json:"pay_type_desc"` Remark string `json:"remark"` UserNickname string `json:"user_nickname"` ProductCount int64 `json:"product_count"` CreatedAt string `json:"created_at"` } type listDouyinOrdersResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []douyinOrderItem `json:"list"` } func (h *handler) ListDouyinOrders() core.HandlerFunc { return func(ctx core.Context) { req := new(listDouyinOrdersRequest) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } filter := &douyin.ListOrdersFilter{ Status: req.Status, } if req.Match != "" { filter.MatchStatus = &req.Match } filter.ShopOrderID = strings.TrimSpace(req.ShopOrderID) filter.DouyinUserID = strings.TrimSpace(req.DouyinUserID) orders, total, err := h.douyinSvc.ListOrders(ctx.RequestContext(), req.Page, req.PageSize, filter) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } // 获取匹配用户的昵称 userNicknameMap := make(map[int64]string) var userIDs []int64 for _, o := range orders { if uid, err := strconv.ParseInt(o.LocalUserID, 10, 64); err == nil && uid > 0 { userIDs = append(userIDs, uid) } } if len(userIDs) > 0 { var users []struct { ID int64 Nickname string } h.repo.GetDbR().Table("users").Where("id IN ?", userIDs).Select("id, nickname").Find(&users) for _, u := range users { userNicknameMap[u.ID] = u.Nickname } } // 构建响应 list := make([]douyinOrderItem, len(orders)) for i, o := range orders { uid, _ := strconv.ParseInt(o.LocalUserID, 10, 64) list[i] = douyinOrderItem{ ID: o.ID, ShopOrderID: o.ShopOrderID, OrderStatus: o.OrderStatus, OrderStatusText: getOrderStatusText(o.OrderStatus), DouyinUserID: o.DouyinUserID, LocalUserID: uid, LocalUserNickname: userNicknameMap[uid], ActualReceiveAmount: formatAmount(o.ActualReceiveAmount), ActualPayAmount: formatAmount(o.ActualPayAmount), PayTypeDesc: o.PayTypeDesc, Remark: o.Remark, UserNickname: o.UserNickname, ProductCount: int64(o.ProductCount), CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"), } } ctx.Payload(listDouyinOrdersResponse{ Page: req.Page, PageSize: req.PageSize, Total: total, List: list, }) } } // ---------- 手动同步 API ---------- type syncDouyinOrdersResponse struct { Message string `json:"message"` TotalFetched int `json:"total_fetched"` NewOrders int `json:"new_orders"` MatchedUsers int `json:"matched_users"` TotalUsers int `json:"total_users"` ProcessedUsers int `json:"processed_users"` SkippedUsers int `json:"skipped_users"` ElapsedMS int64 `json:"elapsed_ms"` } type syncDouyinOrdersRequest struct { OnlyUnmatched *bool `json:"only_unmatched"` MaxUsers int `json:"max_users"` BatchSize int `json:"batch_size"` Concurrency int `json:"concurrency"` InterBatchDelayMS *int `json:"inter_batch_delay_ms"` } func (h *handler) SyncDouyinOrders() core.HandlerFunc { return func(ctx core.Context) { req := new(syncDouyinOrdersRequest) if err := ctx.ShouldBindJSON(req); err != nil && !errors.Is(err, io.EOF) { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } fetchOpts := &douyin.FetchOptions{ OnlyUnmatched: true, MaxUsers: req.MaxUsers, BatchSize: req.BatchSize, Concurrency: req.Concurrency, } if req.OnlyUnmatched != nil { fetchOpts.OnlyUnmatched = *req.OnlyUnmatched } if req.InterBatchDelayMS != nil { delay := time.Duration(*req.InterBatchDelayMS) * time.Millisecond fetchOpts.InterBatchDelay = delay } // 使用独立 Context 防止 HTTP 请求断开导致同步中断 // 设置 5 分钟超时,确保有足够时间完成全量同步 bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() result, err := h.douyinSvc.FetchAndSyncOrders(bgCtx, fetchOpts) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(syncDouyinOrdersResponse{ Message: fmt.Sprintf("同步成功,处理 %d/%d 个用户,用时 %.2f 秒", result.ProcessedUsers, result.TotalUsers, float64(result.ElapsedMS)/1000.0), TotalFetched: result.TotalFetched, NewOrders: result.NewOrders, MatchedUsers: result.MatchedUsers, TotalUsers: result.TotalUsers, ProcessedUsers: result.ProcessedUsers, SkippedUsers: result.SkippedUsers, ElapsedMS: result.ElapsedMS, }) } } // ---------- 新增: 手动同步接口 (优化版) ---------- type manualSyncAllRequest struct { DurationHours int `json:"duration_hours"` // 可选,默认1小时 } type manualSyncAllResponse struct { Message string `json:"message"` DebugInfo string `json:"debug_info"` } // ManualSyncAll 手动全量订单同步 func (h *handler) ManualSyncAll() core.HandlerFunc { return func(ctx core.Context) { req := new(manualSyncAllRequest) if err := ctx.ShouldBindJSON(req); err != nil { // 如果没有传参数,使用默认值 req.DurationHours = 1 } if req.DurationHours <= 0 { req.DurationHours = 1 } // 使用独立 Context,设置 5 分钟超时 bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() duration := time.Duration(req.DurationHours) * time.Hour result, err := h.douyinSvc.SyncAllOrders(bgCtx, duration, true) // 管理后台手动同步使用代理 if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(manualSyncAllResponse{ Message: fmt.Sprintf("全量同步成功 (同步范围: %d小时)", req.DurationHours), DebugInfo: result.DebugInfo, }) } } type manualSyncRefundResponse struct { Message string `json:"message"` RefundedCount int `json:"refunded_count"` } // ManualSyncRefund 手动退款状态同步 func (h *handler) ManualSyncRefund() core.HandlerFunc { return func(ctx core.Context) { // 使用独立 Context,设置 3 分钟超时 bgCtx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() err := h.douyinSvc.SyncRefundStatus(bgCtx) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(manualSyncRefundResponse{ Message: "退款状态同步成功", RefundedCount: 0, // TODO: 可以从 SyncRefundStatus 返回实际退款数量 }) } } type manualGrantPrizesResponse struct { Message string `json:"message"` GrantedCount int `json:"granted_count"` } type grantOrderRewardResponse struct { ShopOrderID string `json:"shop_order_id"` Message string `json:"message"` Granted bool `json:"granted"` RewardGranted int32 `json:"reward_granted"` ProductCount int32 `json:"product_count"` OrderStatus int32 `json:"order_status"` LocalUserID string `json:"local_user_id"` } // ManualGrantPrizes 手动发放直播间奖品 func (h *handler) ManualGrantPrizes() core.HandlerFunc { return func(ctx core.Context) { // 使用独立 Context,设置 3 分钟超时 bgCtx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() err := h.douyinSvc.GrantLivestreamPrizes(bgCtx) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(manualGrantPrizesResponse{ Message: "直播奖品发放成功", GrantedCount: 0, // TODO: 可以从 GrantLivestreamPrizes 返回实际发放数量 }) } } // GrantOrderReward 手动触发单个订单的发奖 func (h *handler) GrantOrderReward() core.HandlerFunc { return func(ctx core.Context) { shopOrderID := ctx.Param("shop_order_id") if shopOrderID == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "shop_order_id 不能为空")) return } bgCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() res, err := h.douyinSvc.GrantOrderReward(bgCtx, shopOrderID) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err))) return } ctx.Payload(grantOrderRewardResponse(*res)) } } // ---------- 辅助函数 ---------- func getOrderStatusText(status int32) string { switch status { case 1: return "待付款" case 2: return "待发货" case 3: return "已发货" case 4: return "已退款/已取消" case 5: return "已完成" default: return "未知" } } func formatAmount(amountCents int64) string { yuan := float64(amountCents) / 100.0 return fmt.Sprintf("%.2f", yuan) } // SetDouyinService 设置抖店服务 (用于 handler 初始化) func (h *handler) SetDouyinService(svc douyin.Service) { h.douyinSvc = svc }