322 lines
9.0 KiB
Go
322 lines
9.0 KiB
Go
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/sysconfig"
|
||
"compress/gzip"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
func New(l logger.CustomLogger, repo mysql.Repo, syscfg sysconfig.Service) Service {
|
||
return &service{
|
||
logger: l,
|
||
repo: repo,
|
||
readDB: dao.Use(repo.GetDbR()),
|
||
writeDB: dao.Use(repo.GetDbW()),
|
||
syscfg: syscfg,
|
||
}
|
||
}
|
||
|
||
// 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 从抖店 API 获取订单并同步到本地
|
||
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 未配置")
|
||
}
|
||
|
||
// 调用抖店 API 获取订单
|
||
orders, err := s.fetchDouyinOrders(cfg.Cookie)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("获取抖店订单失败: %w", err)
|
||
}
|
||
|
||
result := &SyncResult{TotalFetched: len(orders)}
|
||
|
||
// 统计各状态订单数量
|
||
statusCount := make(map[int]int)
|
||
for _, order := range orders {
|
||
statusCount[order.OrderStatus]++
|
||
}
|
||
s.logger.Info("[抖店同步] 订单状态分布",
|
||
zap.Any("status_count", statusCount),
|
||
)
|
||
|
||
// 同步订单到本地(只同步 order_status=5 已完成的订单)
|
||
for _, order := range orders {
|
||
if order.OrderStatus != 5 {
|
||
// 跳过非已完成订单
|
||
continue
|
||
}
|
||
isNew, matched := s.syncOrder(ctx, order)
|
||
if isNew {
|
||
result.NewOrders++
|
||
s.logger.Info("[抖店同步] 新增订单",
|
||
zap.String("shop_order_id", order.ShopOrderID),
|
||
zap.Int("order_status", order.OrderStatus),
|
||
)
|
||
}
|
||
if matched {
|
||
result.MatchedUsers++
|
||
}
|
||
}
|
||
|
||
s.logger.Info("[抖店同步] 同步完成",
|
||
zap.Int("total_fetched", result.TotalFetched),
|
||
zap.Int("completed_orders", statusCount[5]),
|
||
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"`
|
||
}
|
||
|
||
// fetchDouyinOrders 调用抖店 API 获取订单
|
||
func (s *service) fetchDouyinOrders(cookie string) ([]douyinOrderItem, error) {
|
||
url := "https://fxg.jinritemai.com/api/order/searchlist?page=0&pageSize=50&order_by=create_time&order=desc&tab=all"
|
||
|
||
req, err := http.NewRequest("GET", url, 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("Accept-Encoding", "gzip, deflate, br")
|
||
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()
|
||
|
||
// 处理 gzip 响应
|
||
var reader io.Reader = resp.Body
|
||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||
gzReader, err := gzip.NewReader(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("gzip 解压失败: %w", err)
|
||
}
|
||
defer gzReader.Close()
|
||
reader = gzReader
|
||
}
|
||
|
||
body, err := io.ReadAll(reader)
|
||
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)
|
||
}
|
||
|
||
// 抖店使用 st 字段表示状态,0 表示成功
|
||
if respData.St != 0 && respData.Code != 0 {
|
||
return nil, fmt.Errorf("API 返回错误: %s", respData.Msg)
|
||
}
|
||
|
||
s.logger.Info("[抖店API] 获取订单成功", zap.Int("count", len(respData.Data)))
|
||
|
||
return respData.Data, nil
|
||
}
|
||
|
||
// syncOrder 同步单个订单到本地
|
||
func (s *service) syncOrder(ctx context.Context, item douyinOrderItem) (isNew bool, isMatched bool) {
|
||
db := s.repo.GetDbW().WithContext(ctx)
|
||
|
||
// 检查订单是否已存在
|
||
var existing model.DouyinOrders
|
||
if err := db.Where("shop_order_id = ?", item.ShopOrderID).First(&existing).Error; err == nil {
|
||
// 订单已存在,更新状态
|
||
db.Model(&existing).Updates(map[string]any{
|
||
"order_status": item.OrderStatus,
|
||
"remark": item.Remark,
|
||
})
|
||
return false, existing.LocalUserID != "" && existing.LocalUserID != "0"
|
||
}
|
||
|
||
// 新订单,尝试匹配本地用户
|
||
var localUserID int64
|
||
var user model.Users
|
||
// 尝试通过 douyin_id 匹配用户
|
||
if item.UserID != "" {
|
||
if err := s.repo.GetDbR().Where("douyin_id = ?", item.UserID).First(&user).Error; err == nil {
|
||
localUserID = user.ID
|
||
isMatched = true
|
||
}
|
||
}
|
||
|
||
// 解析金额 (抖店返回的是元,需要转换为分)
|
||
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,
|
||
LocalUserID: strconv.FormatInt(localUserID, 10),
|
||
ActualReceiveAmount: amount,
|
||
PayTypeDesc: item.PayTypeDesc,
|
||
Remark: item.Remark,
|
||
UserNickname: item.UserNickname,
|
||
RawData: string(rawData),
|
||
}
|
||
|
||
if err := db.Create(order).Error; err != nil {
|
||
s.logger.Error("[抖店同步] 创建订单失败",
|
||
zap.String("shop_order_id", item.ShopOrderID),
|
||
zap.Error(err),
|
||
)
|
||
return false, false
|
||
}
|
||
|
||
return true, isMatched
|
||
}
|
||
|
||
// min 返回两个整数的最小值
|
||
func min(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|