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:
Alex-larget
2026-03-17 11:44:36 +08:00
parent b971420090
commit 0d12ab1d07
65 changed files with 3836 additions and 180 deletions

View File

@@ -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)
}
}