355 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go.uber.org/zap"
)
// 系统配置键
const (
ConfigKeyDouyinCookie = "douyin_cookie"
ConfigKeyDouyinInterval = "douyin_sync_interval_minutes"
)
type Service interface {
// FetchAndSyncOrders 从抖店 API 获取订单并同步到本地
FetchAndSyncOrders(ctx context.Context) (*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
SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64) (isNew bool, isMatched bool)
}
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"`
}
type service struct {
logger logger.CustomLogger
repo mysql.Repo
readDB *dao.Query
writeDB *dao.Query
syscfg sysconfig.Service
ticketSvc game.TicketService
}
func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service, ticketSvc game.TicketService) Service {
return &service{
logger: l,
repo: repo,
readDB: dao.Use(repo.GetDbR()),
writeDB: dao.Use(repo.GetDbW()),
syscfg: syscfg,
ticketSvc: ticketSvc,
}
}
// 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++
}
}
}
s.logger.Info("[抖店同步] 全量同步完成",
zap.Int("users_count", len(users)),
zap.Int("total_fetched", result.TotalFetched),
zap.Int("new_orders", result.NewOrders),
zap.Int("matched_users", result.MatchedUsers),
)
return result, nil
}
// 抖店 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 string `json:"actual_receive_amount"`
PayTypeDesc string `json:"pay_type_desc"`
Remark string `json:"remark"`
UserNickname string `json:"user_nickname"`
}
// fetchDouyinOrdersByBuyer 调用抖店 API 按 Buyer ID 获取订单
func (s *service) fetchDouyinOrdersByBuyer(cookie string, buyer string) ([]DouyinOrderItem, error) {
// 拼接带有业务标识的搜索 URL
baseUrl := "https://fxg.jinritemai.com/api/order/searchlist"
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")
fullUrl := baseUrl + "?" + params.Encode()
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")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var respData douyinOrderResponse
if err := json.Unmarshal(body, &respData); err != nil {
s.logger.Error("[抖店API] 解析响应失败", zap.String("response", string(body[:min(len(body), 500)])))
return nil, fmt.Errorf("解析响应失败: %w", err)
}
if respData.St != 0 && respData.Code != 0 {
return nil, fmt.Errorf("API 返回错误: %s", respData.Msg)
}
return respData.Data, nil
}
// SyncOrder 同步单个订单到本地
func (s *service) SyncOrder(ctx context.Context, item *DouyinOrderItem, suggestUserID int64) (isNew bool, isMatched bool) {
db := s.repo.GetDbW().WithContext(ctx)
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)
}
// 更新状态
db.Model(&order).Updates(map[string]any{
"order_status": item.OrderStatus,
"remark": item.Remark,
})
// 重要:同步内存状态,防止后续判断逻辑失效
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)
// 解析金额
var amount int64
if item.ActualReceiveAmount != "" {
if f, err := strconv.ParseFloat(strings.TrimSpace(item.ActualReceiveAmount), 64); err == nil {
amount = int64(f * 100)
}
}
rawData, _ := json.Marshal(item)
order = model.DouyinOrders{
ShopOrderID: item.ShopOrderID,
OrderStatus: int32(item.OrderStatus),
DouyinUserID: item.UserID,
ActualReceiveAmount: amount,
PayTypeDesc: item.PayTypeDesc,
Remark: item.Remark,
UserNickname: item.UserNickname,
RawData: string(rawData),
RewardGranted: 0,
LocalUserID: localUserIDStr,
}
if err := db.Create(&order).Error; err != nil {
s.logger.Error("[抖店同步] 创建订单失败", zap.String("shop_order_id", item.ShopOrderID), zap.Error(err))
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"
if isMatched && order.RewardGranted == 0 && order.OrderStatus == 5 {
localUserID, _ := strconv.ParseInt(order.LocalUserID, 10, 64)
fmt.Printf("[DEBUG] 准备发放奖励: User: %d, ShopOrder: %s\n", localUserID, item.ShopOrderID)
if localUserID > 0 && s.ticketSvc != nil {
err := s.ticketSvc.GrantTicket(ctx, localUserID, "minesweeper", 1, "douyin_order", order.ID, "抖店订单奖励")
if err == nil {
db.Model(&order).Update("reward_granted", 1)
order.RewardGranted = 1
fmt.Printf("[DEBUG] 订单 %s 发放奖励成功\n", item.ShopOrderID)
} else {
fmt.Printf("[DEBUG] 订单 %s 发放奖励失败: %v\n", item.ShopOrderID, err)
}
}
}
return isNew, isMatched
}
// min 返回两个整数的最小值
func min(a, b int) int {
if a < b {
return a
}
return b
}