@@ -16,6 +16,7 @@ import (
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var (
@@ -44,8 +45,104 @@ func SyncOrdersLogf(format string, args ...interface{}) {
syncOrdersLogf ( format , args ... )
}
// processOrderPaidPostProcess 订单已支付后的统一后置逻辑:全书/VIP/匹配/章节权益、取消同商品未支付订单、分佣
func processOrderPaidPostProcess ( db * gorm . DB , o * model . Order , transactionID string , totalAmount float64 ) {
pt := "fullbook"
if o . ProductType != "" {
pt = o . ProductType
}
productID := ""
if o . ProductID != nil {
productID = * o . ProductID
}
if productID == "" {
productID = "fullbook"
}
now := time . Now ( )
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 )
}
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 )
}
// PollOrderUntilPaidOrTimeout 用户支付发起后,仅轮询该笔订单直到微信返回已支付或超时(防漏单,替代频繁全量扫描)
// 轮询间隔 8 秒,总超时 6 分钟;若微信已支付则更新订单并执行与 PayNotify 一致的后置逻辑
func PollOrderUntilPaidOrTimeout ( orderSn string ) {
const pollInterval = 8 * time . Second
const pollTimeout = 6 * time . Minute
ctx , cancel := context . WithTimeout ( context . Background ( ) , pollTimeout )
defer cancel ( )
db := database . DB ( )
for {
select {
case <- ctx . Done ( ) :
return
default :
}
qCtx , qCancel := context . WithTimeout ( ctx , 15 * time . Second )
tradeState , transactionID , totalFee , qerr := wechat . QueryOrderByOutTradeNo ( qCtx , orderSn )
qCancel ( )
if qerr != nil {
syncOrdersLogf ( "轮询查询订单 %s 失败: %v" , orderSn , qerr )
time . Sleep ( pollInterval )
continue
}
if tradeState == "SUCCESS" {
var order model . Order
if err := db . Where ( "order_sn = ?" , orderSn ) . First ( & order ) . Error ; err != nil {
syncOrdersLogf ( "轮询订单 %s 查库失败: %v" , orderSn , err )
return
}
if order . Status != nil && * order . Status == "paid" {
return
}
now := time . Now ( )
if err := db . Model ( & order ) . Updates ( map [ string ] interface { } {
"status" : "paid" ,
"transaction_id" : transactionID ,
"pay_time" : now ,
"updated_at" : now ,
} ) . Error ; err != nil {
syncOrdersLogf ( "轮询更新订单 %s 失败: %v" , orderSn , err )
return
}
totalAmount := float64 ( totalFee ) / 100
syncOrdersLogf ( "轮询补齐: %s, amount=%.2f" , orderSn , totalAmount )
processOrderPaidPostProcess ( db , & order , transactionID , totalAmount )
return
}
switch tradeState {
case "CLOSED" , "REVOKED" , "PAYERROR" :
return
}
time . Sleep ( pollInterval )
}
}
// RunSyncOrders 订单对账:查询 status=created 的订单,向微信查询实际状态,若已支付则补齐更新(防漏单)
// 可被 HTTP 接口和内置定时任务调用。days 为查询范围(天),建议 7 。
// 可被 HTTP 接口和内置定时任务调用;日常以 PollOrderUntilPaidOrTimeout 单笔轮询为主,本方法作兜底 。
func RunSyncOrders ( ctx context . Context , days int ) ( synced , total int , err error ) {
if days < 1 {
days = 7
@@ -75,7 +172,6 @@ func RunSyncOrders(ctx context.Context, days int) (synced, total int, err error)
if tradeState != "SUCCESS" {
continue
}
// 微信已支付,本地未更新 → 补齐
totalAmount := float64 ( totalFee ) / 100
now := time . Now ( )
if err := db . Model ( & o ) . Updates ( map [ string ] interface { } {
@@ -89,45 +185,7 @@ func RunSyncOrders(ctx context.Context, days int) (synced, total int, err error)
}
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 )
processOrderPaidPostProcess ( db , & o , transactionID , totalAmount )
}
return synced , total , nil
}