Update project documentation and enhance user interaction features
- Added a new entry for user interaction habit analysis based on agent transcripts, summarizing key insights into communication styles and preferences. - Updated project indices to reflect the latest developments, including the addition of a wallet balance feature and enhancements to the mini program's user interface for better user experience. - Improved the handling of loading states in the chapters page, ensuring a smoother user experience during data retrieval. - Implemented a gift payment sharing feature, allowing users to share payment requests with friends for collaborative purchases.
This commit is contained in:
363
soul-api/internal/handler/balance.go
Normal file
363
soul-api/internal/handler/balance.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/wechat"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BalanceGet GET /api/miniprogram/balance?userId=
|
||||
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 ub model.UserBalance
|
||||
if err := db.Where("user_id = ?", userID).First(&ub).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": 0}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": ub.Balance}})
|
||||
}
|
||||
|
||||
// BalanceTransactionsGet GET /api/miniprogram/balance/transactions?userId=&page=&pageSize=
|
||||
func BalanceTransactionsGet(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
db := database.DB()
|
||||
var total int64
|
||||
db.Model(&model.BalanceTransaction{}).Where("user_id = ?", userID).Count(&total)
|
||||
var list []model.BalanceTransaction
|
||||
if err := db.Where("user_id = ?", userID).Order("created_at DESC").
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "data": []interface{}{}, "total": 0})
|
||||
return
|
||||
}
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for _, t := range list {
|
||||
orderID := ""
|
||||
if t.OrderID != nil {
|
||||
orderID = *t.OrderID
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": t.ID, "type": t.Type, "amount": t.Amount,
|
||||
"orderId": orderID, "createdAt": t.CreatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out, "total": total})
|
||||
}
|
||||
|
||||
// BalanceRechargePost POST /api/miniprogram/balance/recharge
|
||||
func BalanceRechargePost(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required,gte=0.01"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"})
|
||||
return
|
||||
}
|
||||
orderSn := wechat.GenerateOrderSn()
|
||||
db := database.DB()
|
||||
desc := fmt.Sprintf("余额充值 ¥%.2f", req.Amount)
|
||||
status := "created"
|
||||
order := model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: req.UserID,
|
||||
ProductType: "balance_recharge",
|
||||
ProductID: &orderSn,
|
||||
Amount: req.Amount,
|
||||
Description: &desc,
|
||||
Status: &status,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建订单失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"orderSn": orderSn}})
|
||||
}
|
||||
|
||||
// BalanceRechargeConfirmPost POST /api/miniprogram/balance/recharge/confirm
|
||||
func BalanceRechargeConfirmPost(c *gin.Context) {
|
||||
var req struct {
|
||||
OrderSn string `json:"orderSn" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 orderSn"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.Order
|
||||
if err := tx.Where("order_sn = ? AND product_type = ?", req.OrderSn, "balance_recharge").First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
status := ""
|
||||
if order.Status != nil {
|
||||
status = *order.Status
|
||||
}
|
||||
if status != "paid" {
|
||||
return fmt.Errorf("订单未支付")
|
||||
}
|
||||
// 幂等:检查是否已处理
|
||||
var cnt int64
|
||||
tx.Model(&model.BalanceTransaction{}).Where("order_id = ? AND type = ?", req.OrderSn, "recharge").Count(&cnt)
|
||||
if cnt > 0 {
|
||||
return nil // 已处理,直接成功
|
||||
}
|
||||
// 增加余额
|
||||
tx.Exec("INSERT INTO user_balances (user_id, balance, updated_at) VALUES (?, 0, NOW()) ON DUPLICATE KEY UPDATE balance = balance + ?, updated_at = NOW()", order.UserID, order.Amount)
|
||||
txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000)
|
||||
tx.Create(&model.BalanceTransaction{
|
||||
ID: txID, UserID: order.UserID, Type: "recharge", Amount: order.Amount,
|
||||
OrderID: &req.OrderSn, CreatedAt: time.Now(),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// BalanceConsumePost POST /api/miniprogram/balance/consume
|
||||
func BalanceConsumePost(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
ProductType string `json:"productType" binding:"required"`
|
||||
ProductID string `json:"productId"`
|
||||
Amount float64 `json:"amount" binding:"required,gte=0.01"`
|
||||
ReferralCode string `json:"referralCode"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
// 后端价格校验
|
||||
standardPrice, priceErr := getStandardPrice(db, req.ProductType, req.ProductID)
|
||||
if priceErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": priceErr.Error()})
|
||||
return
|
||||
}
|
||||
if req.Amount < standardPrice-0.01 || req.Amount > standardPrice+0.01 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "金额校验失败"})
|
||||
return
|
||||
}
|
||||
amount := standardPrice
|
||||
|
||||
// 解析推荐人
|
||||
var referrerID *string
|
||||
if req.ReferralCode != "" {
|
||||
var refUser model.User
|
||||
if err := db.Where("referral_code = ?", req.ReferralCode).First(&refUser).Error; err == nil {
|
||||
referrerID = &refUser.ID
|
||||
}
|
||||
}
|
||||
if referrerID == nil {
|
||||
var binding struct {
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
}
|
||||
_ = db.Raw("SELECT referrer_id FROM referral_bindings WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW() ORDER BY binding_date DESC LIMIT 1", req.UserID).Scan(&binding).Error
|
||||
if binding.ReferrerID != "" {
|
||||
referrerID = &binding.ReferrerID
|
||||
}
|
||||
}
|
||||
|
||||
productID := req.ProductID
|
||||
if productID == "" {
|
||||
switch req.ProductType {
|
||||
case "vip":
|
||||
productID = "vip_annual"
|
||||
case "fullbook":
|
||||
productID = "fullbook"
|
||||
default:
|
||||
productID = req.ProductID
|
||||
}
|
||||
}
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var ub model.UserBalance
|
||||
if err := tx.Where("user_id = ?", req.UserID).First(&ub).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("余额不足")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ub.Balance < amount {
|
||||
return fmt.Errorf("余额不足")
|
||||
}
|
||||
tx.Model(&model.UserBalance{}).Where("user_id = ?", req.UserID).Update("balance", gorm.Expr("balance - ?", amount))
|
||||
orderSn := wechat.GenerateOrderSn()
|
||||
desc := ""
|
||||
switch req.ProductType {
|
||||
case "section":
|
||||
desc = "章节购买-" + productID
|
||||
case "fullbook":
|
||||
desc = "《一场Soul的创业实验》全书"
|
||||
case "vip":
|
||||
desc = "卡若创业派对VIP年度会员(365天)"
|
||||
default:
|
||||
desc = "余额消费"
|
||||
}
|
||||
pm := "balance"
|
||||
status := "paid"
|
||||
now := time.Now()
|
||||
order := model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: req.UserID,
|
||||
ProductType: req.ProductType,
|
||||
ProductID: &productID,
|
||||
Amount: amount,
|
||||
Description: &desc,
|
||||
Status: &status,
|
||||
PaymentMethod: &pm,
|
||||
ReferrerID: referrerID,
|
||||
PayTime: &now,
|
||||
}
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000)
|
||||
tx.Create(&model.BalanceTransaction{
|
||||
ID: txID, UserID: req.UserID, Type: "consume", Amount: -amount,
|
||||
OrderID: &orderSn, CreatedAt: now,
|
||||
})
|
||||
// 激活权益
|
||||
if req.ProductType == "fullbook" {
|
||||
tx.Model(&model.User{}).Where("id = ?", req.UserID).Update("has_full_book", true)
|
||||
} else if req.ProductType == "vip" {
|
||||
activateVIP(tx, req.UserID, 365, now)
|
||||
}
|
||||
// 分佣
|
||||
if referrerID != nil {
|
||||
processReferralCommission(tx, req.UserID, amount, orderSn, &order)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if err.Error() == "余额不足" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "余额不足"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// BalanceRefundPost POST /api/miniprogram/balance/refund
|
||||
func BalanceRefundPost(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required,gte=0.01"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数无效"})
|
||||
return
|
||||
}
|
||||
// 首版简化:暂不实现微信原路退,仅扣减余额并记录
|
||||
db := database.DB()
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var ub model.UserBalance
|
||||
if err := tx.Where("user_id = ?", req.UserID).First(&ub).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return fmt.Errorf("余额为零")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ub.Balance < req.Amount {
|
||||
return fmt.Errorf("余额不足")
|
||||
}
|
||||
tx.Model(&model.UserBalance{}).Where("user_id = ?", req.UserID).Update("balance", gorm.Expr("balance - ?", req.Amount))
|
||||
txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000)
|
||||
tx.Create(&model.BalanceTransaction{
|
||||
ID: txID, UserID: req.UserID, Type: "refund", Amount: -req.Amount,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "退款申请已提交,1-3个工作日内原路返回"}})
|
||||
}
|
||||
|
||||
// AdminUserBalanceGet GET /api/admin/users/:id/balance 管理端-用户余额与最近交易
|
||||
func AdminUserBalanceGet(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少用户ID"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var ub model.UserBalance
|
||||
balance := 0.0
|
||||
if err := db.Where("user_id = ?", userID).First(&ub).Error; err == nil {
|
||||
balance = ub.Balance
|
||||
}
|
||||
var list []model.BalanceTransaction
|
||||
db.Where("user_id = ?", userID).Order("created_at DESC").Limit(20).Find(&list)
|
||||
transactions := make([]gin.H, 0, len(list))
|
||||
for _, t := range list {
|
||||
orderID := ""
|
||||
if t.OrderID != nil {
|
||||
orderID = *t.OrderID
|
||||
}
|
||||
transactions = append(transactions, gin.H{
|
||||
"id": t.ID, "type": t.Type, "amount": t.Amount,
|
||||
"orderId": orderID, "createdAt": t.CreatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"balance": balance, "transactions": transactions}})
|
||||
}
|
||||
|
||||
// ConfirmBalanceRechargeByOrder 支付成功后确认充值(幂等),供 PayNotify 和 activateOrderBenefits 调用
|
||||
func ConfirmBalanceRechargeByOrder(db *gorm.DB, order *model.Order) error {
|
||||
if order == nil || order.ProductType != "balance_recharge" {
|
||||
return nil
|
||||
}
|
||||
orderSn := order.OrderSN
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var cnt int64
|
||||
tx.Model(&model.BalanceTransaction{}).Where("order_id = ? AND type = ?", orderSn, "recharge").Count(&cnt)
|
||||
if cnt > 0 {
|
||||
return nil // 已处理,幂等
|
||||
}
|
||||
tx.Exec("INSERT INTO user_balances (user_id, balance, updated_at) VALUES (?, 0, NOW()) ON DUPLICATE KEY UPDATE balance = balance + ?, updated_at = NOW()", order.UserID, order.Amount)
|
||||
txID := fmt.Sprintf("bt_%d", time.Now().UnixNano()%100000000000)
|
||||
return tx.Create(&model.BalanceTransaction{
|
||||
ID: txID, UserID: order.UserID, Type: "recharge", Amount: order.Amount,
|
||||
OrderID: &orderSn, CreatedAt: time.Now(),
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
@@ -613,9 +613,15 @@ func bookHotChaptersSorted(db *gorm.DB, limit int) []model.Chapter {
|
||||
return out
|
||||
}
|
||||
|
||||
// BookHot GET /api/book/hot 热门章节(按阅读量排序,排除序言/尾声/附录)
|
||||
// BookHot GET /api/book/hot 热门章节(按阅读量排序,排除序言/尾声/附录;支持 ?limit=,最大 50)
|
||||
func BookHot(c *gin.Context) {
|
||||
list := bookHotChaptersSorted(database.DB(), 10)
|
||||
limit := 10
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 50 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
list := bookHotChaptersSorted(database.DB(), limit)
|
||||
if len(list) == 0 {
|
||||
// 兜底:按 sort_order 取前 10,同样排除序言/尾声/附录
|
||||
q := database.DB().Model(&model.Chapter{})
|
||||
|
||||
@@ -739,13 +739,14 @@ func DBUsersAction(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "user": u, "isNew": true, "message": "用户创建成功"})
|
||||
return
|
||||
}
|
||||
// PUT 更新(含 VIP 手动设置:is_vip、vip_expire_date、vip_name、vip_avatar、vip_project、vip_contact、vip_bio)
|
||||
// PUT 更新(含 VIP 手动设置:is_vip、vip_expire_date、vip_name、vip_avatar、vip_project、vip_contact、vip_bio;tags 存 ckb_tags)
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
Nickname *string `json:"nickname"`
|
||||
Phone *string `json:"phone"`
|
||||
WechatID *string `json:"wechatId"`
|
||||
Avatar *string `json:"avatar"`
|
||||
Tags *string `json:"tags"` // JSON 数组字符串,如 ["创业者","电商"],存 ckb_tags
|
||||
HasFullBook *bool `json:"hasFullBook"`
|
||||
IsAdmin *bool `json:"isAdmin"`
|
||||
Earnings *float64 `json:"earnings"`
|
||||
@@ -790,6 +791,9 @@ func DBUsersAction(c *gin.Context) {
|
||||
if body.Avatar != nil {
|
||||
updates["avatar"] = *body.Avatar
|
||||
}
|
||||
if body.Tags != nil {
|
||||
updates["ckb_tags"] = *body.Tags
|
||||
}
|
||||
if body.HasFullBook != nil {
|
||||
updates["has_full_book"] = *body.HasFullBook
|
||||
}
|
||||
|
||||
382
soul-api/internal/handler/gift_pay.go
Normal file
382
soul-api/internal/handler/gift_pay.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/wechat"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const giftPayExpireHours = 24
|
||||
|
||||
// GiftPayCreate POST /api/miniprogram/gift-pay/create 创建代付请求
|
||||
func GiftPayCreate(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
ProductType string `json:"productType" binding:"required"`
|
||||
ProductID string `json:"productId"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
// 校验发起人
|
||||
var initiator model.User
|
||||
if err := db.Where("id = ?", req.UserID).First(&initiator).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 价格与商品校验
|
||||
productID := req.ProductID
|
||||
if productID == "" {
|
||||
switch req.ProductType {
|
||||
case "vip":
|
||||
productID = "vip_annual"
|
||||
case "match":
|
||||
productID = "match"
|
||||
case "fullbook":
|
||||
productID = "fullbook"
|
||||
}
|
||||
}
|
||||
amount, priceErr := getStandardPrice(db, req.ProductType, productID)
|
||||
if priceErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": priceErr.Error()})
|
||||
return
|
||||
}
|
||||
// 发起人若有推荐人绑定,享受好友优惠
|
||||
var referrerID *string
|
||||
var binding struct {
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
}
|
||||
if err := db.Raw(`
|
||||
SELECT referrer_id FROM referral_bindings
|
||||
WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW()
|
||||
ORDER BY binding_date DESC LIMIT 1
|
||||
`, req.UserID).Scan(&binding).Error; err == nil && binding.ReferrerID != "" {
|
||||
referrerID = &binding.ReferrerID
|
||||
var cfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil {
|
||||
var config map[string]interface{}
|
||||
if json.Unmarshal(cfg.ConfigValue, &config) == nil {
|
||||
if userDiscount, ok := config["userDiscount"].(float64); ok && userDiscount > 0 {
|
||||
amount = amount * (1 - userDiscount/100)
|
||||
if amount < 0.01 {
|
||||
amount = 0.01
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = referrerID // 分佣在 PayNotify 时按发起人计算
|
||||
|
||||
// 校验发起人是否已拥有
|
||||
if req.ProductType == "section" && productID != "" {
|
||||
var cnt int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND product_type = ? AND product_id = ? AND status IN ?",
|
||||
req.UserID, "section", productID, []string{"paid", "completed"}).Count(&cnt)
|
||||
if cnt > 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "您已拥有该章节"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.ProductType == "fullbook" || req.ProductType == "vip" {
|
||||
var u model.User
|
||||
db.Where("id = ?", req.UserID).Select("has_full_book", "is_vip", "vip_expire_date").First(&u)
|
||||
if u.HasFullBook != nil && *u.HasFullBook {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "您已拥有全书"})
|
||||
return
|
||||
}
|
||||
if req.ProductType == "vip" && u.IsVip != nil && *u.IsVip && u.VipExpireDate != nil && u.VipExpireDate.After(time.Now()) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "您已是有效VIP"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 描述
|
||||
desc := ""
|
||||
switch req.ProductType {
|
||||
case "fullbook":
|
||||
desc = "《一场Soul的创业实验》全书"
|
||||
case "vip":
|
||||
desc = "卡若创业派对VIP年度会员(365天)"
|
||||
case "match":
|
||||
desc = "购买匹配次数"
|
||||
case "section":
|
||||
var ch model.Chapter
|
||||
if err := db.Select("section_title").Where("id = ?", productID).First(&ch).Error; err == nil && ch.SectionTitle != "" {
|
||||
desc = ch.SectionTitle
|
||||
} else {
|
||||
desc = fmt.Sprintf("章节-%s", productID)
|
||||
}
|
||||
default:
|
||||
desc = fmt.Sprintf("%s-%s", req.ProductType, productID)
|
||||
}
|
||||
|
||||
expireAt := time.Now().Add(giftPayExpireHours * time.Hour)
|
||||
requestSN := "GPR" + wechat.GenerateOrderSn()
|
||||
id := "gpr_" + fmt.Sprintf("%d", time.Now().UnixNano()%100000000000)
|
||||
|
||||
gpr := model.GiftPayRequest{
|
||||
ID: id,
|
||||
RequestSN: requestSN,
|
||||
InitiatorUserID: req.UserID,
|
||||
ProductType: req.ProductType,
|
||||
ProductID: productID,
|
||||
Amount: amount,
|
||||
Description: desc,
|
||||
Status: "pending",
|
||||
ExpireAt: expireAt,
|
||||
}
|
||||
if err := db.Create(&gpr).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建失败"})
|
||||
return
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("pages/gift-pay/detail?requestSn=%s", requestSN)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"requestSn": requestSN,
|
||||
"path": path,
|
||||
"amount": amount,
|
||||
"expireAt": expireAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// GiftPayDetail GET /api/miniprogram/gift-pay/detail?requestSn=xxx 代付详情(代付人用)
|
||||
func GiftPayDetail(c *gin.Context) {
|
||||
requestSn := strings.TrimSpace(c.Query("requestSn"))
|
||||
if requestSn == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少代付请求号"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
var gpr model.GiftPayRequest
|
||||
if err := db.Where("request_sn = ?", requestSn).First(&gpr).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付请求不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if gpr.Status != "pending" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "该代付已处理"})
|
||||
return
|
||||
}
|
||||
if time.Now().After(gpr.ExpireAt) {
|
||||
db.Model(&gpr).Update("status", "expired")
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付已过期"})
|
||||
return
|
||||
}
|
||||
|
||||
// 发起人昵称(脱敏)
|
||||
var initiator model.User
|
||||
nickname := "好友"
|
||||
if err := db.Where("id = ?", gpr.InitiatorUserID).Select("nickname").First(&initiator).Error; err == nil && initiator.Nickname != nil {
|
||||
n := *initiator.Nickname
|
||||
if len(n) > 2 {
|
||||
n = string([]rune(n)[0]) + "**"
|
||||
}
|
||||
nickname = n
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"requestSn": gpr.RequestSN,
|
||||
"productType": gpr.ProductType,
|
||||
"productId": gpr.ProductID,
|
||||
"amount": gpr.Amount,
|
||||
"description": gpr.Description,
|
||||
"initiatorNickname": nickname,
|
||||
"expireAt": gpr.ExpireAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// GiftPayPay POST /api/miniprogram/gift-pay/pay 代付人发起支付
|
||||
func GiftPayPay(c *gin.Context) {
|
||||
var req struct {
|
||||
RequestSn string `json:"requestSn" binding:"required"`
|
||||
OpenID string `json:"openId" binding:"required"`
|
||||
UserID string `json:"userId"` // 代付人ID,用于校验不能自己付
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
var gpr model.GiftPayRequest
|
||||
if err := db.Where("request_sn = ? AND status = ?", req.RequestSn, "pending").First(&gpr).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付请求不存在或已处理"})
|
||||
return
|
||||
}
|
||||
if time.Now().After(gpr.ExpireAt) {
|
||||
db.Model(&gpr).Update("status", "expired")
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付已过期"})
|
||||
return
|
||||
}
|
||||
|
||||
// 不能自己给自己代付
|
||||
if req.UserID != "" && req.UserID == gpr.InitiatorUserID {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不能为自己代付"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取代付人信息
|
||||
var payer model.User
|
||||
if err := db.Where("open_id = ?", req.OpenID).First(&payer).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请先登录"})
|
||||
return
|
||||
}
|
||||
if payer.ID == gpr.InitiatorUserID {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不能为自己代付"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建订单(归属发起人,记录代付信息)
|
||||
orderSn := wechat.GenerateOrderSn()
|
||||
status := "created"
|
||||
pm := "wechat"
|
||||
productID := gpr.ProductID
|
||||
desc := gpr.Description
|
||||
gprID := gpr.ID
|
||||
payerID := payer.ID
|
||||
order := model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: gpr.InitiatorUserID,
|
||||
OpenID: req.OpenID,
|
||||
ProductType: gpr.ProductType,
|
||||
ProductID: &productID,
|
||||
Amount: gpr.Amount,
|
||||
Description: &desc,
|
||||
Status: &status,
|
||||
PaymentMethod: &pm,
|
||||
GiftPayRequestID: &gprID,
|
||||
PayerUserID: &payerID,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建订单失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 唤起微信支付,attach 中 userId=发起人,giftPayRequestSn=请求号
|
||||
attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s","giftPayRequestSn":"%s"}`,
|
||||
gpr.ProductType, gpr.ProductID, gpr.InitiatorUserID, gpr.RequestSN)
|
||||
totalFee := int(gpr.Amount * 100)
|
||||
ctx := c.Request.Context()
|
||||
prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, "代付-"+gpr.Description, attach)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": fmt.Sprintf("支付请求失败: %v", err)})
|
||||
return
|
||||
}
|
||||
payParams, err := wechat.GetJSAPIPayParams(prepayID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成支付参数失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 预占:更新请求状态为 paying(可选,防并发)
|
||||
// 简化:不预占,PayNotify 时再更新
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"orderSn": orderSn,
|
||||
"prepayId": prepayID,
|
||||
"payParams": payParams,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GiftPayCancel POST /api/miniprogram/gift-pay/cancel 发起人取消
|
||||
func GiftPayCancel(c *gin.Context) {
|
||||
var req struct {
|
||||
RequestSn string `json:"requestSn" binding:"required"`
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
var gpr model.GiftPayRequest
|
||||
if err := db.Where("request_sn = ?", req.RequestSn).First(&gpr).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付请求不存在"})
|
||||
return
|
||||
}
|
||||
if gpr.InitiatorUserID != req.UserID {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无权取消"})
|
||||
return
|
||||
}
|
||||
if gpr.Status != "pending" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "该代付已处理"})
|
||||
return
|
||||
}
|
||||
|
||||
db.Model(&gpr).Update("status", "cancelled")
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已取消"})
|
||||
}
|
||||
|
||||
// GiftPayMyRequests GET /api/miniprogram/gift-pay/my-requests?userId= 我发起的
|
||||
func GiftPayMyRequests(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少userId"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
var list []model.GiftPayRequest
|
||||
db.Where("initiator_user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&list)
|
||||
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for _, r := range list {
|
||||
out = append(out, gin.H{
|
||||
"requestSn": r.RequestSN,
|
||||
"productType": r.ProductType,
|
||||
"productId": r.ProductID,
|
||||
"amount": r.Amount,
|
||||
"description": r.Description,
|
||||
"status": r.Status,
|
||||
"expireAt": r.ExpireAt.Format(time.RFC3339),
|
||||
"createdAt": r.CreatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "list": out})
|
||||
}
|
||||
|
||||
// GiftPayMyPayments GET /api/miniprogram/gift-pay/my-payments?userId= 我帮付的
|
||||
func GiftPayMyPayments(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少userId"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
var list []model.GiftPayRequest
|
||||
db.Where("payer_user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&list)
|
||||
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for _, r := range list {
|
||||
out = append(out, gin.H{
|
||||
"requestSn": r.RequestSN,
|
||||
"productType": r.ProductType,
|
||||
"amount": r.Amount,
|
||||
"description": r.Description,
|
||||
"status": r.Status,
|
||||
"createdAt": r.CreatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "list": out})
|
||||
}
|
||||
@@ -302,66 +302,81 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
|
||||
db := database.DB()
|
||||
|
||||
// -------- V1.1 后端价格:从 DB 读取标准价,客户端传值仅用于日志对比,实际以后端计算为准 --------
|
||||
standardPrice, priceErr := getStandardPrice(db, req.ProductType, req.ProductID)
|
||||
if priceErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": priceErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询用户的有效推荐人(先查 binding,再查 referralCode)
|
||||
var finalAmount float64
|
||||
var orderSn string
|
||||
var referrerID *string
|
||||
if req.UserID != "" {
|
||||
var binding struct {
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
}
|
||||
err := db.Raw(`
|
||||
SELECT referrer_id
|
||||
FROM referral_bindings
|
||||
WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW()
|
||||
ORDER BY binding_date DESC
|
||||
LIMIT 1
|
||||
`, req.UserID).Scan(&binding).Error
|
||||
if err == nil && binding.ReferrerID != "" {
|
||||
referrerID = &binding.ReferrerID
|
||||
}
|
||||
}
|
||||
if referrerID == nil && req.ReferralCode != "" {
|
||||
var refUser model.User
|
||||
if err := db.Where("referral_code = ?", req.ReferralCode).First(&refUser).Error; err == nil {
|
||||
referrerID = &refUser.ID
|
||||
}
|
||||
}
|
||||
|
||||
// 有推荐人时应用好友优惠,以后端标准价为基准计算最终金额,忽略客户端传值
|
||||
finalAmount := standardPrice
|
||||
if referrerID != nil {
|
||||
var cfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil {
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal(cfg.ConfigValue, &config); err == nil {
|
||||
if userDiscount, ok := config["userDiscount"].(float64); ok && userDiscount > 0 {
|
||||
discountRate := userDiscount / 100
|
||||
finalAmount = standardPrice * (1 - discountRate)
|
||||
if finalAmount < 0.01 {
|
||||
finalAmount = 0.01
|
||||
if req.ProductType == "balance_recharge" {
|
||||
// 充值:从已创建的订单取金额,productId=orderSn
|
||||
var existOrder model.Order
|
||||
if err := db.Where("order_sn = ? AND product_type = ? AND status = ?", req.ProductID, "balance_recharge", "created").First(&existOrder).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "充值订单不存在或已支付"})
|
||||
return
|
||||
}
|
||||
orderSn = existOrder.OrderSN
|
||||
finalAmount = existOrder.Amount
|
||||
if req.UserID != "" && existOrder.UserID != req.UserID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "订单用户不匹配"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// -------- V1.1 后端价格:从 DB 读取标准价 --------
|
||||
standardPrice, priceErr := getStandardPrice(db, req.ProductType, req.ProductID)
|
||||
if priceErr != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": priceErr.Error()})
|
||||
return
|
||||
}
|
||||
finalAmount = standardPrice
|
||||
|
||||
if req.UserID != "" {
|
||||
var binding struct {
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
}
|
||||
err := db.Raw(`
|
||||
SELECT referrer_id
|
||||
FROM referral_bindings
|
||||
WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW()
|
||||
ORDER BY binding_date DESC
|
||||
LIMIT 1
|
||||
`, req.UserID).Scan(&binding).Error
|
||||
if err == nil && binding.ReferrerID != "" {
|
||||
referrerID = &binding.ReferrerID
|
||||
}
|
||||
}
|
||||
if referrerID == nil && req.ReferralCode != "" {
|
||||
var refUser model.User
|
||||
if err := db.Where("referral_code = ?", req.ReferralCode).First(&refUser).Error; err == nil {
|
||||
referrerID = &refUser.ID
|
||||
}
|
||||
}
|
||||
if referrerID != nil {
|
||||
var cfg model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil {
|
||||
var config map[string]interface{}
|
||||
if err := json.Unmarshal(cfg.ConfigValue, &config); err == nil {
|
||||
if userDiscount, ok := config["userDiscount"].(float64); ok && userDiscount > 0 {
|
||||
discountRate := userDiscount / 100
|
||||
finalAmount = finalAmount * (1 - discountRate)
|
||||
if finalAmount < 0.01 {
|
||||
finalAmount = 0.01
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 记录客户端与后端金额差异(仅日志,不拦截)
|
||||
if req.Amount-finalAmount > 0.05 || finalAmount-req.Amount > 0.05 {
|
||||
fmt.Printf("[PayCreate] 金额差异: 客户端=%.2f 后端=%.2f productType=%s productId=%s userId=%s\n",
|
||||
req.Amount, finalAmount, req.ProductType, req.ProductID, req.UserID)
|
||||
if req.Amount-finalAmount > 0.05 || finalAmount-req.Amount > 0.05 {
|
||||
fmt.Printf("[PayCreate] 金额差异: 客户端=%.2f 后端=%.2f productType=%s productId=%s userId=%s\n",
|
||||
req.Amount, finalAmount, req.ProductType, req.ProductID, req.UserID)
|
||||
}
|
||||
orderSn = wechat.GenerateOrderSn()
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
orderSn := wechat.GenerateOrderSn()
|
||||
totalFee := int(finalAmount * 100) // 转为分
|
||||
description := req.Description
|
||||
if description == "" {
|
||||
if req.ProductType == "fullbook" {
|
||||
if req.ProductType == "balance_recharge" {
|
||||
description = fmt.Sprintf("余额充值 ¥%.2f", finalAmount)
|
||||
} else if req.ProductType == "fullbook" {
|
||||
description = "《一场Soul的创业实验》全书"
|
||||
} else if req.ProductType == "vip" {
|
||||
description = "卡若创业派对VIP年度会员(365天)"
|
||||
@@ -378,7 +393,6 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
clientIP = "127.0.0.1"
|
||||
}
|
||||
|
||||
// 插入订单到数据库
|
||||
userID := req.UserID
|
||||
if userID == "" {
|
||||
userID = req.OpenID
|
||||
@@ -396,24 +410,27 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
status := "created"
|
||||
order := model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: userID,
|
||||
OpenID: req.OpenID,
|
||||
ProductType: req.ProductType,
|
||||
ProductID: &productID,
|
||||
Amount: finalAmount,
|
||||
Description: &description,
|
||||
Status: &status,
|
||||
ReferrerID: referrerID,
|
||||
ReferralCode: &req.ReferralCode,
|
||||
}
|
||||
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
// 订单创建失败,但不中断支付流程
|
||||
fmt.Printf("[MiniprogramPay] 插入订单失败: %v\n", err)
|
||||
// 充值订单已存在,不重复创建
|
||||
if req.ProductType != "balance_recharge" {
|
||||
status := "created"
|
||||
pm := "wechat"
|
||||
order := model.Order{
|
||||
ID: orderSn,
|
||||
OrderSN: orderSn,
|
||||
UserID: userID,
|
||||
OpenID: req.OpenID,
|
||||
ProductType: req.ProductType,
|
||||
ProductID: &productID,
|
||||
Amount: finalAmount,
|
||||
Description: &description,
|
||||
Status: &status,
|
||||
ReferrerID: referrerID,
|
||||
ReferralCode: &req.ReferralCode,
|
||||
PaymentMethod: &pm,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
fmt.Printf("[MiniprogramPay] 插入订单失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID)
|
||||
@@ -479,7 +496,7 @@ func miniprogramPayGet(c *gin.Context) {
|
||||
orderPollLogf("主动同步订单已支付: %s", orderSn)
|
||||
// 激活权益
|
||||
if order.UserID != "" {
|
||||
activateOrderBenefits(db, order.UserID, order.ProductType, now)
|
||||
activateOrderBenefits(db, &order, now)
|
||||
}
|
||||
}
|
||||
case "CLOSED", "REVOKED", "PAYERROR":
|
||||
@@ -506,9 +523,10 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
fmt.Printf("[PayNotify] 支付成功: orderSn=%s, transactionId=%s, amount=%.2f\n", orderSn, transactionID, totalAmount)
|
||||
|
||||
var attach struct {
|
||||
ProductType string `json:"productType"`
|
||||
ProductID string `json:"productId"`
|
||||
UserID string `json:"userId"`
|
||||
ProductType string `json:"productType"`
|
||||
ProductID string `json:"productId"`
|
||||
UserID string `json:"userId"`
|
||||
GiftPayRequestSn string `json:"giftPayRequestSn"`
|
||||
}
|
||||
if attachStr != "" {
|
||||
_ = json.Unmarshal([]byte(attachStr), &attach)
|
||||
@@ -564,11 +582,12 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
} else if *order.Status != "paid" {
|
||||
status := "paid"
|
||||
now := time.Now()
|
||||
if err := db.Model(&order).Updates(map[string]interface{}{
|
||||
updates := map[string]interface{}{
|
||||
"status": status,
|
||||
"transaction_id": transactionID,
|
||||
"pay_time": now,
|
||||
}).Error; err != nil {
|
||||
}
|
||||
if err := db.Model(&order).Updates(updates).Error; err != nil {
|
||||
fmt.Printf("[PayNotify] 更新订单状态失败: %s, err=%v\n", orderSn, err)
|
||||
return fmt.Errorf("update order: %w", err)
|
||||
}
|
||||
@@ -577,6 +596,25 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn)
|
||||
}
|
||||
|
||||
// 代付订单:更新 gift_pay_request、订单 payer_user_id
|
||||
if attach.GiftPayRequestSn != "" {
|
||||
var payerUserID string
|
||||
if openID != "" {
|
||||
var payer model.User
|
||||
if err := db.Where("open_id = ?", openID).First(&payer).Error; err == nil {
|
||||
payerUserID = payer.ID
|
||||
db.Model(&order).Update("payer_user_id", payerUserID)
|
||||
}
|
||||
}
|
||||
db.Model(&model.GiftPayRequest{}).Where("request_sn = ?", attach.GiftPayRequestSn).
|
||||
Updates(map[string]interface{}{
|
||||
"status": "paid",
|
||||
"payer_user_id": payerUserID,
|
||||
"order_id": orderSn,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
if buyerUserID != "" && attach.ProductType != "" {
|
||||
if attach.ProductType == "fullbook" {
|
||||
db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true)
|
||||
@@ -591,6 +629,12 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
fmt.Printf("[VIP] 设置方式=支付设置, userId=%s, orderSn=%s, 过期日=%s, activatedAt=%s\n", buyerUserID, orderSn, expireDate.Format("2006-01-02"), vipActivatedAt.Format("2006-01-02 15:04:05"))
|
||||
} else if attach.ProductType == "match" {
|
||||
fmt.Printf("[PayNotify] 用户购买匹配次数: %s,订单 %s\n", buyerUserID, orderSn)
|
||||
} else if attach.ProductType == "balance_recharge" {
|
||||
if err := ConfirmBalanceRechargeByOrder(db, &order); err != nil {
|
||||
fmt.Printf("[PayNotify] 余额充值确认失败: %s, err=%v\n", orderSn, err)
|
||||
} else {
|
||||
fmt.Printf("[PayNotify] 余额充值成功: %s, 金额 %.2f\n", buyerUserID, totalAmount)
|
||||
}
|
||||
} else if attach.ProductType == "section" && attach.ProductID != "" {
|
||||
var count int64
|
||||
db.Model(&model.Order{}).Where(
|
||||
@@ -827,6 +871,45 @@ func MiniprogramQrcodeImage(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "image/png", imageData)
|
||||
}
|
||||
|
||||
// GiftLinkGet GET /api/miniprogram/gift/link 代付链接(需登录,传 userId)
|
||||
// 返回 path、ref、scene,供 gift-link 页展示与复制;qrcodeImageUrl 供生成小程序码
|
||||
func GiftLinkGet(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId,请先登录"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var user model.User
|
||||
if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
ref := getStringValue(user.ReferralCode)
|
||||
if ref == "" {
|
||||
suffix := userID
|
||||
if len(userID) >= 6 {
|
||||
suffix = userID[len(userID)-6:]
|
||||
}
|
||||
ref = "SOUL" + strings.ToUpper(suffix)
|
||||
}
|
||||
path := fmt.Sprintf("pages/gift-link/gift-link?ref=%s&gift=1", ref)
|
||||
scene := fmt.Sprintf("ref_%s_gift_1", strings.ReplaceAll(ref, "&", "_"))
|
||||
if len(scene) > 32 {
|
||||
scene = scene[:32]
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"path": path,
|
||||
"ref": ref,
|
||||
"scene": scene,
|
||||
})
|
||||
}
|
||||
|
||||
// base64 编码
|
||||
func base64Encode(data []byte) string {
|
||||
const base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
@@ -953,13 +1036,20 @@ func activateVIP(db *gorm.DB, userID string, days int, activatedAt time.Time) ti
|
||||
return expireDate
|
||||
}
|
||||
|
||||
// activateOrderBenefits 订单支付成功后激活对应权益(VIP / 全书)
|
||||
func activateOrderBenefits(db *gorm.DB, userID, productType string, payTime time.Time) {
|
||||
// activateOrderBenefits 订单支付成功后激活对应权益(VIP / 全书 / 余额充值)
|
||||
func activateOrderBenefits(db *gorm.DB, order *model.Order, payTime time.Time) {
|
||||
if order == nil {
|
||||
return
|
||||
}
|
||||
userID := order.UserID
|
||||
productType := order.ProductType
|
||||
switch productType {
|
||||
case "fullbook":
|
||||
db.Model(&model.User{}).Where("id = ?", userID).Update("has_full_book", true)
|
||||
case "vip":
|
||||
activateVIP(db, userID, 365, payTime)
|
||||
case "balance_recharge":
|
||||
ConfirmBalanceRechargeByOrder(db, order)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func OrdersList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 收集订单中的 user_id、referrer_id,查用户信息
|
||||
// 收集订单中的 user_id、referrer_id、payer_user_id(代付人),查用户信息
|
||||
userIDs := make(map[string]bool)
|
||||
for _, o := range orders {
|
||||
if o.UserID != "" {
|
||||
@@ -112,6 +112,9 @@ func OrdersList(c *gin.Context) {
|
||||
if o.ReferrerID != nil && *o.ReferrerID != "" {
|
||||
userIDs[*o.ReferrerID] = true
|
||||
}
|
||||
if o.PayerUserID != nil && *o.PayerUserID != "" {
|
||||
userIDs[*o.PayerUserID] = true
|
||||
}
|
||||
}
|
||||
ids := make([]string, 0, len(userIDs))
|
||||
for id := range userIDs {
|
||||
@@ -156,6 +159,14 @@ func OrdersList(c *gin.Context) {
|
||||
m["referrerCode"] = getStr(u.ReferralCode)
|
||||
}
|
||||
}
|
||||
// 代付人信息(实际付款人)
|
||||
if o.PayerUserID != nil && *o.PayerUserID != "" {
|
||||
if u := userMap[*o.PayerUserID]; u != nil {
|
||||
m["payerNickname"] = getStr(u.Nickname)
|
||||
} else {
|
||||
m["payerNickname"] = ""
|
||||
}
|
||||
}
|
||||
// 分销佣金:仅对已支付且存在推荐人的订单,按 computeOrderCommission(会员 20%/10%,内容 90%)
|
||||
status := getStr(o.Status)
|
||||
if status == "paid" && o.ReferrerID != nil && *o.ReferrerID != "" {
|
||||
|
||||
@@ -626,6 +626,45 @@ func UserTrackPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "trackId": trackID, "message": "行为记录成功"})
|
||||
}
|
||||
|
||||
// MiniprogramTrackPost POST /api/miniprogram/track 小程序埋点(userId 可选,支持匿名)
|
||||
func MiniprogramTrackPost(c *gin.Context) {
|
||||
var body struct {
|
||||
UserID string `json:"userId"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
ExtraData interface{} `json:"extraData"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
if body.Action == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "行为类型不能为空"})
|
||||
return
|
||||
}
|
||||
userId := body.UserID
|
||||
if userId == "" {
|
||||
userId = "anonymous"
|
||||
}
|
||||
db := database.DB()
|
||||
trackID := fmt.Sprintf("track_%d", time.Now().UnixNano()%100000000)
|
||||
chID := body.Target
|
||||
if body.Action == "view_chapter" && body.Target != "" {
|
||||
chID = body.Target
|
||||
}
|
||||
t := model.UserTrack{
|
||||
ID: trackID, UserID: userId, Action: body.Action, Target: &body.Target,
|
||||
}
|
||||
if body.Target != "" {
|
||||
t.ChapterID = &chID
|
||||
}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "trackId": trackID, "message": "记录成功"})
|
||||
}
|
||||
|
||||
// UserUpdate POST /api/user/update 更新昵称、头像、手机、微信号等
|
||||
func UserUpdate(c *gin.Context) {
|
||||
var body struct {
|
||||
|
||||
Reference in New Issue
Block a user