@@ -0,0 +1,785 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/productser"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/proto"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/rchgutil"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/goldcfgmod"
|
||||
"91porn-server/models/v/goldextramod"
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/productposimod"
|
||||
"91porn-server/models/v/rchgamtmod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
// New_CurrencyList 货币列表
|
||||
func New_CurrencyList(ctx context.Context, uid uint64, sysType string, t commod.CurrencyType) ([]*proto.CurrencyListResponse, stderr.Code) {
|
||||
if !isLegalCurrencyType(t) {
|
||||
log.ErrorX(ctx, "货币类型异常", log.Any("CurrencyType", t))
|
||||
return nil, stderr.ErrParamError
|
||||
}
|
||||
var (
|
||||
wg sync.WaitGroup // 同步控制
|
||||
currencys []currencymod.Currency // 货币列表
|
||||
moneys []string // 金额区间
|
||||
removeRepeat = make(map[int64]struct{}, 0) // 金额去重
|
||||
productType int // 产品类型 棋牌:1 站群:0
|
||||
err error // 错误
|
||||
isFirstPay bool // 棋牌首次充值
|
||||
)
|
||||
wg.Add(1)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
currencys, err = currencymod.List(t)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "货币列表==>获取列表异常", log.Any("CurrencyType", t), log.E(err))
|
||||
}
|
||||
})
|
||||
if t == commod.GameCoin {
|
||||
wg.Add(1)
|
||||
productType = 1
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("货币列表==>棋牌首次充值查询异常[%v]", err))
|
||||
return
|
||||
}
|
||||
if u.HasFirstChargeWaLi {
|
||||
//userLevel = 1
|
||||
} else {
|
||||
isFirstPay = true
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
return nil, stderr.ErrNetWorkBusy
|
||||
}
|
||||
for _, c := range currencys {
|
||||
if _, ok := removeRepeat[c.Price]; ok {
|
||||
continue
|
||||
}
|
||||
removeRepeat[c.Price] = struct{}{}
|
||||
moneys = append(moneys, FenToYuan(c.Price))
|
||||
}
|
||||
|
||||
req := rchgutil.GainPayTypeReq{Money: moneys}
|
||||
bc, err := req.GetPayType()
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "获取支付通道列表异常", log.Any("moneys", moneys), log.Any("sysType", sysType),
|
||||
log.Any("productType", productType), log.E(err))
|
||||
return nil, stderr.ErrNetWorkBusy
|
||||
}
|
||||
data := make([]*proto.CurrencyListResponse, 0, len(currencys))
|
||||
for _, v := range currencys {
|
||||
if v.Price < 100 {
|
||||
continue
|
||||
}
|
||||
rechargeType := GetPayChannelDetails_New(v.Price, bc, productType)
|
||||
if len(rechargeType) <= 0 { // 无支付方式的充值金额不需要展示
|
||||
continue
|
||||
}
|
||||
c := proto.CurrencyListResponse{
|
||||
ID: v.ID,
|
||||
Amount: v.Coins,
|
||||
TypeName: v.Name,
|
||||
Money: v.Price,
|
||||
GiveVipDays: v.GiveVipDays,
|
||||
LouFengUnlockTimes: v.LouFengUnlockTimes,
|
||||
GiveGold: v.GiveGold,
|
||||
}
|
||||
if isFirstPay || v.NotFirst || t == commod.FruitCoin || t == commod.Gold {
|
||||
c.CouponDesc = v.CouponDesc
|
||||
}
|
||||
c.RechargeType = rechargeType
|
||||
data = append(data, &c)
|
||||
}
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
// getChannelDisplayName 获取支付渠道显示名称
|
||||
func getChannelDisplayName(pType string) string {
|
||||
channelNames := map[string]string{
|
||||
"wechat": "微信(当前通道拥挤)",
|
||||
"alipay": "支付宝(官方强烈推荐)",
|
||||
"union": "银联",
|
||||
"daichong": "代充",
|
||||
"usdt": "USDT(官方)",
|
||||
"quickUnion": "快捷银联",
|
||||
}
|
||||
|
||||
if name, exists := channelNames[pType]; exists {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func FenToYuan(price int64) string {
|
||||
return strconv.FormatInt(price/100, 10)
|
||||
}
|
||||
|
||||
func YuanToFen(money string) int64 {
|
||||
// 去除前后空格
|
||||
money = strings.TrimSpace(money)
|
||||
if money == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 检查是否为负数
|
||||
isNegative := false
|
||||
if strings.HasPrefix(money, "-") {
|
||||
isNegative = true
|
||||
money = money[1:]
|
||||
}
|
||||
|
||||
// 按小数点分割
|
||||
parts := strings.Split(money, ".")
|
||||
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// 只有整数部分,如 "123", "456"
|
||||
yuan, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("invalid integer part: %v", err))
|
||||
return 0
|
||||
}
|
||||
result := yuan * 100
|
||||
if isNegative {
|
||||
result = -result
|
||||
}
|
||||
return result
|
||||
|
||||
case 2:
|
||||
// 有整数和小数部分,如 "123.45", "78.9"
|
||||
yuan, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("invalid integer part: %v", err))
|
||||
return 0
|
||||
}
|
||||
|
||||
// 处理小数部分
|
||||
decimalPart := parts[1]
|
||||
if len(decimalPart) > 2 {
|
||||
// 如果小数部分超过2位,进行四舍五入或截断
|
||||
// 这里选择截断,也可以根据需要改为四舍五入
|
||||
decimalPart = decimalPart[:2]
|
||||
} else if len(decimalPart) == 1 {
|
||||
// 如果只有1位小数,补零
|
||||
decimalPart += "0"
|
||||
}
|
||||
|
||||
fen, err := strconv.ParseInt(decimalPart, 10, 64)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("invalid decimal part: %v", err))
|
||||
return 0
|
||||
}
|
||||
|
||||
result := yuan*100 + fen
|
||||
if isNegative {
|
||||
result = -result
|
||||
}
|
||||
return result
|
||||
|
||||
default:
|
||||
log.Warn(fmt.Sprintf("invalid money format: %s", money))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func GetPayChannelDetails_New(money int64, payTypes []rchgutil.AllPayType, proT int) []rchgamtmod.PayChannelRes {
|
||||
// 后台系统配置的支付方式展示名称与顺序
|
||||
cfg := getPayTypeDisplayConfig()
|
||||
channels := make([]rchgamtmod.PayChannelRes, 0)
|
||||
for _, payType := range payTypes {
|
||||
// 金额匹配检查
|
||||
if YuanToFen(payType.Money) != money {
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理支持的支付类型
|
||||
for _, t := range payType.Types {
|
||||
// 展示名称优先取后台配置,未配置则回退默认硬编码名称
|
||||
channelName := cfg.typeName(t.Type)
|
||||
if channelName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
channels = append(channels, rchgamtmod.PayChannelRes{
|
||||
Type: t.Type,
|
||||
TypeName: channelName,
|
||||
Channel: t.Type, // 根据实际情况调整
|
||||
})
|
||||
}
|
||||
}
|
||||
// 按后台配置的顺序排序,未配置顺序的支付方式排在最后
|
||||
cfg.sortChannels(channels)
|
||||
return channels
|
||||
}
|
||||
|
||||
func New_ProductList(uid uint64, sysType string, newUser bool, proT int) (res []proto.ProductList, err error) {
|
||||
var data []productmod.Product
|
||||
var discounts []*goldcfgmod.DiscountConfig
|
||||
var positionM map[string]string
|
||||
var positionL []string
|
||||
var ps map[string]int64
|
||||
var (
|
||||
goldsError, discountsError, positionError error
|
||||
)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
//获取金币配置优惠
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
discounts, discountsError = goldcfgmod.GetGoldConfigList()
|
||||
if discountsError != nil {
|
||||
err = goldsError
|
||||
return
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
flag := true
|
||||
|
||||
data, goldsError = productmod.FindProductsByProductType(&flag, newUser, sysType)
|
||||
if goldsError != nil {
|
||||
err = goldsError
|
||||
return
|
||||
}
|
||||
// 根据用户当前卡进行升级
|
||||
productser.CheckUserUpgradeProducts(uid, data)
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
positionM, ps, positionL, positionError = productposimod.FindNamesWithStatusTrue()
|
||||
if positionError != nil {
|
||||
err = positionError
|
||||
return
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
|
||||
moneys := GetUniqueMoneys(data)
|
||||
|
||||
// 缓存支付通道信息
|
||||
req := rchgutil.GainPayTypeReq{Money: moneys}
|
||||
bc, err := req.GetPayTypeFromCache()
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;rchgutil GetPayType:%v", uid, err))
|
||||
}
|
||||
// 构建结果
|
||||
res = make([]proto.ProductList, len(positionL))
|
||||
for i := range positionL {
|
||||
pl := make([]proto.VIPListRes, 0)
|
||||
res[i] = proto.ProductList{
|
||||
ShowType: ps[positionL[i]],
|
||||
Position: positionM[positionL[i]],
|
||||
List: pl,
|
||||
PositionID: positionL[i],
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
chans := GetPayChannelDetails_New(v.DiscountedPrice*10, bc, proT)
|
||||
if v.ProductType == commod.PhysicalGoods {
|
||||
v.Name = ""
|
||||
}
|
||||
temp := proto.VIPListRes{
|
||||
Product: v,
|
||||
}
|
||||
if discountsError == nil && len(discounts) > 0 {
|
||||
for i := 0; i < len(chans); i++ {
|
||||
for _, d := range discounts {
|
||||
if chans[i].Type == d.Type && v.DiscountedPrice == d.Amount {
|
||||
chans[i].IncrAmount = d.IncrAmount
|
||||
chans[i].IncTax = d.IncTax
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
temp.RechargeType = chans
|
||||
for k := range res {
|
||||
if positionM[v.Position] == res[k].Position {
|
||||
res[k].List = append(res[k].List, temp)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetUniqueMoneys(data []productmod.Product) []string {
|
||||
moneySet := make(map[string]bool)
|
||||
|
||||
for _, p := range data {
|
||||
money := FenToYuan(p.DiscountedPrice * 10)
|
||||
moneySet[money] = true
|
||||
if p.AdvanceAmount > 0 {
|
||||
advanceAmount := FenToYuan(p.AdvanceAmount * 10)
|
||||
moneySet[advanceAmount] = true
|
||||
}
|
||||
if p.BalanceAmount > 0 {
|
||||
balanceAmount := FenToYuan(p.BalanceAmount * 10)
|
||||
moneySet[balanceAmount] = true
|
||||
}
|
||||
}
|
||||
|
||||
moneys := make([]string, 0, len(moneySet))
|
||||
for money := range moneySet {
|
||||
moneys = append(moneys, money)
|
||||
}
|
||||
|
||||
return moneys
|
||||
}
|
||||
|
||||
// RefundCallBack 退款回调处理逻辑
|
||||
func RefundCallBack(ctx context.Context, oid string, tradeNo string) error {
|
||||
log.InfoX(ctx, "退款回调", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
// 查询订单
|
||||
r, err := rchgordmod.FindRechargeOrderByID(tradeNo)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "充值回调查询订单异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
if r.ID.IsZero() {
|
||||
log.WarnX(ctx, "充值回调订单不存在", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
|
||||
if r.Status == rchgordmod.REFUND {
|
||||
log.Warn(fmt.Sprintf("oid:%v, tradeNo:%v, 退款回调重复回调", oid, tradeNo))
|
||||
return nil
|
||||
}
|
||||
|
||||
if r.Status != rchgordmod.SUCCESS {
|
||||
log.Warn(fmt.Sprintf("oid:%v, tradeNo:%v, 退款回调订单状态异常", oid, tradeNo))
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
fn func(*db.MongoTool) error
|
||||
now = time.Now()
|
||||
desc = rchgordmod.Status(rchgordmod.REFUND).Desc()
|
||||
remark = "订单退款"
|
||||
)
|
||||
switch r.BuyType {
|
||||
case commod.BuyGold:
|
||||
if fn, err = RefundGold(ctx, r); err != nil {
|
||||
log.ErrorX(ctx, "退款回调购买金币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
case commod.BuyProduct:
|
||||
if fn, err = RefundProduct(ctx, r); err != nil {
|
||||
log.ErrorX(ctx, "退款回调购买商品异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
default: // 默认为旧版购买处理
|
||||
log.InfoX(ctx, "退款回调旧版本处理", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
return errors.New("旧版退款回调不再支持, 请使用新版回调方式")
|
||||
}
|
||||
|
||||
// 新版处理
|
||||
if r.BuyType > 0 {
|
||||
err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if err = rchgordmod.RefundCallBackModify(ctx, t, r.ID, rchgordmod.EditSelector{
|
||||
OID: &oid,
|
||||
UpdatedAt: now,
|
||||
RefundAt: &now,
|
||||
Status: rchgordmod.REFUND,
|
||||
StatusDesc: &desc,
|
||||
Remark: &remark,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(t)
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("oid:%s,refund callBack err:%v", oid, err))
|
||||
return errors.New("退款失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefundGold 退款金币 payMoney单位为分
|
||||
func RefundGold(ctx context.Context, order rchgordmod.RechargeOrder) (func(*db.MongoTool) error, error) {
|
||||
payMoney := order.PayMoney
|
||||
var gold = payMoney / 10
|
||||
// 查询金币配置
|
||||
currency, err := currencymod.Get(order.ProductID)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 查询异常[%v]", order.ID, order.ProductID, err))
|
||||
return nil, err
|
||||
}
|
||||
// 未匹配到对应配置
|
||||
if currency.ID.IsZero() {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值金币-订单号[%s] 产品ID[%v] 不存在", order.ID, order.ProductID))
|
||||
} else { // 匹配到对应配置
|
||||
// 金额不能低于配置金额1元并且有赠送金额
|
||||
if (payMoney+100-currency.Price) > 0 && currency.GiveGold > 0 {
|
||||
gold += currency.GiveGold
|
||||
}
|
||||
}
|
||||
return func(t *db.MongoTool) error {
|
||||
// 增加金币与总充值金额
|
||||
originGold := gold
|
||||
goldExtra := uint64(0)
|
||||
if order.GoldExtraID != nil {
|
||||
extra, err := goldextramod.GetUserGoldExtraByID(t, *order.GoldExtraID)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值退款金币-订单号[%s] 查询金币加购券[%v] 出错: %s", order.ID, *order.GoldExtraID, err.Error()))
|
||||
return err
|
||||
}
|
||||
if extra.Uid != order.UID || extra.Used || extra.Expired.Before(time.Now()) {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值退款金币-订单号[%s] 查询金币加购券[%v] 无效", order.ID, *order.GoldExtraID))
|
||||
return errors.New("无效的金币加购券")
|
||||
}
|
||||
if err := goldextramod.UseGoldExtra(t, order.UID, *order.GoldExtraID); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值退款金币-订单号[%s] 使用金币加购券[%v] 出错: %s", order.ID, *order.GoldExtraID, err.Error()))
|
||||
return err
|
||||
}
|
||||
goldExtra = extra.Amount
|
||||
gold += int64(extra.Amount)
|
||||
}
|
||||
|
||||
gold = -gold
|
||||
payMoney = -payMoney
|
||||
w, err := walletmod.Credit(t, walletmod.CreditPlan{
|
||||
Amount: &gold,
|
||||
Consumption: &payMoney,
|
||||
}, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc := "官方充值-退款" + strconv.FormatInt(gold, 10) + "金币"
|
||||
if goldExtra != 0 {
|
||||
desc = "官方充值-退款" + strconv.FormatInt(originGold, 10) + "金币, 消耗金币加购券退款" + strconv.FormatUint(goldExtra, 10) + "金币"
|
||||
}
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: order.ID,
|
||||
UID: order.UID,
|
||||
Amount: gold,
|
||||
ActualAmount: float64(gold),
|
||||
TranType: txnmod.OrderRefund.Key(),
|
||||
TranTypeInt: int64(txnmod.OrderRefund),
|
||||
ChannelType: order.RechargeType,
|
||||
Desc: desc,
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefundProduct 退款购买商品
|
||||
func RefundProduct(ctx context.Context, order rchgordmod.RechargeOrder) (func(*db.MongoTool) error, error) {
|
||||
payMoney := order.PayMoney
|
||||
var (
|
||||
creditPlan = walletmod.CreditPlan{Consumption: &payMoney}
|
||||
u *usermod.User
|
||||
p *productmod.Product
|
||||
err error
|
||||
wg sync.WaitGroup
|
||||
w *walletmod.Wallet
|
||||
)
|
||||
wg.Add(3)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
// 查询会员卡信息
|
||||
p, err = productmod.FindProduct(order.ProductID, order.DevType)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 产品ID[%v] 查询会员卡信息异常[%v]", order.ID, order.ProductID, err))
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
// 查询用户信息
|
||||
u, err = usermod.FindUserByUID(order.UID)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%s] 用户ID[%d] 查询用户信息异常 [%v]", order.ID, order.UID, err))
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
var errG error
|
||||
w, errG = walletmod.GetWallet(order.UID)
|
||||
if errG != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%s] 用户ID[%d] 查询用户钱包异常 [%v]", order.ID, order.UID, err))
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
if p == nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID))
|
||||
return nil, fmt.Errorf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID)
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID))
|
||||
return nil, fmt.Errorf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID)
|
||||
}
|
||||
// 充值金额低于会员卡售价1元 转化为充值对应金额金币
|
||||
var (
|
||||
disPay = payMoney - p.DiscountedPrice*10
|
||||
fn func(t *db.MongoTool) error
|
||||
)
|
||||
if disPay < -100 {
|
||||
var (
|
||||
gold = -(payMoney / 10)
|
||||
)
|
||||
creditPlan.Amount = &gold
|
||||
return func(t *db.MongoTool) error {
|
||||
w, err := walletmod.Credit(t, creditPlan, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//插入一条支出流水
|
||||
if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: order.ID,
|
||||
UID: order.UID,
|
||||
Amount: gold,
|
||||
ActualAmount: float64(gold),
|
||||
TranType: txnmod.OrderRefund.Key(),
|
||||
TranTypeInt: int64(txnmod.OrderRefund),
|
||||
ChannelType: order.RechargeType,
|
||||
Desc: fmt.Sprintf("退款官方充值购买[%s]实际支付[%d]元;退款[%d]金币", p.Name, payMoney/100, payMoney/10),
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
RealAmount: w.RealAmount(),
|
||||
}); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
// 购买商品处理
|
||||
switch p.ProductType {
|
||||
case commod.VIP, commod.NEWUSERCard:
|
||||
if fn, err = RefundBuyVIP(order, p, "退款官方充值-购买[%s]", w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("退款充值购买会员卡-订单号[%s] 商品ID[%s] 未知商品", order.ID, order.ProductID)
|
||||
}
|
||||
// 返回事务函数
|
||||
return func(t *db.MongoTool) error {
|
||||
// 充值金额大于会员卡售价1角 转化为充值对应金额金币
|
||||
if disPay > 9 {
|
||||
var (
|
||||
gold = -(disPay / 10)
|
||||
)
|
||||
creditPlan.Amount = &gold
|
||||
w, err := walletmod.Credit(t, creditPlan, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//插入一条支出流水
|
||||
if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: order.ID,
|
||||
UID: order.UID,
|
||||
Amount: gold,
|
||||
ActualAmount: float64(gold),
|
||||
TranType: txnmod.OrderRefund.Key(),
|
||||
TranTypeInt: int64(txnmod.OrderRefund),
|
||||
ChannelType: order.RechargeType,
|
||||
Desc: fmt.Sprintf("退款官方充值购买[%s]实际支付[%d]元;超额部分退款为[%d]金币", p.Name, payMoney/100, disPay),
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
RealAmount: w.RealAmount(),
|
||||
}); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = fn(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RefundBuyVIP(order rchgordmod.RechargeOrder, p *productmod.Product, desc string, w *walletmod.Wallet) (func(*db.MongoTool) error, error) {
|
||||
var (
|
||||
//商品购买记录
|
||||
history = prdcthsomod.ProductHistory{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: order.UID,
|
||||
ProductID: p.ID,
|
||||
Name: p.Name,
|
||||
Amount: p.DiscountedPrice,
|
||||
ProductType: prdcthsomod.VIP,
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
ProductSnapShot: p,
|
||||
}
|
||||
sel usermod.UserSelector
|
||||
creditPlan = walletmod.CreditPlan{}
|
||||
)
|
||||
// 资金流水记录
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: order.UID,
|
||||
TranType: txnmod.OrderRefundVip.Key(),
|
||||
TranTypeInt: int64(txnmod.OrderRefundVip),
|
||||
TransNo: history.ID,
|
||||
Desc: fmt.Sprintf(desc, p.Name),
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
CurrencyType: txnmod.CurrencyTypeCash,
|
||||
Amount: -p.DiscountedPrice,
|
||||
ActualAmount: float64(-p.DiscountedPrice),
|
||||
RealAmount: w.RealAmount(),
|
||||
},
|
||||
}
|
||||
// 查询用户信息
|
||||
u, err := usermod.FindUserByUID(order.UID)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("退款充值购买会员卡-订单号[%s] 查询用户信息异常 [%v]", order.ID, err))
|
||||
return nil, err
|
||||
}
|
||||
// VIP变更
|
||||
if p.Duration > 0 {
|
||||
var expire time.Time
|
||||
// VIP未过期
|
||||
if u.VipExpireDate.After(time.Now()) {
|
||||
expire = u.VipExpireDate.AddDate(0, 0, -p.Duration)
|
||||
} else {
|
||||
expire = time.Now().AddDate(0, 0, -p.Duration)
|
||||
}
|
||||
log.Info(fmt.Sprintf("到期时间:%v", expire))
|
||||
sel.VipExpireDate = &expire
|
||||
}
|
||||
// 增加金币视频免费天数
|
||||
if p.GoldVideoFreeDay > 0 {
|
||||
var expire time.Time
|
||||
if u.GoldVideoFreeExpire.IsZero() || u.GoldVideoFreeExpire.Before(time.Now()) {
|
||||
expire = time.Now().AddDate(0, 0, -p.GoldVideoFreeDay)
|
||||
} else {
|
||||
expire = u.GoldVideoFreeExpire.AddDate(0, 0, -p.GoldVideoFreeDay)
|
||||
}
|
||||
sel.GoldVideoFreeExpire = &expire
|
||||
}
|
||||
if p.BroadcastDays > 0 {
|
||||
expire := time.Time{}
|
||||
if u.BroadcastExpire.IsZero() || u.BroadcastExpire.Before(time.Now()) {
|
||||
expire = time.Now().AddDate(0, 0, -p.BroadcastDays)
|
||||
} else {
|
||||
expire = u.BroadcastExpire.AddDate(0, 0, -p.BroadcastDays)
|
||||
}
|
||||
sel.BroadcastExpire = &expire
|
||||
}
|
||||
if p.DramaDays > 0 {
|
||||
expire := usermod.RenewDramaExpire(u.DramaExpire, time.Now(), -p.DramaDays)
|
||||
sel.DramaExpire = &expire
|
||||
}
|
||||
// 回收赠送金币
|
||||
if p.GiveCoin > 0 {
|
||||
coin := -p.GiveCoin
|
||||
creditPlan.Amount = &coin
|
||||
}
|
||||
|
||||
// 回收赠送下载次数
|
||||
if p.DownloadCount > 0 {
|
||||
count := -p.DownloadCount
|
||||
creditPlan.DownloadCount = &count
|
||||
}
|
||||
|
||||
// 回收赠送AI免费脱衣次数
|
||||
if p.AiUndressCount > 0 {
|
||||
aiUndressFreeTimes := int64(-p.AiUndressCount)
|
||||
creditPlan.AiUndressFreeTimes = &aiUndressFreeTimes
|
||||
}
|
||||
|
||||
return func(t *db.MongoTool) error {
|
||||
// 钱包变更
|
||||
if p.GiveCoin > 0 || p.DownloadCount > 0 || p.AiUndressCount > 0 {
|
||||
w, err := walletmod.Credit(t, creditPlan, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.DownloadCount > 0 {
|
||||
txnLogs = append(txnLogs, txnmod.TransactionLog{
|
||||
UID: order.UID,
|
||||
DownloadCount: p.DownloadCount,
|
||||
TranType: txnmod.AdminDebitDownloadCount.Key(),
|
||||
TranTypeInt: int64(txnmod.AdminDebitDownloadCount),
|
||||
TransNo: history.ID,
|
||||
Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-回收赠送[%d]次数", p.DownloadCount),
|
||||
DiscDoc: u.DiscDoc,
|
||||
SysType: u.SysType,
|
||||
})
|
||||
}
|
||||
|
||||
// 插入购买会员卡回收赠送金币流水
|
||||
if p.GiveCoin > 0 {
|
||||
giveLog := txnmod.TransactionLog{
|
||||
UID: order.UID,
|
||||
Amount: p.GiveCoin,
|
||||
ActualAmount: float64(p.GiveCoin),
|
||||
TranType: txnmod.RefundVipCardGive.Key(),
|
||||
TranTypeInt: int64(txnmod.RefundVipCardGive),
|
||||
TransNo: history.ID,
|
||||
Desc: fmt.Sprintf("购买%s-回收赠送金币[%v个]", p.Name, p.GiveCoin),
|
||||
SysType: u.SysType,
|
||||
RealAmount: w.RealAmount(),
|
||||
}
|
||||
txnLogs = append(txnLogs, giveLog)
|
||||
}
|
||||
|
||||
// 回收赠送免费次数
|
||||
if p.AiUndressCount > 0 {
|
||||
txnLogs = append(txnLogs, txnmod.TransactionLog{
|
||||
UID: order.UID,
|
||||
Amount: int64(p.AiUndressCount),
|
||||
ActualAmount: float64(p.AiUndressCount),
|
||||
TranType: txnmod.AdminDebitAiUndressFreeTimes.Key(),
|
||||
TranTypeInt: int64(txnmod.AdminDebitAiUndressFreeTimes),
|
||||
TransNo: history.ID,
|
||||
Desc: fmt.Sprintf(desc, p.Name) + fmt.Sprintf("-回收赠送AI脱衣免费次数[%d次]", p.AiUndressCount),
|
||||
DiscDoc: u.DiscDoc,
|
||||
SysType: u.SysType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
}
|
||||
}
|
||||
// 用户信息变更
|
||||
if p.Duration > 0 || p.GoldVideoFreeDay > 0 || p.BroadcastDays > 0 || p.DramaDays > 0 {
|
||||
if _, err = usermod.UpdateTrans(t, u.UID, sel); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 保存资金流水记录
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/v/rchgamtmod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
)
|
||||
|
||||
// payTypeDisplayConfig 支付方式展示配置(来自后台系统配置 GPCodePay)
|
||||
type payTypeDisplayConfig struct {
|
||||
nameMap map[string]string // 支付方式 -> 展示名称
|
||||
sortIndex map[string]int // 支付方式 -> 顺序(越小越靠前)
|
||||
}
|
||||
|
||||
// getPayTypeDisplayConfig 读取支付方式展示配置(带缓存)。
|
||||
// 读取失败时返回空配置,调用方会回退到默认硬编码名称与上游默认顺序。
|
||||
func getPayTypeDisplayConfig() payTypeDisplayConfig {
|
||||
cfg := payTypeDisplayConfig{}
|
||||
conf, err := sysconfdata.GetByGpCodeFromCache(sysconfmod.GPCodePay)
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
cfg.nameMap = conf.GetObject(sysconfmod.VCodePayTypeName)
|
||||
sortList := conf.GetStrSlice(sysconfmod.VCodePayTypeSort)
|
||||
cfg.sortIndex = make(map[string]int, len(sortList))
|
||||
for i, t := range sortList {
|
||||
if _, ok := cfg.sortIndex[t]; !ok {
|
||||
cfg.sortIndex[t] = i
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// typeName 返回支付方式展示名称,未配置则回退到默认硬编码名称
|
||||
func (c payTypeDisplayConfig) typeName(payType string) string {
|
||||
if name, ok := c.nameMap[payType]; ok && name != "" {
|
||||
return name
|
||||
}
|
||||
return getChannelDisplayName(payType)
|
||||
}
|
||||
|
||||
// sortChannels 按后台配置的顺序对支付方式排序,未配置顺序的支付方式排在最后(保持相对顺序稳定)
|
||||
func (c payTypeDisplayConfig) sortChannels(channels []rchgamtmod.PayChannelRes) {
|
||||
if len(c.sortIndex) == 0 || len(channels) <= 1 {
|
||||
return
|
||||
}
|
||||
const last = 1 << 30
|
||||
orderOf := func(t string) int {
|
||||
if idx, ok := c.sortIndex[t]; ok {
|
||||
return idx
|
||||
}
|
||||
return last
|
||||
}
|
||||
sort.SliceStable(channels, func(i, j int) bool {
|
||||
return orderOf(channels[i].Type) < orderOf(channels[j].Type)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/activityclient"
|
||||
"91porn-server/app/service/adser"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/proto"
|
||||
"91porn-server/app/service"
|
||||
"91porn-server/app/service/productser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/rchgutil"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/goldcfgmod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/productposimod"
|
||||
"91porn-server/models/v/rchgamegoldmod"
|
||||
"91porn-server/models/v/rchgamtmod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
/*
|
||||
Recharge 充值
|
||||
返回 充值链接、充值模式、错误
|
||||
*/
|
||||
func Recharge(ctx context.Context, in *RechargeRequest, ua ua.UA, deduct *ActivityDeduct) (string, string, error) {
|
||||
u, err := usermod.FindUserByUID(in.UID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if u == nil {
|
||||
return "", "", errors.New("invalid user")
|
||||
}
|
||||
if u.HasLocked {
|
||||
return "", "", errors.New("user is banned")
|
||||
}
|
||||
if err = in.validateAttribution(u.UID); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// 创建订单
|
||||
order, advanceOrder, err := in.CreateOrder(u, ua, deduct)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 创建订单异常[%v]",
|
||||
in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err))
|
||||
return "", "", err
|
||||
}
|
||||
if err = rchgordmod.InsertRechargeOrder(order); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 保存订单异常[%v]",
|
||||
in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err))
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if advanceOrder == nil || advanceOrder.ID.IsZero() {
|
||||
advanceOrder.AdvOid = order.ID.Hex()
|
||||
_, err = advanceordermod.CreditOrder(nil, advanceOrder)
|
||||
if err != nil {
|
||||
log.Error("RechargeRequest.CreateOrder 创建预售订单失败", log.Any("advanceOrder", advanceOrder), log.E(err))
|
||||
return "", "", err
|
||||
}
|
||||
} else if advanceOrder.Status == advanceordermod.AdvanceProcessing {
|
||||
// 修改订单号和预付款
|
||||
err := advanceordermod.Update(nil, advanceOrder.ID, bson.M{"advOid": order.ID.Hex(), "advanceAmount": advanceOrder.AdvanceAmount, "totalAmount": advanceOrder.TotalAmount})
|
||||
if err != nil {
|
||||
log.Error("RechargeRequest.CreateOrder 修改预售订单失败", log.Any("advanceOrder.Status", advanceOrder.Status), log.E(err))
|
||||
return "", "", err
|
||||
}
|
||||
} else if advanceOrder.Status == advanceordermod.AdvanceSUCCESS || advanceOrder.Status == advanceordermod.BalanceProcessing {
|
||||
err := advanceordermod.Update(nil, advanceOrder.ID, bson.M{"status": advanceordermod.BalanceProcessing, "balOid": order.ID.Hex()})
|
||||
if err != nil {
|
||||
log.Error("RechargeRequest.CreateOrder 修改预售订单失败", log.Any("advanceOrder.Status", advanceOrder.Status), log.E(err))
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
||||
// 下单
|
||||
now := time.Now()
|
||||
res, err := (&rchgutil.Recharge{
|
||||
TradeNo: order.ID.Hex(),
|
||||
Money: rchgutil.FenToYuan(order.Money),
|
||||
Type: order.RechargeType,
|
||||
Info: rchgutil.PayInfo{
|
||||
PlayerId: strconv.FormatUint(order.UID, 10),
|
||||
PlayerIp: order.UserIP,
|
||||
DeviceId: order.DevID,
|
||||
Tel: order.Tel,
|
||||
DeviceType: order.DevType,
|
||||
},
|
||||
}).ToPayNew(ctx)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 下单请求异常[%v]",
|
||||
in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err))
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
log.InfoX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 下单返回信息[%v]",
|
||||
in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, res))
|
||||
// 更新订单
|
||||
set := rchgordmod.EditSelector{
|
||||
Status: rchgordmod.Processing,
|
||||
ProgressAt: &now,
|
||||
OID: &res.OID,
|
||||
Mode: &res.Mode,
|
||||
//Channel: &res.CID,
|
||||
Rate: &res.Rebate,
|
||||
}
|
||||
if err = rchgordmod.Update(nil, order.ID, set); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 更新订单异常[%v]",
|
||||
in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID, err))
|
||||
return "", "", err
|
||||
}
|
||||
log.InfoX(ctx, fmt.Sprintf("用户ID[%d] 支付方式[%s] 购买类型[%d] 产品ID[%v] 产品子ID[%v] 【下单成功】",
|
||||
in.UID, in.RechargeType, in.BuyType, in.ProductID, in.ProductChildID))
|
||||
// 会员抵扣券:建单成功异步回调活动服占用(active -> used),携实际抵扣额
|
||||
if order.CouponID != "" {
|
||||
activityclient.NotifyDeductCouponAsync(order.CouponID, order.ID.Hex(), activityclient.DeductCouponStatusUsed, order.DeductAmount)
|
||||
}
|
||||
return res.PayUrl, res.Mode, nil
|
||||
}
|
||||
|
||||
func Recharge_new(ctx context.Context, rchg *rchgordmod.RechargeOrder) (string, string, error) {
|
||||
u, err := usermod.FindUserByUID(rchg.UID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if u == nil {
|
||||
return "", "", errors.New("invalid user")
|
||||
}
|
||||
rchg.DevID = u.DevID
|
||||
rchg.Name = u.Name
|
||||
rchg.DiscDoc = u.DiscDoc
|
||||
if rchg.ProductType == 0 {
|
||||
if rchg.VipID.IsZero() {
|
||||
// 金币充值
|
||||
g, err := rchgamtmod.GetGoldByID(rchg.ProductID)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if g == nil {
|
||||
err = errors.New("invalid productID")
|
||||
return "", "", err
|
||||
}
|
||||
rchg.Amount = g.Coins
|
||||
rchg.Money = g.Price
|
||||
} else {
|
||||
p, err := productmod.FindProduct(rchg.VipID, rchg.DevType)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if p == nil {
|
||||
return "", "", errors.New("invalid productID")
|
||||
}
|
||||
//新用户8折
|
||||
/*if u.CreatedAt.Add(time.Hour*24).After(time.Now()) && p.ProductType == prdcthsomod.VIP && (p.DiscountedPrice == 5000 || p.DiscountedPrice == 10000) {
|
||||
p.DiscountedPrice = int64(float64(p.DiscountedPrice) * 0.8)
|
||||
}*/
|
||||
rchg.Amount = p.DiscountedPrice
|
||||
rchg.Money = p.DiscountedPrice * 10
|
||||
}
|
||||
}
|
||||
if rchg.ProductType == 1 && rchg.Money <= 0 {
|
||||
return "", "", errors.New("invalid money")
|
||||
}
|
||||
now := time.Now()
|
||||
set := rchgordmod.EditSelector{
|
||||
Status: rchgordmod.Processing,
|
||||
ProgressAt: &now,
|
||||
ProductType: &rchg.ProductType,
|
||||
}
|
||||
statusDesc := rchgordmod.Status(set.Status).Desc()
|
||||
set.StatusDesc = &statusDesc
|
||||
channel := "self"
|
||||
rchg.Channel = channel
|
||||
if err = rchgordmod.InsertRechargeOrder(rchg); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
var userLevel int
|
||||
if rchg.ProductType == 1 && u.HasFirstChargeWaLi {
|
||||
userLevel = 1
|
||||
}
|
||||
res, err := (&rchgutil.Rchg{
|
||||
TransNo: rchg.ID.Hex(),
|
||||
UID: strconv.FormatUint(u.UID, 10),
|
||||
DevID: rchg.DevID,
|
||||
UserIP: rchg.UserIP,
|
||||
Name: rchg.Name,
|
||||
Tel: u.Mobile,
|
||||
DevType: rchg.DevType,
|
||||
Money: rchg.Money,
|
||||
Channel: channel,
|
||||
CreatedAt: now,
|
||||
PayMethod: rchg.RechargeType,
|
||||
ProductType: rchg.ProductType,
|
||||
UserLevel: userLevel,
|
||||
}).ToPayNew(ctx)
|
||||
if err != nil {
|
||||
common.Go(func() {
|
||||
if appg.Bot == nil {
|
||||
return
|
||||
}
|
||||
//判断是否返回 渠道已经关闭 错误,此时商户将该渠道关闭
|
||||
if err == rchgutil.PAY_CHANNEL_CLOSE_ERROR {
|
||||
return
|
||||
}
|
||||
})
|
||||
return "", "", err
|
||||
}
|
||||
set.OID = &res.OID
|
||||
payUrl := res.PayUrl
|
||||
mode := res.Mode
|
||||
set.Mode = &mode
|
||||
set.Channel = &res.CID
|
||||
set.Rate = &(res.Rate)
|
||||
return payUrl, mode, rchgordmod.Update(nil, rchg.ID, set)
|
||||
}
|
||||
|
||||
// GetPayChannel根据充值金额选择支付渠道
|
||||
func GetPayChannel_new(ctx context.Context, sysType string, proT int) (data []*rchgamtmod.GoldRes, err error) {
|
||||
var golds []*currencymod.Currency
|
||||
// var discounts []*goldcfgmod.DiscountConfig
|
||||
var goldsError error
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
golds, goldsError = currencymod.GetCurrencyListByRedis(commod.Gold)
|
||||
if goldsError != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
moneysM := make(map[int64]int)
|
||||
for vsi := range golds {
|
||||
moneysM[golds[vsi].Price] = 1
|
||||
}
|
||||
moneys := make([]int64, len(moneysM))
|
||||
mi := 0
|
||||
for k := range moneysM {
|
||||
moneys[mi] = k
|
||||
mi++
|
||||
}
|
||||
bc, _ := rchgutil.GetPayType(ctx, moneys, sysType, 0, 0)
|
||||
data = make([]*rchgamtmod.GoldRes, 0, len(golds))
|
||||
for _, v := range golds {
|
||||
chans := GetPayChannelDetails_new(v.Price, bc, proT)
|
||||
if len(chans) <= 0 { // 没有支付方式的充值金额不展示
|
||||
continue
|
||||
}
|
||||
data = append(data, &rchgamtmod.GoldRes{
|
||||
ID: v.ID,
|
||||
RechargeType: chans,
|
||||
Amount: v.Coins,
|
||||
TypeName: v.Name,
|
||||
Money: v.Price,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isLegalCurrencyType(t commod.CurrencyType) bool {
|
||||
return t == commod.Gold || t == commod.FruitCoin || t == commod.GameCoin
|
||||
}
|
||||
|
||||
// CurrencyList 货币列表(新增果币)
|
||||
func CurrencyList(ctx context.Context, uid uint64, sysType string, t commod.CurrencyType) ([]*proto.CurrencyListResponse, stderr.Code) {
|
||||
if !isLegalCurrencyType(t) {
|
||||
log.ErrorX(ctx, "货币类型异常", log.Any("CurrencyType", t))
|
||||
return nil, stderr.ErrParamError
|
||||
}
|
||||
var (
|
||||
wg sync.WaitGroup // 同步控制
|
||||
currencys []currencymod.Currency // 货币列表
|
||||
moneys []int64 // 金额区间
|
||||
removeRepeat = make(map[int64]struct{}, 0) // 金额去重
|
||||
productType int // 产品类型 棋牌:1 站群:0
|
||||
err error // 错误
|
||||
isFirstPay bool // 棋牌首次充值
|
||||
userLevel = 0
|
||||
)
|
||||
wg.Add(1)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
currencys, err = currencymod.List(t)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "货币列表==>获取列表异常", log.Any("CurrencyType", t), log.E(err))
|
||||
}
|
||||
})
|
||||
if t == commod.GameCoin {
|
||||
wg.Add(1)
|
||||
productType = 1
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("货币列表==>棋牌首次充值查询异常[%v]", err))
|
||||
return
|
||||
}
|
||||
if u.HasFirstChargeWaLi {
|
||||
userLevel = 1
|
||||
} else {
|
||||
isFirstPay = true
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
return nil, stderr.ErrNetWorkBusy
|
||||
}
|
||||
for _, c := range currencys {
|
||||
if _, ok := removeRepeat[c.Price]; ok {
|
||||
continue
|
||||
}
|
||||
removeRepeat[c.Price] = struct{}{}
|
||||
moneys = append(moneys, c.Price)
|
||||
}
|
||||
bc, err := rchgutil.GetPayType(ctx, moneys, sysType, productType, userLevel)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "获取支付通道列表异常", log.Any("moneys", moneys), log.Any("sysType", sysType),
|
||||
log.Any("productType", productType), log.E(err))
|
||||
return nil, stderr.ErrNetWorkBusy
|
||||
}
|
||||
data := make([]*proto.CurrencyListResponse, 0, len(currencys))
|
||||
for _, v := range currencys {
|
||||
if v.Price < 100 {
|
||||
continue
|
||||
}
|
||||
rechargeType := GetPayChannelDetails_new(v.Price, bc, productType)
|
||||
if len(rechargeType) <= 0 { // 无支付方式的充值金额不需要展示
|
||||
continue
|
||||
}
|
||||
c := proto.CurrencyListResponse{
|
||||
ID: v.ID,
|
||||
Amount: v.Coins,
|
||||
TypeName: v.Name,
|
||||
Money: v.Price,
|
||||
GiveVipDays: v.GiveVipDays,
|
||||
LouFengUnlockTimes: v.LouFengUnlockTimes,
|
||||
GiveGold: v.GiveGold,
|
||||
}
|
||||
if isFirstPay || v.NotFirst || t == commod.FruitCoin || t == commod.Gold {
|
||||
c.CouponDesc = v.CouponDesc
|
||||
}
|
||||
c.RechargeType = rechargeType
|
||||
data = append(data, &c)
|
||||
}
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
func getChanByte(pType string) string {
|
||||
switch pType {
|
||||
case "wechat": //微信
|
||||
return "微信【当前通道拥挤】"
|
||||
case "alipay": //支付宝
|
||||
//return "支付宝【🔥限时返利15%金币】"
|
||||
return "支付宝【官方强烈推荐】"
|
||||
case "union": //支付宝
|
||||
return "银联"
|
||||
case "daichong": //代充
|
||||
return "代充"
|
||||
case "usdt":
|
||||
return "usdt"
|
||||
case "quickUnion":
|
||||
return "快捷银联"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetPayChannelDetails_new(money int64, payTypes []rchgutil.RchgType, proT int) []rchgamtmod.PayChannelRes {
|
||||
chans := make([]rchgamtmod.PayChannelRes, 0)
|
||||
for k := range payTypes {
|
||||
if payTypes[k].Money == money {
|
||||
if payTypes[k].Alipay {
|
||||
pName := getChanByte("alipay")
|
||||
chans = append(chans, rchgamtmod.PayChannelRes{
|
||||
Type: "alipay",
|
||||
TypeName: pName,
|
||||
Channel: "alipay",
|
||||
})
|
||||
}
|
||||
if payTypes[k].Wechat {
|
||||
pName := getChanByte("wechat")
|
||||
chans = append(chans, rchgamtmod.PayChannelRes{
|
||||
Type: "wechat",
|
||||
TypeName: pName,
|
||||
Channel: "wechat",
|
||||
})
|
||||
}
|
||||
if payTypes[k].DaiChong {
|
||||
pName := getChanByte("daichong")
|
||||
channel := appg.Conf.DaiChong.H5domain
|
||||
if proT == 1 {
|
||||
channel = appg.Conf.DaiChong.H5domainQP
|
||||
}
|
||||
chans = append(chans, rchgamtmod.PayChannelRes{
|
||||
Type: "daichong",
|
||||
TypeName: pName,
|
||||
Channel: channel,
|
||||
})
|
||||
}
|
||||
if payTypes[k].USDT {
|
||||
pName := getChanByte("usdt")
|
||||
chans = append(chans, rchgamtmod.PayChannelRes{
|
||||
Type: "usdt",
|
||||
TypeName: pName,
|
||||
Channel: "usdt",
|
||||
})
|
||||
}
|
||||
if payTypes[k].QuickUnion {
|
||||
pName := getChanByte("quickUnion")
|
||||
chans = append(chans, rchgamtmod.PayChannelRes{
|
||||
Type: "quickUnion",
|
||||
TypeName: pName,
|
||||
Channel: "quickUnion",
|
||||
})
|
||||
}
|
||||
if payTypes[k].Union {
|
||||
pName := getChanByte("union")
|
||||
chans = append(chans, rchgamtmod.PayChannelRes{
|
||||
Type: "union",
|
||||
TypeName: pName,
|
||||
Channel: "union",
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return chans
|
||||
}
|
||||
|
||||
func CallbackAddCoins(ctx context.Context, t *db.MongoTool, amount, decPay int64, desc string,
|
||||
order rchgordmod.RechargeOrder) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wallet, err1 := walletmod.Credit(t, walletmod.CreditPlan{Amount: &amount, Consumption: &decPay}, order.UID)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
tt := txnmod.TransactionLog{
|
||||
TransNo: order.ID,
|
||||
UID: order.UID,
|
||||
Amount: amount,
|
||||
ActualAmount: float64(amount),
|
||||
TranType: txnmod.Rchg.Key(),
|
||||
TranTypeInt: int64(txnmod.Rchg),
|
||||
ChannelType: order.RechargeType,
|
||||
Desc: desc,
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
}
|
||||
//插入一条支出流水
|
||||
if err = txnmod.InsertTransactionLog(t, &tt); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("rechargeSer CallbackAddCoins Trans fail error:%+v order:%+v", err, order))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RechargeCallBack 充值成功回调处理逻辑
|
||||
func RechargeCallBack(ctx context.Context, oid string, payMoney int64, tradeNo string, code int, paymentAt, successAt time.Time) error {
|
||||
log.InfoX(ctx, "充值回调", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
// 查询订单
|
||||
r, err := rchgordmod.FindRechargeOrderByID(tradeNo)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "充值回调查询订单异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
if r.ID.IsZero() {
|
||||
log.WarnX(ctx, "充值回调订单不存在", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
return errors.New("订单不存在")
|
||||
}
|
||||
if r.Status == rchgordmod.SUCCESS {
|
||||
log.WarnX(ctx, "充值回调重复回调", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
return nil
|
||||
}
|
||||
if r.Status == rchgordmod.REFUND {
|
||||
log.WarnX(ctx, "充值回调重复回调,订单已退款", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
return nil
|
||||
}
|
||||
if r.Status != rchgordmod.Processing {
|
||||
log.WarnX(ctx, "充值回调订单状态异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.Any("status", r.Status))
|
||||
return errors.New("订单状态异常")
|
||||
}
|
||||
if r.ProductType == 1 {
|
||||
return errors.New("瓦力游戏已下架")
|
||||
}
|
||||
// 新版充值---根据购买类型处理
|
||||
var (
|
||||
fn func(*db.MongoTool) error
|
||||
now = time.Now()
|
||||
desc = rchgordmod.Status(rchgordmod.SUCCESS).Desc()
|
||||
csm int // 分成模式
|
||||
)
|
||||
|
||||
switch r.BuyType {
|
||||
case commod.BuyGold:
|
||||
csm = rchgordmod.ChanShareCoin
|
||||
if fn, _, err = service.BuyGold(ctx, r, payMoney); err != nil {
|
||||
log.ErrorX(ctx, "充值回调购买金币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
case commod.BuyGameCoin:
|
||||
if fn, err = service.BuyGameCoin(ctx, r, payMoney); err != nil {
|
||||
log.ErrorX(ctx, "充值回调购买游戏币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
case commod.BuyFruitCoin:
|
||||
if fn, err = service.BuyFruitCoin(ctx, r, payMoney); err != nil {
|
||||
log.ErrorX(ctx, "充值回调购买果币异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
case commod.BuyProduct:
|
||||
if fn, csm, _, err = service.BuyProduct(ctx, r, payMoney); err != nil {
|
||||
log.ErrorX(ctx, "充值回调购买商品异常", log.Any("oid", oid), log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
default: // 默认为旧版购买处理
|
||||
log.InfoX(ctx, "充值回调旧版本处理", log.Any("oid", oid), log.Any("tradeNo", tradeNo))
|
||||
return errors.New("旧版回调不再支持, 请使用新版回调方式")
|
||||
}
|
||||
// 新版处理
|
||||
if r.BuyType > 0 {
|
||||
err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, r.ID, rchgordmod.EditSelector{
|
||||
PayMoney: &payMoney,
|
||||
OID: &oid,
|
||||
PaymentAt: &paymentAt,
|
||||
SuccessAt: &successAt,
|
||||
Status: rchgordmod.SUCCESS,
|
||||
StatusDesc: &desc,
|
||||
ChanShareMod: &csm,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(t)
|
||||
})
|
||||
if err == nil {
|
||||
// ABC广告分组测试
|
||||
common.Go(func() {
|
||||
user, _ := usermod.FindUserByUID(r.UID)
|
||||
if user != nil {
|
||||
_ = adser.UpsertAdStat(context.Background(), user, time.Now(), 0, 0, payMoney)
|
||||
}
|
||||
})
|
||||
|
||||
// 会员抵扣券:支付成功异步回调活动服核销(used -> verified)
|
||||
if r.CouponID != "" {
|
||||
activityclient.NotifyDeductCouponAsync(r.CouponID, r.ID.Hex(), activityclient.DeductCouponStatusVerified, r.DeductAmount)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
id, err := primitive.ObjectIDFromHex(tradeNo)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "ObjectIDFromHex fail", log.Any("tradeNo", tradeNo), log.E(err))
|
||||
return err
|
||||
}
|
||||
set := rchgordmod.EditSelector{
|
||||
PayMoney: &payMoney,
|
||||
OID: &oid,
|
||||
PaymentAt: &paymentAt,
|
||||
}
|
||||
if stderr.Code(code) != stderr.Success {
|
||||
set.Status = rchgordmod.FAILURE
|
||||
desc := rchgordmod.Status(rchgordmod.FAILURE).Desc()
|
||||
set.StatusDesc = &desc
|
||||
set.FailureAt = &now
|
||||
set.SuccessAt = &successAt
|
||||
return rchgordmod.CallBackModifyWithProcessing(ctx, nil, id, set)
|
||||
}
|
||||
if r.ProductID.IsZero() { //人民币购买会员卡
|
||||
p, err := productmod.FindProduct(r.VipID, r.DevType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if p == nil {
|
||||
return errors.New("invalid vipID")
|
||||
}
|
||||
disMoney := payMoney - (p.DiscountedPrice * 10)
|
||||
if disMoney < -constant.RechargeAmtTolerance {
|
||||
if payMoney <= 0 {
|
||||
return nil
|
||||
}
|
||||
tdesc := "购买「" + p.Name + "卡」实际支付" + strconv.FormatFloat(float64(payMoney)/100, 'f', 2, 64) +
|
||||
",全额转为「" + strconv.FormatInt(payMoney/10, 10) + "」金币"
|
||||
return appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
csm := rchgordmod.ChanShareCoin
|
||||
set.ChanShareMod = &csm
|
||||
if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil {
|
||||
return err
|
||||
}
|
||||
return CallbackAddCoins(ctx, nil, payMoney/10, payMoney, tdesc, r)
|
||||
})
|
||||
}
|
||||
//查询金币优惠
|
||||
localAmount := p.DiscountedPrice
|
||||
giveGold := false
|
||||
var gold int64
|
||||
disCount, err := goldcfgmod.GetGoldCgfByAmountAndType(ctx, localAmount, r.RechargeType)
|
||||
if disCount != nil && err == nil {
|
||||
giveGold = true
|
||||
if disCount.IncrAmount != 0 {
|
||||
localAmount = disCount.IncrAmount
|
||||
gold = disCount.IncrAmount
|
||||
} else if disCount.IncTax != 0.0 {
|
||||
incr := decimal.NewFromFloat(disCount.IncTax)
|
||||
local := decimal.NewFromInt(localAmount)
|
||||
localAmount = localAmount + local.Mul(incr).IntPart()
|
||||
}
|
||||
}
|
||||
if p.ChanSplitMod == 1 { //正常分成
|
||||
csm := rchgordmod.ChanShareModVIP
|
||||
set.ChanShareMod = &csm
|
||||
}
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
set.SuccessAt = &successAt
|
||||
set.Status = rchgordmod.SUCCESS
|
||||
desc := rchgordmod.Status(rchgordmod.SUCCESS).Desc()
|
||||
set.StatusDesc = &desc
|
||||
if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil {
|
||||
return err
|
||||
}
|
||||
creditPlan := walletmod.CreditPlan{
|
||||
Consumption: &payMoney,
|
||||
}
|
||||
if giveGold {
|
||||
creditPlan.Amount = &localAmount
|
||||
}
|
||||
if _, err = walletmod.Credit(t, creditPlan, r.UID); err != nil {
|
||||
log.ErrorX(ctx, "walletmod.Credit failed", log.Any("creditPlan", creditPlan), log.Any("uid", r.UID),
|
||||
log.E(err))
|
||||
return err
|
||||
}
|
||||
fPayMoney := float32(payMoney)
|
||||
if disMoney > 9 {
|
||||
tdesc := "购买「" + p.Name + "卡」实际支付" + strconv.FormatFloat(float64(fPayMoney)/100, 'f', 2, 64) +
|
||||
",超额部分转为「" + strconv.FormatInt(disMoney/10, 10) + "」金币"
|
||||
if err = CallbackAddCoins(ctx, t, disMoney/10, 0, tdesc, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = RechargeBuy_p(t, r.UID, *p, gold); err != nil {
|
||||
log.ErrorX(ctx, "RechargeBuy_p failed", log.Any("uid", r.UID), log.Any("product", p),
|
||||
log.Any("gold", gold), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.ErrorX(ctx, "WalletSer RechargeCallBack Trans fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
} else { //充值金币
|
||||
amount := payMoney / 10
|
||||
g, err := rchgamtmod.GetGoldByID(r.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if g == nil {
|
||||
return errors.New("invalid productID")
|
||||
}
|
||||
if !r.VipID.IsZero() {
|
||||
if amount > (g.Coins-10) && amount < g.Coins {
|
||||
amount = g.Coins
|
||||
}
|
||||
} else {
|
||||
//充值赠送金币
|
||||
localAmount := amount
|
||||
giveGold := false
|
||||
disCount, err := goldcfgmod.GetGoldCgfByAmountAndType(ctx, localAmount, r.RechargeType)
|
||||
if disCount != nil && err == nil {
|
||||
giveGold = true
|
||||
if disCount.IncrAmount != 0 {
|
||||
localAmount = disCount.IncrAmount
|
||||
} else if disCount.IncTax != 0.0 {
|
||||
incr := decimal.NewFromFloat(disCount.IncTax)
|
||||
local := decimal.NewFromInt(localAmount)
|
||||
localAmount = localAmount + local.Mul(incr).IntPart()
|
||||
}
|
||||
}
|
||||
if giveGold {
|
||||
amount += localAmount
|
||||
}
|
||||
}
|
||||
csm := rchgordmod.ChanShareCoin
|
||||
set.ChanShareMod = &csm
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
set.SuccessAt = &successAt
|
||||
set.Status = rchgordmod.SUCCESS
|
||||
desc := rchgordmod.Status(rchgordmod.SUCCESS).Desc()
|
||||
set.StatusDesc = &desc
|
||||
if err = rchgordmod.CallBackModifyWithProcessing(ctx, t, id, set); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = walletmod.CreditConsumption(t, payMoney, r.UID); err != nil {
|
||||
return err
|
||||
}
|
||||
wallet, err := walletmod.CreditAmount(t, amount, r.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tt := txnmod.TransactionLog{
|
||||
TransNo: r.ID,
|
||||
UID: r.UID,
|
||||
Amount: amount,
|
||||
ActualAmount: float64(amount),
|
||||
TranType: txnmod.Rchg.Key(),
|
||||
TranTypeInt: int64(txnmod.Rchg),
|
||||
ChannelType: r.RechargeType,
|
||||
Desc: "充值新增-" + strconv.FormatInt(amount, 10),
|
||||
DiscDoc: r.DiscDoc,
|
||||
SysType: r.DevType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
}
|
||||
//插入一条支出流水
|
||||
return txnmod.InsertTransactionLog(t, &tt)
|
||||
}); err != nil {
|
||||
log.ErrorX(ctx, "WalletSer RechargeCallBack Trans fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
// ABC广告分组测试
|
||||
common.Go(func() {
|
||||
user, _ := usermod.FindUserByUID(r.UID)
|
||||
if user != nil {
|
||||
_ = adser.UpsertAdStat(context.Background(), user, time.Now(), 0, 0, payMoney)
|
||||
}
|
||||
})
|
||||
// //用户充值成功时做充值扣量
|
||||
// executionRechargeDeduction(ctx, tradeNo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindProductByTypeNew(ctx context.Context, uid uint64, sysType string, newUser bool, proT int) (res []proto.ProductList, err error) {
|
||||
redisKey := redisconst.DataCachKey(productmod.RedisSetKey, "FindProductByTypeNew"+sysType+strconv.FormatBool(newUser))
|
||||
if !appg.Redis.IsExist(redisKey) {
|
||||
var data []productmod.Product
|
||||
var discounts []*goldcfgmod.DiscountConfig
|
||||
var positionM map[string]string
|
||||
var positionL []string
|
||||
var ps map[string]int64
|
||||
var (
|
||||
goldsError, discountsError, positionError error
|
||||
)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
//获取金币配置优惠
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
discounts, discountsError = goldcfgmod.GetGoldConfigList()
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
flag := true
|
||||
data, goldsError = productmod.FindProductsByProductType(&flag, newUser, sysType)
|
||||
if goldsError != nil {
|
||||
err = goldsError
|
||||
return
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
positionM, ps, positionL, positionError = productposimod.FindNamesWithStatusTrue()
|
||||
if positionError != nil {
|
||||
err = positionError
|
||||
return
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
moneysM := make(map[int64]int)
|
||||
for vsi := range data {
|
||||
moneysM[data[vsi].DiscountedPrice*10] = 1
|
||||
}
|
||||
moneys := make([]int64, len(moneysM))
|
||||
mi := 0
|
||||
for k := range moneysM {
|
||||
moneys[mi] = k
|
||||
mi++
|
||||
}
|
||||
bc, _ := rchgutil.GetPayType(ctx, moneys, sysType, proT, 0)
|
||||
res = make([]proto.ProductList, len(positionL))
|
||||
for i := range positionL {
|
||||
pl := make([]proto.VIPListRes, 0)
|
||||
res[i] = proto.ProductList{
|
||||
ShowType: ps[positionL[i]],
|
||||
Position: positionM[positionL[i]],
|
||||
List: pl,
|
||||
PositionID: positionL[i],
|
||||
}
|
||||
}
|
||||
for _, v := range data {
|
||||
price := v.DiscountedPrice * 10
|
||||
if v.ProductType == productmod.AdvanceCard {
|
||||
price = v.AdvanceAmount * 10
|
||||
}
|
||||
chans := GetPayChannelDetails_new(price, bc, proT)
|
||||
if v.ProductType == commod.PhysicalGoods {
|
||||
v.Name = ""
|
||||
}
|
||||
temp := proto.VIPListRes{
|
||||
Product: v,
|
||||
}
|
||||
if discountsError == nil && len(discounts) > 0 {
|
||||
for i := 0; i < len(chans); i++ {
|
||||
for _, d := range discounts {
|
||||
if chans[i].Type == d.Type && v.DiscountedPrice == d.Amount {
|
||||
chans[i].IncrAmount = d.IncrAmount
|
||||
chans[i].IncTax = d.IncTax
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
temp.RechargeType = chans
|
||||
for k := range res {
|
||||
if positionM[v.Position] == res[k].Position {
|
||||
res[k].List = append(res[k].List, temp)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
jsonBytes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
_ = appg.Redis.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire)
|
||||
}
|
||||
str, err := appg.Redis.Get(redisKey)
|
||||
if err != nil || str == nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal([]byte(*str), &res); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RechargeBuy 充值回调时购买w
|
||||
func RechargeBuy_p(t *db.MongoTool, uid uint64, p productmod.Product, giveGold int64) error {
|
||||
switch p.ProductType {
|
||||
case productmod.MeetingCard: //约会卡:
|
||||
return productser.BuyMeetingCard_d(t, uid, p)
|
||||
case productmod.OTHER, productmod.PHYSICALGOODS:
|
||||
return productser.BuyOtherCard_d(t, uid, p)
|
||||
case productmod.VIP, productmod.NewUser:
|
||||
return productser.BuyVIP_d(t, uid, p, giveGold)
|
||||
case productmod.VideoFreeCard:
|
||||
return productser.BuyVideoFreeCard_d(t, uid, p)
|
||||
case productmod.VideoDiscount:
|
||||
return productser.BuyVideoDiscountCard_d(t, uid, p)
|
||||
}
|
||||
return errors.New("invalid productType")
|
||||
}
|
||||
|
||||
func GetPayChannel_qp(ctx context.Context, uid uint64, sysType string) (data []*rchgamtmod.GameGoldRes, err error) {
|
||||
gameGolds, err := rchgamegoldmod.GetGoldListByActiveTrue()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
moneys := make([]int64, len(gameGolds))
|
||||
for i, v := range gameGolds {
|
||||
moneys[i] = v.Price
|
||||
}
|
||||
if len(gameGolds) == 0 { //@todo:兼容旧版本,以后删除
|
||||
moneys = []int64{5000, 10000, 28800, 39800, 59600, 100000, 300000, 500000} //单位分 金额50, ... ,5000元
|
||||
for _, v := range moneys {
|
||||
gameGolds = append(gameGolds, &rchgamegoldmod.Gold{
|
||||
Coins: v,
|
||||
Price: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
firstPay := true
|
||||
userLevel := 0
|
||||
if u, _ := usermod.FindUserByUID(uid); u != nil {
|
||||
if u.HasFirstChargeWaLi {
|
||||
firstPay = false
|
||||
userLevel = 1
|
||||
}
|
||||
}
|
||||
bc, err := rchgutil.GetPayType(ctx, moneys, sysType, 1, userLevel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = make([]*rchgamtmod.GameGoldRes, 0, len(gameGolds))
|
||||
for _, v := range gameGolds {
|
||||
chans := GetPayChannelDetails_new(v.Price, bc, 1)
|
||||
if len(chans) <= 0 { // 没有支付方式的充值金额不展示
|
||||
continue
|
||||
}
|
||||
gold := rchgamtmod.GameGoldRes{
|
||||
Amount: v.Coins * 100,
|
||||
TypeName: fmt.Sprintf("%d元充值", v.Price/100),
|
||||
Money: v.Price,
|
||||
}
|
||||
if firstPay || v.NotFirst {
|
||||
gold.CouponDesc = v.CouponDesc
|
||||
}
|
||||
gold.RechargeType = chans
|
||||
data = append(data, &gold)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetUserTransactionDetails(uid, pageNum, pageSize uint64) (total int64, data []proto.TransactionResp, hasNext bool, err error) {
|
||||
//获取用户成功的交易记录
|
||||
total, rechangeOrders, hasNext, _ := rchgordmod.GetSuccessOrderUidList(uid, pageSize, pageNum)
|
||||
rechangeOrdersLen := len(rechangeOrders)
|
||||
goldProductIds := make([]primitive.ObjectID, 0, rechangeOrdersLen)
|
||||
vipProductIds := make([]primitive.ObjectID, 0, rechangeOrdersLen)
|
||||
productIdMap := make(map[primitive.ObjectID]primitive.ObjectID)
|
||||
for _, v := range rechangeOrders {
|
||||
if _, ok := productIdMap[v.ProductID]; !ok {
|
||||
if v.BuyType == commod.BuyGold || v.BuyType == commod.BuyGameCoin || v.BuyType == commod.BuyFruitCoin {
|
||||
goldProductIds = append(goldProductIds, v.ProductID)
|
||||
} else {
|
||||
vipProductIds = append(vipProductIds, v.ProductID)
|
||||
}
|
||||
productIdMap[v.ProductID] = v.ProductID
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
//根据产品id 查询对应的奖品
|
||||
vipProductMap, err := productmod.ListByIDsMap(vipProductIds)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "ListByIDsMap failed", log.Any("vipProductIds", vipProductIds), log.E(err))
|
||||
return
|
||||
}
|
||||
//根据产品id 查询金币对应的一个奖品
|
||||
goldProductMap, err := currencymod.ListByIdsMap(goldProductIds)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "ListByIdsMap failed", log.Any("goldProductIds", goldProductIds), log.E(err))
|
||||
return
|
||||
}
|
||||
data = make([]proto.TransactionResp, len(rechangeOrders))
|
||||
for i, v := range rechangeOrders {
|
||||
resp := proto.TransactionResp{
|
||||
OrderId: v.OID,
|
||||
Money: v.Money,
|
||||
PaymentAt: v.PaymentAt,
|
||||
CreatedAt: v.CreatedAt,
|
||||
PayType: v.RechargeType,
|
||||
Status: v.Status,
|
||||
}
|
||||
if resp.Status == 3 {
|
||||
resp.Money = v.PayMoney
|
||||
}
|
||||
if v.BuyType == commod.BuyProduct {
|
||||
if p := vipProductMap[v.ProductID]; p != nil {
|
||||
resp.ProductName = p.Name
|
||||
}
|
||||
} else {
|
||||
if p := goldProductMap[v.ProductID]; p != nil {
|
||||
resp.ProductName = p.Name
|
||||
}
|
||||
}
|
||||
data[i] = resp
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/productser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/goldextramod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
// Recommended stable values for reporting. The API also accepts custom values.
|
||||
OrderSourcePageHomeUserSegment = "HOME_USER_SEGMENT"
|
||||
OrderSourcePageVideoBottomBanner = "VIDEO_BOTTOM_BANNER"
|
||||
OrderSourcePageVideoBottomSheet = "VIDEO_BOTTOM_SHEET"
|
||||
OrderSourcePageVIPCenter = "VIP_CENTER"
|
||||
OrderSourcePageH5Activity = "H5_ACTIVITY"
|
||||
OrderSourcePageDramaPaywall = "DRAMA_PAYWALL"
|
||||
OrderSourcePageUnknown = "UNKNOWN"
|
||||
)
|
||||
|
||||
// NormalizeOrderSourcePage trims sourcePage and maps an empty value to UNKNOWN.
|
||||
// Non-empty values are intentionally preserved to support new frontend sources
|
||||
// without requiring a backend allowlist release.
|
||||
func NormalizeOrderSourcePage(sourcePage string) string {
|
||||
normalized := strings.TrimSpace(sourcePage)
|
||||
if normalized == "" {
|
||||
return OrderSourcePageUnknown
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// CurrencyListRequest 货币列表请求
|
||||
type CurrencyListRequest struct {
|
||||
Type int `form:"type" binding:"required"` //货币类型 1-金币 2-游戏币 3-果币
|
||||
}
|
||||
|
||||
// CurrencyListResponse 货币列表回复
|
||||
|
||||
// RechargeRequest 充值请求
|
||||
type RechargeRequest struct {
|
||||
UID uint64 `swaggerignore:"true"`
|
||||
IP string `swaggerignore:"true"`
|
||||
RechargeType string `json:"rechargeType" binding:"required"` // 充值类型
|
||||
ProductID primitive.ObjectID `json:"productID" binding:"required"` // 产品ID 如:会员卡ID
|
||||
ProductChildID primitive.ObjectID `json:"productChildId"` // 产品子ID 如:服务ID
|
||||
GoldExtraID *primitive.ObjectID `json:"goldExtraID"` // 金币加购券id(仅仅在购买金币时使用)
|
||||
BuyType commod.BuyType `json:"buyType" binding:"required"` // 购买类型
|
||||
SourcePage string `json:"sourcePage"` // 下单来源页面
|
||||
SourceRef string `json:"sourceRef"` // 来源业务对象
|
||||
VideoID string `json:"videoId"` // 来源视频ID
|
||||
ActivityID string `json:"activityId"` // 来源活动ID
|
||||
ExperimentID string `json:"experimentId"` // A/B实验ID
|
||||
ExperimentVariant string `json:"experimentVariant"` // A/B实验分组
|
||||
SessionID string `json:"sessionId"` // 前端会话ID
|
||||
MediaID primitive.ObjectID `json:"mediaId"` // 短剧ID
|
||||
ContentID primitive.ObjectID `json:"contentId"` // 短剧单集ID
|
||||
CheckoutContextID string `json:"checkoutContextId"` // 短剧付费墙上下文
|
||||
}
|
||||
|
||||
// ActivityDeduct 活动服下单附加的会员抵扣券信息。
|
||||
// 仅由活动服 HMAC 鉴权入口(CreateRechargeOrder)构造并显式传入,与客户端请求体(RechargeRequest)解耦,
|
||||
// 客户端普通下单(/api/app/mine/topay)传 nil,无从注入抵扣,杜绝自设 deductAmount 低价购卡。
|
||||
type ActivityDeduct struct {
|
||||
CouponID string // 会员抵扣券ID(活动服券号,不透明串)
|
||||
DeductAmount int64 // 活动服建议抵扣金额(分),本服再自行校验上限后折价
|
||||
}
|
||||
|
||||
func (r *RechargeRequest) CreateOrder(u *usermod.User, ua ua.UA, deduct *ActivityDeduct) (*rchgordmod.RechargeOrder, *advanceordermod.AdvanceOrder, error) {
|
||||
orderSysType := u.SysType
|
||||
if strings.TrimSpace(ua.SysType) != "" {
|
||||
orderSysType = ua.SysType
|
||||
}
|
||||
order := &rchgordmod.RechargeOrder{
|
||||
UID: u.UID,
|
||||
DevID: u.DevID,
|
||||
UserIP: r.IP,
|
||||
DevType: common.HandleSysType(orderSysType),
|
||||
DiscDoc: u.DiscDoc,
|
||||
Name: u.Name,
|
||||
Tel: u.Mobile,
|
||||
RechargeType: r.RechargeType,
|
||||
ProductID: r.ProductID,
|
||||
Channel: "self",
|
||||
BuyType: r.BuyType,
|
||||
Ua: ua,
|
||||
SourcePage: NormalizeOrderSourcePage(r.SourcePage),
|
||||
SourceRef: r.SourceRef,
|
||||
VideoID: r.VideoID,
|
||||
ActivityID: r.ActivityID,
|
||||
ExperimentID: r.ExperimentID,
|
||||
ExperimentVariant: r.ExperimentVariant,
|
||||
SessionID: r.SessionID,
|
||||
MediaID: r.MediaID,
|
||||
ContentID: r.ContentID,
|
||||
CheckoutContextID: r.CheckoutContextID,
|
||||
}
|
||||
var advanceOrder advanceordermod.AdvanceOrder
|
||||
if r.GoldExtraID != nil && !(*r.GoldExtraID).IsZero() {
|
||||
if r.BuyType != commod.BuyGold {
|
||||
return nil, nil, errors.New("金币加购券只能在购买金币时使用")
|
||||
}
|
||||
goldExtra, err := goldextramod.GetUserGoldExtraByID(nil, *r.GoldExtraID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if goldExtra.Uid != u.UID || goldExtra.Used || goldExtra.Expired.Before(time.Now()) {
|
||||
return nil, nil, errors.New("无效的金币加购券")
|
||||
}
|
||||
order.GoldExtraID = r.GoldExtraID
|
||||
}
|
||||
if r.RechargeType == "daichong" {
|
||||
order.RechargeType = "alipay"
|
||||
}
|
||||
switch r.BuyType {
|
||||
case commod.BuyGold, commod.BuyFruitCoin, commod.BuyGameCoin:
|
||||
// 查询货币信息
|
||||
currency, err := currencymod.Get(r.ProductID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
order.Money = currency.Price
|
||||
order.Amount = currency.Coins
|
||||
if r.BuyType == commod.BuyGameCoin {
|
||||
order.ProductType = 1
|
||||
}
|
||||
order.ProductName = currency.Name
|
||||
case commod.BuyProduct:
|
||||
// 查询会员卡信息
|
||||
product, err := productmod.FindProduct(r.ProductID, order.DevType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// 用户会员卡升级检查
|
||||
productser.CheckUserUpgrade(u.UID, product)
|
||||
|
||||
order.Amount = product.DiscountedPrice
|
||||
order.Money = product.DiscountedPrice * 10
|
||||
order.VipID = r.ProductID
|
||||
order.ProductName = product.Name
|
||||
if product.ProductType == commod.AdvanceCard {
|
||||
data, err := advanceordermod.IsExist(bson.M{"productID": r.ProductID, "uid": u.UID})
|
||||
if err != nil {
|
||||
return nil, nil, errors.New("advance order query IsExist err")
|
||||
}
|
||||
if data == nil || data.ID.IsZero() {
|
||||
prepaidPrivilege := advanceordermod.AdvanceCardPrepaidPrivilege{}
|
||||
if product.PrepaidPrivilege != nil {
|
||||
prepaidPrivilege = advanceordermod.AdvanceCardPrepaidPrivilege{
|
||||
CoinVideoLimitPerDay: product.PrepaidPrivilege.CoinVideoLimitPerDay,
|
||||
LuckyDrawLimitPerDay: product.PrepaidPrivilege.LuckyDrawLimitPerDay,
|
||||
AiUndressLimitPerDay: product.PrepaidPrivilege.AiUndressLimitPerDay,
|
||||
DownloadLimitPerDay: product.PrepaidPrivilege.DownloadLimitPerDay,
|
||||
}
|
||||
}
|
||||
// 没有创建预售订单,创建预售订单(首付状态)
|
||||
order.Amount = product.AdvanceAmount
|
||||
order.Money = product.AdvanceAmount * 10
|
||||
// 创建预售订单
|
||||
now := time.Now()
|
||||
advanceOrder.UID = u.UID
|
||||
advanceOrder.CreatedAt = now
|
||||
advanceOrder.Status = advanceordermod.AdvanceProcessing
|
||||
advanceOrder.AdvanceAmount = product.AdvanceAmount * 10
|
||||
advanceOrder.BalanceAmount = product.BalanceAmount * 10
|
||||
advanceOrder.StartTime = product.StartTime
|
||||
advanceOrder.EndTime = product.EndTime
|
||||
advanceOrder.ProductID = product.ID
|
||||
advanceOrder.TotalAmount = (product.AdvanceAmount + product.BalanceAmount) * 10
|
||||
advanceOrder.PrepaidPrivilege = prepaidPrivilege
|
||||
advanceOrder.TodayUse = advanceordermod.DayUse{}
|
||||
} else if data.Status == advanceordermod.AdvanceProcessing {
|
||||
// 有预售订单,且是首付状态
|
||||
order.Amount = product.AdvanceAmount
|
||||
order.Money = product.AdvanceAmount * 10
|
||||
|
||||
data.AdvanceAmount = product.AdvanceAmount * 10
|
||||
data.TotalAmount = (product.AdvanceAmount + product.BalanceAmount) * 10
|
||||
advanceOrder = *data
|
||||
} else if (data.Status == advanceordermod.AdvanceSUCCESS && time.Now().After(product.StartTime)) || data.Status == advanceordermod.BalanceProcessing {
|
||||
// 尾款预付状态
|
||||
order.Amount = product.BalanceAmount
|
||||
order.Money = product.BalanceAmount * 10
|
||||
advanceOrder = *data
|
||||
} else if data.Status == advanceordermod.AdvanceSUCCESS && !time.Now().After(product.StartTime) {
|
||||
log.Error("RechargeRequest.CreateOrder 支付预售订单失败,未到尾款支付时间", log.Any("advanceOrder.Status", data.Status), log.Any("product", product), log.E(err))
|
||||
return nil, nil, errors.New("advance order status is err")
|
||||
} else if data.Status == advanceordermod.BalanceSUCCESS {
|
||||
log.Error("RechargeRequest.CreateOrder 支付预售订单失败,该订单已经预售支付完成", log.Any("advanceOrder.Status", data.Status), log.Any("product", product), log.E(err))
|
||||
return nil, nil, errors.New("advance order is paid")
|
||||
}
|
||||
advanceOrder.ProductID = product.ID
|
||||
}
|
||||
// 会员抵扣券支付抵扣:折后价写入 order.Money,couponId 持久化到订单
|
||||
applyDeductCoupon(order, deduct, product)
|
||||
|
||||
switch product.ProductType {
|
||||
case commod.VIP, commod.MeetingCard, commod.AdvanceCard, commod.GameAdvanceCard,
|
||||
commod.NEWUSERCard, commod.PhysicalGoods, commod.VideoDiscount, commod.VideoFreeCard, commod.CoinMonthCard:
|
||||
order.Category = 1
|
||||
default:
|
||||
order.Category = 0
|
||||
}
|
||||
}
|
||||
return order, &advanceOrder, nil
|
||||
}
|
||||
|
||||
// minChannelPayFen 支付渠道最低可支付金额(分),抵扣后实付不得低于此值
|
||||
const minChannelPayFen = 1
|
||||
|
||||
// applyDeductCoupon 应用会员抵扣券支付抵扣。
|
||||
// deduct 由活动服入口传入(客户端下单为 nil);其 DeductAmount 仅为建议值,不可无条件信任,须自行封顶:
|
||||
//
|
||||
// 实际抵扣 = min(DeductAmount, 原价 - 最低实付),折后实付 >= 渠道最低可支付金额。
|
||||
//
|
||||
// 仅普通会员卡(buyType=商品且非预售卡)生效;预售卡分阶段付款不参与抵扣。
|
||||
// 折后价写回 order.Money(下单/回调均以此为准),couponId 与实际抵扣额持久化到订单,
|
||||
// 供支付结果回调向活动服核销时原样回传。
|
||||
func applyDeductCoupon(order *rchgordmod.RechargeOrder, deduct *ActivityDeduct, product *productmod.Product) {
|
||||
if deduct == nil || deduct.CouponID == "" || deduct.DeductAmount <= 0 {
|
||||
return
|
||||
}
|
||||
if product.ProductType == commod.AdvanceCard {
|
||||
return
|
||||
}
|
||||
maxDeduct := order.Money - minChannelPayFen
|
||||
if maxDeduct <= 0 {
|
||||
return
|
||||
}
|
||||
amount := deduct.DeductAmount
|
||||
if amount > maxDeduct {
|
||||
amount = maxDeduct
|
||||
}
|
||||
order.Money -= amount
|
||||
order.CouponID = deduct.CouponID
|
||||
order.DeductAmount = amount
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/usermod"
|
||||
)
|
||||
|
||||
func TestNormalizeOrderSourcePage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sourcePage string
|
||||
want string
|
||||
}{
|
||||
{name: "canonical", sourcePage: OrderSourcePageVideoBottomSheet, want: OrderSourcePageVideoBottomSheet},
|
||||
{name: "trim arbitrary source", sourcePage: " custom_Page-01 ", want: "custom_Page-01"},
|
||||
{name: "empty", sourcePage: "", want: OrderSourcePageUnknown},
|
||||
{name: "whitespace", sourcePage: " ", want: OrderSourcePageUnknown},
|
||||
{name: "previously unsupported", sourcePage: "OTHER", want: "OTHER"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := NormalizeOrderSourcePage(tt.sourcePage); got != tt.want {
|
||||
t.Fatalf("NormalizeOrderSourcePage(%q) = %q, want %q", tt.sourcePage, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRechargeRequestCreateOrderCopiesAttribution(t *testing.T) {
|
||||
request := &RechargeRequest{
|
||||
SourcePage: " custom_source_page ",
|
||||
SourceRef: "source-ref",
|
||||
VideoID: "video-id",
|
||||
ActivityID: "activity-id",
|
||||
ExperimentID: "experiment-id",
|
||||
ExperimentVariant: "A",
|
||||
SessionID: "session-id",
|
||||
}
|
||||
|
||||
order, _, err := request.CreateOrder(&usermod.User{UID: 123}, ua.UA{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrder() error = %v", err)
|
||||
}
|
||||
if order.SourcePage != "custom_source_page" {
|
||||
t.Fatalf("SourcePage = %q, want custom_source_page", order.SourcePage)
|
||||
}
|
||||
if order.SourceRef != request.SourceRef ||
|
||||
order.VideoID != request.VideoID ||
|
||||
order.ActivityID != request.ActivityID ||
|
||||
order.ExperimentID != request.ExperimentID ||
|
||||
order.ExperimentVariant != request.ExperimentVariant ||
|
||||
order.SessionID != request.SessionID {
|
||||
t.Fatalf("order attribution fields were not copied: %+v", order)
|
||||
}
|
||||
|
||||
legacyOrder, _, err := (&RechargeRequest{}).CreateOrder(&usermod.User{UID: 456}, ua.UA{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("legacy CreateOrder() error = %v", err)
|
||||
}
|
||||
if legacyOrder.SourcePage != OrderSourcePageUnknown {
|
||||
t.Fatalf("legacy SourcePage = %q, want %q", legacyOrder.SourcePage, OrderSourcePageUnknown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRechargeRequestCreateOrderUsesCurrentRequestSysType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userSysType string
|
||||
requestSysType string
|
||||
want string
|
||||
}{
|
||||
{name: "android request overrides historical ios", userSysType: "ios", requestSysType: "android", want: "android"},
|
||||
{name: "ios request overrides historical android", userSysType: "android", requestSysType: "IOS-18", want: "ios"},
|
||||
{name: "missing request falls back to user", userSysType: "ios", want: "ios"},
|
||||
{name: "missing request and user use default", want: "android"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
order, _, err := (&RechargeRequest{}).CreateOrder(
|
||||
&usermod.User{UID: 123, SysType: tt.userSysType},
|
||||
ua.UA{SysType: tt.requestSysType},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOrder() error = %v", err)
|
||||
}
|
||||
if order.DevType != tt.want {
|
||||
t.Fatalf("DevType = %q, want %q", order.DevType, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeAttributionPreservesNonExperimentSession(t *testing.T) {
|
||||
request := RechargeRequest{
|
||||
SourcePage: " H5_ACTIVITY ",
|
||||
ActivityID: " activity-1 ",
|
||||
ExperimentVariant: "A",
|
||||
SessionID: " session-1 ",
|
||||
}
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() error = %v", err)
|
||||
}
|
||||
if request.SourcePage != OrderSourcePageH5Activity {
|
||||
t.Fatalf("SourcePage = %q, want %q", request.SourcePage, OrderSourcePageH5Activity)
|
||||
}
|
||||
if request.ActivityID != "activity-1" {
|
||||
t.Fatalf("ActivityID = %q", request.ActivityID)
|
||||
}
|
||||
if request.ExperimentVariant != "" {
|
||||
t.Fatalf("ExperimentVariant = %q, want empty without experimentId", request.ExperimentVariant)
|
||||
}
|
||||
if request.SessionID != "session-1" {
|
||||
t.Fatalf("SessionID = %q, want session-1", request.SessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttributionSourcePageLength(t *testing.T) {
|
||||
request := RechargeRequest{SourcePage: strings.Repeat("页", maxAttributionValueLength)}
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() rejected 128 characters: %v", err)
|
||||
}
|
||||
|
||||
request.SourcePage = strings.Repeat("页", maxAttributionValueLength+1)
|
||||
if err := request.validateAttribution(123); err == nil {
|
||||
t.Fatal("validateAttribution() should reject sourcePage longer than 128 characters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttributionDramaPaywallGoldRechargeAllowsSourceOnly(t *testing.T) {
|
||||
request := RechargeRequest{
|
||||
BuyType: commod.BuyGold,
|
||||
SourcePage: OrderSourcePageDramaPaywall,
|
||||
}
|
||||
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() rejected drama paywall gold recharge: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttributionDramaPaywallProductAllowsNoContext(t *testing.T) {
|
||||
request := RechargeRequest{
|
||||
BuyType: commod.BuyProduct,
|
||||
SourcePage: OrderSourcePageDramaPaywall,
|
||||
}
|
||||
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() rejected drama product without context: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttributionDramaPaywallProductAllowsPartialContext(t *testing.T) {
|
||||
request := RechargeRequest{
|
||||
BuyType: commod.BuyProduct,
|
||||
SourcePage: OrderSourcePageDramaPaywall,
|
||||
SourceRef: "legacy-client-value",
|
||||
SessionID: "session-1",
|
||||
CheckoutContextID: "checkout-context-1",
|
||||
}
|
||||
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() rejected partial drama context: %v", err)
|
||||
}
|
||||
if request.SourceRef != request.CheckoutContextID {
|
||||
t.Fatalf("SourceRef = %q, want checkoutContextId %q", request.SourceRef, request.CheckoutContextID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttributionDramaPaywallSessionDoesNotRequireContext(t *testing.T) {
|
||||
request := RechargeRequest{
|
||||
BuyType: commod.BuyProduct,
|
||||
SourcePage: OrderSourcePageDramaPaywall,
|
||||
SourceRef: "legacy-client-value",
|
||||
SessionID: "session-1",
|
||||
}
|
||||
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() rejected experiment session without drama context: %v", err)
|
||||
}
|
||||
if request.SourceRef != "" {
|
||||
t.Fatalf("SourceRef = %q, want empty without checkoutContextId", request.SourceRef)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttributionDramaPaywallIgnoresOversizedCheckoutContext(t *testing.T) {
|
||||
request := RechargeRequest{
|
||||
BuyType: commod.BuyProduct,
|
||||
SourcePage: OrderSourcePageDramaPaywall,
|
||||
CheckoutContextID: strings.Repeat("x", maxAttributionValueLength+1),
|
||||
}
|
||||
|
||||
if err := request.validateAttribution(123); err != nil {
|
||||
t.Fatalf("validateAttribution() rejected oversized optional drama context: %v", err)
|
||||
}
|
||||
if request.CheckoutContextID != "" || request.SourceRef != "" {
|
||||
t.Fatalf("optional drama context was not cleared: checkout=%q sourceRef=%q", request.CheckoutContextID, request.SourceRef)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package rechargeser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/vipcardexperimentmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const maxAttributionValueLength = 128
|
||||
|
||||
func (r *RechargeRequest) normalizeAttribution() {
|
||||
r.SourcePage = NormalizeOrderSourcePage(r.SourcePage)
|
||||
r.SourceRef = strings.TrimSpace(r.SourceRef)
|
||||
r.VideoID = strings.TrimSpace(r.VideoID)
|
||||
r.ActivityID = strings.TrimSpace(r.ActivityID)
|
||||
r.ExperimentID = strings.TrimSpace(r.ExperimentID)
|
||||
r.ExperimentVariant = strings.ToUpper(strings.TrimSpace(r.ExperimentVariant))
|
||||
r.SessionID = strings.TrimSpace(r.SessionID)
|
||||
r.CheckoutContextID = strings.TrimSpace(r.CheckoutContextID)
|
||||
}
|
||||
|
||||
func (r *RechargeRequest) validateAttribution(uid uint64) error {
|
||||
r.normalizeAttribution()
|
||||
if utf8.RuneCountInString(r.SourcePage) > maxAttributionValueLength {
|
||||
return fmt.Errorf("sourcePage must not exceed %d characters", maxAttributionValueLength)
|
||||
}
|
||||
if r.SourcePage == OrderSourcePageDramaPaywall {
|
||||
if r.BuyType != commod.BuyGold && r.BuyType != commod.BuyProduct {
|
||||
return fmt.Errorf("buyType must be 1 or 4 for DRAMA_PAYWALL")
|
||||
}
|
||||
// sourceRef is the generic order attribution field. For drama paywall
|
||||
// orders, derive it from the backend-issued checkout context instead of
|
||||
// requiring clients to submit the same identifier twice. The remaining
|
||||
// drama fields are attribution-only and must not block a real payment.
|
||||
if len(r.CheckoutContextID) > maxAttributionValueLength {
|
||||
r.CheckoutContextID = ""
|
||||
}
|
||||
r.SourceRef = r.CheckoutContextID
|
||||
}
|
||||
values := map[string]string{
|
||||
"sourceRef": r.SourceRef,
|
||||
"videoId": r.VideoID,
|
||||
"activityId": r.ActivityID,
|
||||
"experimentId": r.ExperimentID,
|
||||
"experimentVariant": r.ExperimentVariant,
|
||||
"sessionId": r.SessionID,
|
||||
"checkoutContextId": r.CheckoutContextID,
|
||||
}
|
||||
for name, value := range values {
|
||||
if len(value) > maxAttributionValueLength {
|
||||
return fmt.Errorf("%s must not exceed %d characters", name, maxAttributionValueLength)
|
||||
}
|
||||
}
|
||||
if r.ExperimentID == "" {
|
||||
r.ExperimentVariant = ""
|
||||
return nil
|
||||
}
|
||||
if r.ExperimentVariant == "" || r.SessionID == "" {
|
||||
return fmt.Errorf("experimentVariant and sessionId are required with experimentId")
|
||||
}
|
||||
experiment, err := vipcardexperimentmod.FindByExperimentID(r.ExperimentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if experiment == nil {
|
||||
return fmt.Errorf("experiment does not exist")
|
||||
}
|
||||
config, ok := experiment.ConfigFor(r.ExperimentVariant)
|
||||
if !ok {
|
||||
return fmt.Errorf("experimentVariant must be A or B")
|
||||
}
|
||||
if assigned := experiment.Assign(uid); assigned != r.ExperimentVariant {
|
||||
return fmt.Errorf("experimentVariant does not match user assignment")
|
||||
}
|
||||
if !containsObjectID(config.ProductIDs, r.ProductID) {
|
||||
return fmt.Errorf("productID does not belong to experiment variant")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsObjectID(ids []primitive.ObjectID, target primitive.ObjectID) bool {
|
||||
for _, id := range ids {
|
||||
if id == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user