322 lines
9.0 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/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
}