package activityser import ( "context" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "time" "91porn-server/app/appg" "91porn-server/app/proto" "91porn-server/app/service/activityclient" "91porn-server/app/service/customerser" "91porn-server/app/service/productser" "91porn-server/app/service/rechargeser" "91porn-server/common" "91porn-server/common/constant" "91porn-server/common/crypt" "91porn-server/common/log" "91porn-server/common/rchgutil" "91porn-server/common/stderr" "91porn-server/middleware/ua" "91porn-server/models/commod" "91porn-server/models/v/advanceordermod" "91porn-server/models/v/currencymod" "91porn-server/models/v/productmod" "91porn-server/models/v/rchgamtmod" "91porn-server/models/v/sourcemod" "91porn-server/models/v/usermod" "91porn-server/models/v/walletmod" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) // PayChannel 支付渠道(精简字段) type PayChannel struct { Type string `json:"type"` TypeName string `json:"typeName"` } // ActivityCurrencyItem 活动服金币充值列表项 type ActivityCurrencyItem struct { ID primitive.ObjectID `json:"id"` Coin int64 `json:"coin"` Money int64 `json:"money"` Name string `json:"typeName"` GiveCoin int64 `json:"giveCoin"` RechargeType []PayChannel `json:"rechargeType"` } func toPayChannels(src []rchgamtmod.PayChannelRes) []PayChannel { channels := make([]PayChannel, 0, len(src)) for _, ch := range src { channels = append(channels, PayChannel{ Type: ch.Type, TypeName: ch.TypeName, }) } return channels } // GetCurrencyList 获取金币充值列表 func GetCurrencyList(ctx context.Context) ([]*ActivityCurrencyItem, stderr.Code) { currencys, err := currencymod.List(commod.Gold) if err != nil { log.ErrorX(ctx, "活动服-获取金币列表异常", log.E(err)) return nil, stderr.ErrNetWorkBusy } removeRepeat := make(map[int64]struct{}) var moneys []string for _, cur := range currencys { if _, ok := removeRepeat[cur.Price]; ok { continue } removeRepeat[cur.Price] = struct{}{} moneys = append(moneys, rechargeser.FenToYuan(cur.Price)) } payReq := rchgutil.GainPayTypeReq{Money: moneys} bc, err := payReq.GetPayType() if err != nil { log.ErrorX(ctx, "活动服-获取支付通道列表异常", log.E(err)) return nil, stderr.ErrNetWorkBusy } data := make([]*ActivityCurrencyItem, 0, len(currencys)) for _, v := range currencys { if v.Price < 100 { continue } rechargeType := rechargeser.GetPayChannelDetails_New(v.Price, bc, 0) if len(rechargeType) <= 0 { continue } data = append(data, &ActivityCurrencyItem{ ID: v.ID, Coin: v.Coins, Money: v.Price, Name: v.Name, GiveCoin: v.GiveGold, RechargeType: toPayChannels(rechargeType), }) } return data, stderr.Success } // CardCategory 会员卡分类 type CardCategory int const ( DefaultCard CardCategory = 0 PreSaleCard CardCategory = 1 NewbieCard CardCategory = 2 ) // VipCardInfo 活动服会员卡信息 type VipCardInfo struct { Id string `json:"id"` Category CardCategory `json:"category"` VipGrade string `json:"vipGrade"` Name string `json:"name"` Desc string `json:"desc"` PreMoney int64 `json:"preMoney"` Money int64 `json:"money"` CanUseCoin bool `json:"canUseCoin"` DayCount int64 `json:"dayCount"` GiveCoin int64 `json:"giveCoin"` Sort int32 `json:"sort"` Image string `json:"image"` Rights []RightInfo `json:"rights"` RechargeType []PayChannel `json:"rechargeType"` ActivityStartAt time.Time `json:"activityStartAt"` ActivityEndAt time.Time `json:"activityEndAt"` CanUpgrade bool `json:"canUpgrade"` PreSaleStep int `json:"preSaleStep"` // 预售卡阶段 0-预付阶段 1-尾款阶段 2-尾款付款完成(结束) } // RightInfo 权益信息 type RightInfo struct { Name string `json:"name"` Desc string `json:"desc"` Image string `json:"image"` } // VipDeduct 会员卡当前可用抵扣(vip/list 按抵扣后金额匹配支付通道用) type VipDeduct struct { ProductID string // 会员卡ID DeductAmount int64 // 券面额(分) } // GetProductList 获取会员卡列表。 // deducts 非空时,对携带抵扣的普通会员卡按"抵扣后有效金额"重新匹配支付通道(rechargeType), // money/preMoney 仍返回原价(避免与下单预览重复扣减)。 func GetProductList(ctx context.Context, uid uint64, deducts []VipDeduct) ([]VipCardInfo, error) { u, err := usermod.FindUserByUID(uid) if err != nil { return nil, fmt.Errorf("查询用户异常: %w", err) } if u == nil { return nil, fmt.Errorf("用户不存在: %d", uid) } productList, err := rechargeser.New_ProductList(uid, u.SysType, true, 0) if err != nil { return nil, err } // 抵扣券:按抵扣后有效金额预匹配支付通道(命中的卡覆盖 rechargeType) deductedChans := buildDeductedRechargeTypes(productList, deducts) var cards []VipCardInfo for _, group := range productList { for _, item := range group.List { p := item.Product card := VipCardInfo{ Id: p.ID.Hex(), VipGrade: fmt.Sprintf("%d", p.VipLevel), Name: p.Name, Desc: p.Desc, PreMoney: p.OriginalPrice * 10, Money: p.DiscountedPrice * 10, CanUseCoin: p.IsAmountPay, DayCount: int64(p.Duration), GiveCoin: p.GiveCoin, Sort: int32(p.Sort), Image: p.BGImg, CanUpgrade: p.IsUpgrade, } switch p.ProductType { case commod.AdvanceCard: card.Category = PreSaleCard advOrder, _ := advanceordermod.IsExist(bson.M{"productID": p.ID, "uid": uid}) // advOrder.Status 取值(advanceordermod): // 0 DEFAULT / 1 AdvanceProcessing(预付中) / 2 AdvanceSUCCESS(预付成功) // 3 BalanceProcessing(尾款中) / 4 BalanceSUCCESS(尾款成功) if advOrder == nil || advOrder.Status == advanceordermod.DEFAULT || advOrder.Status == advanceordermod.AdvanceProcessing { // 预付阶段:尚未下单 / 预付中 card.PreSaleStep = 0 card.ActivityStartAt = p.ActivityTime card.ActivityEndAt = p.EndTime card.Money = p.AdvanceAmount * 10 } else if advOrder.Status == advanceordermod.AdvanceSUCCESS || advOrder.Status == advanceordermod.BalanceProcessing { // 尾款阶段:预付成功 / 尾款支付中 card.PreSaleStep = 1 card.ActivityStartAt = p.StartTime card.ActivityEndAt = p.EndTime card.Money = p.BalanceAmount * 10 card.Image = p.NewBgImg } else if advOrder.Status == advanceordermod.BalanceSUCCESS { // 尾款付款完成(结束) card.PreSaleStep = 2 } case commod.NEWUSERCard: card.Category = NewbieCard card.ActivityStartAt = u.CreatedAt card.ActivityEndAt = card.ActivityStartAt.Add(time.Duration(p.ShowCountdownTime) * time.Hour) if time.Now().After(card.ActivityEndAt) { continue } default: card.Category = DefaultCard } if card.Image == "" { card.Image = p.BGImg } rights := make([]RightInfo, 0, len(p.NewPrivilege)) for _, pr := range p.NewPrivilege { rights = append(rights, RightInfo{ Name: pr.Name, Desc: pr.Desc, Image: pr.Image, }) } card.Rights = rights card.RechargeType = toPayChannels(item.RechargeType) // 抵扣券:按抵扣后金额匹配的支付通道覆盖原价通道 if chans, ok := deductedChans[p.ID.Hex()]; ok { card.RechargeType = chans } cards = append(cards, card) } } return cards, nil } // minDeductPayFen 抵扣后最低实付(分),与下单折价封顶保持一致 const minDeductPayFen = 1 // buildDeductedRechargeTypes 依据 deducts 计算各会员卡"抵扣后有效金额"对应的支付通道。 // 支付通道按支付金额匹配(如微信要求达标金额),券抵扣后实付变小可能改变可用通道, // 故对携带抵扣的普通会员卡(非预售卡)按 min(券面额, 原价-最低实付) 折后金额重新匹配 rechargeType。 // 返回 productIdHex -> 支付通道;deducts 为空或无命中时返回 nil,调用方保留原价通道。 func buildDeductedRechargeTypes(productList []proto.ProductList, deducts []VipDeduct) map[string][]PayChannel { deductMap := make(map[string]int64, len(deducts)) for _, d := range deducts { if d.ProductID != "" && d.DeductAmount > 0 { deductMap[d.ProductID] = d.DeductAmount } } if len(deductMap) == 0 { return nil } type effEntry struct { hex string effFen int64 } var entries []effEntry moneySet := make(map[string]struct{}) var moneys []string for _, group := range productList { for _, item := range group.List { p := item.Product if p.ProductType == commod.AdvanceCard { continue // 预售卡分阶段付款,不参与抵扣 } deduct, ok := deductMap[p.ID.Hex()] if !ok { continue } originFen := p.DiscountedPrice * 10 maxDeduct := originFen - minDeductPayFen if maxDeduct <= 0 { continue } if deduct > maxDeduct { deduct = maxDeduct } effFen := originFen - deduct entries = append(entries, effEntry{hex: p.ID.Hex(), effFen: effFen}) y := rechargeser.FenToYuan(effFen) if _, exist := moneySet[y]; !exist { moneySet[y] = struct{}{} moneys = append(moneys, y) } } } if len(entries) == 0 { return nil } bc, err := (&rchgutil.GainPayTypeReq{Money: moneys}).GetPayTypeFromCache() if err != nil { log.Warn("活动服-抵扣后支付通道查询失败", log.Any("moneys", moneys), log.E(err)) } out := make(map[string][]PayChannel, len(entries)) for _, e := range entries { out[e.hex] = toPayChannels(rechargeser.GetPayChannelDetails_New(e.effFen, bc, 0)) } return out } // UserBalance 用户余额信息 type UserBalance struct { Gold int64 `json:"gold"` // 金币余额 Integral int64 `json:"integral"` // 积分余额 LotteryFreeTimes int64 `json:"lotteryFreeTimes"` // 抽奖免费次数 VipLevel int `json:"vipLevel,omitempty"` // VIP 等级;用户未开通或已过期时不下发 VipExpireDate time.Time `json:"vipExpireDate,omitempty"` // VIP 截止时间;用户未开通或已过期时不下发 PromotionCode string `json:"promotionCode,omitempty"` // 邀请码 PromoteURL string `json:"promoteURL,omitempty"` // 邀请链接(已拼好 PromotionField + 邀请码) } // GetUserBalance 获取用户金币/积分余额及 VIP 信息(VIP 已过期时不下发 VIP 字段) func GetUserBalance(uid uint64) (*UserBalance, error) { w, err := walletmod.GetWallet(uid) if err != nil { return nil, fmt.Errorf("查询钱包异常: %w", err) } res := &UserBalance{} if w != nil { res.Gold = w.Amount res.Integral = w.Integral res.LotteryFreeTimes = w.LotteryTimes } // 用户信息独立取数,单点失败不影响余额下发 u, uerr := usermod.FindUserByUID(uid) if uerr != nil { log.Warn("活动服-查询用户信息失败", log.Any("uid", uid), log.E(uerr)) return res, nil } if u == nil { return res, nil } if u.IsVIP(time.Now()) { res.VipLevel = u.VipLevel res.VipExpireDate = u.VipExpireDate } // 邀请码 + 邀请链接(与 /api/app/mine/info 的 promoteURL 拼接方式一致) res.PromotionCode = u.PromCode if base := sourcemod.GetRandomPromotionURL(); base != "" && u.PromCode != "" { res.PromoteURL = common.BindUrl(base, constant.PromotionField+u.PromCode) } return res, nil } func convertBuyType(actBuyType int) commod.BuyType { switch actBuyType { case 1: return commod.BuyGold case 2: return commod.BuyProduct default: return commod.BuyType(actBuyType) } } type RechargeAttribution struct { ActivityID string ExperimentID string ExperimentVariant string SessionID string } // CreateRechargeOrder 创建充值订单。活动入口的 sourcePage 由服务端固定, // 避免调用方伪造其他来源;其余实验字段仍走统一下单校验。 func CreateRechargeOrder( ctx context.Context, uid uint64, rechargeType, productID string, buyType int, ip string, attribution RechargeAttribution, couponID string, deductAmount int64, ) (payUrl, mode string, err error) { pid, err := primitive.ObjectIDFromHex(productID) if err != nil { return "", "", fmt.Errorf("invalid productId: %w", err) } u, err := usermod.FindUserByUID(uid) if err != nil { return "", "", fmt.Errorf("查询用户异常: %w", err) } if u == nil { return "", "", fmt.Errorf("用户不存在: %d", uid) } realBuyType := convertBuyType(buyType) in := &rechargeser.RechargeRequest{ UID: uid, IP: ip, RechargeType: rechargeType, ProductID: pid, BuyType: realBuyType, SourcePage: rechargeser.OrderSourcePageH5Activity, SourceRef: attribution.ActivityID, ActivityID: attribution.ActivityID, ExperimentID: attribution.ExperimentID, ExperimentVariant: attribution.ExperimentVariant, SessionID: attribution.SessionID, } // 抵扣券作为活动服附加对象单独传入,不混入客户端下单请求体 var deduct *rechargeser.ActivityDeduct if couponID != "" && deductAmount > 0 { deduct = &rechargeser.ActivityDeduct{CouponID: couponID, DeductAmount: deductAmount} } return rechargeser.Recharge(ctx, in, ua.UA{SysType: u.SysType}, deduct) } // BuyCoinProduct 金币购买商品 func BuyCoinProduct(ctx context.Context, uid uint64, productID string, sysType string) (stderr.Code, error) { pid, err := primitive.ObjectIDFromHex(productID) if err != nil { return stderr.ErrParamError, fmt.Errorf("invalid productId: %w", err) } p, err := productmod.FindProduct(pid, sysType) if err != nil || p == nil { return stderr.ErrParamError, fmt.Errorf("商品不存在: %s", productID) } if !p.IsAmountPay { return stderr.ErrParamError, fmt.Errorf("该商品不支持金币购买") } code := productser.Buy(uid, p.ProductType, pid, primitive.NilObjectID, primitive.NilObjectID, 0, "", sysType, "", 0, false, ua.UA{SysType: sysType}, "", productser.VIPExperimentAttribution{}) if code != stderr.Success { return code, fmt.Errorf("购买失败") } return stderr.Success, nil } // GenerateSecretKey 生成 AES-256 密钥 func GenerateSecretKey() (string, error) { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return "", err } return base64.StdEncoding.EncodeToString(key), nil } // DecryptSign AES-CBC 解密签名载荷 func DecryptSign(secretKeyBase64 string, signBase64 string) (*activityclient.SignPayload, error) { secretKey, err := base64.StdEncoding.DecodeString(secretKeyBase64) if err != nil { return nil, fmt.Errorf("decode secretKey failed: %w", err) } signBytes, err := base64.URLEncoding.DecodeString(signBase64) if err != nil { signBytes, err = base64.StdEncoding.DecodeString(signBase64) if err != nil { return nil, fmt.Errorf("decode sign failed: %w", err) } } plaintext, err := crypt.CoreAesDecrypt(signBytes, string(secretKey)) if err != nil { return nil, fmt.Errorf("decrypt sign failed: %w", err) } var payload activityclient.SignPayload if err := json.Unmarshal([]byte(plaintext), &payload); err != nil { return nil, fmt.Errorf("parse sign payload failed: %w", err) } return &payload, nil } // HmacSHA256 用于回调签名 func HmacSHA256(secretKeyBase64 string, data []byte) (string, error) { keyBytes, err := base64.StdEncoding.DecodeString(secretKeyBase64) if err != nil { return "", fmt.Errorf("decode secretKey failed: %w", err) } h := hmac.New(sha256.New, keyBytes) h.Write(data) return hex.EncodeToString(h.Sum(nil)), nil } // BuildSignedURL 生成带 sign 的活动服请求 URL func BuildSignedURL(userId, nickname, avatar string) (signedURL, sign string, err error) { conf := appg.Conf.ActivityServer baseURL := activityclient.GetActivityDomain() if baseURL == "" { return "", "", fmt.Errorf("活动服域名未获取,请检查配置") } now := time.Now() payload := &activityclient.SignPayload{ AppId: conf.AppId, UserId: userId, Nickname: nickname, Avatar: avatar, Ts: now.Unix(), } sign, err = activityclient.EncryptSign(conf.SecretKey, payload) if err != nil { return "", "", fmt.Errorf("encrypt sign failed: %w", err) } signedURL = fmt.Sprintf("%s?appId=%s&sign=%s", baseURL, conf.AppId, sign) return signedURL, sign, nil } // AppInfoResp 应用信息 type AppInfoResp struct { CustomerServiceUrl string `json:"customerServiceUrl"` } // GetAppInfo 获取应用信息 func GetAppInfo(ctx context.Context, uid uint64) (*AppInfoResp, error) { resp, err := customerser.GetUrl(uid, ua.UA{}) if err != nil { return nil, fmt.Errorf("获取客服地址失败: %w", err) } return &AppInfoResp{ CustomerServiceUrl: resp.Data.Url, }, nil } // BuildLoginCurl 生成活动服登录接口的 curl 命令(调试用) func BuildLoginCurl(userId, nickname, avatar string) (string, error) { conf := appg.Conf.ActivityServer activityHost := activityclient.GetActivityDomain() if activityHost == "" { return "", fmt.Errorf("活动服域名未获取,请检查配置") } now := time.Now() payload := &activityclient.SignPayload{ AppId: conf.AppId, UserId: userId, Nickname: nickname, Avatar: avatar, Ts: now.Unix(), } sign, err := activityclient.EncryptSign(conf.SecretKey, payload) if err != nil { return "", fmt.Errorf("encrypt sign failed: %w", err) } payloadJSON, _ := json.MarshalIndent(payload, "", " ") result := fmt.Sprintf("AppId: %s\nSecretKey: %s\n\n", conf.AppId, conf.SecretKey) result += fmt.Sprintf("--- Generated Sign ---\nsign: %s\n\n", sign) result += fmt.Sprintf("payload: %s\n\n", string(payloadJSON)) result += fmt.Sprintf("--- CURL ---\ncurl -X POST '%s/api/app/index/login' \\\n -H 'Content-Type: application/json' \\\n -d '{\"appId\":\"%s\",\"sign\":\"%s\"}'", activityHost, conf.AppId, sign) return result, nil }