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 }