Files
soul-yongping/soul-api/internal/handler/cron.go
2026-03-07 22:58:43 +08:00

162 lines
4.5 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 handler
import (
"context"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
)
var (
syncOrdersLogger *log.Logger
syncOrdersLoggerOnce sync.Once
)
// syncOrdersLogf 将订单同步日志写入 log/sync-orders.log不输出到控制台
func syncOrdersLogf(format string, args ...interface{}) {
syncOrdersLoggerOnce.Do(func() {
_ = os.MkdirAll("log", 0755)
f, err := os.OpenFile(filepath.Join("log", "sync-orders.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
syncOrdersLogger = log.New(io.Discard, "", 0)
return
}
syncOrdersLogger = log.New(f, "[SyncOrders] ", log.Ldate|log.Ltime)
})
if syncOrdersLogger != nil {
syncOrdersLogger.Printf(format, args...)
}
}
// SyncOrdersLogf 供 main 等调用,将订单同步相关日志写入 log/sync-orders.log
func SyncOrdersLogf(format string, args ...interface{}) {
syncOrdersLogf(format, args...)
}
// RunSyncOrders 订单对账:查询 status=created 的订单,向微信查询实际状态,若已支付则补齐更新(防漏单)
// 可被 HTTP 接口和内置定时任务调用。days 为查询范围(天),建议 7。
func RunSyncOrders(ctx context.Context, days int) (synced, total int, err error) {
if days < 1 {
days = 7
}
if days > 30 {
days = 30
}
db := database.DB()
cutoff := time.Now().AddDate(0, 0, -days)
var createdOrders []model.Order
if err := db.Where("status = ? AND created_at > ?", "created", cutoff).Find(&createdOrders).Error; err != nil {
return 0, 0, err
}
total = len(createdOrders)
for _, o := range createdOrders {
select {
case <-ctx.Done():
return synced, total, ctx.Err()
default:
}
tradeState, transactionID, totalFee, qerr := wechat.QueryOrderByOutTradeNo(ctx, o.OrderSN)
if qerr != nil {
syncOrdersLogf("查询订单 %s 失败: %v", o.OrderSN, qerr)
continue
}
if tradeState != "SUCCESS" {
continue
}
// 微信已支付,本地未更新 → 补齐
totalAmount := float64(totalFee) / 100
now := time.Now()
if err := db.Model(&o).Updates(map[string]interface{}{
"status": "paid",
"transaction_id": transactionID,
"pay_time": now,
"updated_at": now,
}).Error; err != nil {
syncOrdersLogf("更新订单 %s 失败: %v", o.OrderSN, err)
continue
}
synced++
syncOrdersLogf("补齐漏单: %s, amount=%.2f", o.OrderSN, totalAmount)
// 同步后续逻辑全书、VIP、分销等与 PayNotify 一致)
pt := "fullbook"
if o.ProductType != "" {
pt = o.ProductType
}
productID := ""
if o.ProductID != nil {
productID = *o.ProductID
}
if productID == "" {
productID = "fullbook"
}
switch pt {
case "fullbook":
db.Model(&model.User{}).Where("id = ?", o.UserID).Update("has_full_book", true)
syncOrdersLogf("用户已购全书: %s", o.UserID)
case "vip":
expireDate := now.AddDate(0, 0, 365)
db.Model(&model.User{}).Where("id = ?", o.UserID).Updates(map[string]interface{}{
"is_vip": true,
"vip_expire_date": expireDate,
"vip_activated_at": now,
})
syncOrdersLogf("用户 VIP 已激活: %s, 过期日=%s", o.UserID, expireDate.Format("2006-01-02"))
case "match":
syncOrdersLogf("用户购买匹配次数: %s", o.UserID)
case "section":
syncOrdersLogf("用户购买章节: %s - %s", o.UserID, productID)
}
// 取消同商品未支付订单(与 PayNotify 一致)
db.Where(
"user_id = ? AND product_type = ? AND product_id = ? AND status = ? AND order_sn != ?",
o.UserID, pt, productID, "created", o.OrderSN,
).Delete(&model.Order{})
processReferralCommission(db, o.UserID, totalAmount, o.OrderSN, &o)
}
return synced, total, nil
}
// CronSyncOrders GET/POST /api/cron/sync-orders
// 对账:查询 status=created 的订单,向微信查询实际状态,若已支付则补齐更新(防漏单)
// 支持 ?days=7 扩展时间范围,默认 7 天
func CronSyncOrders(c *gin.Context) {
days := 7
if d := c.Query("days"); d != "" {
if n, err := strconv.Atoi(d); err == nil && n > 0 && n <= 30 {
days = n
}
}
synced, total, err := RunSyncOrders(c.Request.Context(), days)
if err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"synced": synced,
"total": total,
"days": days,
})
}
// CronUnbindExpired GET/POST /api/cron/unbind-expired
func CronUnbindExpired(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}