1. Bug修复: - 修复Markdown星号/下划线在小程序端原样显示问题(markdownToHtml增加__和_支持,contentParser增加Markdown格式剥离) - 修复@提及无反应(MentionSuggestion使用ref保持persons最新值,解决闭包捕获空数组问题) - 修复#链接标签点击"未找到小程序配置"(增加appId直接跳转降级路径) 2. 分享功能优化: - "分享到朋友圈"改为"分享给好友"(open-type从shareTimeline改为share) - 90%收益提示移到分享按钮下方 - 阅读20%后向上滑动弹出分享浮层提示(4秒自动消失) 3. 代付功能: - 后端:新增UserBalance/BalanceTransaction/GiftUnlock三个模型 - 后端:新增8个余额相关API(查询/充值/充值确认/代付/领取/退款/交易记录/礼物信息) - 小程序:阅读页新增"代付分享"按钮,支持用余额为好友解锁章节 - 分享链接携带gift参数,好友打开自动领取解锁 Made-with: Cursor
353 lines
10 KiB
Go
353 lines
10 KiB
Go
package handler
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"net/http"
|
||
"time"
|
||
|
||
"soul-api/internal/database"
|
||
"soul-api/internal/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// GET /api/miniprogram/balance 小程序-查询余额
|
||
func BalanceGet(c *gin.Context) {
|
||
userID := c.Query("userId")
|
||
if userID == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"})
|
||
return
|
||
}
|
||
db := database.DB()
|
||
var bal model.UserBalance
|
||
if err := db.Where("user_id = ?", userID).First(&bal).Error; err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"userId": userID, "balance": 0, "totalRecharged": 0, "totalGifted": 0, "totalRefunded": 0}})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": bal})
|
||
}
|
||
|
||
// POST /api/miniprogram/balance/recharge 小程序-充值(创建充值订单)
|
||
func BalanceRecharge(c *gin.Context) {
|
||
var body struct {
|
||
UserID string `json:"userId" binding:"required"`
|
||
Amount float64 `json:"amount" binding:"required,gt=0"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
orderSN := fmt.Sprintf("BAL_%d", time.Now().UnixNano())
|
||
|
||
order := model.Order{
|
||
ID: orderSN,
|
||
OrderSN: orderSN,
|
||
UserID: body.UserID,
|
||
ProductType: "balance_recharge",
|
||
Amount: body.Amount,
|
||
}
|
||
desc := fmt.Sprintf("余额充值 ¥%.2f", body.Amount)
|
||
status := "pending"
|
||
order.Description = &desc
|
||
order.Status = &status
|
||
|
||
if err := db.Create(&order).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "创建充值订单失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"orderSn": orderSN, "amount": body.Amount}})
|
||
}
|
||
|
||
// POST /api/miniprogram/balance/recharge/confirm 充值完成回调(内部或手动确认)
|
||
func BalanceRechargeConfirm(c *gin.Context) {
|
||
var body struct {
|
||
OrderSN string `json:"orderSn" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
var order model.Order
|
||
if err := db.Where("order_sn = ? AND product_type = ?", body.OrderSN, "balance_recharge").First(&order).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "订单不存在"})
|
||
return
|
||
}
|
||
if order.Status != nil && *order.Status == "paid" {
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已确认"})
|
||
return
|
||
}
|
||
|
||
err := db.Transaction(func(tx *gorm.DB) error {
|
||
paid := "paid"
|
||
now := time.Now()
|
||
if err := tx.Model(&order).Updates(map[string]interface{}{"status": paid, "pay_time": now}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
var bal model.UserBalance
|
||
if err := tx.Where("user_id = ?", order.UserID).First(&bal).Error; err != nil {
|
||
bal = model.UserBalance{UserID: order.UserID}
|
||
tx.Create(&bal)
|
||
}
|
||
if err := tx.Model(&bal).Updates(map[string]interface{}{
|
||
"balance": gorm.Expr("balance + ?", order.Amount),
|
||
"total_recharged": gorm.Expr("total_recharged + ?", order.Amount),
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
tx.Create(&model.BalanceTransaction{
|
||
UserID: order.UserID,
|
||
Type: "recharge",
|
||
Amount: order.Amount,
|
||
BalanceAfter: bal.Balance + order.Amount,
|
||
RelatedOrder: &order.OrderSN,
|
||
Description: fmt.Sprintf("充值 ¥%.2f", order.Amount),
|
||
})
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "确认失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "充值成功"})
|
||
}
|
||
|
||
// POST /api/miniprogram/balance/gift 小程序-代付解锁(用余额帮他人解锁章节)
|
||
func BalanceGift(c *gin.Context) {
|
||
var body struct {
|
||
GiverID string `json:"giverId" binding:"required"`
|
||
SectionID string `json:"sectionId" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
|
||
var chapter model.Chapter
|
||
if err := db.Where("id = ?", body.SectionID).First(&chapter).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "章节不存在"})
|
||
return
|
||
}
|
||
price := float64(1)
|
||
if chapter.Price != nil {
|
||
price = *chapter.Price
|
||
}
|
||
if price <= 0 {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "该章节免费,无需代付"})
|
||
return
|
||
}
|
||
|
||
var giftCode string
|
||
err := db.Transaction(func(tx *gorm.DB) error {
|
||
var bal model.UserBalance
|
||
if err := tx.Where("user_id = ?", body.GiverID).First(&bal).Error; err != nil || bal.Balance < price {
|
||
return fmt.Errorf("余额不足,当前 ¥%.2f,需要 ¥%.2f", bal.Balance, price)
|
||
}
|
||
|
||
if err := tx.Model(&bal).Updates(map[string]interface{}{
|
||
"balance": gorm.Expr("balance - ?", price),
|
||
"total_gifted": gorm.Expr("total_gifted + ?", price),
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
code := make([]byte, 16)
|
||
rand.Read(code)
|
||
giftCode = hex.EncodeToString(code)
|
||
|
||
tx.Create(&model.GiftUnlock{
|
||
GiftCode: giftCode,
|
||
GiverID: body.GiverID,
|
||
SectionID: body.SectionID,
|
||
Amount: price,
|
||
Status: "pending",
|
||
})
|
||
|
||
tx.Create(&model.BalanceTransaction{
|
||
UserID: body.GiverID,
|
||
Type: "gift",
|
||
Amount: -price,
|
||
BalanceAfter: bal.Balance - price,
|
||
SectionID: &body.SectionID,
|
||
Description: fmt.Sprintf("代付章节 %s (¥%.2f)", body.SectionID, price),
|
||
})
|
||
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||
"giftCode": giftCode,
|
||
"sectionId": body.SectionID,
|
||
"amount": price,
|
||
}})
|
||
}
|
||
|
||
// POST /api/miniprogram/balance/gift/redeem 领取代付礼物
|
||
func BalanceGiftRedeem(c *gin.Context) {
|
||
var body struct {
|
||
GiftCode string `json:"giftCode" binding:"required"`
|
||
ReceiverID string `json:"receiverId" binding:"required"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
var gift model.GiftUnlock
|
||
if err := db.Where("gift_code = ?", body.GiftCode).First(&gift).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "礼物码无效"})
|
||
return
|
||
}
|
||
if gift.Status != "pending" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "礼物已被领取"})
|
||
return
|
||
}
|
||
|
||
err := db.Transaction(func(tx *gorm.DB) error {
|
||
now := time.Now()
|
||
tx.Model(&gift).Updates(map[string]interface{}{
|
||
"receiver_id": body.ReceiverID,
|
||
"status": "redeemed",
|
||
"redeemed_at": now,
|
||
})
|
||
|
||
orderSN := fmt.Sprintf("GIFT_%s", body.GiftCode[:8])
|
||
paid := "paid"
|
||
desc := fmt.Sprintf("来自好友的代付解锁")
|
||
tx.Create(&model.Order{
|
||
ID: orderSN,
|
||
OrderSN: orderSN,
|
||
UserID: body.ReceiverID,
|
||
ProductType: "section",
|
||
ProductID: &gift.SectionID,
|
||
Amount: 0,
|
||
Description: &desc,
|
||
Status: &paid,
|
||
PayTime: &now,
|
||
ReferrerID: &gift.GiverID,
|
||
})
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "领取失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||
"sectionId": gift.SectionID,
|
||
"message": "解锁成功!",
|
||
}})
|
||
}
|
||
|
||
// POST /api/miniprogram/balance/refund 申请余额退款(9折)
|
||
func BalanceRefund(c *gin.Context) {
|
||
var body struct {
|
||
UserID string `json:"userId" binding:"required"`
|
||
Amount float64 `json:"amount" binding:"required,gt=0"`
|
||
}
|
||
if err := c.ShouldBindJSON(&body); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
refundAmount := body.Amount * 0.9
|
||
|
||
err := db.Transaction(func(tx *gorm.DB) error {
|
||
var bal model.UserBalance
|
||
if err := tx.Where("user_id = ?", body.UserID).First(&bal).Error; err != nil || bal.Balance < body.Amount {
|
||
return fmt.Errorf("余额不足")
|
||
}
|
||
|
||
if err := tx.Model(&bal).Updates(map[string]interface{}{
|
||
"balance": gorm.Expr("balance - ?", body.Amount),
|
||
"total_refunded": gorm.Expr("total_refunded + ?", body.Amount),
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
tx.Create(&model.BalanceTransaction{
|
||
UserID: body.UserID,
|
||
Type: "refund",
|
||
Amount: -body.Amount,
|
||
BalanceAfter: bal.Balance - body.Amount,
|
||
Description: fmt.Sprintf("退款 ¥%.2f(原额 ¥%.2f,9折退回 ¥%.2f)", body.Amount, body.Amount, refundAmount),
|
||
})
|
||
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||
"deducted": body.Amount,
|
||
"refundAmount": refundAmount,
|
||
"message": fmt.Sprintf("退款成功,实际退回 ¥%.2f", refundAmount),
|
||
}})
|
||
}
|
||
|
||
// GET /api/miniprogram/balance/transactions 交易记录
|
||
func BalanceTransactions(c *gin.Context) {
|
||
userID := c.Query("userId")
|
||
if userID == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
var txns []model.BalanceTransaction
|
||
db.Where("user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&txns)
|
||
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": txns})
|
||
}
|
||
|
||
// GET /api/miniprogram/balance/gift/info 查询礼物码信息
|
||
func BalanceGiftInfo(c *gin.Context) {
|
||
code := c.Query("code")
|
||
if code == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 code"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
var gift model.GiftUnlock
|
||
if err := db.Where("gift_code = ?", code).First(&gift).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "礼物码无效"})
|
||
return
|
||
}
|
||
|
||
var chapter model.Chapter
|
||
db.Where("id = ?", gift.SectionID).First(&chapter)
|
||
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||
"giftCode": gift.GiftCode,
|
||
"sectionId": gift.SectionID,
|
||
"sectionTitle": chapter.SectionTitle,
|
||
"amount": gift.Amount,
|
||
"status": gift.Status,
|
||
"giverId": gift.GiverID,
|
||
}})
|
||
}
|