bindbox-game/internal/api/admin/douyin_orders_admin.go
2026-02-18 23:23:34 +08:00

305 lines
8.7 KiB
Go

package admin
import (
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/service/douyin"
"context"
"fmt"
"net/http"
"strconv"
"time"
)
// ---------- 抖店配置 API ----------
type getDouyinConfigResponse struct {
Cookie string `json:"cookie"`
IntervalMinutes int `json:"interval_minutes"`
}
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,
})
}
}
type saveDouyinConfigRequest struct {
Cookie string `json:"cookie"`
IntervalMinutes int `json:"interval_minutes"`
}
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.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"`
}
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
}
orders, total, err := h.douyinSvc.ListOrders(ctx.RequestContext(), req.Page, req.PageSize, req.Status)
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"`
}
func (h *handler) SyncDouyinOrders() core.HandlerFunc {
return func(ctx core.Context) {
// 使用独立 Context 防止 HTTP 请求断开导致同步中断
// 设置 5 分钟超时,确保有足够时间完成全量同步
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
result, err := h.douyinSvc.FetchAndSyncOrders(bgCtx)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, validation.Error(err)))
return
}
ctx.Payload(syncDouyinOrdersResponse{
Message: "同步成功",
TotalFetched: result.TotalFetched,
NewOrders: result.NewOrders,
MatchedUsers: result.MatchedUsers,
})
}
}
// ---------- 新增: 手动同步接口 (优化版) ----------
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"`
}
// 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 返回实际发放数量
})
}
}
// ---------- 辅助函数 ----------
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
}