feat: 内容管理第5批优化 - Bug修复 + 分享功能 + 代付功能
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
This commit is contained in:
352
soul-api/internal/handler/balance.go
Normal file
352
soul-api/internal/handler/balance.go
Normal file
@@ -0,0 +1,352 @@
|
||||
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,
|
||||
}})
|
||||
}
|
||||
Reference in New Issue
Block a user