实现@提及功能,允许用户在阅读页中高亮并点击提及的用户,触发添加好友流程。更新内容解析逻辑以支持提及格式,调整页面展示以适应新功能,并优化样式以提升用户体验。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -343,6 +343,8 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
"payParams": payParams,
|
||||
},
|
||||
})
|
||||
// 用户支付发起后,仅轮询该笔订单直到微信返回已支付或超时(防漏单)
|
||||
go PollOrderUntilPaidOrTimeout(orderSn)
|
||||
}
|
||||
|
||||
// GET - 查询订单状态(并主动同步:若微信已支付但本地未标记,则更新本地订单,便于配额即时生效)
|
||||
|
||||
@@ -166,6 +166,11 @@ func UserCheckPurchased(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
// 超级VIP(管理端开通):is_vip=1 且 vip_expire_date>NOW 时,所有文章阅读免费,无需再查订单
|
||||
if user.IsVip != nil && *user.IsVip && user.VipExpireDate != nil && user.VipExpireDate.After(time.Now()) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_vip"}})
|
||||
return
|
||||
}
|
||||
hasFullBook := user.HasFullBook != nil && *user.HasFullBook
|
||||
if hasFullBook {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_full_book"}})
|
||||
@@ -377,8 +382,13 @@ func UserPurchaseStatus(c *gin.Context) {
|
||||
if user.PendingEarnings != nil {
|
||||
pendingEarnings = *user.PendingEarnings
|
||||
}
|
||||
// 超级VIP(管理端开通):与 check-purchased 一致,视为全章可读
|
||||
hasFullBook := user.HasFullBook != nil && *user.HasFullBook
|
||||
if !hasFullBook && user.IsVip != nil && *user.IsVip && user.VipExpireDate != nil && user.VipExpireDate.After(time.Now()) {
|
||||
hasFullBook = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||||
"hasFullBook": user.HasFullBook != nil && *user.HasFullBook,
|
||||
"hasFullBook": hasFullBook,
|
||||
"purchasedSections": purchasedSections,
|
||||
"sectionMidMap": sectionMidMap,
|
||||
"purchasedCount": len(purchasedSections),
|
||||
|
||||
Reference in New Issue
Block a user