feat: 同步本地三端改动并清理上传凭证风险
整合小程序、管理端与后端的最新本地改动,补齐用户管理与首页入口相关能力;提交前已完成敏感信息扫描,并移除本地 gitea 远程 URL 中的明文凭证,避免隐私信息进入远程仓库。 Made-with: Cursor
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -26,6 +27,40 @@ var (
|
||||
orderPollLoggerOnce sync.Once
|
||||
)
|
||||
|
||||
// normalizePayProductType 清洗 productType,避免不可见字符导致未命中 link_karuo_tip 分支
|
||||
func normalizePayProductType(raw string) string {
|
||||
s := strings.TrimSpace(raw)
|
||||
s = strings.ReplaceAll(s, "\ufeff", "")
|
||||
s = strings.ReplaceAll(s, "\u00a0", "")
|
||||
s = strings.ReplaceAll(s, "\u200b", "")
|
||||
s = strings.ReplaceAll(s, "\u200c", "")
|
||||
s = strings.ReplaceAll(s, "\u200d", "")
|
||||
s = strings.ReplaceAll(s, "\uff3f", "_")
|
||||
s = strings.ReplaceAll(s, "-", "_")
|
||||
s = strings.ToLower(s)
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
out := b.String()
|
||||
if strings.ReplaceAll(out, "_", "") == "linkkaruotip" {
|
||||
return "link_karuo_tip"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// isLinkKaruoTipProductType 打赏/上麦商品类型(兼容 normalize 漏网、同形连接符等)
|
||||
func isLinkKaruoTipProductType(s string) bool {
|
||||
s = strings.TrimSpace(strings.ToLower(normalizePayProductType(s)))
|
||||
if s == "link_karuo_tip" {
|
||||
return true
|
||||
}
|
||||
compact := strings.ReplaceAll(strings.ReplaceAll(s, "_", ""), "-", "")
|
||||
return compact == "linkkaruotip"
|
||||
}
|
||||
|
||||
// orderPollLogf 将订单轮询检测日志写入 log/order-poll.log,不输出到控制台
|
||||
func orderPollLogf(format string, args ...interface{}) {
|
||||
orderPollLoggerOnce.Do(func() {
|
||||
@@ -377,6 +412,7 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
Description string `json:"description"`
|
||||
UserID string `json:"userId"`
|
||||
ReferralCode string `json:"referralCode"`
|
||||
TipSource string `json:"tipSource"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -384,6 +420,11 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
req.ProductType = normalizePayProductType(req.ProductType)
|
||||
if isLinkKaruoTipProductType(req.ProductType) {
|
||||
req.ProductType = "link_karuo_tip"
|
||||
}
|
||||
|
||||
if req.Amount <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "支付金额无效"})
|
||||
return
|
||||
@@ -391,6 +432,11 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
|
||||
db := database.DB()
|
||||
|
||||
productID := strings.TrimSpace(req.ProductID)
|
||||
if req.ProductType == "link_karuo_tip" && strings.TrimSpace(req.TipSource) == "live_mic" && productID != "" && !strings.HasPrefix(productID, "live_mic|") {
|
||||
productID = "live_mic|" + productID
|
||||
}
|
||||
|
||||
var finalAmount float64
|
||||
var orderSn string
|
||||
var referrerID *string
|
||||
@@ -410,44 +456,66 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
}
|
||||
} else {
|
||||
// -------- V1.1 后端价格:从 DB 读取标准价 --------
|
||||
standardPrice, priceErr := getStandardPrice(db, req.ProductType, req.ProductID)
|
||||
pricePID := productID
|
||||
if pricePID == "" {
|
||||
pricePID = strings.TrimSpace(req.ProductID)
|
||||
}
|
||||
standardPrice, priceErr := getStandardPrice(db, req.ProductType, pricePID)
|
||||
if priceErr != nil && req.ProductType == "link_karuo_tip" {
|
||||
if fb, _, canonID, ferr := linkKaruoTipFallbackByAmount(db, pricePID, req.Amount); ferr == nil {
|
||||
standardPrice = fb
|
||||
priceErr = nil
|
||||
_, qty, qe := parseLinkKaruoGiftQty(pricePID)
|
||||
if qe != nil {
|
||||
qty = 1
|
||||
}
|
||||
if strings.TrimSpace(req.TipSource) == "live_mic" {
|
||||
productID = "live_mic|" + canonID + "|" + strconv.FormatInt(qty, 10)
|
||||
} else {
|
||||
productID = canonID + "|" + strconv.FormatInt(qty, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
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(`
|
||||
// 打赏不参与分销好友折扣(实付=标准价)
|
||||
if req.ProductType != "link_karuo_tip" {
|
||||
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 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 && 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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,26 +523,12 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
}
|
||||
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)
|
||||
req.Amount, finalAmount, req.ProductType, pricePID, req.UserID)
|
||||
}
|
||||
orderSn = wechat.GenerateOrderSn()
|
||||
}
|
||||
|
||||
totalFee := int(finalAmount * 100) // 转为分
|
||||
description := req.Description
|
||||
if description == "" {
|
||||
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天)"
|
||||
} else if req.ProductType == "match" {
|
||||
description = "购买匹配次数"
|
||||
} else {
|
||||
description = fmt.Sprintf("章节购买-%s", req.ProductID)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取客户端 IP
|
||||
clientIP := c.ClientIP()
|
||||
@@ -489,21 +543,64 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
if err := db.Where("open_id = ?", req.OpenID).First(&u).Error; err == nil {
|
||||
userID = u.ID
|
||||
} else {
|
||||
// 查不到用户:可能是未登录或软删除后未重新登录,避免用 openid 导致订单归属到旧账号
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请先登录后再支付"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
productID := req.ProductID
|
||||
if productID == "" {
|
||||
if req.ProductType != "balance_recharge" {
|
||||
if productID == "" {
|
||||
switch req.ProductType {
|
||||
case "vip":
|
||||
productID = "vip_annual"
|
||||
case "match":
|
||||
productID = "match"
|
||||
case "link_karuo_tip":
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "打赏缺少礼物标识"})
|
||||
return
|
||||
default:
|
||||
productID = "fullbook"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description := strings.TrimSpace(req.Description)
|
||||
if description == "" {
|
||||
switch req.ProductType {
|
||||
case "balance_recharge":
|
||||
description = fmt.Sprintf("余额充值 ¥%.2f", finalAmount)
|
||||
case "fullbook":
|
||||
description = "《一场Soul的创业实验》全书"
|
||||
case "vip":
|
||||
productID = "vip_annual"
|
||||
description = "卡若创业派对VIP年度会员(365天)"
|
||||
case "match":
|
||||
productID = "match"
|
||||
description = "购买匹配次数"
|
||||
case "link_karuo_tip":
|
||||
root := loadMpConfigRoot(db)
|
||||
giftID := productID
|
||||
if strings.HasPrefix(giftID, "live_mic|") {
|
||||
parts := strings.Split(giftID, "|")
|
||||
if len(parts) >= 2 {
|
||||
giftID = strings.TrimSpace(parts[1])
|
||||
}
|
||||
} else {
|
||||
parts := strings.Split(giftID, "|")
|
||||
giftID = strings.TrimSpace(parts[0])
|
||||
}
|
||||
g, _ := findLinkKaruoGift(root, giftID)
|
||||
name := "打赏"
|
||||
if g != nil {
|
||||
if n, ok := g["name"].(string); ok && strings.TrimSpace(n) != "" {
|
||||
name = strings.TrimSpace(n)
|
||||
}
|
||||
}
|
||||
if strings.Contains(productID, "live_mic|") {
|
||||
description = "上麦礼遇 · " + name
|
||||
} else {
|
||||
description = "首页打赏 · " + name
|
||||
}
|
||||
default:
|
||||
productID = "fullbook"
|
||||
description = fmt.Sprintf("章节购买-%s", productID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +627,29 @@ func miniprogramPayPost(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID)
|
||||
type payAttach struct {
|
||||
ProductType string `json:"pt"`
|
||||
ProductID string `json:"pid,omitempty"`
|
||||
UserID string `json:"uid,omitempty"`
|
||||
TipSource string `json:"ts,omitempty"`
|
||||
}
|
||||
attachBytes, _ := json.Marshal(payAttach{
|
||||
ProductType: req.ProductType,
|
||||
ProductID: productID,
|
||||
UserID: userID,
|
||||
TipSource: strings.TrimSpace(req.TipSource),
|
||||
})
|
||||
attach := string(attachBytes)
|
||||
if len(attach) > 128 {
|
||||
shortAttach, _ := json.Marshal(payAttach{
|
||||
ProductType: req.ProductType,
|
||||
UserID: userID,
|
||||
})
|
||||
attach = string(shortAttach)
|
||||
if len(attach) > 128 {
|
||||
attach = fmt.Sprintf(`{"pt":"%s"}`, req.ProductType)
|
||||
}
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, description, attach)
|
||||
if err != nil {
|
||||
@@ -623,11 +742,13 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
ProductType string `json:"productType"`
|
||||
ProductID string `json:"productId"`
|
||||
UserID string `json:"userId"`
|
||||
TipSource string `json:"tipSource"`
|
||||
GiftPayRequestSn string `json:"giftPayRequestSn"`
|
||||
GiftPayInitiatorPay bool `json:"giftPayInitiatorPay"`
|
||||
PT string `json:"pt"`
|
||||
PID string `json:"pid"`
|
||||
UID string `json:"uid"`
|
||||
TS string `json:"ts"`
|
||||
SN string `json:"sn"`
|
||||
IP int `json:"ip"`
|
||||
}
|
||||
@@ -646,6 +767,9 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
if attach.UserID == "" {
|
||||
attach.UserID = attach.UID
|
||||
}
|
||||
if attach.TipSource == "" {
|
||||
attach.TipSource = attach.TS
|
||||
}
|
||||
if attach.GiftPayRequestSn == "" {
|
||||
attach.GiftPayRequestSn = attach.SN
|
||||
}
|
||||
@@ -787,6 +911,8 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
} else {
|
||||
fmt.Printf("[PayNotify] 用户已有该章节的其他已支付订单: %s - %s\n", beneficiaryUserID, attach.ProductID)
|
||||
}
|
||||
} else if attach.ProductType == "link_karuo_tip" {
|
||||
fmt.Printf("[PayNotify] 打赏/上麦礼遇订单完成: user=%s order=%s\n", beneficiaryUserID, orderSn)
|
||||
}
|
||||
productID := attach.ProductID
|
||||
if productID == "" {
|
||||
@@ -824,6 +950,10 @@ func MiniprogramPayNotify(c *gin.Context) {
|
||||
|
||||
// 处理分销佣金(会员订单 20%/10%,内容订单 90%)
|
||||
func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64, orderSn string, order *model.Order) {
|
||||
if order != nil && order.ProductType == "link_karuo_tip" {
|
||||
fmt.Printf("[PayNotify] 打赏订单跳过分销佣金: orderSn=%s\n", orderSn)
|
||||
return
|
||||
}
|
||||
type Binding struct {
|
||||
ID int `gorm:"column:id"`
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
@@ -1203,13 +1333,241 @@ func activateOrderBenefits(db *gorm.DB, order *model.Order, payTime time.Time) {
|
||||
}
|
||||
case "balance_recharge":
|
||||
ConfirmBalanceRechargeByOrder(db, order)
|
||||
case "link_karuo_tip":
|
||||
// 首页打赏 / 上麦礼遇:仅收款,无额外会员或章节权益
|
||||
}
|
||||
}
|
||||
|
||||
// loadMpConfigRoot 读取 system_config.mp_config 原始 JSON(用于礼物价、上麦 Webhook 等)
|
||||
func loadMpConfigRoot(db *gorm.DB) map[string]interface{} {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "mp_config").First(&row).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(row.ConfigValue, &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func parseMoneyAny(v interface{}) float64 {
|
||||
if v == nil {
|
||||
return 0
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return t
|
||||
case int:
|
||||
return float64(t)
|
||||
case int64:
|
||||
return float64(t)
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
case string:
|
||||
f, err := strconv.ParseFloat(strings.TrimSpace(t), 64)
|
||||
if err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func defaultLinkKaruoGifts() []map[string]interface{} {
|
||||
return []map[string]interface{}{
|
||||
{"id": "gift_star", "name": "人气票", "price": 99, "emoji": "🎫", "icon": "", "micOrder": 0, "iconScale": 1, "micFeatured": false},
|
||||
{"id": "gift_lite", "name": "小心心", "price": 9.9, "emoji": "❤️", "icon": "", "micOrder": 1, "iconScale": 1, "micFeatured": true},
|
||||
{"id": "gift_super", "name": "嘉年华", "price": 1980, "emoji": "🎡", "icon": "", "micOrder": 2, "iconScale": 1, "micFeatured": false},
|
||||
}
|
||||
}
|
||||
|
||||
func linkKaruoRewardSection(root map[string]interface{}) map[string]interface{} {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
mpUi, _ := root["mpUi"].(map[string]interface{})
|
||||
if mpUi == nil {
|
||||
return nil
|
||||
}
|
||||
home, _ := mpUi["homePage"].(map[string]interface{})
|
||||
if home == nil {
|
||||
return nil
|
||||
}
|
||||
rw, _ := home["linkKaruoReward"].(map[string]interface{})
|
||||
return rw
|
||||
}
|
||||
|
||||
func linkKaruoGiftsList(root map[string]interface{}) []map[string]interface{} {
|
||||
sec := linkKaruoRewardSection(root)
|
||||
if sec == nil {
|
||||
return defaultLinkKaruoGifts()
|
||||
}
|
||||
raw, ok := sec["gifts"]
|
||||
if !ok || raw == nil {
|
||||
return defaultLinkKaruoGifts()
|
||||
}
|
||||
arr, ok := raw.([]interface{})
|
||||
if !ok || len(arr) == 0 {
|
||||
return defaultLinkKaruoGifts()
|
||||
}
|
||||
out := make([]map[string]interface{}, 0, len(arr))
|
||||
for _, it := range arr {
|
||||
m, ok := it.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return defaultLinkKaruoGifts()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findLinkKaruoGift(root map[string]interface{}, giftID string) (map[string]interface{}, float64) {
|
||||
giftID = strings.TrimSpace(giftID)
|
||||
if giftID == "" {
|
||||
return nil, 0
|
||||
}
|
||||
for _, g := range linkKaruoGiftsList(root) {
|
||||
id, _ := g["id"].(string)
|
||||
id = strings.TrimSpace(id)
|
||||
if id == giftID || strings.EqualFold(id, giftID) {
|
||||
price := parseMoneyAny(g["price"])
|
||||
if price > 0 {
|
||||
return g, price
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
const linkKaruoAmountEpsilon = 0.051
|
||||
|
||||
// parseLinkKaruoGiftQty 解析 giftId|qty 或 live_mic|giftId|qty
|
||||
func parseLinkKaruoGiftQty(productID string) (giftID string, qty int64, err error) {
|
||||
s := strings.TrimSpace(productID)
|
||||
parts := strings.Split(s, "|")
|
||||
if len(parts) >= 2 && strings.TrimSpace(parts[0]) == "live_mic" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" {
|
||||
return "", 0, fmt.Errorf("打赏商品格式错误")
|
||||
}
|
||||
giftID = strings.TrimSpace(parts[0])
|
||||
qty = 1
|
||||
if len(parts) >= 2 {
|
||||
q, e := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
|
||||
if e != nil || q < 1 || q > 99 {
|
||||
return "", 0, fmt.Errorf("打赏数量无效")
|
||||
}
|
||||
qty = q
|
||||
}
|
||||
return giftID, qty, nil
|
||||
}
|
||||
|
||||
// linkKaruoTipFallbackByAmount 客户端礼物 id 与库不一致时,按金额唯一匹配后台档位并返回规范 id
|
||||
func linkKaruoTipFallbackByAmount(db *gorm.DB, productID string, clientAmount float64) (total float64, giftName string, canonicalID string, err error) {
|
||||
if clientAmount <= 0 {
|
||||
return 0, "", "", fmt.Errorf("支付金额无效")
|
||||
}
|
||||
root := loadMpConfigRoot(db)
|
||||
wantID, qty, err := parseLinkKaruoGiftQty(productID)
|
||||
if err != nil {
|
||||
return 0, "", "", err
|
||||
}
|
||||
type hit struct {
|
||||
id string
|
||||
unit float64
|
||||
name string
|
||||
}
|
||||
var hits []hit
|
||||
for _, g := range linkKaruoGiftsList(root) {
|
||||
id, _ := g["id"].(string)
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
unit := parseMoneyAny(g["price"])
|
||||
if unit <= 0 {
|
||||
continue
|
||||
}
|
||||
t := unit * float64(qty)
|
||||
if math.Abs(t-clientAmount) < linkKaruoAmountEpsilon {
|
||||
nm, _ := g["name"].(string)
|
||||
hits = append(hits, hit{id, unit, strings.TrimSpace(nm)})
|
||||
}
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
return 0, "", "", fmt.Errorf("打赏金额与后台档位不一致,请同步 mp_config 礼物后重试")
|
||||
}
|
||||
if len(hits) == 1 {
|
||||
h := hits[0]
|
||||
name := h.name
|
||||
if name == "" {
|
||||
name = "礼物"
|
||||
}
|
||||
return h.unit * float64(qty), name, h.id, nil
|
||||
}
|
||||
for _, h := range hits {
|
||||
if strings.EqualFold(h.id, wantID) {
|
||||
name := h.name
|
||||
if name == "" {
|
||||
name = "礼物"
|
||||
}
|
||||
return h.unit * float64(qty), name, h.id, nil
|
||||
}
|
||||
}
|
||||
return 0, "", "", fmt.Errorf("后台存在同价多档礼物,请为每档设置不同价格或唯一 id")
|
||||
}
|
||||
|
||||
// linkKaruoTipPriceTotal 打赏标准价:普通 giftId|qty;上麦 live_mic|giftId|qty
|
||||
func linkKaruoTipPriceTotal(db *gorm.DB, productID string) (float64, error) {
|
||||
productID = strings.TrimSpace(productID)
|
||||
if productID == "" {
|
||||
return 0, fmt.Errorf("打赏缺少礼物标识")
|
||||
}
|
||||
root := loadMpConfigRoot(db)
|
||||
var giftID string
|
||||
var qty int64 = 1
|
||||
if strings.HasPrefix(productID, "live_mic|") {
|
||||
parts := strings.Split(productID, "|")
|
||||
if len(parts) < 2 {
|
||||
return 0, fmt.Errorf("上麦礼物参数无效")
|
||||
}
|
||||
giftID = strings.TrimSpace(parts[1])
|
||||
if len(parts) >= 3 {
|
||||
if q, err := strconv.ParseInt(strings.TrimSpace(parts[2]), 10, 64); err == nil && q > 0 {
|
||||
qty = q
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parts := strings.Split(productID, "|")
|
||||
giftID = strings.TrimSpace(parts[0])
|
||||
if len(parts) >= 2 {
|
||||
if q, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil && q > 0 {
|
||||
qty = q
|
||||
}
|
||||
}
|
||||
}
|
||||
_, price := findLinkKaruoGift(root, giftID)
|
||||
if price <= 0 {
|
||||
return 0, fmt.Errorf("礼物不存在或未定价: %s", giftID)
|
||||
}
|
||||
return price * float64(qty), nil
|
||||
}
|
||||
|
||||
// getStandardPrice 从 DB 读取商品标准价(后端校验用),防止客户端篡改金额
|
||||
// productType: fullbook / vip / section / match
|
||||
// productId: 章节购买时为章节 ID
|
||||
// productType: fullbook / vip / section / match / link_karuo_tip
|
||||
// productId: 章节购买时为章节 ID;打赏为 giftId|qty 或 live_mic|giftId|qty
|
||||
func getStandardPrice(db *gorm.DB, productType, productID string) (float64, error) {
|
||||
productType = normalizePayProductType(productType)
|
||||
// 打赏/上麦:显式分支 + 模糊识别双保险,避免误入 default 报「未知商品类型」
|
||||
if productType == "link_karuo_tip" || isLinkKaruoTipProductType(productType) {
|
||||
return linkKaruoTipPriceTotal(db, productID)
|
||||
}
|
||||
switch productType {
|
||||
case "fullbook", "vip", "match":
|
||||
// 从 system_config 读取
|
||||
|
||||
Reference in New Issue
Block a user