@@ -0,0 +1,414 @@
|
||||
package active2023ser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/active2023mod"
|
||||
"91porn-server/models/v/goldextramod"
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/videocoupon"
|
||||
"91porn-server/models/v/videodiscountmod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func Lottery(userInfo *usermod.User, req LotteryReq) error {
|
||||
wallet, err := walletmod.GetWallet(userInfo.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wallet == nil {
|
||||
return errors.New("钱包信息未找到")
|
||||
}
|
||||
isLottery := true // 默认为抽奖
|
||||
desc := "2023新春抽奖"
|
||||
if req.Gold == 0 && req.Count == 0 { // 不使用免费抽奖次数又不消耗金币, 则为领取保底福利
|
||||
isLottery = false
|
||||
desc = "2023新春福利"
|
||||
}
|
||||
return appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var transactionLogs []txnmod.TransactionLog // 用户所有抽奖花费金, 获奖金币以及游戏币发放记录
|
||||
if isLottery {
|
||||
if req.Gold == 0 { // 使用免费抽奖次数
|
||||
if err = active2023mod.UseLotteryCountFree(t, int64(userInfo.UID), int64(req.Count)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else { // 使用金币抽奖
|
||||
if err = walletmod.DebitAmountDefault(t, int64(userInfo.UID), int64(req.Gold)); err != nil {
|
||||
return err
|
||||
}
|
||||
wallet.Amount -= int64(req.Gold)
|
||||
if err = active2023mod.UserLotteryCountPay(t, int64(userInfo.UID), int64(req.Count)); err != nil {
|
||||
return err
|
||||
}
|
||||
transactionType := txnmod.Active2023Cost
|
||||
transactionLogs = []txnmod.TransactionLog{txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: userInfo.UID,
|
||||
Amount: -int64(req.Gold),
|
||||
ActualAmount: -float64(req.Gold),
|
||||
TranType: transactionType.Key(),
|
||||
TranTypeInt: int64(transactionType),
|
||||
Desc: fmt.Sprintf("%s-金币[%d个]", transactionType.Key(), req.Gold),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
}}
|
||||
}
|
||||
}
|
||||
var totalGold, totalVipDates uint64 // totalGold: 用户总获奖金币数; totalVipDates: 用户总获奖vip天数
|
||||
var pzs []active2023mod.Prize // 用户所有奖励, 用以存库
|
||||
var videoCoupons []videocoupon.UserGoldVideoCoupon // 用户所有获视频折扣券
|
||||
var goldExtras []goldextramod.GoldExtra // 用户所有获奖金币加购券
|
||||
var videoDiscounts []productmod.Product // 用户所有获奖视频折扣券
|
||||
var vipCards []productmod.Product // 用户所有获奖vip卡
|
||||
now := time.Now()
|
||||
pzs = make([]active2023mod.Prize, len(req.Prizes))
|
||||
for i, pz := range req.Prizes {
|
||||
pzs[i] = active2023mod.Prize{
|
||||
UID: int64(userInfo.UID),
|
||||
Type: int64(pz.Type),
|
||||
Name: pz.Name,
|
||||
Unit: int64(pz.Price),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
switch pz.Type {
|
||||
case active2023mod.PrizeTypeGold: // 金币
|
||||
if pz.Price <= 0 {
|
||||
return errors.New("金币数量设置错误")
|
||||
}
|
||||
totalGold += pz.Price
|
||||
transactionType := txnmod.Active2023Reward
|
||||
wallet.Amount += int64(pz.Price)
|
||||
transactionLogs = append(transactionLogs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: userInfo.UID,
|
||||
Amount: int64(pz.Price),
|
||||
ActualAmount: float64(pz.Price),
|
||||
TranType: transactionType.Key(),
|
||||
TranTypeInt: int64(transactionType),
|
||||
Desc: fmt.Sprintf("%s-金币[%d个]", transactionType.Key(), pz.Price),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
case active2023mod.PrizeTypeGameGold: // 游戏币
|
||||
return errors.New("瓦力游戏已下架")
|
||||
case active2023mod.PrizeTypeProduct: // 现有产品
|
||||
if pz.Param1 == "" {
|
||||
return errors.New("产品id为空")
|
||||
}
|
||||
productID, err := primitive.ObjectIDFromHex(pz.Param1)
|
||||
if err != nil {
|
||||
return errors.New("无效的产品id")
|
||||
}
|
||||
p, err := productmod.FindProduct(productID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if p == nil {
|
||||
return errors.New("产品不存在")
|
||||
}
|
||||
pzs[i].ProductID = &productID
|
||||
switch p.ProductType {
|
||||
case commod.VideoDiscount: // 视频折扣卡
|
||||
videoDiscounts = append(videoDiscounts, *p)
|
||||
case commod.VIP: // 会员卡
|
||||
vipCards = append(vipCards, *p)
|
||||
default:
|
||||
return errors.New("不支持的产品类型")
|
||||
}
|
||||
case active2023mod.PrizeTypeVIPDate: // 会员日期
|
||||
if pz.Price <= 0 {
|
||||
return errors.New("vip天数设置错误")
|
||||
}
|
||||
transactionType := txnmod.Active2023Reward
|
||||
transactionLogs = append(transactionLogs, txnmod.TransactionLog{
|
||||
UID: userInfo.UID,
|
||||
Amount: 0,
|
||||
ActualAmount: 0,
|
||||
TranType: transactionType.Key(),
|
||||
TranTypeInt: int64(transactionType),
|
||||
TransNo: primitive.NilObjectID,
|
||||
Desc: fmt.Sprintf("%s获得VIP天数%d天", desc, pz.Price),
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
totalVipDates += pz.Price
|
||||
case active2023mod.PrizeTypeGoldCoinBonus: // 金币加赠券
|
||||
if pz.Price <= 0 {
|
||||
return errors.New("金币加赠券面值设置错误")
|
||||
}
|
||||
expired := now.Add(time.Hour * 24 * 7) // 默认一周过期
|
||||
if pz.ValidDate > 0 {
|
||||
expired = now.Add(time.Hour * 24 * time.Duration(pz.ValidDate))
|
||||
}
|
||||
goldExtras = append(goldExtras, goldextramod.GoldExtra{
|
||||
Uid: userInfo.UID,
|
||||
Name: "金币加赠券",
|
||||
Amount: pz.Price,
|
||||
Expired: expired,
|
||||
Used: false,
|
||||
Desc: "充值加送金币",
|
||||
Source: desc,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
case active2023mod.PrizeTypeGoldWatch: // 金币观影券
|
||||
if pz.Price <= 0 {
|
||||
return errors.New("会员观影券面值设置错误")
|
||||
}
|
||||
videoCoupons = append(videoCoupons, videocoupon.UserGoldVideoCoupon{
|
||||
UID: userInfo.UID,
|
||||
Num: int(pz.Price),
|
||||
Source: videocoupon.GoldVideoCouponSource(desc),
|
||||
Used: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
case active2023mod.PrizeTypeInKind: // 实物奖励
|
||||
default:
|
||||
return fmt.Errorf("不支持的奖品类型: %d", pz.Type)
|
||||
}
|
||||
}
|
||||
needRefreshUserCache := false
|
||||
if totalGold > 0 {
|
||||
if wallet, err = walletmod.CreditAmount(t, int64(totalGold), userInfo.UID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if totalVipDates > 0 {
|
||||
vipLevel := 1 // 默认vip等级为1
|
||||
start := time.Now()
|
||||
discount := userInfo.PayVidDiscount
|
||||
if userInfo.VipExpireDate.After(start) { // 如果用户VIP未过期, 则VIP等级和视频折扣等级需要参考用户现在的情况
|
||||
start = userInfo.VipExpireDate
|
||||
if userInfo.VipLevel > vipLevel {
|
||||
vipLevel = userInfo.VipLevel
|
||||
}
|
||||
}
|
||||
vipExpireDate := start.Add(time.Hour * 24 * time.Duration(totalVipDates))
|
||||
sel := usermod.UserSelector{VipExpireDate: &vipExpireDate, VipLevel: &vipLevel, PayVidDiscount: &discount}
|
||||
if err := usermod.UpdateVIP(t, userInfo.UID, userInfo.VipExpireDate, sel); err != nil {
|
||||
return err
|
||||
}
|
||||
userInfo.VipExpireDate = vipExpireDate
|
||||
userInfo.VipLevel = vipLevel
|
||||
needRefreshUserCache = true
|
||||
}
|
||||
if len(videoCoupons) > 0 {
|
||||
if err = videocoupon.InsertManyTrans(t, videoCoupons); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(transactionLogs) > 0 {
|
||||
if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(goldExtras) > 0 {
|
||||
if err = goldextramod.InsertGoldExtras(t, &goldExtras); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(videoDiscounts) > 0 {
|
||||
if err = dealVideoDiscardCards(t, userInfo, videoDiscounts, desc); err != nil {
|
||||
return err
|
||||
}
|
||||
needRefreshUserCache = true
|
||||
}
|
||||
if len(vipCards) > 0 {
|
||||
if err = dealVipCards(t, userInfo, vipCards, desc); err != nil {
|
||||
return err
|
||||
}
|
||||
needRefreshUserCache = true
|
||||
}
|
||||
ids, err := active2023mod.InsertUserPrizes(t, &pzs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := active2023mod.InsertUserLottery(t, &active2023mod.UserLottery{
|
||||
UID: int64(userInfo.UID),
|
||||
Count: int64(req.Count),
|
||||
UseGold: int64(req.Gold),
|
||||
Prizes: ids,
|
||||
Desc: desc,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if needRefreshUserCache {
|
||||
go func() { _, _ = usermod.RefreshCacheAndGetUser(userInfo.UID) }()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func dealVideoDiscardCards(t *db.MongoTool, userInfo *usermod.User, products []productmod.Product, desc string) error {
|
||||
productIDsLen := len(products)
|
||||
if productIDsLen == 0 {
|
||||
return nil
|
||||
}
|
||||
videoDiscountLog, err := videodiscountmod.GetByUID(userInfo.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 最终过期时间
|
||||
var videoDiscountTotal int // 最终折扣
|
||||
var totalDates int // 总增加天数
|
||||
// 所有视频折扣卡都加入日志
|
||||
// 计算出最后用户的折扣和过期时间, 只更新一次用户配置
|
||||
if videoDiscountLog != (videodiscountmod.VideoDiscountLog{}) { // 当存在用户配置时, 需要跟用户配置做对比
|
||||
if videoDiscountLog.VideoDiscount > 0 && videoDiscountLog.VideoDiscount < 10 { // 如果用户有有效现有折扣
|
||||
videoDiscountTotal = videoDiscountLog.VideoDiscount // 默认折扣取用户折扣
|
||||
}
|
||||
}
|
||||
productHistorys := make([]prdcthsomod.ProductHistory, productIDsLen)
|
||||
transactionLogs := make([]txnmod.TransactionLog, productIDsLen)
|
||||
for i, p := range products {
|
||||
// 查找视频折扣卡详情
|
||||
if p.Duration <= 0 || p.VideoDiscount >= 10 || p.VideoDiscount <= 0 {
|
||||
return errors.New("视频折扣卡配置不正确")
|
||||
}
|
||||
// 添加记录
|
||||
historyID := primitive.NewObjectID()
|
||||
productHistorys[i] = prdcthsomod.ProductHistory{
|
||||
ID: historyID,
|
||||
UID: userInfo.UID,
|
||||
ProductID: p.ID,
|
||||
Name: p.Name,
|
||||
ProductType: commod.VideoDiscount,
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
ProductSnapShot: &p,
|
||||
}
|
||||
transactionLogs[i] = txnmod.TransactionLog{
|
||||
UID: userInfo.UID,
|
||||
Amount: -p.DiscountedPrice,
|
||||
ActualAmount: 0,
|
||||
TranType: txnmod.VideoDiscount.Key(),
|
||||
TranTypeInt: int64(txnmod.VideoDiscount),
|
||||
TransNo: historyID,
|
||||
Desc: desc + "获得-" + p.Name,
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
}
|
||||
if p.Duration <= 0 {
|
||||
p.Duration = 7 // 如果没有设置有效过期时间, 则默认为7天
|
||||
}
|
||||
totalDates += p.Duration
|
||||
videoDiscount := p.VideoDiscount // 默认折扣: 产品折扣
|
||||
// 最终取折扣值比较小的(折扣值越小说明享受的折扣越大)
|
||||
if videoDiscountTotal == 0 {
|
||||
videoDiscountTotal = videoDiscount
|
||||
} else if videoDiscountTotal > videoDiscount {
|
||||
videoDiscountTotal = videoDiscount
|
||||
}
|
||||
}
|
||||
// 写入历史纪录
|
||||
if err = prdcthsomod.InsertManyProductHistory(t, productHistorys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
start := now
|
||||
if videoDiscountLog != (videodiscountmod.VideoDiscountLog{}) { // 当存在用户配置时, 需要跟用户配置做对比
|
||||
if videoDiscountLog.Expiration.After(now) { // 如果用户现有折扣未过期, 则过期时间从用户现有折扣过期时间算起去加折扣卡有效期
|
||||
start = videoDiscountLog.Expiration
|
||||
}
|
||||
}
|
||||
expireAt := start.Add(time.Hour * 24 * time.Duration(totalDates))
|
||||
// 修改用户折扣
|
||||
set := videodiscountmod.EditSelector{
|
||||
UID: &userInfo.UID,
|
||||
Expiration: &expireAt,
|
||||
VideoDiscount: &videoDiscountTotal,
|
||||
}
|
||||
return videodiscountmod.Upsert(t, &set)
|
||||
}
|
||||
|
||||
// 抽奖中的vip卡只计算等级, 天数和视频折扣三种权益
|
||||
// 不计算vip卡配置中附赠的金币和游戏币
|
||||
func dealVipCards(t *db.MongoTool, userInfo *usermod.User, products []productmod.Product, desc string) error {
|
||||
productIDsLen := len(products)
|
||||
if productIDsLen == 0 {
|
||||
return nil
|
||||
}
|
||||
productHistorys := make([]prdcthsomod.ProductHistory, productIDsLen)
|
||||
transactionLogs := make([]txnmod.TransactionLog, productIDsLen)
|
||||
totalDuration := 0 // 所有VIP卡增加的天数总和
|
||||
lowestDiscount := 10 // 所有VIP卡最大打折, 10表示原价不打折
|
||||
hightestVipLevel := 1 // 最高VIP等级
|
||||
for i, p := range products {
|
||||
if p.PayVidDiscount <= 0 || p.PayVidDiscount > 10 {
|
||||
return errors.New("VIP卡配置不正确")
|
||||
}
|
||||
if lowestDiscount > p.PayVidDiscount {
|
||||
lowestDiscount = p.PayVidDiscount
|
||||
}
|
||||
if hightestVipLevel < p.VipLevel {
|
||||
hightestVipLevel = p.VipLevel
|
||||
}
|
||||
totalDuration += p.Duration
|
||||
// 添加记录
|
||||
historyID := primitive.NewObjectID()
|
||||
productHistorys[i] = prdcthsomod.ProductHistory{
|
||||
ID: historyID,
|
||||
UID: userInfo.UID,
|
||||
ProductID: p.ID,
|
||||
Name: p.Name,
|
||||
ProductType: commod.VIP,
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
ProductSnapShot: &p,
|
||||
}
|
||||
transactionLogs[i] = txnmod.TransactionLog{
|
||||
UID: userInfo.UID,
|
||||
Amount: -p.DiscountedPrice,
|
||||
ActualAmount: 0,
|
||||
TranType: txnmod.Active2023VIP.Key(),
|
||||
TranTypeInt: int64(txnmod.Active2023VIP),
|
||||
TransNo: historyID,
|
||||
Desc: desc + "获得-" + p.Name,
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
}
|
||||
}
|
||||
// 写入历史纪录
|
||||
if err := prdcthsomod.InsertManyProductHistory(t, productHistorys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
start := time.Now()
|
||||
if userInfo.VipExpireDate.After(start) { // 如果用户VIP未过期, 则VIP等级和视频折扣等级需要参考用户现在的情况
|
||||
start = userInfo.VipExpireDate
|
||||
}
|
||||
vipExpireDate := start.Add(time.Hour * 24 * time.Duration(totalDuration))
|
||||
if userInfo.VipLevel > hightestVipLevel {
|
||||
hightestVipLevel = userInfo.VipLevel
|
||||
}
|
||||
if (userInfo.PayVidDiscount > 0 && userInfo.PayVidDiscount < 10) && userInfo.PayVidDiscount < lowestDiscount {
|
||||
lowestDiscount = userInfo.PayVidDiscount
|
||||
}
|
||||
|
||||
sel := usermod.UserSelector{VipExpireDate: &vipExpireDate, VipLevel: &hightestVipLevel, PayVidDiscount: &lowestDiscount}
|
||||
if err := usermod.UpdateVIP(t, userInfo.UID, userInfo.VipExpireDate, sel); err != nil {
|
||||
return err
|
||||
}
|
||||
userInfo.VipExpireDate = vipExpireDate
|
||||
userInfo.VipLevel = hightestVipLevel
|
||||
userInfo.PayVidDiscount = lowestDiscount
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package active2023ser
|
||||
|
||||
type LotteryReq struct {
|
||||
Count uint64 `form:"count" json:"count"` // 抽奖次数
|
||||
Gold uint64 `form:"gold" json:"gold"` // 抽奖花费金币. 如果使用的是免费次数, 该值为0
|
||||
Prizes []Prize `form:"prizes" json:"prizes" binding:"required,min=1,max=10"` // 奖品情况
|
||||
}
|
||||
|
||||
type Prize struct {
|
||||
Param1 string `json:"param1"` // 附加字段. 目前仅用于视频折扣卡, 表示视频折扣卡id
|
||||
Type uint64 `form:"prizeType" json:"prizeType" binding:"required"` // 奖品类型
|
||||
Name string `form:"name" json:"name"` // 奖品名称. 非实物奖品可以不传
|
||||
Price uint64 `form:"price" json:"price"` // 奖品面值, 配合奖品类型代表具体奖品. 金币: 金币数量; 金币加购券: 加购券金币面值; 观影券: 观影券抵扣面值; 会员礼包: 会员延续天数
|
||||
ValidDate uint64 `form:"validDate" json:"validDate"` // 有效期(单位: 天)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package activityclient
|
||||
|
||||
// Start 启动活动服客户端的所有定时同步任务(域名 + 红包场次等)
|
||||
func Start() {
|
||||
startDomainSync()
|
||||
startHongbaoSync()
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package activityclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
// 抵扣券状态回调(与 activity-public-server /api/app/deduct/coupon/notify 约定一致)。
|
||||
// 券状态流转由本服双回调驱动:建单成功->used、支付成功->verified。
|
||||
const (
|
||||
DeductCouponStatusUsed = "used" // 建单成功: 券 active -> used(下单占用/已使用),需携 deductAmount
|
||||
DeductCouponStatusVerified = "verified" // 支付成功: 券 used -> verified(已核销),usedCount++
|
||||
DeductCouponStatusFailed = "failed" // 支付失败: 券 used -> active(释放)
|
||||
DeductCouponStatusClosed = "closed" // 订单关闭/超时: 券 used -> active(释放)
|
||||
)
|
||||
|
||||
const deductCouponNotifyPath = "/api/app/deduct/coupon/notify"
|
||||
|
||||
// deductCouponNotifyReq 支付结果回调请求体
|
||||
type deductCouponNotifyReq struct {
|
||||
CouponID string `json:"couponId"` // 下单时收到的券号,原样回传
|
||||
OrderID string `json:"orderId,omitempty"` // 本服订单号,活动服存档对账用
|
||||
Status string `json:"status"` // paid/failed/closed
|
||||
DeductAmount int64 `json:"deductAmount,omitempty"` // 实际抵扣金额(分),对账用
|
||||
}
|
||||
|
||||
// NotifyDeductCoupon 向活动服回调抵扣券支付结果。
|
||||
// 鉴权走方向B(X-Svc-*),密钥用 secretKey 原始字节(BuildSvcHeaders 已按此实现)。
|
||||
// 单次调用;活动服对 couponId 幂等,重复回调安全。
|
||||
// 返回 nil 表示活动服已受理(HTTP 200 且 body.code==200)。
|
||||
func NotifyDeductCoupon(ctx context.Context, couponID, orderID, status string, deductAmount int64) error {
|
||||
conf := appg.Conf.ActivityServer
|
||||
if conf.ApiUrl == "" || conf.AppId == "" {
|
||||
return fmt.Errorf("活动服配置缺失(apiUrl/appId)")
|
||||
}
|
||||
if couponID == "" {
|
||||
return fmt.Errorf("couponId 为空")
|
||||
}
|
||||
|
||||
body, err := json.Marshal(deductCouponNotifyReq{
|
||||
CouponID: couponID,
|
||||
OrderID: orderID,
|
||||
Status: status,
|
||||
DeductAmount: deductAmount,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal notify body failed: %w", err)
|
||||
}
|
||||
|
||||
// 签名针对下面实际发送的 body 字节;httputil 对 []byte 原样透传,两者一致
|
||||
headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "POST", deductCouponNotifyPath, "", body)
|
||||
if headers == nil {
|
||||
return fmt.Errorf("生成服务间签名头失败")
|
||||
}
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
resp, err := httputil.DefaultClientPostWithCtx(ctx, conf.ApiUrl+deductCouponNotifyPath, headers, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求活动服失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("活动服回调HTTP状态异常: %d, body: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
var out struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &out); err != nil {
|
||||
return fmt.Errorf("解析活动服回调响应失败: %w, body: %s", err, string(respBody))
|
||||
}
|
||||
if out.Code != 200 {
|
||||
return fmt.Errorf("活动服回调返回非成功: code=%d, msg=%s", out.Code, out.Msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyDeductCouponAsync 异步回调抵扣券状态,失败按 1s/3s/5s/10s 退避重试。
|
||||
// 独立 goroutine 执行,不阻塞下单/支付主流程;最终仍失败仅记录错误——
|
||||
// 活动服会在 used 券超 30 分钟未核销时由定时任务兜底释放回 active,不会丢一致性。
|
||||
func NotifyDeductCouponAsync(couponID, orderID, status string, deductAmount int64) {
|
||||
if couponID == "" {
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
backoffs := []time.Duration{0, time.Second, 3 * time.Second, 5 * time.Second, 10 * time.Second}
|
||||
var lastErr error
|
||||
for i, d := range backoffs {
|
||||
if d > 0 {
|
||||
time.Sleep(d)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
lastErr = NotifyDeductCoupon(ctx, couponID, orderID, status, deductAmount)
|
||||
cancel()
|
||||
if lastErr == nil {
|
||||
if i > 0 {
|
||||
log.Info("活动服-抵扣券回调重试成功",
|
||||
log.Any("couponId", couponID), log.Any("status", status), log.Any("attempt", i+1))
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Warn("活动服-抵扣券回调失败,待重试",
|
||||
log.Any("couponId", couponID), log.Any("orderId", orderID),
|
||||
log.Any("status", status), log.Any("attempt", i+1), log.E(lastErr))
|
||||
}
|
||||
log.Error("活动服-抵扣券回调最终失败(依赖活动服到期兜底释放)",
|
||||
log.Any("couponId", couponID), log.Any("orderId", orderID),
|
||||
log.Any("status", status), log.Any("deductAmount", deductAmount), log.E(lastErr))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package activityclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
const (
|
||||
ReplaceDomain = "https://activity.domain.com"
|
||||
)
|
||||
|
||||
// DomainItem 活动服域名项(与 activity-public-server 的 /api/app/domains 返回结构一致)
|
||||
type DomainItem struct {
|
||||
Domain string `json:"domain"` // 域名
|
||||
Remark string `json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// domainsResp 活动服域名接口响应
|
||||
type domainsResp struct {
|
||||
Code int `json:"code"`
|
||||
Data []DomainItem `json:"data"`
|
||||
}
|
||||
|
||||
var (
|
||||
cachedDomains []DomainItem
|
||||
domainsMu sync.RWMutex
|
||||
)
|
||||
|
||||
// GetActivityDomains 获取缓存的活动服域名列表
|
||||
func GetActivityDomains() []DomainItem {
|
||||
domainsMu.RLock()
|
||||
defer domainsMu.RUnlock()
|
||||
return cachedDomains
|
||||
}
|
||||
|
||||
// GetActivityDomain 随机获取一个可用的活动服域名
|
||||
func GetActivityDomain() string {
|
||||
domainsMu.RLock()
|
||||
defer domainsMu.RUnlock()
|
||||
n := len(cachedDomains)
|
||||
if n > 0 {
|
||||
return cachedDomains[rand.Intn(n)].Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ReplaceActivityDomain 替换活动服占位域名,并追加授权参数 appId、sign
|
||||
func ReplaceActivityDomain(rawURL string, user *usermod.User, w *walletmod.Wallet) string {
|
||||
if !strings.Contains(rawURL, ReplaceDomain) {
|
||||
return rawURL
|
||||
}
|
||||
domain := GetActivityDomain()
|
||||
if domain == "" {
|
||||
return rawURL
|
||||
}
|
||||
rawURL = strings.ReplaceAll(rawURL, ReplaceDomain, domain)
|
||||
|
||||
if user == nil {
|
||||
return rawURL
|
||||
}
|
||||
|
||||
conf := appg.Conf.ActivityServer
|
||||
now := time.Now()
|
||||
rechargeAmount := 0
|
||||
if w != nil {
|
||||
rechargeAmount = int(w.Consumption)
|
||||
}
|
||||
payload := &SignPayload{
|
||||
AppId: conf.AppId,
|
||||
UserId: strconv.FormatUint(user.UID, 10),
|
||||
Nickname: user.Name,
|
||||
Avatar: user.Portrait,
|
||||
Ts: now.Unix(),
|
||||
RegisteredAt: user.CreatedAt.Unix(),
|
||||
RechargeAmount: rechargeAmount,
|
||||
}
|
||||
sign, err := EncryptSign(conf.SecretKey, payload)
|
||||
if err != nil {
|
||||
log.Error("活动服-生成签名失败", log.E(err))
|
||||
return rawURL
|
||||
}
|
||||
|
||||
if strings.Contains(rawURL, "?") {
|
||||
rawURL += "&inner=1&appId=" + conf.AppId + "&sign=" + sign
|
||||
} else {
|
||||
rawURL += "?inner=1&appId=" + conf.AppId + "&sign=" + sign
|
||||
}
|
||||
return rawURL
|
||||
}
|
||||
|
||||
var hrefRe = regexp.MustCompile(`href="([^"]*)"`)
|
||||
|
||||
// ReplaceActivityDomainInHTML 替换富文本 HTML 中所有 href 里的活动服占位域名
|
||||
func ReplaceActivityDomainInHTML(htmlStr string, user *usermod.User, w *walletmod.Wallet) string {
|
||||
if !strings.Contains(htmlStr, ReplaceDomain) {
|
||||
return htmlStr
|
||||
}
|
||||
return hrefRe.ReplaceAllStringFunc(htmlStr, func(match string) string {
|
||||
url := match[6 : len(match)-1] // 去掉 href=" 前缀和 " 后缀
|
||||
return `href="` + ReplaceActivityDomain(url, user, w) + `"`
|
||||
})
|
||||
}
|
||||
|
||||
// startDomainSync 定时拉取活动服域名(每5分钟)
|
||||
func startDomainSync() {
|
||||
fetchDomains()
|
||||
common.Go(func() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
fetchDomains()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fetchDomains 从活动服拉取可用域名列表
|
||||
func fetchDomains() {
|
||||
conf := appg.Conf.ActivityServer
|
||||
apiUrl := conf.ApiUrl
|
||||
if apiUrl == "" {
|
||||
return
|
||||
}
|
||||
|
||||
const path = "/api/app/domains"
|
||||
url := apiUrl + path
|
||||
headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "GET", path, "", nil)
|
||||
_, body, err := httputil.DefaultClientGetBytes(url, headers)
|
||||
if err != nil {
|
||||
log.Error("活动服-拉取域名列表失败", log.Any("url", url), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
var resp domainsResp
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
log.Error("活动服-解析域名列表失败", log.Any("body", string(body)), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Code != 200 {
|
||||
log.Warn("活动服-域名列表返回非成功状态", log.Any("code", resp.Code))
|
||||
return
|
||||
}
|
||||
|
||||
domainsMu.Lock()
|
||||
cachedDomains = resp.Data
|
||||
domainsMu.Unlock()
|
||||
|
||||
log.Info("活动服-域名列表更新成功", log.Any("count", len(resp.Data)))
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package activityclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
// HongbaoActivity 与 activity-public-server activityMod.HongbaoActivity 保持一致
|
||||
type HongbaoActivity struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
TriggerTimes []string `json:"triggerTimes"`
|
||||
SessionDuration int `json:"sessionDuration"`
|
||||
SessionClickLimit int `json:"sessionClickLimit"`
|
||||
AppIds []string `json:"appIds"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// UpcomingSessionItem 进行中/即将开启的场次
|
||||
type UpcomingSessionItem struct {
|
||||
SessionId string `json:"sessionId,omitempty"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Status string `json:"status"` // active=进行中, upcoming=未来即将开启
|
||||
}
|
||||
|
||||
// HongbaoUpcomingResp 拉取响应内容
|
||||
type HongbaoUpcomingResp struct {
|
||||
Activity *HongbaoActivity `json:"activity"`
|
||||
List []UpcomingSessionItem `json:"list"`
|
||||
}
|
||||
|
||||
type hongbaoApiResp struct {
|
||||
Code int `json:"code"`
|
||||
Data HongbaoUpcomingResp `json:"data"`
|
||||
}
|
||||
|
||||
var (
|
||||
cachedHongbao HongbaoUpcomingResp
|
||||
cachedHongbaoMu sync.RWMutex
|
||||
)
|
||||
|
||||
// GetHongbaoUpcoming 获取缓存的红包场次
|
||||
func GetHongbaoUpcoming() HongbaoUpcomingResp {
|
||||
cachedHongbaoMu.RLock()
|
||||
defer cachedHongbaoMu.RUnlock()
|
||||
return cachedHongbao
|
||||
}
|
||||
|
||||
// startHongbaoSync 启动定时拉取红包场次(每10秒)
|
||||
func startHongbaoSync() {
|
||||
fetchHongbaoUpcoming()
|
||||
common.Go(func() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
fetchHongbaoUpcoming()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// fetchHongbaoUpcoming 调活动服 /api/app/hongbao/upcoming
|
||||
func fetchHongbaoUpcoming() {
|
||||
conf := appg.Conf.ActivityServer
|
||||
if conf.ApiUrl == "" || conf.AppId == "" {
|
||||
return
|
||||
}
|
||||
|
||||
const path = "/api/app/hongbao/upcoming"
|
||||
rawQuery := "appId=" + conf.AppId
|
||||
url := conf.ApiUrl + path + "?" + rawQuery
|
||||
headers := BuildSvcHeaders(conf.AppId, conf.SecretKey, "GET", path, rawQuery, nil)
|
||||
_, body, err := httputil.DefaultClientGetBytes(url, headers)
|
||||
if err != nil {
|
||||
log.Error("活动服-拉取红包场次失败", log.Any("url", url), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
var resp hongbaoApiResp
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
log.Error("活动服-解析红包场次失败", log.Any("body", string(body)), log.E(err))
|
||||
return
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
log.Warn("活动服-红包场次返回非成功状态", log.Any("code", resp.Code))
|
||||
return
|
||||
}
|
||||
|
||||
cachedHongbaoMu.Lock()
|
||||
cachedHongbao = resp.Data
|
||||
cachedHongbaoMu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package activityclient
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LinkTypeHongbaoRain 跳转链接中标识红包雨活动的 type 参数值
|
||||
const LinkTypeHongbaoRain = "hongbaoRain"
|
||||
|
||||
// ResolveCountdownByLink 根据跳转链接的 type 参数推导倒计时类型和场次时间。
|
||||
// 适用于 link 自身携带活动类型标识、但 DB 未单独建 countdownType 字段的业务(如任务列表)。
|
||||
//
|
||||
// 返回值约定:
|
||||
// - link 含 type=hongbaoRain 且有可用红包雨场次:返回 (场次开始, 场次结束, 1, true)
|
||||
// - link 含 type=hongbaoRain 但无可用场次:返回 (零, 零, 1, false),调用方据此过滤该条目
|
||||
// - 其他情况:返回 (零, 零, 0, true),无倒计时
|
||||
func ResolveCountdownByLink(link string) (start, end time.Time, countdownType int, ok bool) {
|
||||
if linkType(link) != LinkTypeHongbaoRain {
|
||||
return time.Time{}, time.Time{}, 0, true
|
||||
}
|
||||
s, e, hit := NextHongbaoSession(time.Now())
|
||||
if !hit {
|
||||
return time.Time{}, time.Time{}, 1, false
|
||||
}
|
||||
return s, e, 1, true
|
||||
}
|
||||
|
||||
// linkType 提取链接 query 中的 type 参数,解析失败返回空串
|
||||
func linkType(link string) string {
|
||||
if link == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Query().Get("type")
|
||||
}
|
||||
|
||||
// NextHongbaoSession 取一场用于倒计时展示的红包雨场次:
|
||||
// - 当前已处于某场次内(StartTime <= now < EndTime):返回该场次
|
||||
// - 否则返回最近一场未来未过期场次
|
||||
// - 无任何未过期场次:返回 (零值, 零值, false)
|
||||
func NextHongbaoSession(now time.Time) (start, end time.Time, ok bool) {
|
||||
data := GetHongbaoUpcoming()
|
||||
var (
|
||||
nearestStart time.Time
|
||||
nearestEnd time.Time
|
||||
)
|
||||
for _, s := range data.List {
|
||||
// 跳过已结束场次
|
||||
if !s.EndTime.IsZero() && !s.EndTime.After(now) {
|
||||
continue
|
||||
}
|
||||
// 当前已在场次内,直接返回
|
||||
if !s.StartTime.After(now) {
|
||||
return s.StartTime, s.EndTime, true
|
||||
}
|
||||
// 未来场次,取开始时间最早的
|
||||
if nearestStart.IsZero() || s.StartTime.Before(nearestStart) {
|
||||
nearestStart = s.StartTime
|
||||
nearestEnd = s.EndTime
|
||||
}
|
||||
}
|
||||
if nearestStart.IsZero() {
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
return nearestStart, nearestEnd, true
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package activityclient
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"91porn-server/common/crypt"
|
||||
)
|
||||
|
||||
// SignPayload 签名载荷(与 activity-public-server 保持一致)
|
||||
type SignPayload struct {
|
||||
AppId string `json:"appId"` // 应用ID
|
||||
UserId string `json:"userId"` // 用户ID
|
||||
Nickname string `json:"nickname"` // 用户昵称
|
||||
Avatar string `json:"avatar"` // 用户头像
|
||||
Ts int64 `json:"ts"` // 时间戳(秒)
|
||||
RegisteredAt int64 `json:"registeredAt"` // 用户注册时间(Unix秒)
|
||||
RechargeAmount int `json:"rechargeAmount"` // 充值金额(分)
|
||||
}
|
||||
|
||||
// EncryptSign AES-CBC 加密签名载荷,返回 Base64 URL Safe 编码
|
||||
func EncryptSign(secretKeyBase64 string, payload *SignPayload) (string, error) {
|
||||
secretKey, err := base64.StdEncoding.DecodeString(secretKeyBase64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode secretKey failed: %w", err)
|
||||
}
|
||||
plaintext, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal payload failed: %w", err)
|
||||
}
|
||||
ciphertext, err := crypt.AESCBCPck5Encrypt(plaintext, secretKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encrypt failed: %w", err)
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package activityclient
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 与 activity-public-server middleware/svcauth 协议保持一致:
|
||||
//
|
||||
// X-Svc-AppId : 调用方 appId
|
||||
// X-Svc-Ts : 请求生成的 Unix 秒(服务端校验 ±300s)
|
||||
// X-Svc-Sign : HMAC_SHA256(canonical, secret) 的 hex 小写
|
||||
//
|
||||
// canonical 串结构("\n" 分隔):
|
||||
//
|
||||
// appId
|
||||
// ts
|
||||
// METHOD // 大写
|
||||
// path // URL.Path,不含 query
|
||||
// rawQuery // URL.RawQuery 原样,不重排序
|
||||
// sha256_hex(body) // 空 body 也参与运算
|
||||
const (
|
||||
headerSvcAppId = "X-Svc-AppId"
|
||||
headerSvcTs = "X-Svc-Ts"
|
||||
headerSvcSign = "X-Svc-Sign"
|
||||
)
|
||||
|
||||
// BuildSvcHeaders 生成服务间调用鉴权头。
|
||||
// method/path/rawQuery 必须与服务端 c.Request 上看到的一致;
|
||||
// body 传原始字节(GET 等无 body 接口传 nil)。
|
||||
// appId 或 secret 为空时返回 nil,调用方按未签名处理。
|
||||
func BuildSvcHeaders(appId, secret, method, path, rawQuery string, body []byte) map[string]string {
|
||||
if appId == "" || secret == "" {
|
||||
return nil
|
||||
}
|
||||
ts := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
return map[string]string{
|
||||
headerSvcAppId: appId,
|
||||
headerSvcTs: ts,
|
||||
headerSvcSign: computeSvcSign(secret, appId, ts, strings.ToUpper(method), path, rawQuery, hashBody(body)),
|
||||
}
|
||||
}
|
||||
|
||||
// hashBody 计算 body 的 sha256 hex;nil/空 body 也返回 sha256("") 的 hex
|
||||
func hashBody(body []byte) string {
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// computeSvcSign 按 canonical 串拼接后做 HMAC-SHA256
|
||||
func computeSvcSign(secret, appId, ts, method, path, rawQuery, bodyHash string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(appId))
|
||||
h.Write([]byte{'\n'})
|
||||
h.Write([]byte(ts))
|
||||
h.Write([]byte{'\n'})
|
||||
h.Write([]byte(method))
|
||||
h.Write([]byte{'\n'})
|
||||
h.Write([]byte(path))
|
||||
h.Write([]byte{'\n'})
|
||||
h.Write([]byte(rawQuery))
|
||||
h.Write([]byte{'\n'})
|
||||
h.Write([]byte(bodyHash))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package activityser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
// DeductType 扣款类型
|
||||
type DeductType int
|
||||
|
||||
const (
|
||||
DeductGold DeductType = 1 // 金币
|
||||
DeductIntegral DeductType = 2 // 积分
|
||||
DeductLotteryTimes DeductType = 3 // 抽奖免费次数
|
||||
)
|
||||
|
||||
// DeductReq 扣款请求
|
||||
type DeductReq struct {
|
||||
UserId string `json:"userId" binding:"required"`
|
||||
DeductType DeductType `json:"deductType" binding:"required"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
ActivityId string `json:"activityId"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// Deduct 活动扣款
|
||||
func Deduct(ctx context.Context, req *DeductReq) (stderr.Code, error) {
|
||||
uid, err := strconv.ParseUint(req.UserId, 10, 64)
|
||||
if err != nil {
|
||||
return stderr.ErrParamError, fmt.Errorf("invalid userId: %s", req.UserId)
|
||||
}
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.Failure, fmt.Errorf("查询用户异常: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
return stderr.ErrParamError, fmt.Errorf("用户不存在: %s", req.UserId)
|
||||
}
|
||||
|
||||
if req.Amount <= 0 {
|
||||
return stderr.ErrParamError, fmt.Errorf("扣款数量必须大于0")
|
||||
}
|
||||
|
||||
w, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.Failure, fmt.Errorf("查询钱包异常: %w", err)
|
||||
}
|
||||
if w == nil {
|
||||
w = &walletmod.Wallet{}
|
||||
}
|
||||
|
||||
desc := req.Remark
|
||||
|
||||
switch req.DeductType {
|
||||
case DeductGold:
|
||||
if w.Amount < req.Amount {
|
||||
return stderr.InsufficientGold, fmt.Errorf("金币余额不足,当前%d,需要%d", w.Amount, req.Amount)
|
||||
}
|
||||
return deductGold(ctx, uid, u, req, desc)
|
||||
case DeductIntegral:
|
||||
if w.Integral < req.Amount {
|
||||
return stderr.InsufficientPoint, fmt.Errorf("积分余额不足,当前%d,需要%d", w.Integral, req.Amount)
|
||||
}
|
||||
return deductIntegral(ctx, uid, u, req, desc)
|
||||
case DeductLotteryTimes:
|
||||
if w.LotteryTimes < req.Amount {
|
||||
return stderr.InsufficientLotteryFreeTimes, fmt.Errorf("抽奖免费次数不足,当前%d,需要%d", w.LotteryTimes, req.Amount)
|
||||
}
|
||||
return deductLotteryTimes(ctx, uid, u, req, desc)
|
||||
default:
|
||||
return stderr.ErrParamError, fmt.Errorf("不支持的扣款类型: %d", req.DeductType)
|
||||
}
|
||||
}
|
||||
|
||||
func deductGold(ctx context.Context, uid uint64, u *usermod.User, req *DeductReq, desc string) (stderr.Code, error) {
|
||||
amount := req.Amount
|
||||
|
||||
err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
w, err := walletmod.DebitAmount(t, amount, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动扣款-扣除金币", log.Any("uid", uid), log.Any("amount", amount), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: -amount,
|
||||
ActualAmount: float64(-amount),
|
||||
TranType: txnmod.ActivityDeductGold.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityDeductGold),
|
||||
Desc: fmt.Sprintf("%s-扣除%d金币", desc, amount),
|
||||
SysType: u.SysType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return stderr.InsufficientGold, fmt.Errorf("金币余额不足")
|
||||
}
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
func deductIntegral(ctx context.Context, uid uint64, u *usermod.User, req *DeductReq, desc string) (stderr.Code, error) {
|
||||
integral := req.Amount
|
||||
|
||||
err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
_, err := walletmod.DebitIntegral(t, integral, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动扣款-扣除积分", log.Any("uid", uid), log.Any("integral", integral), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Integral: -integral,
|
||||
TranType: txnmod.ActivityDeductIntegral.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityDeductIntegral),
|
||||
Desc: fmt.Sprintf("%s-扣除%d积分", desc, integral),
|
||||
SysType: u.SysType,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return stderr.InsufficientPoint, fmt.Errorf("积分余额不足")
|
||||
}
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
func deductLotteryTimes(ctx context.Context, uid uint64, u *usermod.User, req *DeductReq, desc string) (stderr.Code, error) {
|
||||
times := req.Amount
|
||||
|
||||
err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
_, err := walletmod.DebitLotteryTimes(t, times, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动扣款-扣除抽奖免费次数", log.Any("uid", uid), log.Any("times", times), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: -times,
|
||||
TranType: txnmod.ActivityDeductLotteryTimes.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityDeductLotteryTimes),
|
||||
Desc: fmt.Sprintf("%s-扣除抽奖免费%d次", desc, times),
|
||||
SysType: u.SysType,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return stderr.InsufficientLotteryFreeTimes, fmt.Errorf("抽奖免费次数不足")
|
||||
}
|
||||
return stderr.Success, nil
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package activityser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/backpackmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/videocoupon"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
// RewardType 奖励类型
|
||||
type RewardType int
|
||||
|
||||
const (
|
||||
RewardGold RewardType = 1 // 金币
|
||||
RewardVIP RewardType = 2 // 会员卡(VIP天数)
|
||||
RewardGoldBonusCoupon RewardType = 3 // 金币加赠券
|
||||
RewardGoldVideoCoupon RewardType = 4 // 金币观影券
|
||||
RewardAiChangeFaceFree RewardType = 5 // AI视频换脸免费次数
|
||||
RewardAiUndressFree RewardType = 6 // AI脱衣免费次数
|
||||
RewardIntegral RewardType = 7 // 积分
|
||||
RewardPhysical RewardType = 8 // 实物奖品
|
||||
)
|
||||
|
||||
// AddressInfo 收货地址信息
|
||||
type AddressInfo struct {
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// RewardReq 发奖请求
|
||||
type RewardReq struct {
|
||||
RequestId string `json:"requestId"`
|
||||
UserId string `json:"userId" binding:"required"`
|
||||
RewardType RewardType `json:"rewardType" binding:"required"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
CouponValue int64 `json:"couponValue"`
|
||||
Duration int `json:"duration"`
|
||||
ActivityId string `json:"activityId"`
|
||||
RewardName string `json:"rewardName"`
|
||||
Remark string `json:"remark"`
|
||||
Address *AddressInfo `json:"address"`
|
||||
}
|
||||
|
||||
// RewardItem 批量发奖中的单个奖品
|
||||
type RewardItem struct {
|
||||
RequestId string `json:"requestId"`
|
||||
RewardType RewardType `json:"rewardType"`
|
||||
Amount int64 `json:"amount"`
|
||||
CouponValue int64 `json:"couponValue"`
|
||||
Duration int `json:"duration"`
|
||||
RewardName string `json:"rewardName"`
|
||||
Address *AddressInfo `json:"address"`
|
||||
}
|
||||
|
||||
// BatchRewardReq 批量发奖请求
|
||||
type BatchRewardReq struct {
|
||||
UserId string `json:"userId" binding:"required"`
|
||||
ActivityId string `json:"activityId"`
|
||||
Remark string `json:"remark"`
|
||||
Rewards []RewardItem `json:"rewards" binding:"required"`
|
||||
}
|
||||
|
||||
// GrantBatchReward 批量发放活动奖励
|
||||
func GrantBatchReward(ctx context.Context, req *BatchRewardReq) error {
|
||||
if len(req.Rewards) == 0 {
|
||||
return nil
|
||||
}
|
||||
uid, err := strconv.ParseUint(req.UserId, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid userId: %s", req.UserId)
|
||||
}
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询用户异常: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
return fmt.Errorf("用户不存在: %s", req.UserId)
|
||||
}
|
||||
return appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
for _, item := range req.Rewards {
|
||||
single := &RewardReq{
|
||||
RequestId: item.RequestId,
|
||||
UserId: req.UserId,
|
||||
RewardType: item.RewardType,
|
||||
Amount: item.Amount,
|
||||
CouponValue: item.CouponValue,
|
||||
Duration: item.Duration,
|
||||
ActivityId: req.ActivityId,
|
||||
RewardName: item.RewardName,
|
||||
Remark: req.Remark,
|
||||
Address: item.Address,
|
||||
}
|
||||
if err := grantRewardTx(ctx, t, uid, u, single); err != nil {
|
||||
log.ErrorX(ctx, "批量发奖-单项失败已跳过",
|
||||
log.Any("uid", req.UserId),
|
||||
log.Any("requestId", item.RequestId),
|
||||
log.Any("rewardType", item.RewardType),
|
||||
log.E(err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GrantReward 发放活动奖励(单个)
|
||||
func GrantReward(ctx context.Context, req *RewardReq) error {
|
||||
uid, err := strconv.ParseUint(req.UserId, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid userId: %s", req.UserId)
|
||||
}
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查询用户异常: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
return fmt.Errorf("用户不存在: %s", req.UserId)
|
||||
}
|
||||
|
||||
return appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
return grantRewardTx(ctx, t, uid, u, req)
|
||||
})
|
||||
}
|
||||
|
||||
func grantRewardTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq) error {
|
||||
desc := req.Remark
|
||||
|
||||
switch req.RewardType {
|
||||
case RewardGold:
|
||||
return grantGoldTx(ctx, t, uid, u, req, desc)
|
||||
case RewardVIP:
|
||||
return grantVIPTx(ctx, t, uid, u, req, desc)
|
||||
case RewardGoldBonusCoupon:
|
||||
return grantGoldBonusCouponTx(ctx, t, uid, u, req, desc)
|
||||
case RewardGoldVideoCoupon:
|
||||
return grantGoldVideoCouponTx(ctx, t, uid, u, req, desc)
|
||||
case RewardAiChangeFaceFree:
|
||||
return grantAiChangeFaceFreeTx(ctx, t, uid, u, req, desc)
|
||||
case RewardAiUndressFree:
|
||||
return grantAiUndressFreeTx(ctx, t, uid, u, req, desc)
|
||||
case RewardIntegral:
|
||||
return grantIntegralTx(ctx, t, uid, u, req, desc)
|
||||
case RewardPhysical:
|
||||
return grantPhysicalTx(ctx, t, uid, u, req, desc)
|
||||
default:
|
||||
return fmt.Errorf("不支持的奖励类型: %d", req.RewardType)
|
||||
}
|
||||
}
|
||||
|
||||
func grantGoldTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
amount := req.Amount
|
||||
creditPlan := walletmod.CreditPlan{Amount: &amount}
|
||||
|
||||
w, err := walletmod.Credit(t, creditPlan, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放金币", log.Any("uid", uid), log.Any("amount", amount), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: amount,
|
||||
ActualAmount: float64(amount),
|
||||
TranType: txnmod.ActivityRewardGold.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardGold),
|
||||
Desc: fmt.Sprintf("%s-发放%d金币", desc, amount),
|
||||
SysType: u.SysType,
|
||||
RealAmount: w.RealAmount(),
|
||||
UniqueOrder: req.RequestId,
|
||||
})
|
||||
}
|
||||
|
||||
func grantVIPTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
days := int(req.Amount)
|
||||
if days <= 0 {
|
||||
return errors.New("VIP天数必须大于0")
|
||||
}
|
||||
|
||||
var expire time.Time
|
||||
if u.VipExpireDate.After(time.Now()) {
|
||||
expire = u.VipExpireDate.AddDate(0, 0, days)
|
||||
} else {
|
||||
expire = time.Now().AddDate(0, 0, days)
|
||||
}
|
||||
|
||||
vipLevel := 1
|
||||
if u.VipLevel > vipLevel {
|
||||
vipLevel = u.VipLevel
|
||||
}
|
||||
|
||||
sel := usermod.UserSelector{
|
||||
VipExpireDate: &expire,
|
||||
VipLevel: &vipLevel,
|
||||
}
|
||||
if err := usermod.UpdateVIP(t, uid, u.VipExpireDate, sel); err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放VIP", log.Any("uid", uid), log.Any("days", days), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
TranType: txnmod.ActivityRewardVIP.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardVIP),
|
||||
Desc: fmt.Sprintf("%s-发放VIP%d天", desc, days),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
})
|
||||
}
|
||||
|
||||
func grantGoldBonusCouponTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
if req.CouponValue <= 0 {
|
||||
return errors.New("券额度(couponValue)必须大于0")
|
||||
}
|
||||
|
||||
if err := txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: req.Amount,
|
||||
TranType: txnmod.ActivityRewardGoldBonusCoupon.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardGoldBonusCoupon),
|
||||
Desc: fmt.Sprintf("%s-发放金币加赠券%d张", desc, req.Amount),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiredAt := now.AddDate(0, 0, 30)
|
||||
for i := int64(0); i < req.Amount; i++ {
|
||||
item := backpackmod.Backpack{
|
||||
UID: uid,
|
||||
GoodsType: backpackmod.GoldBonusCoupon,
|
||||
GoodsValue: req.CouponValue,
|
||||
GoodsOrigin: desc,
|
||||
Status: backpackmod.Unused,
|
||||
ExpiredTime: expiredAt,
|
||||
CreateTime: now,
|
||||
}
|
||||
if err := backpackmod.AddGoods(t, uid, item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放金币加赠券", log.Any("uid", uid), log.Any("couponValue", req.CouponValue), log.Any("amount", req.Amount))
|
||||
return nil
|
||||
}
|
||||
|
||||
func grantGoldVideoCouponTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
if req.CouponValue <= 0 {
|
||||
return errors.New("券额度(couponValue)必须大于0")
|
||||
}
|
||||
|
||||
if err := txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: req.Amount,
|
||||
TranType: txnmod.ActivityRewardGoldVideoCoupon.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardGoldVideoCoupon),
|
||||
Desc: fmt.Sprintf("%s-发放金币观影券%d张", desc, req.Amount),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
coupons := make([]videocoupon.UserGoldVideoCoupon, 0, req.Amount)
|
||||
for i := int64(0); i < req.Amount; i++ {
|
||||
coupons = append(coupons, videocoupon.UserGoldVideoCoupon{
|
||||
UID: uid,
|
||||
Num: int(req.CouponValue),
|
||||
Used: false,
|
||||
Source: videocoupon.GoldVideoCouponSource(desc),
|
||||
})
|
||||
}
|
||||
if err := videocoupon.InsertManyTrans(t, coupons); err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放金币观影券", log.Any("uid", uid), log.Any("couponValue", req.CouponValue), log.Any("amount", req.Amount))
|
||||
return nil
|
||||
}
|
||||
|
||||
func grantAiChangeFaceFreeTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
if err := txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: req.Amount,
|
||||
TranType: txnmod.ActivityRewardAiChangeFaceFree.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardAiChangeFaceFree),
|
||||
Desc: fmt.Sprintf("%s-发放AI换脸免费%d次", desc, req.Amount),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
permanent := time.Date(2099, 12, 31, 23, 59, 59, 0, time.Local)
|
||||
item := backpackmod.Backpack{
|
||||
UID: uid,
|
||||
GoodsType: backpackmod.AiChangeFaceDiscount,
|
||||
GoodsValue: req.Amount,
|
||||
GoodsOrigin: desc,
|
||||
Status: backpackmod.Unused,
|
||||
ExpiredTime: permanent,
|
||||
CreateTime: now,
|
||||
}
|
||||
if err := backpackmod.AddGoods(t, uid, item); err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放AI换脸免费次数", log.Any("uid", uid), log.Any("times", req.Amount))
|
||||
return nil
|
||||
}
|
||||
|
||||
func grantIntegralTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
integral := req.Amount
|
||||
|
||||
_, err := walletmod.CreditIntegral(t, integral, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放积分", log.Any("uid", uid), log.Any("integral", integral), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: integral,
|
||||
TranType: txnmod.ActivityRewardIntegral.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardIntegral),
|
||||
Desc: fmt.Sprintf("%s-发放%d积分", desc, integral),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
})
|
||||
}
|
||||
|
||||
func grantAiUndressFreeTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
times := req.Amount
|
||||
creditPlan := walletmod.CreditPlan{AiUndressFreeTimes: ×}
|
||||
|
||||
_, err := walletmod.Credit(t, creditPlan, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.InfoX(ctx, "活动发放-发放AI脱衣免费次数", log.Any("uid", uid), log.Any("times", times), log.Any("activityId", req.ActivityId))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: times,
|
||||
TranType: txnmod.ActivityRewardAiUndressFree.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardAiUndressFree),
|
||||
Desc: fmt.Sprintf("%s-发放AI脱衣免费%d次", desc, times),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
})
|
||||
}
|
||||
|
||||
func grantPhysicalTx(ctx context.Context, t *db.MongoTool, uid uint64, u *usermod.User, req *RewardReq, desc string) error {
|
||||
addrDesc := ""
|
||||
if req.Address != nil {
|
||||
addrDesc = fmt.Sprintf("【 收件人:%s 手机:%s 地址:%s】", req.Address.Name, req.Address.Phone, req.Address.Address)
|
||||
}
|
||||
rewardName := req.RewardName
|
||||
if rewardName == "" {
|
||||
rewardName = "实物奖品"
|
||||
}
|
||||
|
||||
log.InfoX(ctx, "活动发放-发放实物奖品", log.Any("uid", uid), log.Any("rewardName", rewardName), log.Any("activityId", req.ActivityId), log.Any("address", addrDesc))
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: req.Amount,
|
||||
TranType: txnmod.ActivityRewardPhysical.Key(),
|
||||
TranTypeInt: int64(txnmod.ActivityRewardPhysical),
|
||||
Desc: fmt.Sprintf("%s-发放%s%s", desc, rewardName, addrDesc),
|
||||
SysType: u.SysType,
|
||||
UniqueOrder: req.RequestId,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package actvser
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"91porn-server/models/v/actmod"
|
||||
)
|
||||
|
||||
var dateMax = time.Date(2999, 12, 31, 0, 0, 0, 0, time.Local)
|
||||
|
||||
func GetActivities(req GetActitiesRequest) ([]actmod.Activity, bool, error) {
|
||||
skip := uint64((req.PageNumber - 1) * req.PageSize)
|
||||
limit := uint64(req.PageSize + 1)
|
||||
actvs, err := actmod.GetActivitiesValid(skip, limit)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasNext := false
|
||||
if uint64(len(actvs)) > req.PageSize {
|
||||
hasNext = true
|
||||
actvs = actvs[:req.PageSize]
|
||||
}
|
||||
now := time.Now()
|
||||
for i := range actvs {
|
||||
if actvs[i].ExpiredIn.IsZero() {
|
||||
actvs[i].ExpiredIn = dateMax
|
||||
continue
|
||||
}
|
||||
if actvs[i].ExpiredIn.Before(now) {
|
||||
actvs[i].Status = 2 // 状态设置为已过期
|
||||
}
|
||||
}
|
||||
return actvs, hasNext, nil
|
||||
}
|
||||
|
||||
func GetActiveByID(id primitive.ObjectID) (*actmod.Activity, error) {
|
||||
actv, err := actmod.GetActivityByActivityID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if actv.ExpiredIn.IsZero() {
|
||||
actv.ExpiredIn = dateMax
|
||||
return actv, nil
|
||||
}
|
||||
if actv.Status == 1 && actv.ExpiredIn.Before(time.Now()) {
|
||||
actv.Status = 2
|
||||
}
|
||||
return actv, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package actvser
|
||||
|
||||
import "91porn-server/models/commod"
|
||||
|
||||
type GetActitiesRequest struct {
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type GetActiveByIDRequest struct {
|
||||
ID string `form:"id" json:"id" binding:"required"`
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package adser
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"91porn-server/app/proto"
|
||||
"91porn-server/app/service/annouser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/models/v/adsmod"
|
||||
"91porn-server/models/v/annoumod"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func toAdsResSlice(slice adsmod.AdsSlice) []proto.AdsInfo {
|
||||
ads := make([]proto.AdsInfo, len(slice))
|
||||
for i, v := range slice {
|
||||
ads[i] = proto.AdsInfo{
|
||||
// ID: v.ID,
|
||||
Cover: v.Cover,
|
||||
Href: v.Href,
|
||||
Position: int(v.Position),
|
||||
SortCode: v.SortCode,
|
||||
}
|
||||
}
|
||||
return ads
|
||||
}
|
||||
|
||||
var sysAds = adsmod.SysAds
|
||||
|
||||
func AdsList(adsType adsmod.AdsType, discCode string) (res proto.AdsRes) {
|
||||
var ads []proto.AdsInfo
|
||||
var announ []*annouser.Annou
|
||||
wait := sync.WaitGroup{}
|
||||
wait.Add(2)
|
||||
common.Go(func() {
|
||||
defer wait.Done()
|
||||
if adsType == adsmod.SysAds {
|
||||
adSlice, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, (&adsmod.AdsTypeMatch{AdsType: &sysAds}).New())
|
||||
ads = toAdsResSlice(adSlice)
|
||||
} else {
|
||||
ads = toAdsResSlice(GetDiscAdSlice(discCode))
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wait.Done()
|
||||
announInfo, _ := annouser.GetAnnouList(annoumod.TotalType)
|
||||
announ = append(announ, announInfo)
|
||||
})
|
||||
wait.Wait()
|
||||
res.AdsList = ads
|
||||
if gin.Mode() == gin.DebugMode { //测试模式下向外暴露链接
|
||||
res.AnnounInfo = announ
|
||||
return
|
||||
}
|
||||
res.AnnounInfo = announ
|
||||
return
|
||||
}
|
||||
|
||||
func GetDiscAdSlice(discCode string) adsmod.AdsSlice {
|
||||
mats := []adsmod.Matcher{
|
||||
(&adsmod.DistrictCodeMatch{DistrictCode: &discCode}).New(),
|
||||
}
|
||||
discADSlice, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, mats...)
|
||||
//未配置渠道广告的广告位,展示系统广告
|
||||
mats = []adsmod.Matcher{
|
||||
(&adsmod.AdsTypeMatch{AdsType: &sysAds}).New(),
|
||||
(&adsmod.AdPositionNinMatch{PositionList: discADSlice.ToPositions()}).New(),
|
||||
}
|
||||
ads, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, mats...)
|
||||
ads = append(ads, discADSlice...)
|
||||
sort.Sort(ads)
|
||||
return ads
|
||||
}
|
||||
|
||||
func AdList(adsType adsmod.AdsType, discCode string) []proto.AdsInfo {
|
||||
if adsType == adsmod.SysAds {
|
||||
adSlice, _ := adsmod.ActiveStartAndEndList(adsmod.Sort_sortCode_N1, nil, nil, (&adsmod.AdsTypeMatch{AdsType: &sysAds}).New())
|
||||
return toAdsResSlice(adSlice)
|
||||
}
|
||||
return toAdsResSlice(GetDiscAdSlice(discCode))
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package adser
|
||||
|
||||
import (
|
||||
"91porn-server/models/v/dailytaskmod"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/taskser"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/l/adsclicklogmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AdvertiseReq struct {
|
||||
AppId int32 `form:"appId" json:"appId" binding:"required"` // 应用id
|
||||
LocId *int32 `form:"locId" json:"locId" binding:"omitempty"` // 位置id
|
||||
UserId *uint64 `form:"userId" json:"userId" binding:"omitempty"` // 用户ID
|
||||
IP *string `form:"ip" json:"ip" binding:"omitempty"` // 用户IP
|
||||
}
|
||||
|
||||
type AdvertiseRes struct {
|
||||
AdvertiseList []AdvertiseInfo `json:"advertiseList"` // 广告列表
|
||||
}
|
||||
|
||||
type AdsClickReq struct {
|
||||
Id string `from:"id" json:"id" binding:"required"` // 广告id
|
||||
AppId int32 `from:"appId" json:"appId" binding:"required"` // 应用id
|
||||
ChannelCode string `from:"channelCode" json:"channelCode" binding:"channelCode"`
|
||||
}
|
||||
|
||||
type AdsClickResp struct {
|
||||
Code int64 `from:"code" json:"code"` // 错误骂
|
||||
}
|
||||
|
||||
// 外部广告信息
|
||||
type AdvertiseInfo struct {
|
||||
Id int64 `json:"id"` // 广告id
|
||||
Title string `json:"title"` // 广告标题
|
||||
CoverImg string `json:"coverImg"` // 封面
|
||||
LocId int32 `json:"locId"` // 位置id
|
||||
JumpType int32 `json:"jumpType"` // 跳转方式 0:外部浏览器跳转 1:内部浏览器跳转 2:app内部跳转
|
||||
Link string `json:"link"` // 链接地址
|
||||
Sort int64 `json:"sort"` // 排序
|
||||
}
|
||||
|
||||
func AdvertiseThreeServer(userId uint64, ip string) (*AdvertiseRes, error) {
|
||||
str, err := appg.Redis.Get(redisconst.CenterAdvertiseCache)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;缓存获取广告列表信息异常:%v", userId, ip, err))
|
||||
}
|
||||
var resp AdvertiseRes
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return &resp, nil
|
||||
}
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;解析缓存数据异常:%v", userId, ip, err))
|
||||
}
|
||||
req := AdvertiseReq{
|
||||
AppId: commod.KFK_APPID,
|
||||
IP: &ip,
|
||||
}
|
||||
if userId > 0 {
|
||||
req.UserId = &userId
|
||||
}
|
||||
url := appg.Conf.URL.ProductUrl + "/api/stat/ads/get"
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("AdvertiseThreeServer POSTWithJResp ", log.Any("url", url), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("AdvertiseThreeServer response status ", log.Any("code", code))
|
||||
return nil, fmt.Errorf("response status err")
|
||||
}
|
||||
data, err := json.Marshal(&resp)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;json序列化缓存数据异常:%v", userId, ip, err))
|
||||
return &resp, nil
|
||||
}
|
||||
if err = appg.Redis.Set(redisconst.CenterAdvertiseCache, data, 45*time.Second); err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;保存缓存数据异常:%v", userId, ip, err))
|
||||
}
|
||||
// log.Info("AdvertiseThreeServer response", log.Any("resp", resp))
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func AdvertiseClickThreeServer(advertiseId string, uid uint64) error {
|
||||
//查询用户信息
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.DistrictCode == "" {
|
||||
u.DistrictCode = "system"
|
||||
}
|
||||
req := AdsClickReq{
|
||||
AppId: commod.KFK_APPID,
|
||||
Id: advertiseId,
|
||||
ChannelCode: u.DistrictCode,
|
||||
}
|
||||
resp := AdsClickResp{}
|
||||
url := appg.Conf.URL.ProductUrl + "/api/product/ads/dayClick"
|
||||
bodyStr, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("AdvertiseClickThreeServer json.Marshal is fail error:%+v/data:%+v", err, req))
|
||||
return err
|
||||
}
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("AdvertiseClickThreeServer POSTWithJResp ", log.Any("url", url), log.E(err))
|
||||
return err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("AdvertiseClickThreeServer response status ", log.Any("code", code))
|
||||
return fmt.Errorf("response status err")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AdvertiseNavigate(advertiseId string, uid uint64, ua, logType string) error {
|
||||
advId, err := primitive.ObjectIDFromHex(advertiseId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if advId.IsZero() {
|
||||
return errors.New("empty advid")
|
||||
}
|
||||
header := map[string]string{
|
||||
"log-id": advertiseId,
|
||||
"User-Agent": ua,
|
||||
"log-type": logType,
|
||||
}
|
||||
targetUrl := appg.Conf.URL.NavigateUrl + "/api/application/dHH9h0Kp1074"
|
||||
if _, _, err = httputil.DefaultClientGetBytes(targetUrl, header, nil); err != nil {
|
||||
log.Error("navigate report fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
if err := adsclicklogmod.AddClick(uid, advId, logType, 1); err != nil {
|
||||
log.Error("adclick fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
go func() { _ = taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyTaskTypeAdsClick) }()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package adser
|
||||
|
||||
import (
|
||||
"91porn-server/app/proto"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/activityclient"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
/*
|
||||
测试:
|
||||
部门CODE:1991129769626804224
|
||||
商户CODE:1991129808640122880
|
||||
应用CODE:jh_appid01
|
||||
客户CODE:1991129948159451136
|
||||
*/
|
||||
|
||||
type JtAdvertiseReq struct {
|
||||
MerchantCode string `json:"merchantCode"` // 商户 code,必填
|
||||
AppCode string `json:"appCode"` // 应用 code,必填
|
||||
AdStatus int `json:"adStatus"` // 广告状态 1-上架,0-下架,2-未开始,3-已过期,4-即将到期 默认查询全部
|
||||
//AdvertiseLocationCode string `json:"advertiseLocationCode,omitempty"` // 广告位唯一标识,选填
|
||||
//AdvertiseCode string `json:"advertiseCode,omitempty"` // 广告唯一标识,选填
|
||||
//PageNo int `json:"pageNo,omitempty"` // 页码,全量查询不需要传,选填
|
||||
//PageSize int `json:"pageSize,omitempty"` // 每页条数,全量查询不需要传,选填
|
||||
}
|
||||
|
||||
type JtAdvertiseRes struct {
|
||||
Code int `json:"code"` // 返回状态码
|
||||
Msg string `json:"msg"` // 返回消息
|
||||
Data string `json:"data"` // 广告信息 - 加密
|
||||
}
|
||||
|
||||
// AdSlot 广告位信息
|
||||
type AdSlot struct {
|
||||
DeptCode string `json:"deptCode"` // 部门 code
|
||||
MerchantCode string `json:"merchantCode"` // 商户 code
|
||||
AppCode string `json:"appCode"` // 应用 code
|
||||
LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
|
||||
AdvertiseLocationCode string `json:"advertiseLocationCode"` // 广告位标识
|
||||
AdvertiseLocationName string `json:"advertiseLocationName"` // 广告位名称
|
||||
DisplayMode int `json:"displayMode"` // 展示模式:0 → 多个轮播, 1 → 单体, 2 → 三分屏, 3 → 四分屏, 4 → 小图标, 5 → 大图标
|
||||
AdvertiseHeight int `json:"advertiseHeight"` // 高
|
||||
AdvertiseWidth int `json:"advertiseWidth"` // 宽
|
||||
MaterialType int `json:"materialType"` // 素材类型(视频/图片):1 → 视频, 2 → 图片, 3 → 视频、图片
|
||||
AdLimit int `json:"adLimit"` // 广告限制数量
|
||||
AdDetailInfoList []AdDetailInfo `json:"adDetailInfoList"` // 广告详情信息列表
|
||||
}
|
||||
|
||||
// AdDetailInfo 广告详情信息
|
||||
type AdDetailInfo struct {
|
||||
AdvertiseCode string `json:"advertiseCode"` // 广告业务code
|
||||
CustomerCode string `json:"customerCode"` // 客户code
|
||||
AdvertiseName string `json:"advertiseName"` // 广告名称
|
||||
AdvertiseUrl string `json:"advertiseUrl"` // 广告链接
|
||||
AdvertiseIcon string `json:"advertiseIcon"` // 广告图片地址
|
||||
AdvertiseType int `json:"advertiseType"` // 广告类型:1 → 播放器, 2 → 药台, 3 → 炮台, 4 → 黄游, 5 → 直播, 6 → BC
|
||||
AdMode string `json:"adMode"` // 广告模式
|
||||
AppCode string `json:"appCode"` // 应用 code
|
||||
LocationType int `json:"locationType"` // 广告位类型:0 → 开屏广告 1 → 弹窗广告 2 → 图标广告 3 → 九宫格广告 4 → 信息流广告 5 → 瀑布流广告 6 → 详情广告 7 → 片头广告
|
||||
AdvertiseDesc string `json:"advertiseDesc"` // 广告描述
|
||||
PageType string `json:"pageType"` // 内部路由页面类型:如 rechange_page → 会员充值页, gold_rechange_page → 金币充值页, invite_page → 邀请分享页
|
||||
StartTimeStamp int64 `json:"startTimeStamp"` // 广告开始时间 时间戳
|
||||
EndTimeStamp int64 `json:"endTimeStamp"` // 广告结束时间 时间戳
|
||||
Sort int `json:"sort"` // 排序
|
||||
AdExtData string `json:"adExtData"` // 扩展字段1 = CoverImgSize
|
||||
}
|
||||
|
||||
type Extra struct {
|
||||
CoverImgSize string `json:"coverImgSize"`
|
||||
WatchTime int `json:"watchTime"`
|
||||
DownloadCount int64 `json:"downloadCount"`
|
||||
Type string `json:"type"` // 应用类型 app:APP、web:网页
|
||||
Location string `json:"location"` // 位置 1:顶部 2:底部
|
||||
DisplayType string `json:"displayType"` // 展示类型 heng:横版 shu:竖版
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetExtraData() (res Extra) {
|
||||
_ = json.Unmarshal([]byte(ad.AdExtData), &res)
|
||||
return
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetRealLink(user *usermod.User, w *walletmod.Wallet) string {
|
||||
ad.AdvertiseUrl = activityclient.ReplaceActivityDomain(ad.AdvertiseUrl, user, w)
|
||||
return strings.ReplaceAll(ad.AdvertiseUrl, "inner://yinseinner/", "yinseinner://")
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetCoverLsj() string {
|
||||
if ad.AdvertiseIcon == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimLeft(ad.AdvertiseIcon, "/")
|
||||
}
|
||||
|
||||
func (ad AdDetailInfo) GetAdvertiseType() int {
|
||||
// 内部顺序:1 应用 2 游戏 3 炮台 4 棋牌 5 直播
|
||||
// 外部顺序:1:应用 2:药台、3:炮台、4:游戏、5:直播、6:棋牌
|
||||
if ad.AdvertiseType == 4 {
|
||||
return 2
|
||||
}
|
||||
if ad.AdvertiseType == 6 {
|
||||
return 4
|
||||
}
|
||||
|
||||
return ad.AdvertiseType
|
||||
}
|
||||
|
||||
// JtAdvertiseThreeServer 集团广告中心
|
||||
func JtAdvertiseThreeServer() ([]AdSlot, error) {
|
||||
var foreverCacheKey = fmt.Sprintf("jtAdForever-%s-%s", appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode)
|
||||
redisKey := fmt.Sprintf("jtAd-%s-%s", appg.Conf.AdCenter.MerchantCode, appg.Conf.AdCenter.AppCode)
|
||||
str, err := appg.Redis.Get(redisKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 缓存获取广告列表信息异常:%v", err))
|
||||
}
|
||||
var resp []AdSlot
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析缓存数据异常:%v", err))
|
||||
}
|
||||
req := JtAdvertiseReq{
|
||||
MerchantCode: appg.Conf.AdCenter.MerchantCode,
|
||||
AppCode: appg.Conf.AdCenter.AppCode,
|
||||
AdStatus: 1,
|
||||
}
|
||||
|
||||
var serverResp *JtAdvertiseRes
|
||||
url := appg.Conf.AdCenter.ApiDomain + "/openapi/getAdvertiseList"
|
||||
bodyStr, _ := json.Marshal(req)
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&serverResp, url, nil, bodyStr)
|
||||
if err != nil {
|
||||
log.Error("JtAdvertiseThreeServer POSTWithJResp ", log.Any("url", url), log.E(err))
|
||||
// 从最后请求成功的一次拿数据
|
||||
return getFromLastTime(foreverCacheKey, err)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("JtAdvertiseThreeServer response status ", log.Any("code", code))
|
||||
// 从最后请求成功的一次拿数据
|
||||
return getFromLastTime(foreverCacheKey, errors.New("response status err"))
|
||||
}
|
||||
|
||||
if serverResp.Code != 0 {
|
||||
log.Error("JtAdvertiseThreeServer response code ", log.Any("code", serverResp.Code), log.Any("msg", serverResp.Msg))
|
||||
// 从最后请求成功的一次拿数据
|
||||
return getFromLastTime(foreverCacheKey, errors.New("response code err"))
|
||||
}
|
||||
|
||||
adsData, err := crypt.AdDecrypt(serverResp.Data, appg.Conf.AdCenter.AesKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解密广告数据异常:%v", err))
|
||||
return nil, errors.New("response data err")
|
||||
}
|
||||
err = json.Unmarshal([]byte(adsData), &resp)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 解析 JSON 失败:%v", err))
|
||||
return nil, errors.New("data format error")
|
||||
}
|
||||
|
||||
// 对广告进行升序排序
|
||||
for i := range resp {
|
||||
sort.Slice(resp[i].AdDetailInfoList, func(j, k int) bool {
|
||||
return resp[i].AdDetailInfoList[j].Sort < resp[i].AdDetailInfoList[k].Sort
|
||||
})
|
||||
}
|
||||
|
||||
// 将最后一次请求成功的数据更新到二级缓存
|
||||
common.Go(func() {
|
||||
setAdsCache(foreverCacheKey, resp)
|
||||
})
|
||||
|
||||
common.Go(func() {
|
||||
data, err := json.Marshal(&resp)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer json序列化缓存数据异常:%v", err))
|
||||
return
|
||||
}
|
||||
if err = appg.Redis.Set(redisKey, data, time.Minute); err != nil {
|
||||
log.Error(fmt.Sprintf("JtAdvertiseThreeServer 保存缓存数据异常:%v", err))
|
||||
}
|
||||
})
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// getFromLastTime 返回最后一次请求的广告数据
|
||||
func getFromLastTime(foreverCacheKey string, e error) ([]AdSlot, error) {
|
||||
str, err := appg.Redis.Get(foreverCacheKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("getFromLastTime 缓存获取广告列表信息异常:%v", err))
|
||||
return nil, e
|
||||
}
|
||||
|
||||
var resp []AdSlot
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
log.Error(fmt.Sprintf("getFromLastTime 解析缓存数据异常:%v", err))
|
||||
}
|
||||
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// setAdsCache 更新最后一次请求的广告数据
|
||||
func setAdsCache(foreverCacheKey string, resp []AdSlot) {
|
||||
data, err := json.Marshal(&resp)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("setAdsCache json序列化缓存数据异常:%v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := appg.Redis.Set(foreverCacheKey, data, time.Hour*72); err != nil {
|
||||
log.Error(fmt.Sprintf("setAdsCache 保存缓存数据异常:%v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func GetAdvertiseLocation(adverId string) (res proto.AdsInfo) {
|
||||
list, err := JtAdvertiseThreeServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, adverLocation := range list {
|
||||
for _, ad := range adverLocation.AdDetailInfoList {
|
||||
if ad.AdvertiseCode == adverId {
|
||||
pos, _ := strconv.Atoi(adverLocation.AdvertiseLocationCode)
|
||||
extra := ad.GetExtraData()
|
||||
res = proto.AdsInfo{
|
||||
ID: ad.AdvertiseCode,
|
||||
Title: ad.AdvertiseName,
|
||||
Description: ad.AdvertiseDesc,
|
||||
Cover: ad.GetCoverLsj(),
|
||||
Href: ad.GetRealLink(nil, nil),
|
||||
Position: pos,
|
||||
PositionName: adverLocation.AdvertiseLocationName,
|
||||
SortCode: ad.Sort,
|
||||
CoverImgSize: extra.CoverImgSize,
|
||||
WatchTime: extra.WatchTime,
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package adser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
)
|
||||
|
||||
/*
|
||||
福利-游戏广告 100001 GameAppList
|
||||
福利-直播广告 100002 ZbAppList
|
||||
福利-炮台广告 100003 YpAppList
|
||||
福利-棋牌广告 100004 QpAppList
|
||||
福利-视频广告 100005
|
||||
福利-横版广告 100006 HengAppList
|
||||
福利-横一广告 100007
|
||||
福利-竖版广告 100008 ShuAppList
|
||||
福利-文字广告 100009
|
||||
福利-Banner广告 100099 AdvList
|
||||
*/
|
||||
const (
|
||||
AdPosGame = 100001 // 游戏
|
||||
AdPosZhiBo = 100002 // 直播
|
||||
AdPosPaoTai = 100003 // 约炮
|
||||
AdPosQiPai = 100004 // 棋牌
|
||||
AdPosShiPin = 100005 // 视频
|
||||
AdPosHeng = 100006 // 横
|
||||
AdPosHeng1 = 100007 // 横1
|
||||
AdPosShu = 100008 // 竖
|
||||
AdPosWenZi = 100009 // 文字
|
||||
AdPosAds = 100099 // BANNER广告
|
||||
)
|
||||
|
||||
// RecreationListRes 娱乐广告列表请求返回
|
||||
type RecreationListRes struct {
|
||||
HengAppList []AppInfo `json:"hengApp" bson:"hengApp"`
|
||||
ShuAppList []AppInfo `json:"shuApp" bson:"shuApp"`
|
||||
AdvList []AdvInfo `json:"adv" bson:"adv"`
|
||||
GameAppList []AppInfo `json:"gameApp" bson:"gameApp"` // 游戏
|
||||
YpAppList []AppInfo `json:"ypApp" bson:"ypApp"` // 约炮
|
||||
QpAppList []AppInfo `json:"qpApp" bson:"qpApp"` // 棋牌
|
||||
ZbAppList []AppInfo `json:"zbApp" bson:"zbApp"` // 直播
|
||||
}
|
||||
|
||||
type CommonListRes struct {
|
||||
Code int64 `json:"code"`
|
||||
Data RecreationListRes `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
Time string `json:"time"`
|
||||
Tip string `json:"tip"`
|
||||
}
|
||||
|
||||
// AppInfo 娱乐app实体信息
|
||||
type AppInfo struct {
|
||||
Id string `json:"id"` // ID
|
||||
Name string `json:"name"` // 应用名称
|
||||
Desc string `json:"desc"` // 应用描述
|
||||
Icon string `json:"icon"` // 应用图标
|
||||
Url string `json:"url"` // 应用链接
|
||||
Type string `json:"type"` // 应用类型
|
||||
DownloadNum int64 `json:"downloadNum"` // 应用下载次数
|
||||
//广告-集团上报数据
|
||||
Position int `json:"position"` // 集团广告code
|
||||
PositionName string `json:"positionName"` // 集团广告名称
|
||||
}
|
||||
|
||||
// AdvInfo 娱乐广告实体信息
|
||||
type AdvInfo struct {
|
||||
Id string `json:"id"` // ID
|
||||
Name string `json:"name"` // 广告名称
|
||||
Image string `json:"image"` // 广告图片
|
||||
Url string `json:"url"` // 广告链接
|
||||
Location string `json:"location"` // 位置 1:顶部 2:底部
|
||||
ModuleType int `json:"moduleType"` // 广告模块 1:应用 2:游戏 3:约炮 4:棋牌 5:直播
|
||||
//广告-集团上报数据
|
||||
Position int `json:"position"` // 集团广告code
|
||||
PositionName string `json:"positionName"` // 集团广告名称
|
||||
}
|
||||
|
||||
// RecreationClickInfo 娱乐广告点击参数信息
|
||||
type RecreationClickInfo struct {
|
||||
Id string `form:"id" json:"id" binding:"required"` // 数据id
|
||||
Type string `form:"type" json:"type" binding:"required"` // 数据类型,app or adv
|
||||
SysType string `form:"sysType" json:"sysType" binding:"required"` // 用户设备类型. ios or android
|
||||
}
|
||||
|
||||
// RecreationThreeServer 娱乐广告列表,数据来源于导航站(navigate)
|
||||
func RecreationThreeServer() (RecreationListRes, error) {
|
||||
str, err := appg.Redis.Get(redisconst.NavigateRecreationCache)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("缓存获取娱乐广告信息异常:%v", err))
|
||||
return RecreationListRes{}, err
|
||||
}
|
||||
|
||||
var resp CommonListRes
|
||||
if str != nil && *str != "" {
|
||||
if err = msgpack.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return resp.Data, nil
|
||||
}
|
||||
log.Warn(fmt.Sprintf("娱乐广告解析缓存数据异常:%v", err))
|
||||
}
|
||||
//请求url
|
||||
url := appg.Conf.URL.NavigateUrl + "/api/application/list/" + strconv.FormatInt(int64(commod.KFK_APPID), 10)
|
||||
//请求
|
||||
code, err := httputil.DefaultClientPostJsonWithResp(&resp, url, nil, nil)
|
||||
if err != nil {
|
||||
log.Error("RecreationThreeServer POSTWithJResp", log.Any("url", url), log.E(err))
|
||||
return resp.Data, err
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
log.Error("RecreationThreeServer response status", log.Any("code", code))
|
||||
return resp.Data, errors.New("response status err")
|
||||
}
|
||||
if len(resp.Data.AdvList) > 0 || len(resp.Data.HengAppList) > 0 || len(resp.Data.ShuAppList) > 0 ||
|
||||
len(resp.Data.GameAppList) > 0 || len(resp.Data.YpAppList) > 0 || len(resp.Data.QpAppList) > 0 || len(resp.Data.ZbAppList) > 0 {
|
||||
common.Go(func() {
|
||||
data, err := msgpack.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("娱乐广告json序列化缓存数据异常:%v", err))
|
||||
return
|
||||
}
|
||||
if err = appg.Redis.Set(redisconst.NavigateRecreationCache, data, 6*time.Minute); err != nil {
|
||||
log.Warn(fmt.Sprintf("娱乐广告保存缓存数据异常:%v", err))
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
// RecreationClick 娱乐广告点击记录,请求第三方(navigate)
|
||||
func RecreationClick(p RecreationClickInfo) {
|
||||
//请求url
|
||||
url := appg.Conf.URL.NavigateUrl + "/api/application/click"
|
||||
//请求
|
||||
res, err := httputil.DefaultClientPostJson(url, nil, p)
|
||||
if err != nil {
|
||||
log.Error("RecreationClick POSTWithJResp", log.Any("url", url), log.E(err))
|
||||
return
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
log.Error("RecreationClick response status", log.Any("code", res.StatusCode))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RecreationFromJt 从广告中心拉取福利页广告
|
||||
func RecreationFromJt() (RecreationListRes, error) {
|
||||
var resp RecreationListRes
|
||||
|
||||
jtAds, err := JtAdvertiseThreeServer()
|
||||
if err != nil {
|
||||
log.Error("JtAdvertiseThreeServer error occur", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
for _, loc := range jtAds {
|
||||
pos, err := strconv.Atoi(loc.AdvertiseLocationCode)
|
||||
if err != nil {
|
||||
log.Error("广告位置代码错误,必须为数字", log.Any("code", loc.AdvertiseLocationCode))
|
||||
continue
|
||||
}
|
||||
if pos < AdPosGame {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ad := range loc.AdDetailInfoList {
|
||||
extra := ad.GetExtraData()
|
||||
if pos == AdPosAds {
|
||||
resp.AdvList = append(resp.AdvList, AdvInfo{
|
||||
Id: ad.AdvertiseCode,
|
||||
Name: ad.AdvertiseName,
|
||||
Image: ad.GetCoverLsj(),
|
||||
Url: ad.GetRealLink(nil, nil),
|
||||
Location: extra.Location,
|
||||
ModuleType: ad.GetAdvertiseType(),
|
||||
Position: pos,
|
||||
PositionName: loc.AdvertiseLocationName,
|
||||
})
|
||||
continue
|
||||
}
|
||||
appInfo := AppInfo{
|
||||
Id: ad.AdvertiseCode,
|
||||
Name: ad.AdvertiseName,
|
||||
Desc: ad.AdvertiseDesc,
|
||||
Icon: ad.GetCoverLsj(),
|
||||
Url: ad.GetRealLink(nil, nil),
|
||||
Type: extra.Type,
|
||||
DownloadNum: extra.DownloadCount,
|
||||
Position: pos,
|
||||
PositionName: loc.AdvertiseLocationName,
|
||||
}
|
||||
switch pos {
|
||||
case AdPosGame:
|
||||
resp.GameAppList = append(resp.GameAppList, appInfo)
|
||||
case AdPosZhiBo:
|
||||
resp.ZbAppList = append(resp.ZbAppList, appInfo)
|
||||
case AdPosPaoTai:
|
||||
resp.YpAppList = append(resp.YpAppList, appInfo)
|
||||
case AdPosQiPai:
|
||||
resp.QpAppList = append(resp.QpAppList, appInfo)
|
||||
case AdPosHeng:
|
||||
resp.HengAppList = append(resp.HengAppList, appInfo)
|
||||
case AdPosShu:
|
||||
resp.ShuAppList = append(resp.ShuAppList, appInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package adser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/version"
|
||||
"91porn-server/models/s/useradverstatmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func CanRecord(user *usermod.User) bool {
|
||||
if user.SysType == constant.SysTypeAndroid &&
|
||||
//user.OriVer != "" &&
|
||||
user.AdGroup.InABC() {
|
||||
return VersionCompare(user.OriVer, constant.AdGroupClientVersion)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func VersionCompare(v1, v2 string) bool {
|
||||
ver1, err := version.New(v1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ver2, err := version.New(v2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return ver1.GTE(ver2)
|
||||
}
|
||||
|
||||
func UpsertAdStat(ctx context.Context, user *usermod.User, now time.Time, incAppClick, incAdClick int64, payAmount int64) error {
|
||||
if !CanRecord(user) && appg.Conf.Base.Env != "test" {
|
||||
return errors.New("user not eligible for ad stat recording")
|
||||
}
|
||||
local, _ := time.LoadLocation("Asia/Shanghai")
|
||||
|
||||
// 把 day 归一化到当天 0 点,保证和索引一致
|
||||
now = now.In(local)
|
||||
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, local)
|
||||
|
||||
filter := bson.M{
|
||||
"userId": user.UID,
|
||||
"date": day,
|
||||
}
|
||||
|
||||
inc := bson.M{}
|
||||
if incAppClick != 0 {
|
||||
inc["appClick"] = incAppClick
|
||||
}
|
||||
if incAdClick != 0 {
|
||||
inc["adClick"] = incAdClick
|
||||
}
|
||||
deltaTotal := incAppClick + incAdClick
|
||||
if deltaTotal != 0 {
|
||||
inc["totalClick"] = deltaTotal
|
||||
}
|
||||
if payAmount != 0 {
|
||||
inc["payCount"] = 1
|
||||
inc["payTotal"] = payAmount
|
||||
}
|
||||
createdAt := user.CreatedAt.In(local)
|
||||
regDay := time.Date(createdAt.Year(), createdAt.Month(), createdAt.Day(), 0, 0, 0, 0, createdAt.Location())
|
||||
|
||||
update := bson.M{
|
||||
// 只在插入时写入的字段
|
||||
"$setOnInsert": bson.M{
|
||||
"userId": user.UID,
|
||||
"date": day,
|
||||
"createdAt": now,
|
||||
"districtCode": user.DistrictCode,
|
||||
"sysType": user.SysType,
|
||||
"adGroup": user.AdGroup,
|
||||
"regDay": regDay,
|
||||
"version": user.OriVer,
|
||||
},
|
||||
// 每次更新都要改的字段
|
||||
"$set": bson.M{
|
||||
"updatedAt": now,
|
||||
},
|
||||
}
|
||||
if len(inc) > 0 {
|
||||
update["$inc"] = inc
|
||||
}
|
||||
return useradverstatmod.UpsertOne(filter, update)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package advance_config_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/advanceconfigmod"
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"fmt"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GainAdvanceConfig(uid uint64) (data advanceconfigmod.AppAdvanceRes, err error) {
|
||||
var resp advanceconfigmod.AppAdvanceRes
|
||||
|
||||
str, err := appg.Redis.Get(redisconst.GameAdvanceKey(uid))
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("uid:%v;缓存获取游戏预售列表信息异常:%v", uid, err))
|
||||
}
|
||||
if str != nil {
|
||||
if err = msgpack.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
log.Warn(fmt.Sprintf("uid:%v;解析游戏预售缓存数据异常:%v", uid, err))
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
filter := bson.M{"isDelete": false, "status": 1}
|
||||
op := options.Find().SetSort(bson.M{"sortCode": -1})
|
||||
list, err := advanceconfigmod.QueryAllDocument(filter, op)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("uid:%v, advanceconfigmod QueryAllDocument err:%v", uid, err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, a := range list {
|
||||
ad := advanceconfigmod.AppAdvanceConfig{
|
||||
Type: a.Type,
|
||||
Cover: a.Cover,
|
||||
Url: a.Url,
|
||||
Name: a.Name,
|
||||
SourceURL: a.SourceURL,
|
||||
}
|
||||
resp.List = append(resp.List, &ad)
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
|
||||
products, err := productmod.FindByProductType(commod.GameAdvanceCard)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("uid:%v, productmod FindByProductType err:%v", uid, err))
|
||||
return
|
||||
}
|
||||
|
||||
var gameCards []*advanceconfigmod.ProductCard
|
||||
if products != nil && len(products) > 0 {
|
||||
resp.ActivityDetail.ActivityTime = products[0].ActivityTime
|
||||
resp.ActivityDetail.StartTime = products[0].StartTime
|
||||
resp.ActivityDetail.EndTime = products[0].EndTime
|
||||
for _, p := range products {
|
||||
var privilege []advanceconfigmod.PrivilegeInfo
|
||||
if len(p.NewPrivilege) > 0 {
|
||||
for _, np := range p.NewPrivilege {
|
||||
privilege = append(privilege, advanceconfigmod.PrivilegeInfo{
|
||||
Name: np.Name,
|
||||
Image: np.Image,
|
||||
Desc: np.Desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
gc := &advanceconfigmod.ProductCard{
|
||||
ID: p.ID,
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Position: p.Position,
|
||||
BGImg: p.BGImg,
|
||||
SendGame: p.SendGame,
|
||||
OriginalPrice: p.OriginalPrice,
|
||||
DiscountedPrice: *p.DiscountedPriceIos,
|
||||
DiscountedPriceIos: *p.DiscountedPriceIos,
|
||||
DiscountedPriceAnd: *p.DiscountedPriceAnd,
|
||||
Desc: p.Desc,
|
||||
NewPrivilege: privilege,
|
||||
}
|
||||
gameCards = append(gameCards, gc)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户购买记录
|
||||
histories, err := prdcthsomod.FindUserProductHistoryByType(uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("uid:%v, prdcthsomod FindUserProductHistoryByType err:%v", uid, err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(histories) > 0 {
|
||||
resp.ActivityDetail.Status = 1
|
||||
if len(gameCards) > 0 {
|
||||
for _, g := range gameCards {
|
||||
for _, h := range histories {
|
||||
if g.ID.Hex() == h.ProductID.Hex() {
|
||||
g.IsPay = true
|
||||
g.GameCode = append(g.GameCode, h.GameCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.CardList = gameCards
|
||||
|
||||
})
|
||||
wg.Wait()
|
||||
|
||||
common.Go(func() {
|
||||
bytes, _ := msgpack.Marshal(resp)
|
||||
if err = appg.Redis.Set(redisconst.GameAdvanceKey(uid), bytes, 2*time.Minute); err != nil {
|
||||
log.Warn(fmt.Sprintf("uid:%v;保存游戏预售缓存数据异常:%v", uid, err))
|
||||
}
|
||||
})
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package advance_ser
|
||||
|
||||
import (
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GainAdvanceStatus(uid uint64) (resp advanceordermod.AdvanceStatus) {
|
||||
var (
|
||||
data advanceordermod.AdvanceStatus
|
||||
pId primitive.ObjectID
|
||||
activityPopUp bool
|
||||
gameActivityPopUp bool
|
||||
)
|
||||
//str, err := appg.Redis.Get(redisconst.AdvanceKey(uid))
|
||||
//if err != nil {
|
||||
// log.Warn(fmt.Sprintf("uid:%v;缓存获取预售列表信息异常:%v", uid, err))
|
||||
//}
|
||||
//if str != nil {
|
||||
// if err = msgpack.Unmarshal([]byte(*str), &data); err == nil {
|
||||
// return data
|
||||
// }
|
||||
// log.Warn(fmt.Sprintf("uid:%v;解析预售缓存数据异常:%v", uid, err))
|
||||
//}
|
||||
|
||||
// 1 获取活动开启状态
|
||||
var types = []commod.ProductType{productmod.AdvanceCard, productmod.GameAdvanceCard}
|
||||
pr, err := productmod.FindByProductTypes(types)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if pr != nil && len(pr) > 0 {
|
||||
for _, p := range pr {
|
||||
if p.ProductType == productmod.AdvanceCard && p.ActivityTime.Before(now) && p.EndTime.After(now) {
|
||||
if !activityPopUp && p.IsHomePopUp {
|
||||
activityPopUp = true
|
||||
}
|
||||
data.ActivityStatus = true
|
||||
pId = p.ID
|
||||
data.BalanceAmount = p.BalanceAmount / 10
|
||||
data.StartTime = p.StartTime
|
||||
data.EndTime = p.EndTime
|
||||
continue
|
||||
}
|
||||
if p.ProductType == productmod.GameAdvanceCard && p.ActivityTime.Before(now) && p.EndTime.After(now) {
|
||||
if !gameActivityPopUp && p.IsHomePopUp {
|
||||
gameActivityPopUp = true
|
||||
}
|
||||
data.GameActivityStatus = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
data.ActivityPopUp = activityPopUp
|
||||
data.GameActivityPopUp = gameActivityPopUp
|
||||
|
||||
// 2 获取尾款支付状态
|
||||
advanceOrders, err := advanceordermod.IsExist(bson.M{"uid": uid, "productID": pId})
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
if advanceOrders != nil && !advanceOrders.ID.IsZero() {
|
||||
if (advanceOrders.Status == advanceordermod.AdvanceSUCCESS || advanceOrders.Status == advanceordermod.BalanceProcessing) && advanceOrders.EndTime.After(now) {
|
||||
data.BalancePayment = true
|
||||
data.OId = advanceOrders.ID.Hex()
|
||||
// 预付款相关免费权益信息
|
||||
data.PrivilegeLimit = advanceordermod.PrivilegeLimit{
|
||||
HasLimit: true,
|
||||
Limit: struct {
|
||||
CoinVideoLimitPerDay int64 `json:"coinVideoLimitPerDay"`
|
||||
LuckyDrawLimitPerDay int64 `json:"luckyDrawLimitPerDay"`
|
||||
AiUndressLimitPerDay int64 `json:"aiUndressLimitPerDay"`
|
||||
DownloadLimitPerDay int64 `json:"downloadLimitPerDay"`
|
||||
}{
|
||||
advanceOrders.PrepaidPrivilege.CoinVideoLimitPerDay,
|
||||
advanceOrders.PrepaidPrivilege.LuckyDrawLimitPerDay,
|
||||
advanceOrders.PrepaidPrivilege.AiUndressLimitPerDay,
|
||||
advanceOrders.PrepaidPrivilege.DownloadLimitPerDay,
|
||||
},
|
||||
Remain: struct {
|
||||
TodayCoinVideoCount int64 `json:"todayCoinVideoCount"`
|
||||
TodayLuckyDrawCount int64 `json:"todayLuckyDrawCount"`
|
||||
TodayAiUndressCount int64 `json:"todayAiUndressCount"`
|
||||
TodayDownloadCount int64 `json:"todayDownloadCount"`
|
||||
}{
|
||||
advanceOrders.PrepaidPrivilege.CoinVideoLimitPerDay - advanceOrders.TodayUse.CoinVideoCount,
|
||||
advanceOrders.PrepaidPrivilege.LuckyDrawLimitPerDay - advanceOrders.TodayUse.LuckyDrawCount,
|
||||
advanceOrders.PrepaidPrivilege.AiUndressLimitPerDay - advanceOrders.TodayUse.AiUndressCount,
|
||||
advanceOrders.PrepaidPrivilege.DownloadLimitPerDay - advanceOrders.TodayUse.DownloadCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
data.Status = advanceOrders.Status
|
||||
}
|
||||
|
||||
//common.Go(func() {
|
||||
// bytes, _ := msgpack.Marshal(data)
|
||||
// if err = appg.Redis.Set(redisconst.AdvanceKey(uid), bytes, 2*time.Minute); err != nil {
|
||||
// log.Warn(fmt.Sprintf("uid:%v;保存预售缓存数据异常:%v", uid, err))
|
||||
// }
|
||||
//})
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package ai_changeface_img_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/ai_undress_server"
|
||||
"91porn-server/app/service/aiautoser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/aiService"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/aichangefaceimgmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func List(uid uint64, req *aichangefaceimgmod.ListRequest) (interface{}, stderr.Code) {
|
||||
var data = map[string]interface{}{
|
||||
"list": []interface{}{},
|
||||
"count": 0,
|
||||
}
|
||||
count, err := aichangefaceimgmod.CountDocument(req.Filter(uid))
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
if count == 0 {
|
||||
return data, stderr.Success
|
||||
}
|
||||
opts := options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit() + 1)).SetSort(bson.M{"createdAt": -1})
|
||||
list, err := aichangefaceimgmod.QueryAllDocument(req.Filter(uid), opts)
|
||||
if err != nil {
|
||||
return data, stderr.ErrDbQueryError
|
||||
}
|
||||
hasNext := false
|
||||
if len(list) > int(req.Limit()) {
|
||||
list = list[:req.Limit()]
|
||||
hasNext = true
|
||||
}
|
||||
data["count"] = count // 兼容旧版本
|
||||
data["hasNext"] = hasNext
|
||||
data["list"] = list
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
func Generate(uid uint64, req *aichangefaceimgmod.GenerateRequest, ua ua.UA, ip string) stderr.Code {
|
||||
// 获取AI模版图
|
||||
id, err := primitive.ObjectIDFromHex(req.MId)
|
||||
if err != nil {
|
||||
return stderr.ErrParamError
|
||||
}
|
||||
mod, err := aiService.NewAiService(
|
||||
aiService.AppId(int(commod.KFK_APPID)),
|
||||
aiService.Url(appg.Conf.URL.AiServer),
|
||||
aiService.Redis(appg.Redis),
|
||||
).GetTemplate(id)
|
||||
if mod.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
|
||||
req.Coin = int64(mod.Coin)
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if user.IsVIP(time.Now()) {
|
||||
req.Coin = int64(mod.VipCoin)
|
||||
}
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(uid, []txnmod.TransType{
|
||||
txnmod.AiChangeFaceImgDebitFreeTimes,
|
||||
txnmod.AiChangeFaceImgDebitGold,
|
||||
txnmod.AiChangeFaceImgDebitIncomeGold,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
privilegeFreeCount := int64(0)
|
||||
picCount := int64(1)
|
||||
hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid)
|
||||
if hasPrivilege {
|
||||
// 获取可用免费次数
|
||||
validCount := advance.PrepaidPrivilege.AiUndressLimitPerDay - advance.TodayUse.AiUndressCount
|
||||
if validCount >= picCount {
|
||||
// 扣除免费次数
|
||||
debitPlan := advanceordermod.DebitPlan{}
|
||||
debitPlan.AiUndressCount = &picCount
|
||||
if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
privilegeFreeCount = picCount
|
||||
picCount = 0
|
||||
} else if validCount > 0 {
|
||||
// 扣除免费次数
|
||||
debitPlan := advanceordermod.DebitPlan{}
|
||||
debitPlan.AiUndressCount = &validCount
|
||||
if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
privilegeFreeCount = validCount
|
||||
picCount -= validCount
|
||||
}
|
||||
if validCount > 0 {
|
||||
l := &txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: 1,
|
||||
ActualAmount: float64(-1),
|
||||
TranType: txnmod.AiChangeFaceImgDebitFreeTimes.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimes),
|
||||
Desc: fmt.Sprintf("生成AI图片换脸扣除免费次数[%d次]", privilegeFreeCount),
|
||||
RealAmount: decimal.Decimal{},
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
}
|
||||
txnErr := txnmod.InsertTransactionLog(nil, l)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
}
|
||||
var debitFreeCount, debitAmountCoins, debitIncomeCoins int64
|
||||
|
||||
if picCount > 0 {
|
||||
// 获取用户钱包
|
||||
w, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
// 校验用户余额
|
||||
if w == nil {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
|
||||
debitFreeCount, debitAmountCoins, debitIncomeCoins = ai_undress_server.TotalDebit(req.Coin, picCount, *w)
|
||||
if debitFreeCount > 0 {
|
||||
req.IsFreeTimes = true
|
||||
}
|
||||
// 如果收费的部分余额不够支付
|
||||
if req.Coin*(picCount-debitFreeCount) > (w.Amount + w.Income) {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
}
|
||||
var orderId primitive.ObjectID
|
||||
var orderCreatedAt time.Time
|
||||
if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
var tl []txnmod.TransactionLog
|
||||
if debitFreeCount > 0 {
|
||||
expire := carbon.Tomorrow().StartOfDay().StdTime().Sub(time.Now())
|
||||
// 累加次数(跟ai脱衣共用这个次数)
|
||||
appg.Redis.IncrBy(redisconst.AiFreeUndressTodayUseTimesKey(uid), debitFreeCount, expire)
|
||||
// 扣除钱剩余次数
|
||||
wallet, wErr := walletmod.DebitAiFreeTimes(tool, debitFreeCount, uid)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AIChangeFaceImg Generate walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: 1,
|
||||
ActualAmount: float64(-1),
|
||||
TranType: txnmod.AiChangeFaceImgDebitFreeTimes.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimes),
|
||||
Desc: fmt.Sprintf("生成AI图片换脸扣除免费次数[%d次]", debitFreeCount),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
|
||||
if debitIncomeCoins > 0 || debitAmountCoins > 0 {
|
||||
// 扣除钱包余额
|
||||
wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, uid)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if debitAmountCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -debitAmountCoins,
|
||||
ActualAmount: float64(-debitAmountCoins),
|
||||
TranType: txnmod.AiChangeFaceImgDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangeFaceImgDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI图片换脸金币-%d", debitAmountCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
if debitIncomeCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -debitIncomeCoins,
|
||||
ActualAmount: float64(-debitIncomeCoins),
|
||||
TranType: txnmod.AiChangeFaceImgDebitIncomeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangeFaceImgDebitIncomeGold),
|
||||
Desc: fmt.Sprintf("生成AI图片换脸收益金币-%d", debitIncomeCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(tl) > 0 {
|
||||
txnErr := txnmod.InsertManyTransactionLog(tool, tl)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
// 新增AI脱衣记录
|
||||
orderId = primitive.NewObjectID()
|
||||
orderCreatedAt = time.Now()
|
||||
if err = aichangefaceimgmod.InsertOne(tool, req.Generate(orderId, orderCreatedAt, uid, mod.Title, mod.Cover, privilegeFreeCount, debitFreeCount, debitAmountCoins, debitIncomeCoins)); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle Aichangefaceimg Generate InsertOnes error:%+v:", err), log.Any("uid", uid))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle Aichangefaceimg Generate Trans error:%+v;uid:%v;", err, uid))
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
common.Go(func() {
|
||||
if err := aiautoser.SubmitImage(orderId); err != nil {
|
||||
log.Error("AI图片换脸自动处理失败", log.Any("id", orderId.Hex()), log.E(err))
|
||||
}
|
||||
})
|
||||
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func AiChangeFaceImgHide(uid uint64, req *aichangefaceimgmod.DelRequest) stderr.Code {
|
||||
acf, err := aichangefaceimgmod.FindByID(nil, req.ID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if acf.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
if acf.UID != uid {
|
||||
return stderr.Failure
|
||||
}
|
||||
if acf.Status == aichangefaceimgmod.Processing || acf.Status == aichangefaceimgmod.StatusSubmit {
|
||||
return stderr.AiGenningDelForbidden
|
||||
}
|
||||
if err = aichangefaceimgmod.Edit(nil, req.Filter(), req.Update()); err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func TotalDebit(price int64, count int64, w walletmod.Wallet) (fCount, amountCoins, incomeCoins int64) {
|
||||
var (
|
||||
debitFreeTimes int64
|
||||
debitAmountCoins int64
|
||||
debitIncomeCoins int64
|
||||
)
|
||||
if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes >= count {
|
||||
debitFreeTimes = count
|
||||
}
|
||||
|
||||
if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes < count {
|
||||
debitFreeTimes = w.AiUndressFreeTimes
|
||||
debitCoins := (count - debitFreeTimes) * price
|
||||
if w.Amount >= debitCoins {
|
||||
debitAmountCoins = debitCoins
|
||||
}
|
||||
if w.Amount < debitCoins {
|
||||
debitAmountCoins = w.Amount
|
||||
debitIncomeCoins = debitCoins - w.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if w.AiUndressFreeTimes <= 0 {
|
||||
debitCoins := price * count
|
||||
if w.Amount >= debitCoins {
|
||||
debitAmountCoins = debitCoins
|
||||
}
|
||||
if w.Amount < debitCoins {
|
||||
debitAmountCoins = w.Amount
|
||||
debitIncomeCoins = debitCoins - w.Amount
|
||||
}
|
||||
}
|
||||
return debitFreeTimes, debitAmountCoins, debitIncomeCoins
|
||||
}
|
||||
|
||||
func CheckBalance(price int64, count int64, w walletmod.Wallet) bool {
|
||||
var checkStats bool
|
||||
var totalCoins = w.Amount
|
||||
if w.Income != 0 {
|
||||
totalCoins += w.Income
|
||||
}
|
||||
if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes >= count {
|
||||
checkStats = true
|
||||
}
|
||||
|
||||
if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes < count && totalCoins >= price*(count-w.AiUndressFreeTimes) {
|
||||
checkStats = true
|
||||
}
|
||||
|
||||
if w.AiUndressFreeTimes <= 0 && totalCoins > price*count {
|
||||
checkStats = true
|
||||
}
|
||||
return checkStats
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
package ai_changeface_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/aiautoser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/aiService"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aichangefacemod"
|
||||
"91porn-server/models/v/aichangefacevidmod"
|
||||
"91porn-server/models/v/backpackmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func List(uid uint64, status *aichangefacemod.AiChangeFaceStatus, skip, limit int) ([]aichangefacemod.AiChangeFace, bool, error) {
|
||||
acfs, err := aichangefacemod.ListByUid(uid, status, skip, limit+1)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasNext := false
|
||||
if len(acfs) > limit {
|
||||
acfs = acfs[:limit]
|
||||
hasNext = true
|
||||
}
|
||||
return acfs, hasNext, nil
|
||||
}
|
||||
|
||||
func Generate(uid uint64, pics []string, vidModId primitive.ObjectID, discount []primitive.ObjectID, shareTitle string, shareStatus int, ua ua.UA, ip string) stderr.Code {
|
||||
vidMod, err := aiService.NewAiService(
|
||||
aiService.AppId(int(commod.KFK_APPID)),
|
||||
aiService.Url(appg.Conf.URL.AiServer),
|
||||
aiService.Redis(appg.Redis),
|
||||
).GetTemplate(vidModId)
|
||||
if vidMod.ID.IsZero() {
|
||||
return stderr.Failure
|
||||
}
|
||||
var discountValue int64
|
||||
now := time.Now()
|
||||
var dsId []primitive.ObjectID
|
||||
for _, d := range discount {
|
||||
dsId = append(dsId, d)
|
||||
bp, err := backpackmod.GetByID(nil, d)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if bp == nil { // 抵扣券不存在
|
||||
return stderr.Failure
|
||||
}
|
||||
if bp.GoodsType != backpackmod.AiChangeFaceDiscount { // 抵扣券类型不对
|
||||
return stderr.Failure
|
||||
}
|
||||
if bp.Status == backpackmod.Used { // 抵扣券已被使用
|
||||
return stderr.Failure
|
||||
}
|
||||
if bp.ExpiredTime.Before(now) { // 抵扣券已过期
|
||||
return stderr.Failure
|
||||
}
|
||||
discountValue += bp.GoodsValue
|
||||
}
|
||||
originPrice := vidMod.Coin
|
||||
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if user.IsVIP(time.Now()) {
|
||||
originPrice = vidMod.VipCoin
|
||||
}
|
||||
|
||||
price := int64(originPrice) - discountValue
|
||||
if price < 0 {
|
||||
price = 0
|
||||
}
|
||||
|
||||
// 获取钱包信息
|
||||
wlt, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if wlt == nil || wlt.Amount+wlt.Income < price {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
|
||||
// 扣除的普通金币和收益金币
|
||||
debitAmt, debitIncome := TotalDebit(price, wlt)
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(uid, []txnmod.TransType{
|
||||
txnmod.AiChangefaceDebitGold,
|
||||
txnmod.AiChangefaceDebitInComeGold,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
var orderId primitive.ObjectID
|
||||
var orderCreatedAt time.Time
|
||||
if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
var tl []txnmod.TransactionLog
|
||||
if debitAmt > 0 || debitIncome > 0 {
|
||||
// 扣除钱包余额
|
||||
wallet, err := walletmod.DebitAmountAndIncome(tool, debitAmt, debitIncome, uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiChangeface Generate walletmod.DebitAmount error:%+v:", err), log.Any("uid", uid))
|
||||
if err.Error() == "no enough balance" {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
if debitAmt > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -debitAmt,
|
||||
ActualAmount: -float64(debitAmt),
|
||||
TranType: txnmod.AiChangefaceDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangefaceDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI换脸金币-%d", debitAmt),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
|
||||
if debitIncome > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -debitIncome,
|
||||
ActualAmount: -float64(debitIncome),
|
||||
TranType: txnmod.AiChangefaceDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangefaceDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI换脸收益金币-%d", debitIncome),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(tl) > 0 {
|
||||
txnErr := txnmod.InsertManyTransactionLog(tool, tl)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiChangeface Generate txnmod.InsertManyTransactionLog error:%+v:", txnErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
// 抵扣券使用
|
||||
if len(dsId) > 0 {
|
||||
modified, err := backpackmod.UseManyGoods(tool, dsId)
|
||||
if err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
if modified != int64(len(dsId)) {
|
||||
return stderr.Failure
|
||||
}
|
||||
}
|
||||
|
||||
// 新增AI脱衣记录
|
||||
orderId = primitive.NewObjectID()
|
||||
orderCreatedAt = time.Now()
|
||||
if err = aichangefacemod.AddAiChangeFace(tool, orderId, orderCreatedAt, uid, pics, vidMod, debitAmt, debitIncome, discount, shareTitle, shareStatus); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiChangeface Generate InsertOnes error:%+v:", err), log.Any("uid", uid))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
if e, ok := err.(stderr.Code); ok {
|
||||
return e
|
||||
}
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
common.Go(func() {
|
||||
if err := aiautoser.SubmitVideo(orderId); err != nil {
|
||||
log.Error("AI视频换脸自动处理失败", log.Any("id", orderId.Hex()), log.E(err))
|
||||
}
|
||||
})
|
||||
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func TotalDebit(coins int64, w *walletmod.Wallet) (amtDebit, income int64) {
|
||||
var (
|
||||
deAmt int64
|
||||
deIncome int64
|
||||
)
|
||||
if w.Amount >= coins {
|
||||
deAmt = coins
|
||||
}
|
||||
if w.Amount < coins && w.Amount+w.Income >= coins {
|
||||
deAmt = w.Amount
|
||||
deIncome = coins - w.Amount
|
||||
}
|
||||
return deAmt, deIncome
|
||||
}
|
||||
|
||||
// ModList 获取模版列表
|
||||
func ModList(uid uint64) (data *aichangefacevidmod.AppResponse, err error) {
|
||||
var ret aichangefacevidmod.AppResponse
|
||||
key := redisconst.AIModCache
|
||||
str, err := appg.Redis.Get(key)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;缓存获取AI模版列表信息异常:%v", uid, err))
|
||||
}
|
||||
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &ret); err == nil {
|
||||
return &ret, nil
|
||||
}
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err))
|
||||
}
|
||||
|
||||
mods, err := aichangefacevidmod.GetAllMods()
|
||||
if err != nil {
|
||||
return &ret, err
|
||||
}
|
||||
|
||||
if len(mods) > 0 {
|
||||
for _, m := range mods {
|
||||
if m.ModuleType == aichangefacevidmod.AIUndress {
|
||||
ret.AiUndressMod = append(ret.AiUndressMod, aichangefacevidmod.AiUndressMod{
|
||||
ID: m.ID,
|
||||
Cover: m.Cover,
|
||||
})
|
||||
}
|
||||
if m.ModuleType == aichangefacevidmod.AIImgToVideo {
|
||||
ret.AiImgToVideoMod = append(ret.AiImgToVideoMod, aichangefacevidmod.AiImgToVideoMod{
|
||||
ID: m.ID,
|
||||
Title: m.Title,
|
||||
SceneType: m.SceneType,
|
||||
Cover: m.Cover,
|
||||
NewUrl: m.NewUrl,
|
||||
})
|
||||
}
|
||||
if m.ModuleType == aichangefacevidmod.AITextToImage {
|
||||
ret.AiTextToImgMod = append(ret.AiTextToImgMod, aichangefacevidmod.AiTextToImgMod{
|
||||
ID: m.ID,
|
||||
Cover: m.Cover,
|
||||
Title: m.Title,
|
||||
StyleType: m.HotValue,
|
||||
})
|
||||
}
|
||||
//if m.ModuleType == aichangefacevidmod.AIVideoChangeFace {
|
||||
// ret.AiChangeFaceVideoMod = append(ret.AiChangeFaceVideoMod, aichangefacevidmod.AiChangeMod{
|
||||
// ID: m.ID,
|
||||
// Title: m.Title,
|
||||
// SourceURL: m.SourceURL,
|
||||
// Cover: m.Cover,
|
||||
// PlayTime: m.PlayTime,
|
||||
// Type: m.Type,
|
||||
// HotMark: m.HotMark,
|
||||
// HotValue: m.HotValue,
|
||||
// Coin: m.Coin,
|
||||
// VipCoin: m.VipCoin,
|
||||
// })
|
||||
//}
|
||||
//if m.ModuleType == aichangefacevidmod.AIImgChangeFace {
|
||||
// ret.AiChangeFaceMod = append(ret.AiChangeFaceMod, aichangefacevidmod.AiChangeFaceMod{
|
||||
// Title: m.Title,
|
||||
// ID: m.ID,
|
||||
// Cover: m.Cover,
|
||||
// HotMark: m.HotMark,
|
||||
// HotValue: m.HotValue,
|
||||
// Coin: m.Coin,
|
||||
// VipCoin: m.VipCoin,
|
||||
// })
|
||||
//}
|
||||
}
|
||||
}
|
||||
common.Go(func() {
|
||||
d, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = appg.Redis.Set(key, d, 5*time.Minute); err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err))
|
||||
}
|
||||
})
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
type ModListV2Req struct {
|
||||
Type int `json:"type" form:"type"` // 0-图片换脸 1-视频换脸
|
||||
CategoryId string `json:"categoryId" form:"categoryId"` // 模版分类id,如果为空,则默认第一个模版分类
|
||||
}
|
||||
|
||||
type ModListV2Resp struct {
|
||||
CategoryList []*aiService.Category `json:"categoryList"`
|
||||
TemplateList []*aiService.Template `json:"templateList"`
|
||||
CategoryId string `json:"categoryId"` // 当前返回的分类id下的模版
|
||||
}
|
||||
|
||||
// ModListV2 获取模版列表
|
||||
func ModListV2(req ModListV2Req) (resp ModListV2Resp, err error) {
|
||||
ai := aiService.NewAiService(
|
||||
aiService.AppId(int(commod.KFK_APPID)),
|
||||
aiService.Url(appg.Conf.URL.AiServer),
|
||||
aiService.Redis(appg.Redis),
|
||||
)
|
||||
resp.CategoryList, resp.CategoryId, resp.TemplateList, err = ai.GetTemplateList(req.Type, req.CategoryId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type ModInfoReq struct {
|
||||
Id string `json:"id" form:"id"`
|
||||
Type int `json:"type" form:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画
|
||||
}
|
||||
|
||||
type ModInfoResp struct {
|
||||
AiUndressMod aichangefacevidmod.AiUndressMod `json:"aiUndressMod" bson:"aiUndressMod"` // AI脱衣模版
|
||||
AiTextToImgMod aichangefacevidmod.AiTextToImgMod `json:"aiTextToImgMod" bson:"aiTextToImgMod"` // AI绘图模型
|
||||
AiImgToVideoMod aichangefacevidmod.AiImgToVideoMod `json:"aiImgToVideoMod" bson:"aiImgToVideoMod"` // AI图生视频模型
|
||||
AiChangeFaceMod aiService.Template `json:"aiChangeFaceMod" bson:"aiChangeFaceMod"` // Ai视频/图片换脸模型
|
||||
}
|
||||
|
||||
func (p *ModInfoReq) GetInfo() (resp ModInfoResp, err error) {
|
||||
id, _ := primitive.ObjectIDFromHex(p.Id)
|
||||
if id.IsZero() {
|
||||
return resp, errors.New("id无效")
|
||||
}
|
||||
if p.Type > 2 {
|
||||
// 从本地的表里获取
|
||||
m, err := aichangefacevidmod.GetModById(id)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if m.ModuleType == aichangefacevidmod.AIUndress {
|
||||
resp.AiUndressMod = aichangefacevidmod.AiUndressMod{
|
||||
ID: m.ID,
|
||||
Cover: m.Cover,
|
||||
}
|
||||
}
|
||||
if m.ModuleType == aichangefacevidmod.AIImgToVideo {
|
||||
resp.AiImgToVideoMod = aichangefacevidmod.AiImgToVideoMod{
|
||||
ID: m.ID,
|
||||
Cover: m.Cover,
|
||||
NewUrl: m.NewUrl,
|
||||
}
|
||||
}
|
||||
if m.ModuleType == aichangefacevidmod.AITextToImage {
|
||||
resp.AiTextToImgMod = aichangefacevidmod.AiTextToImgMod{
|
||||
ID: m.ID,
|
||||
Cover: m.Cover,
|
||||
Title: m.Title,
|
||||
StyleType: m.HotValue,
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 获取视频/图片换脸模版
|
||||
ai := aiService.NewAiService(
|
||||
aiService.AppId(int(commod.KFK_APPID)),
|
||||
aiService.Url(appg.Conf.URL.AiServer),
|
||||
aiService.Redis(appg.Redis),
|
||||
)
|
||||
template, err := ai.GetTemplate(id)
|
||||
if err != nil {
|
||||
log.Error("ai.GetTemplate fail", log.Any("id", id), log.E(err))
|
||||
return
|
||||
}
|
||||
resp.AiChangeFaceMod = template
|
||||
return
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
package ai_image_to_video_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/cache/aiimagetovideodata"
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aichangefacevidmod"
|
||||
"91porn-server/models/v/aiimagetovideomod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
Status int `json:"status" form:"status"` // 状态 1、排队中 2、生成成功 3、生成失败
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
commod.Page
|
||||
}
|
||||
|
||||
func (p *AppQueryListReq) Filter() primitive.M {
|
||||
filter := bson.M{}
|
||||
switch p.Status {
|
||||
case 1:
|
||||
filter["status"] = bson.M{"$in": []aiimagetovideomod.AiImageToVideoStatus{aiimagetovideomod.StatusOrderSuccess, aiimagetovideomod.StatusSubmitted}}
|
||||
case 2:
|
||||
filter["status"] = aiimagetovideomod.StatusGenerationSuccess
|
||||
case 3:
|
||||
filter["status"] = bson.M{"$in": []aiimagetovideomod.AiImageToVideoStatus{aiimagetovideomod.StatusGenerationFailed, aiimagetovideomod.StatusRefunded}}
|
||||
}
|
||||
filter["uid"] = p.UID
|
||||
filter["isHide"] = false
|
||||
return filter
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
HasNext bool `json:"hasNext"` // 是否有下一页
|
||||
List []*aiimagetovideomod.AiImageToVideoInfo `json:"list"` // 列表
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() (AppListRes, error) {
|
||||
var res AppListRes
|
||||
var err error
|
||||
|
||||
sort := bson.D{{"createdAt", -1}}
|
||||
|
||||
// 获取列表
|
||||
var data []aiimagetovideomod.AiImageToVideo
|
||||
data, _, res.HasNext, err = aiimagetovideomod.GetList(p.Filter(), int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取AI图生视频列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res, err
|
||||
}
|
||||
res.List = aiimagetovideodata.FormatAppDataList(data)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type GenerateReq struct {
|
||||
MID string `json:"mid" bson:"mid"` // 模版ID
|
||||
OriginPic string `json:"originPic" bson:"originPic"` // 脱衣原图
|
||||
Coin int64 `json:"-"` // 此次脱衣金币个数
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
ShareTitle string `json:"shareTitle" bson:"shareTitle"` // 分享标题
|
||||
ShareStatus int `json:"shareStatus" bson:"shareStatus"` // 是否分享 0-不分享 1-分享
|
||||
|
||||
}
|
||||
|
||||
func (p *GenerateReq) GenerateInfo(orderId primitive.ObjectID, orderCreatedAt time.Time, debitCoins, debitIncomeCoins int64, sceneType int) aiimagetovideomod.AiImageToVideo {
|
||||
return aiimagetovideomod.AiImageToVideo{
|
||||
ID: orderId,
|
||||
SceneType: sceneType,
|
||||
Coin: debitIncomeCoins + debitCoins,
|
||||
DebitAmountCoin: debitCoins,
|
||||
DebitIncomeCoin: debitIncomeCoins,
|
||||
IsFreeTimes: false,
|
||||
ImgUrl: p.OriginPic,
|
||||
UID: p.UID,
|
||||
Status: int(aiimagetovideomod.StatusOrderSuccess),
|
||||
IsHide: false,
|
||||
ShareTitle: p.ShareTitle,
|
||||
ShareStatus: p.ShareStatus,
|
||||
UpdatedAt: orderCreatedAt,
|
||||
CreatedAt: orderCreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成订单
|
||||
func (p *GenerateReq) Generate(ua ua.UA, ip string) (stderr.Code, error) {
|
||||
// 默认脱衣
|
||||
sceneType := int(aiimagetovideomod.SceneTagLiftShirt)
|
||||
|
||||
if p.MID != "" {
|
||||
// 获取模版信息
|
||||
id, _ := primitive.ObjectIDFromHex(p.MID)
|
||||
mod, err := aichangefacevidmod.GetModById(id)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if mod.ID.IsZero() {
|
||||
return stderr.CodeEmptyData, errors.New("mod is null")
|
||||
}
|
||||
sceneType = mod.SceneType
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := usermod.FindUserByUID(p.UID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if user.ID.IsZero() {
|
||||
return stderr.UserIsNotExists, errors.New("user is null")
|
||||
}
|
||||
configure, _ := sysconfdata.GetAllFromCache()
|
||||
p.Coin = configure.GetInt(sysconfmod.VCodeAiImageToVideoPrice)
|
||||
var debitAmountCoins, debitIncomeCoins int64
|
||||
|
||||
if p.Coin > 0 {
|
||||
// 获取用户钱包
|
||||
w, err := walletmod.GetWallet(p.UID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if w == nil {
|
||||
return stderr.InsufficientBalance, errors.New("Insufficient balance")
|
||||
}
|
||||
|
||||
// 如果收费的部分余额不够支付
|
||||
if p.Coin > (w.Amount + w.Income) {
|
||||
return stderr.InsufficientBalance, errors.New("Insufficient balance")
|
||||
}
|
||||
|
||||
debitAmountCoins, debitIncomeCoins = TotalDebit(p.Coin, *w)
|
||||
}
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(p.UID, []txnmod.TransType{
|
||||
txnmod.AiImageToVideoDebitGold,
|
||||
txnmod.AiImageToVideoDebitInComeGold,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
var orderId primitive.ObjectID
|
||||
var orderCreatedAt time.Time
|
||||
if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
var tl []txnmod.TransactionLog
|
||||
if debitIncomeCoins > 0 || debitAmountCoins > 0 {
|
||||
// 扣除钱包余额
|
||||
wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, p.UID)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if debitAmountCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: p.UID,
|
||||
Amount: -debitAmountCoins,
|
||||
ActualAmount: float64(-debitAmountCoins),
|
||||
TranType: txnmod.AiImageToVideoDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiImageToVideoDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI图生视频金币-%d", debitAmountCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
if debitIncomeCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: p.UID,
|
||||
Amount: -debitIncomeCoins,
|
||||
ActualAmount: float64(-debitIncomeCoins),
|
||||
TranType: txnmod.AiImageToVideoDebitInComeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiImageToVideoDebitInComeGold),
|
||||
Desc: fmt.Sprintf("生成AI图生视频收益金币-%d", debitIncomeCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(tl) > 0 {
|
||||
txnErr := txnmod.InsertManyTransactionLog(tool, tl)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
// 新增AI图生视频记录
|
||||
orderId = primitive.NewObjectID()
|
||||
orderCreatedAt = time.Now()
|
||||
order := p.GenerateInfo(orderId, orderCreatedAt, debitAmountCoins, debitIncomeCoins, sceneType)
|
||||
if _, err = aiimagetovideomod.Insert(tool, order); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate InsertOnes error:%+v:", err), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate Trans error:%+v;uid:%v;", err, p.UID))
|
||||
return stderr.ErrDbUpdateError, err
|
||||
}
|
||||
|
||||
common.Go(func() {
|
||||
autoUploadOrder(orderId)
|
||||
})
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
type AutoUploadReq struct {
|
||||
AppID int `json:"appId"` // 应用 ID
|
||||
UID uint64 `json:"uid"` // 用户 ID
|
||||
ImgURL string `json:"imgUrl"` // 图片 URL
|
||||
AppOrderNum string `json:"appOrderNum"` // 应用订单号
|
||||
NotifyURL string `json:"notifyUrl"` // 通知 URL
|
||||
SceneType int `json:"sceneType" bson:"sceneType"` // 模版场景类型
|
||||
}
|
||||
|
||||
func autoUploadOrder(id primitive.ObjectID) error {
|
||||
item, err := aiimagetovideomod.GetInfo(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.ID.IsZero() {
|
||||
return errors.New(" aiimagetovideo order is null")
|
||||
}
|
||||
|
||||
// 将请求参数编码为JSON
|
||||
requestBody := AutoUploadReq{
|
||||
AppID: int(commod.KFK_APPID),
|
||||
UID: item.UID,
|
||||
ImgURL: item.ImgUrl,
|
||||
AppOrderNum: item.ID.Hex(),
|
||||
SceneType: item.SceneType,
|
||||
NotifyURL: fmt.Sprintf("%v/api/web/admin/ai/image_to_video/callback", appg.Conf.URL.AiImageToVideoCallbackUrl),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiURL := getAutoUploadUrl()
|
||||
|
||||
// 创建请求对象
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 通过http.Client发送请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果返回状态码非200,可以考虑返回错误
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("请求失败,状态码:%d,内容:%s", resp.StatusCode, string(respBytes))
|
||||
}
|
||||
|
||||
// 更新订单
|
||||
updateCond := bson.M{
|
||||
"status": aiimagetovideomod.StatusSubmitted,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
_, err = aiimagetovideomod.UpdateByID(nil, id, updateCond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAutoUploadUrl() string {
|
||||
return fmt.Sprintf("%v/api/comfyui/prd/create_job", appg.Conf.URL.AiImageToVideoUrl)
|
||||
}
|
||||
|
||||
func TotalDebit(price int64, w walletmod.Wallet) (amountCoins, incomeCoins int64) {
|
||||
var (
|
||||
debitAmountCoins int64
|
||||
debitIncomeCoins int64
|
||||
)
|
||||
|
||||
// 计算当日实际扣减的金额
|
||||
debitCoins := price
|
||||
if w.Amount >= debitCoins {
|
||||
debitAmountCoins = debitCoins
|
||||
} else {
|
||||
debitAmountCoins = w.Amount
|
||||
debitIncomeCoins = debitCoins - w.Amount
|
||||
}
|
||||
return debitAmountCoins, debitIncomeCoins
|
||||
}
|
||||
|
||||
type HideReq struct {
|
||||
ID string `json:"id"` // AI订单ID
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
}
|
||||
|
||||
// Hide 删除订单
|
||||
func (p *HideReq) Hide() error {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := aiimagetovideomod.GetInfo(oid)
|
||||
if err != nil {
|
||||
log.Error("获取AI图生视频列表详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if item.ID.IsZero() {
|
||||
return errors.New("AI订单不存在")
|
||||
}
|
||||
|
||||
if item.UID != p.UID {
|
||||
return errors.New("只能删除自己的订单")
|
||||
}
|
||||
|
||||
if item.Status == int(aiimagetovideomod.StatusSubmitted) {
|
||||
return errors.New("不能删除排队中的订单")
|
||||
}
|
||||
|
||||
if err = aiimagetovideomod.Hide(p.UID, oid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package ai_mate_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
aimate "91porn-server/common/aiMate"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func GetApiUrl() string {
|
||||
var apiUrl string
|
||||
if appg.Conf.URL.AIMateH5 != "" {
|
||||
if appg.Conf.Base.Env == "prod" {
|
||||
apiUrl = fmt.Sprintf("%s/?aId=%d", appg.Conf.URL.AIMateH5, commod.KFK_APPID)
|
||||
} else {
|
||||
apiUrl = fmt.Sprintf("%s/?aId=%d&devTest=1", appg.Conf.URL.AIMateH5, commod.KFK_APPID)
|
||||
}
|
||||
}
|
||||
return apiUrl
|
||||
}
|
||||
|
||||
type LoginResp struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type loginDependencies struct {
|
||||
findUser func(uint64) (*usermod.User, error)
|
||||
updateUser func(uint64, usermod.UserSelector) (*usermod.User, error)
|
||||
getWallet func(uint64) (*walletmod.Wallet, error)
|
||||
getRemoteBalance func(uint64, string) (float64, error)
|
||||
getAccessURL func(uint64, string, string, float64) (string, error)
|
||||
newUUID func() string
|
||||
}
|
||||
|
||||
func defaultLoginDependencies() loginDependencies {
|
||||
return loginDependencies{
|
||||
findUser: usermod.FindUserByUID,
|
||||
updateUser: usermod.Update,
|
||||
getWallet: walletmod.GetWallet,
|
||||
getRemoteBalance: aimate.GetUserAiBalance,
|
||||
getAccessURL: aimate.GetUserAccessAiUrl,
|
||||
newUUID: common.UUID,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 获取当前用户的 AI 女友第三方登录地址。
|
||||
func Login(uid uint64) (LoginResp, error) {
|
||||
if appg.Redis == nil {
|
||||
return LoginResp{}, errors.New("redis is unavailable")
|
||||
}
|
||||
lock := redis.BuildLock(
|
||||
appg.Redis,
|
||||
redisconst.GetAiMateLoginLockKey(uid),
|
||||
redsync.WithExpiry(30*time.Second),
|
||||
redsync.WithTries(1),
|
||||
)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return LoginResp{}, fmt.Errorf("acquire ai mate login lock: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if _, err := lock.Unlock(); err != nil {
|
||||
log.Warn("ai_mate_ser Login unlock failed", log.Any("uid", uid), log.E(err))
|
||||
}
|
||||
}()
|
||||
return login(uid, defaultLoginDependencies())
|
||||
}
|
||||
|
||||
func login(uid uint64, deps loginDependencies) (LoginResp, error) {
|
||||
user, err := deps.findUser(uid)
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
return LoginResp{}, errors.New("ai mate login user not found")
|
||||
}
|
||||
wallet, err := deps.getWallet(uid)
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
|
||||
localBalance := float64(0)
|
||||
if wallet != nil {
|
||||
localBalance = wallet.AiMateBalance
|
||||
}
|
||||
aiMateUID := user.AiMateUid
|
||||
remoteBalance := float64(0)
|
||||
if aiMateUID == "" {
|
||||
aiMateUID = deps.newUUID()
|
||||
if aiMateUID == "" {
|
||||
return LoginResp{}, errors.New("generate ai mate uid failed")
|
||||
}
|
||||
if _, err = deps.updateUser(uid, usermod.UserSelector{AiMateUid: &aiMateUID}); err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
} else {
|
||||
remoteBalance, err = deps.getRemoteBalance(uid, aiMateUID)
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
}
|
||||
|
||||
url, err := deps.getAccessURL(uid, aiMateUID, user.Name, balanceTopUp(localBalance, remoteBalance))
|
||||
if err != nil {
|
||||
return LoginResp{}, err
|
||||
}
|
||||
if url == "" {
|
||||
return LoginResp{}, errors.New("ai mate login url is empty")
|
||||
}
|
||||
return LoginResp{URL: url}, nil
|
||||
}
|
||||
|
||||
func balanceTopUp(localBalance, remoteBalance float64) float64 {
|
||||
if localBalance <= remoteBalance || localBalance <= 0 {
|
||||
return 0
|
||||
}
|
||||
return localBalance - remoteBalance
|
||||
}
|
||||
|
||||
// GetCurrencyList 获取AI伴侣币列表
|
||||
func GetCurrencyList() (code stderr.Code, data interface{}) {
|
||||
code = stderr.Success
|
||||
cs := make([]currencymod.CurrencyApp, 0)
|
||||
//查询
|
||||
currencys, err := currencymod.List(commod.AiMateCoin)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser GetCurrencyList currencymod.List err", log.E(err))
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if len(currencys) > 0 {
|
||||
for _, c := range currencys {
|
||||
cs = append(cs, currencymod.CurrencyApp{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Type: c.Type,
|
||||
Coins: c.Coins,
|
||||
Price: c.Price,
|
||||
CouponDesc: c.CouponDesc,
|
||||
})
|
||||
}
|
||||
}
|
||||
data = cs
|
||||
return
|
||||
}
|
||||
|
||||
// ExchangeReq 兑换请求参数
|
||||
type ExchangeReq struct {
|
||||
Id primitive.ObjectID `json:"id" binding:"required"`
|
||||
}
|
||||
|
||||
// Exchange 获取AI伴侣币兑换
|
||||
func Exchange(uid uint64, req ExchangeReq, ua ua.UA, ip string) (code stderr.Code) {
|
||||
code = stderr.Success
|
||||
//查询用户信息
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser Exchange usermod.FindUserByUID err", log.Any("uid", uid), log.E(err))
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
log.Error("ai_mate_ser Exchange user not exist", log.Any("uid", uid))
|
||||
code = stderr.UserIsNotExists
|
||||
return
|
||||
}
|
||||
//查询货币是否存在
|
||||
currency, err := currencymod.Get(req.Id)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser Exchange currencymod.List err", log.E(err))
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if currency.ID.IsZero() {
|
||||
log.Error("ai_mate_ser Exchange currency not exist", log.Any("id", req.Id.Hex()))
|
||||
code = stderr.CodeEmptyData
|
||||
return
|
||||
}
|
||||
if !currency.IsActive {
|
||||
log.Error("ai_mate_ser Exchange currency isActive is false", log.Any("id", req.Id.Hex()))
|
||||
code = stderr.ErrDataInvalid
|
||||
return
|
||||
}
|
||||
if currency.Type != commod.AiMateCoin {
|
||||
log.Error("ai_mate_ser Exchange currency type not commod.AiMateCoin", log.Any("id", req.Id.Hex()))
|
||||
code = stderr.ExchangeCodeInvalid
|
||||
return
|
||||
}
|
||||
//查询用户钱包
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
code = stderr.ErrDbQueryError
|
||||
log.Error("ai_mate_ser Exchange walletmod.GetWallet err:", log.E(err), log.Any("uid", uid))
|
||||
return
|
||||
}
|
||||
//需要支付的金额
|
||||
price := currency.Price
|
||||
if wallet == nil || wallet.Amount < price {
|
||||
code = stderr.InsufficientBalance
|
||||
log.Error("ai_mate_ser Exchange user Insufficient balance", log.Any("uid", uid), log.Any("id", currency.ID.Hex()))
|
||||
return
|
||||
}
|
||||
|
||||
//兑换业务
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
//扣除金币余额,增加AI伴侣余额
|
||||
cAmount := -price
|
||||
cAiMateBalance := float64(currency.Coins)
|
||||
p := walletmod.CreditPlan{
|
||||
Amount: &cAmount,
|
||||
AiMateBalance: &cAiMateBalance,
|
||||
}
|
||||
wallet, err := walletmod.Credit(t, p, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
//新增交易日志
|
||||
txnLog := &txnmod.TransactionLog{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -price,
|
||||
ActualAmount: float64(-price),
|
||||
TranType: txnmod.AiMateCurrencyExchange.Key(),
|
||||
TranTypeInt: int64(txnmod.AiMateCurrencyExchange),
|
||||
TransNo: currency.ID,
|
||||
Desc: "AI伴侣币购买-" + currency.Name,
|
||||
DiscDoc: user.DiscDoc,
|
||||
SysType: user.SysType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
AiMatePoint: cAiMateBalance,
|
||||
RealAiMatePoint: wallet.AiMateBalance,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err = txnmod.InsertTransactionLog(t, txnLog); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn("ai_mate_ser Exchange Transaction fail", log.Any("uid", uid), log.Any("id", currency.ID.Hex()), log.E(err))
|
||||
code = stderr.BuyFailed
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type BalanceResp struct {
|
||||
Balance float64 `json:"balance" bson:"balance"` // 余额
|
||||
LastOrderId string `json:"lastOrderId" bson:"lastOrderId"`
|
||||
}
|
||||
|
||||
// GetNewBalance 获取AI伴侣余余额
|
||||
func GetNewBalance(uid uint64) (BalanceResp, error) {
|
||||
var ret BalanceResp
|
||||
|
||||
// 查询用户信息
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser GetBalance usermod.FindUserByUID err:", log.E(err), log.Any("uid", uid))
|
||||
return ret, err
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
log.Error("ai_mate_ser GetBalance user not exist", log.Any("uid", uid))
|
||||
return ret, errors.New("ai_mate_ser GetBalance user not exist")
|
||||
|
||||
}
|
||||
|
||||
// 查询用户钱包ai伴侣余额
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
log.Error("ai_mate_ser GetBalance walletmod.GetWallet err:", log.E(err), log.Any("uid", uid))
|
||||
return ret, err
|
||||
}
|
||||
|
||||
if wallet != nil && wallet.AiMateBalance > 0 {
|
||||
ret.Balance = wallet.AiMateBalance
|
||||
ret.LastOrderId = wallet.LastAiMateRecordId
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
type SyncInfoRes struct {
|
||||
UID uint64 `json:"uid" bson:"uid"` // 用户id
|
||||
TotalTokens int `json:"totalTokens" bson:"totalTokens"` // 总共使用的 token 数量
|
||||
Amount float64 `json:"amount" bson:"amount"` // 花费积分
|
||||
OrderID string `json:"orderId" bson:"orderId"` // 订单ID
|
||||
}
|
||||
|
||||
func (q *SyncInfoRes) Sync() error {
|
||||
// 同步用户信息
|
||||
userInfo, err := usermod.FindUserByUIDForNoCache(q.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userInfo == nil || userInfo.ID.IsZero() {
|
||||
log.Warn(fmt.Sprintf("user is null"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取
|
||||
cond := bson.M{"tranTypeInt": txnmod.AiMateChat, "uid": q.UID, "productID": q.OrderID}
|
||||
transactionLog, err := txnmod.FindByCond(cond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重复上传
|
||||
if !transactionLog.ID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
// 获取余额
|
||||
wallet, err := walletmod.GetWallet(q.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if wallet == nil || wallet.ID.IsZero() {
|
||||
return errors.New("wallet is null")
|
||||
}
|
||||
|
||||
amt := -q.Amount
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(q.UID, []txnmod.TransType{
|
||||
txnmod.AiMateChat,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return err
|
||||
}
|
||||
// 新增日志流水记录
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if q.Amount > 0 {
|
||||
// 扣除余额
|
||||
w, err := walletmod.CreditAiMate(t, amt, q.UID, q.OrderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
po := int64(-q.Amount)
|
||||
|
||||
// 添加流水
|
||||
if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: userInfo.UID,
|
||||
TranType: txnmod.AiMateChat.Key(),
|
||||
TranTypeInt: int64(txnmod.AiMateChat),
|
||||
Desc: fmt.Sprintf("AI女友聊天-花费:%.2f积分", q.Amount),
|
||||
DiscDoc: userInfo.DiscDoc,
|
||||
SysType: userInfo.SysType,
|
||||
Integral: po,
|
||||
ProductID: &q.OrderID,
|
||||
AiMatePoint: -q.Amount,
|
||||
RealAiMatePoint: w.AiMateBalance,
|
||||
IsRepurchase: isRepurchase,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("uid:%v,ai_mate sync trans err:%v", q.UID, err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package ai_mate_ser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestLoginCreatesAiMateUserAndCreditsLocalBalance(t *testing.T) {
|
||||
const uid = uint64(32239213)
|
||||
var (
|
||||
updatedAiMateUID string
|
||||
accessAiMateUID string
|
||||
creditedBalance float64
|
||||
)
|
||||
deps := loginDependencies{
|
||||
findUser: func(gotUID uint64) (*usermod.User, error) {
|
||||
if gotUID != uid {
|
||||
t.Fatalf("findUser uid = %d, want %d", gotUID, uid)
|
||||
}
|
||||
return &usermod.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Name: "tester",
|
||||
}, nil
|
||||
},
|
||||
updateUser: func(gotUID uint64, selector usermod.UserSelector) (*usermod.User, error) {
|
||||
if gotUID != uid || selector.AiMateUid == nil {
|
||||
t.Fatalf("unexpected updateUser arguments: uid=%d selector=%+v", gotUID, selector)
|
||||
}
|
||||
updatedAiMateUID = *selector.AiMateUid
|
||||
return &usermod.User{}, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{AiMateBalance: 12}, nil
|
||||
},
|
||||
getRemoteBalance: func(uint64, string) (float64, error) {
|
||||
t.Fatal("new AI mate user must not query a remote balance")
|
||||
return 0, nil
|
||||
},
|
||||
getAccessURL: func(gotUID uint64, aiMateUID, name string, balance float64) (string, error) {
|
||||
if gotUID != uid || name != "tester" {
|
||||
t.Fatalf("unexpected access URL arguments: uid=%d name=%q", gotUID, name)
|
||||
}
|
||||
accessAiMateUID = aiMateUID
|
||||
creditedBalance = balance
|
||||
return "https://example.com/login", nil
|
||||
},
|
||||
newUUID: func() string { return "new-ai-mate-uid" },
|
||||
}
|
||||
|
||||
got, err := login(uid, deps)
|
||||
if err != nil {
|
||||
t.Fatalf("login() error = %v", err)
|
||||
}
|
||||
if got.URL != "https://example.com/login" {
|
||||
t.Fatalf("login() URL = %q", got.URL)
|
||||
}
|
||||
if updatedAiMateUID != "new-ai-mate-uid" || accessAiMateUID != updatedAiMateUID {
|
||||
t.Fatalf("AI mate uid update=%q access=%q", updatedAiMateUID, accessAiMateUID)
|
||||
}
|
||||
if creditedBalance != 12 {
|
||||
t.Fatalf("credited balance = %v, want 12", creditedBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginOnlyCreditsBalanceDifference(t *testing.T) {
|
||||
const uid = uint64(32239213)
|
||||
var creditedBalance float64
|
||||
deps := loginDependencies{
|
||||
findUser: func(uint64) (*usermod.User, error) {
|
||||
return &usermod.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Name: "tester",
|
||||
AiMateUid: "existing-ai-mate-uid",
|
||||
}, nil
|
||||
},
|
||||
updateUser: func(uint64, usermod.UserSelector) (*usermod.User, error) {
|
||||
t.Fatal("existing AI mate user must not be updated")
|
||||
return nil, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{AiMateBalance: 15}, nil
|
||||
},
|
||||
getRemoteBalance: func(gotUID uint64, aiMateUID string) (float64, error) {
|
||||
if gotUID != uid || aiMateUID != "existing-ai-mate-uid" {
|
||||
t.Fatalf("unexpected remote balance arguments: uid=%d aiMateUID=%q", gotUID, aiMateUID)
|
||||
}
|
||||
return 9, nil
|
||||
},
|
||||
getAccessURL: func(_ uint64, _ string, _ string, balance float64) (string, error) {
|
||||
creditedBalance = balance
|
||||
return "https://example.com/login", nil
|
||||
},
|
||||
newUUID: func() string {
|
||||
t.Fatal("existing AI mate user must not generate another uid")
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := login(uid, deps); err != nil {
|
||||
t.Fatalf("login() error = %v", err)
|
||||
}
|
||||
if creditedBalance != 6 {
|
||||
t.Fatalf("credited balance = %v, want 6", creditedBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginStopsWhenRemoteBalanceCannotBeRead(t *testing.T) {
|
||||
expectedErr := errors.New("remote unavailable")
|
||||
accessCalled := false
|
||||
deps := loginDependencies{
|
||||
findUser: func(uint64) (*usermod.User, error) {
|
||||
return &usermod.User{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: 1,
|
||||
AiMateUid: "existing-ai-mate-uid",
|
||||
}, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{AiMateBalance: 10}, nil
|
||||
},
|
||||
getRemoteBalance: func(uint64, string) (float64, error) {
|
||||
return 0, expectedErr
|
||||
},
|
||||
getAccessURL: func(uint64, string, string, float64) (string, error) {
|
||||
accessCalled = true
|
||||
return "", nil
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := login(1, deps); !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("login() error = %v, want %v", err, expectedErr)
|
||||
}
|
||||
if accessCalled {
|
||||
t.Fatal("access URL must not be requested when the remote balance is unknown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalanceTopUp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
local float64
|
||||
remote float64
|
||||
want float64
|
||||
}{
|
||||
{name: "new credit", local: 10, remote: 4, want: 6},
|
||||
{name: "already synchronized", local: 10, remote: 10, want: 0},
|
||||
{name: "remote ahead", local: 8, remote: 10, want: 0},
|
||||
{name: "empty", local: 0, remote: 0, want: 0},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := balanceTopUp(test.local, test.remote); got != test.want {
|
||||
t.Fatalf("balanceTopUp(%v, %v) = %v, want %v", test.local, test.remote, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
package ai_text_to_image_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/cache/aitexttoimagedata"
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aichangefacevidmod"
|
||||
"91porn-server/models/v/aitexttoimagemod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
Status int `json:"status" form:"status"` // 状态 1、排队中 2、生成成功 3、生成失败
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
commod.Page
|
||||
}
|
||||
|
||||
func (p *AppQueryListReq) Filter() primitive.M {
|
||||
filter := bson.M{}
|
||||
switch p.Status {
|
||||
case 1:
|
||||
filter["status"] = bson.M{"$in": []aitexttoimagemod.AiTextToImageStatus{aitexttoimagemod.StatusOrderSuccess, aitexttoimagemod.StatusSubmitted}}
|
||||
case 2:
|
||||
filter["status"] = aitexttoimagemod.StatusGenerationSuccess
|
||||
case 3:
|
||||
filter["status"] = bson.M{"$in": []aitexttoimagemod.AiTextToImageStatus{aitexttoimagemod.StatusGenerationFailed, aitexttoimagemod.StatusRefunded}}
|
||||
}
|
||||
filter["uid"] = p.UID
|
||||
filter["isHide"] = false
|
||||
return filter
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*aitexttoimagemod.AiTextToImageInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
|
||||
sort := bson.D{{"createdAt", -1}}
|
||||
// 获取列表
|
||||
var data []aitexttoimagemod.AiTextToImage
|
||||
data, res.Total, res.HasNext, err = aitexttoimagemod.GetList(p.Filter(), int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取AI绘图列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = aitexttoimagedata.FormatAppDataList(data)
|
||||
|
||||
// 获取所有的模版
|
||||
cond := bson.M{"status": 1, "moduleType": 5}
|
||||
mods, err := aichangefacevidmod.GetAllModsByCond(cond)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
if len(mods) > 0 {
|
||||
for i := range res.List {
|
||||
for _, m := range mods {
|
||||
if res.List[i].StyleType == m.StyleType {
|
||||
res.List[i].StyleUrl = m.Cover
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type GenerateReq struct {
|
||||
Text string `json:"text"` // 绘图描述
|
||||
StyleType int `json:"styleType"` // 绘图风格
|
||||
AspectRatio string `json:"aspectRatio"` // 绘图比例 1:1,4:3,3:4,16:9,9:16
|
||||
Coin int64 `json:"-"` // 此次脱衣金币个数
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
ShareTitle string `json:"shareTitle"` // 分享标题
|
||||
ShareStatus int `json:"shareStatus"` // 是否分享 0-不分享 1-分享
|
||||
}
|
||||
|
||||
func (p *GenerateReq) GenerateInfo(orderId primitive.ObjectID, orderCreatedAt time.Time, debitCoins, debitIncomeCoins int64) aitexttoimagemod.AiTextToImage {
|
||||
return aitexttoimagemod.AiTextToImage{
|
||||
ID: orderId,
|
||||
Coin: debitIncomeCoins + debitCoins,
|
||||
DebitAmountCoin: debitCoins,
|
||||
DebitIncomeCoin: debitIncomeCoins,
|
||||
IsFreeTimes: false,
|
||||
Text: p.Text,
|
||||
StyleType: p.StyleType,
|
||||
AspectRatio: p.AspectRatio,
|
||||
UID: p.UID,
|
||||
Status: int(aitexttoimagemod.StatusOrderSuccess),
|
||||
IsHide: false,
|
||||
ShareTitle: p.ShareTitle,
|
||||
ShareStatus: p.ShareStatus,
|
||||
UpdatedAt: orderCreatedAt,
|
||||
CreatedAt: orderCreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成订单
|
||||
func (p *GenerateReq) Generate(ua ua.UA, ip string) (stderr.Code, error) {
|
||||
// 获取用户信息
|
||||
user, err := usermod.FindUserByUID(p.UID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if user.ID.IsZero() {
|
||||
return stderr.UserIsNotExists, errors.New("user is null")
|
||||
}
|
||||
configure, _ := sysconfdata.GetAllFromCache()
|
||||
p.Coin = configure.GetInt(sysconfmod.VCodeAiTextToImagePrice)
|
||||
|
||||
var debitAmountCoins, debitIncomeCoins int64
|
||||
|
||||
if p.Coin > 0 {
|
||||
// 获取用户钱包
|
||||
w, err := walletmod.GetWallet(p.UID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if w == nil {
|
||||
return stderr.InsufficientBalance, errors.New("Insufficient balance")
|
||||
}
|
||||
|
||||
// 如果收费的部分余额不够支付
|
||||
if p.Coin > (w.Amount + w.Income) {
|
||||
return stderr.InsufficientBalance, errors.New("Insufficient balance")
|
||||
}
|
||||
|
||||
debitAmountCoins, debitIncomeCoins = TotalDebit(p.Coin, *w)
|
||||
}
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(p.UID, []txnmod.TransType{
|
||||
txnmod.AiTextToImageDebitGold,
|
||||
txnmod.AiTextToImageDebitInComeGold,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
var orderId primitive.ObjectID
|
||||
var orderCreatedAt time.Time
|
||||
if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
var tl []txnmod.TransactionLog
|
||||
if debitIncomeCoins > 0 || debitAmountCoins > 0 {
|
||||
// 扣除钱包余额
|
||||
wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, p.UID)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if debitAmountCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: p.UID,
|
||||
Amount: -debitAmountCoins,
|
||||
ActualAmount: float64(-debitAmountCoins),
|
||||
TranType: txnmod.AiTextToImageDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiTextToImageDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI绘图金币-%d", debitAmountCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
if debitIncomeCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: p.UID,
|
||||
Amount: -debitIncomeCoins,
|
||||
ActualAmount: float64(-debitIncomeCoins),
|
||||
TranType: txnmod.AiTextToImageDebitInComeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiTextToImageDebitInComeGold),
|
||||
Desc: fmt.Sprintf("生成AI绘图收益金币-%d", debitIncomeCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(tl) > 0 {
|
||||
txnErr := txnmod.InsertManyTransactionLog(tool, tl)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
// 新增AI图生视频记录
|
||||
orderId = primitive.NewObjectID()
|
||||
orderCreatedAt = time.Now()
|
||||
order := p.GenerateInfo(orderId, orderCreatedAt, debitAmountCoins, debitIncomeCoins)
|
||||
if _, err = aitexttoimagemod.Insert(tool, order); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate InsertOnes error:%+v:", err), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate Trans error:%+v;uid:%v;", err, p.UID))
|
||||
return stderr.ErrDbUpdateError, err
|
||||
}
|
||||
|
||||
common.Go(func() {
|
||||
autoUploadOrder(orderId)
|
||||
})
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
type AutoUploadReq struct {
|
||||
AppID int `json:"appId"` // 应用 ID
|
||||
UID uint64 `json:"uid"` // 用户 ID
|
||||
Text string `json:"text"` // 绘图描述
|
||||
StyleType int `json:"styleType"` // 绘图风格
|
||||
AspectRatio string `json:"aspectRatio"` // 绘图比例 1:1,4:3,3:4,16:9,9:16
|
||||
AppOrderNum string `json:"appOrderNum"` // 应用订单号
|
||||
NotifyURL string `json:"notifyUrl"` // 通知 URL
|
||||
}
|
||||
|
||||
func autoUploadOrder(id primitive.ObjectID) error {
|
||||
item, err := aitexttoimagemod.GetInfo(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.ID.IsZero() {
|
||||
return errors.New(" aiimagetovideo order is null")
|
||||
}
|
||||
|
||||
// 将请求参数编码为JSON
|
||||
requestBody := AutoUploadReq{
|
||||
AppID: int(commod.KFK_APPID),
|
||||
UID: item.UID,
|
||||
Text: item.Text,
|
||||
AspectRatio: item.AspectRatio,
|
||||
StyleType: item.StyleType,
|
||||
AppOrderNum: item.ID.Hex(),
|
||||
NotifyURL: fmt.Sprintf("%v/api/web/admin/ai/text_to_image/callback", appg.Conf.URL.AiImageToVideoCallbackUrl),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiURL := getAutoUploadUrl()
|
||||
|
||||
// 创建请求对象
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 通过http.Client发送请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果返回状态码非200,可以考虑返回错误
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("请求失败,状态码:%d,内容:%s", resp.StatusCode, string(respBytes))
|
||||
}
|
||||
|
||||
// 更新订单
|
||||
updateCond := bson.M{
|
||||
"status": aitexttoimagemod.StatusSubmitted,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
_, err = aitexttoimagemod.UpdateByID(nil, id, updateCond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAutoUploadUrl() string {
|
||||
return fmt.Sprintf("%v/api/comfyui/prd/create_text_job", appg.Conf.URL.AiImageToVideoUrl)
|
||||
}
|
||||
|
||||
func TotalDebit(price int64, w walletmod.Wallet) (amountCoins, incomeCoins int64) {
|
||||
var (
|
||||
debitAmountCoins int64
|
||||
debitIncomeCoins int64
|
||||
)
|
||||
|
||||
// 计算当日实际扣减的金额
|
||||
debitCoins := price
|
||||
if w.Amount >= debitCoins {
|
||||
debitAmountCoins = debitCoins
|
||||
} else {
|
||||
debitAmountCoins = w.Amount
|
||||
debitIncomeCoins = debitCoins - w.Amount
|
||||
}
|
||||
return debitAmountCoins, debitIncomeCoins
|
||||
}
|
||||
|
||||
type HideReq struct {
|
||||
ID string `json:"id"` // AI订单ID
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
}
|
||||
|
||||
// Hide 删除订单
|
||||
func (p *HideReq) Hide() error {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := aitexttoimagemod.GetInfo(oid)
|
||||
if err != nil {
|
||||
log.Error("获取AI绘图列表详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if item.ID.IsZero() {
|
||||
return errors.New("AI订单不存在")
|
||||
}
|
||||
|
||||
if item.UID != p.UID {
|
||||
return errors.New("只能删除自己的订单")
|
||||
}
|
||||
|
||||
if err = validateHideStatus(item.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = aitexttoimagemod.Hide(p.UID, oid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHideStatus(status int) error {
|
||||
if status == int(aitexttoimagemod.StatusOrderSuccess) ||
|
||||
status == int(aitexttoimagemod.StatusSubmitted) {
|
||||
return stderr.AiGenningDelForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
package ai_text_to_novel_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/cache/aitexttonoveldata"
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aitexttonovelmod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
Status int `json:"status" form:"status"` // 状态 1、排队中 2、生成成功 3、生成失败
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
commod.Page
|
||||
}
|
||||
|
||||
func (p *AppQueryListReq) Filter() primitive.M {
|
||||
filter := bson.M{}
|
||||
switch p.Status {
|
||||
case 1:
|
||||
filter["status"] = bson.M{"$in": []aitexttonovelmod.AiTextToNovelStatus{aitexttonovelmod.StatusOrderSuccess, aitexttonovelmod.StatusSubmitted}}
|
||||
case 2:
|
||||
filter["status"] = aitexttonovelmod.StatusGenerationSuccess
|
||||
case 3:
|
||||
filter["status"] = bson.M{"$in": []aitexttonovelmod.AiTextToNovelStatus{aitexttonovelmod.StatusGenerationFailed, aitexttonovelmod.StatusRefunded}}
|
||||
}
|
||||
filter["uid"] = p.UID
|
||||
filter["isHide"] = false
|
||||
return filter
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*aitexttonovelmod.AiTextToNovel `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
|
||||
sort := bson.D{{"createdAt", -1}}
|
||||
// 获取列表
|
||||
var data []aitexttonovelmod.AiTextToNovel
|
||||
data, res.Total, res.HasNext, err = aitexttonovelmod.GetList(p.Filter(), int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取AI小说列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = aitexttonoveldata.FormatAppDataList(data)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type AppQueryInfoReq struct {
|
||||
ID string `json:"id" form:"id"` // id
|
||||
}
|
||||
type AppQueryInfoRes = *aitexttonovelmod.AiTextToNovel
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var item aitexttonovelmod.AiTextToNovel
|
||||
item, err = aitexttonovelmod.GetInfo(oid)
|
||||
if err != nil {
|
||||
log.Error("获取AI小说列表详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
res = aitexttonoveldata.FormatAppData(item)
|
||||
return
|
||||
}
|
||||
|
||||
type GenerateReq struct {
|
||||
Description string `json:"description"` // 剧情描述/故事情节
|
||||
CharacterSetting string `json:"characterSetting"` // 人物设定
|
||||
LocationScene string `json:"locationScene"` // 地点场景
|
||||
Details string `json:"details"` // 细节说明/其他要求
|
||||
ModelType int `json:"modelType"` // 模型 1:AI小艺 2:AI小萌
|
||||
Coin int64 `json:"-"` // 此次脱衣金币个数
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
}
|
||||
|
||||
func (p *GenerateReq) GenerateInfo(orderId primitive.ObjectID, orderCreatedAt time.Time, debitCoins, debitIncomeCoins int64) aitexttonovelmod.AiTextToNovel {
|
||||
return aitexttonovelmod.AiTextToNovel{
|
||||
ID: orderId,
|
||||
Coin: debitIncomeCoins + debitCoins,
|
||||
DebitAmountCoin: debitCoins,
|
||||
DebitIncomeCoin: debitIncomeCoins,
|
||||
IsFreeTimes: false,
|
||||
Description: p.Description,
|
||||
CharacterSetting: p.CharacterSetting,
|
||||
LocationScene: p.LocationScene,
|
||||
Details: p.Details,
|
||||
ModelType: p.ModelType,
|
||||
UID: p.UID,
|
||||
Status: int(aitexttonovelmod.StatusOrderSuccess),
|
||||
IsHide: false,
|
||||
UpdatedAt: orderCreatedAt,
|
||||
CreatedAt: orderCreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 生成订单
|
||||
func (p *GenerateReq) Generate(ua ua.UA, ip string) (stderr.Code, error) {
|
||||
// 获取用户信息
|
||||
user, err := usermod.FindUserByUID(p.UID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if user.ID.IsZero() {
|
||||
return stderr.UserIsNotExists, errors.New("user is null")
|
||||
}
|
||||
configure, _ := sysconfdata.GetAllFromCache()
|
||||
p.Coin = configure.GetInt(sysconfmod.VCodeAiTextToNovelPrice)
|
||||
|
||||
var debitAmountCoins, debitIncomeCoins int64
|
||||
|
||||
if p.Coin > 0 {
|
||||
// 获取用户钱包
|
||||
w, err := walletmod.GetWallet(p.UID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
if w == nil {
|
||||
return stderr.InsufficientBalance, errors.New("Insufficient balance")
|
||||
}
|
||||
|
||||
// 如果收费的部分余额不够支付
|
||||
if p.Coin > (w.Amount + w.Income) {
|
||||
return stderr.InsufficientBalance, errors.New("Insufficient balance")
|
||||
}
|
||||
|
||||
debitAmountCoins, debitIncomeCoins = TotalDebit(p.Coin, *w)
|
||||
}
|
||||
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(p.UID, []txnmod.TransType{
|
||||
txnmod.AiTextToNovelDebitGold,
|
||||
txnmod.AiTextToNovelDebitInComeGold,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return stderr.ErrDbQueryError, err
|
||||
}
|
||||
|
||||
var orderId primitive.ObjectID
|
||||
var orderCreatedAt time.Time
|
||||
if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
var tl []txnmod.TransactionLog
|
||||
if debitIncomeCoins > 0 || debitAmountCoins > 0 {
|
||||
// 扣除钱包余额
|
||||
wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, p.UID)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if debitAmountCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: p.UID,
|
||||
Amount: -debitAmountCoins,
|
||||
ActualAmount: float64(-debitAmountCoins),
|
||||
TranType: txnmod.AiTextToNovelDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiTextToNovelDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI小说扣除金币-%d", debitAmountCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
if debitIncomeCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: p.UID,
|
||||
Amount: -debitIncomeCoins,
|
||||
ActualAmount: float64(-debitIncomeCoins),
|
||||
TranType: txnmod.AiTextToNovelDebitInComeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiTextToNovelDebitInComeGold),
|
||||
Desc: fmt.Sprintf("生成AI小说扣除收益金币-%d", debitIncomeCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(tl) > 0 {
|
||||
txnErr := txnmod.InsertManyTransactionLog(tool, tl)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
// 新增AI图生视频记录
|
||||
orderId = primitive.NewObjectID()
|
||||
orderCreatedAt = time.Now()
|
||||
order := p.GenerateInfo(orderId, orderCreatedAt, debitAmountCoins, debitIncomeCoins)
|
||||
if _, err = aitexttonovelmod.Insert(tool, order); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate InsertOnes error:%+v:", err), log.Any("uid", p.UID))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle imagetovideo Generate Trans error:%+v;uid:%v;", err, p.UID))
|
||||
return stderr.ErrDbUpdateError, err
|
||||
}
|
||||
common.Go(func() {
|
||||
_ = autoUploadOrder(orderId)
|
||||
})
|
||||
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
type AutoUploadReq struct {
|
||||
AppID int `json:"appId"` // 应用ID
|
||||
UID uint64 `json:"uid"` // 用户ID
|
||||
Description string `json:"description"` // 剧情描述/故事情节
|
||||
CharacterSetting string `json:"characterSetting"` // 绘图风格
|
||||
LocationScene string `json:"locationScene"` // 人物设定
|
||||
Details string `json:"details"` // 地点场景
|
||||
ModelType int `json:"modelType"` // AI模型
|
||||
AppOrderNum string `json:"appOrderNum"` // 应用订单号
|
||||
NotifyURL string `json:"notifyUrl"` // 通知 URL
|
||||
}
|
||||
|
||||
func autoUploadOrder(id primitive.ObjectID) error {
|
||||
item, err := aitexttonovelmod.GetInfo(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.ID.IsZero() {
|
||||
return errors.New(" ai text to novel order is null")
|
||||
}
|
||||
|
||||
// 将请求参数编码为JSON
|
||||
requestBody := AutoUploadReq{
|
||||
AppID: int(commod.KFK_APPID),
|
||||
UID: item.UID,
|
||||
Description: item.Description,
|
||||
CharacterSetting: item.CharacterSetting,
|
||||
LocationScene: item.LocationScene,
|
||||
Details: item.Details,
|
||||
ModelType: item.ModelType,
|
||||
AppOrderNum: item.ID.Hex(),
|
||||
NotifyURL: fmt.Sprintf("%v/api/web/admin/ai/text_to_novel/callback", appg.Conf.URL.AiImageToVideoCallbackUrl),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiURL := getAutoUploadUrl()
|
||||
|
||||
// 创建请求对象
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 通过http.Client发送请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 如果返回状态码非200,可以考虑返回错误
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("请求失败,状态码:%d,内容:%s", resp.StatusCode, string(respBytes))
|
||||
}
|
||||
|
||||
// 更新订单
|
||||
updateCond := bson.M{
|
||||
"status": aitexttonovelmod.StatusSubmitted,
|
||||
"updatedAt": time.Now(),
|
||||
}
|
||||
_, err = aitexttonovelmod.UpdateByID(nil, id, updateCond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAutoUploadUrl() string {
|
||||
return fmt.Sprintf("%v/api/comfyui/prd/create_novel_job", appg.Conf.URL.AiImageToVideoUrl)
|
||||
}
|
||||
|
||||
func TotalDebit(price int64, w walletmod.Wallet) (amountCoins, incomeCoins int64) {
|
||||
var (
|
||||
debitAmountCoins int64
|
||||
debitIncomeCoins int64
|
||||
)
|
||||
|
||||
// 计算当日实际扣减的金额
|
||||
debitCoins := price
|
||||
if w.Amount >= debitCoins {
|
||||
debitAmountCoins = debitCoins
|
||||
} else {
|
||||
debitAmountCoins = w.Amount
|
||||
debitIncomeCoins = debitCoins - w.Amount
|
||||
}
|
||||
return debitAmountCoins, debitIncomeCoins
|
||||
}
|
||||
|
||||
type HideReq struct {
|
||||
ID string `json:"id"` // AI订单ID
|
||||
UID uint64 `json:"-"` // 用户ID
|
||||
}
|
||||
|
||||
// Hide 删除订单
|
||||
func (p *HideReq) Hide() error {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
item, err := aitexttonovelmod.GetInfo(oid)
|
||||
if err != nil {
|
||||
log.Error("获取AI绘图列表详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if item.ID.IsZero() {
|
||||
return errors.New("AI订单不存在")
|
||||
}
|
||||
|
||||
if item.UID != p.UID {
|
||||
return errors.New("只能删除自己的订单")
|
||||
}
|
||||
|
||||
if err = validateHideStatus(item.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = aitexttonovelmod.Hide(p.UID, oid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHideStatus(status int) error {
|
||||
if status == int(aitexttonovelmod.StatusOrderSuccess) ||
|
||||
status == int(aitexttonovelmod.StatusSubmitted) {
|
||||
return stderr.AiGenningDelForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package ai_undress_server
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/aiautoser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/aiUnDressmod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
carbon "github.com/golang-module/carbon/v2"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func List(uid uint64, req *aiUnDressmod.ListRequest) (interface{}, stderr.Code) {
|
||||
var data = map[string]interface{}{
|
||||
"list": []interface{}{},
|
||||
"count": 0,
|
||||
}
|
||||
count, err := aiUnDressmod.CountDocument(req.Filter(uid))
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
if count == 0 {
|
||||
return data, stderr.Success
|
||||
}
|
||||
opts := options.Find().SetSkip(int64(req.Skip())).SetLimit(int64(req.Limit() + 1)).SetSort(bson.M{"updatedAt": -1})
|
||||
list, err := aiUnDressmod.QueryAllDocument(req.Filter(uid), opts)
|
||||
if err != nil {
|
||||
return data, stderr.ErrDbQueryError
|
||||
}
|
||||
hasNext := false
|
||||
if len(list) > int(req.Limit()) {
|
||||
list = list[:req.Limit()]
|
||||
hasNext = true
|
||||
}
|
||||
data["count"] = count // 兼容旧版本
|
||||
data["hasNext"] = hasNext
|
||||
data["list"] = list
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
func Generate(uid uint64, req *aiUnDressmod.GenerateRequest, ua ua.UA, ip string) stderr.Code {
|
||||
if req.OriginPic == nil || len(req.OriginPic) == 0 {
|
||||
return stderr.ErrParamError
|
||||
}
|
||||
// 获取用户信息
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if user.ID.IsZero() {
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
configure, _ := sysconfdata.GetAllFromCache()
|
||||
req.Coin = configure.GetInt(sysconfmod.VCodeAiUndressPrice)
|
||||
picCount := int64(len(req.OriginPic))
|
||||
privilegeFreeCount := int64(0)
|
||||
// 获取用户是否是复购
|
||||
isRepurchase, err := txnmod.CheckRepurchaseByTransTypes(uid, []txnmod.TransType{
|
||||
txnmod.AiUndressDebitFreeTimes,
|
||||
txnmod.AiUndressDebitGold,
|
||||
txnmod.AiUndressDebitIncomeGold,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("txnmod.CheckRepurchaseByTransTypes fail", log.E(err))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid)
|
||||
if hasPrivilege {
|
||||
// 获取可用免费次数
|
||||
validCount := advance.PrepaidPrivilege.AiUndressLimitPerDay - advance.TodayUse.AiUndressCount
|
||||
if validCount >= picCount {
|
||||
// 扣除免费次数
|
||||
debitPlan := advanceordermod.DebitPlan{}
|
||||
debitPlan.AiUndressCount = &picCount
|
||||
if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
privilegeFreeCount = picCount
|
||||
picCount = 0
|
||||
} else if validCount > 0 {
|
||||
// 扣除免费次数
|
||||
debitPlan := advanceordermod.DebitPlan{}
|
||||
debitPlan.AiUndressCount = &validCount
|
||||
if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
privilegeFreeCount = validCount
|
||||
picCount -= validCount
|
||||
}
|
||||
if validCount > 0 {
|
||||
l := &txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: 1,
|
||||
ActualAmount: float64(-1),
|
||||
TranType: txnmod.AiUndressDebitFreeTimes.Key(),
|
||||
TranTypeInt: int64(txnmod.AiUndressDebitFreeTimes),
|
||||
Desc: fmt.Sprintf("生成AI脱衣扣除免费次数[%d次]", privilegeFreeCount),
|
||||
RealAmount: decimal.Decimal{},
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
}
|
||||
txnErr := txnmod.InsertTransactionLog(nil, l)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
}
|
||||
var debitFreeCount, debitAmountCoins, debitIncomeCoins int64
|
||||
|
||||
if picCount > 0 {
|
||||
// 获取用户钱包
|
||||
w, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
// 校验用户余额
|
||||
if w == nil || !CheckBalance(req.Coin, picCount, *w) {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
|
||||
debitFreeCount, debitAmountCoins, debitIncomeCoins = TotalDebit(req.Coin, picCount, *w)
|
||||
if w.AiUndressFreeTimes > 0 {
|
||||
req.IsFreeTimes = true
|
||||
}
|
||||
}
|
||||
var orderId primitive.ObjectID
|
||||
var orderCreatedAt time.Time
|
||||
if err = appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
var tl []txnmod.TransactionLog
|
||||
if debitFreeCount > 0 {
|
||||
expire := carbon.Tomorrow().StartOfDay().StdTime().Sub(time.Now())
|
||||
// 累加次数
|
||||
appg.Redis.IncrBy(redisconst.AiFreeUndressTodayUseTimesKey(uid), debitFreeCount, expire)
|
||||
// 扣除钱剩余次数
|
||||
wallet, wErr := walletmod.DebitAiFreeTimes(tool, debitFreeCount, uid)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate walletmod.DebitAiFreeTimes error:%+v:", wErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: 1,
|
||||
ActualAmount: float64(-1),
|
||||
TranType: txnmod.AiUndressDebitFreeTimes.Key(),
|
||||
TranTypeInt: int64(txnmod.AiUndressDebitFreeTimes),
|
||||
Desc: fmt.Sprintf("生成AI脱衣扣除免费次数[%d次]", debitFreeCount),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
|
||||
if debitIncomeCoins > 0 || debitAmountCoins > 0 {
|
||||
// 扣除钱包余额
|
||||
wallet, wErr := walletmod.DebitAmountAndIncome(tool, debitAmountCoins, debitIncomeCoins, uid)
|
||||
if wErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate walletmod.DebitAmount error:%+v:", wErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if debitAmountCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -debitAmountCoins,
|
||||
ActualAmount: float64(-debitAmountCoins),
|
||||
TranType: txnmod.AiUndressDebitGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiUndressDebitGold),
|
||||
Desc: fmt.Sprintf("生成AI脱衣金币-%d", debitAmountCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
if debitIncomeCoins > 0 {
|
||||
tl = append(tl, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: -debitIncomeCoins,
|
||||
ActualAmount: float64(-debitIncomeCoins),
|
||||
TranType: txnmod.AiUndressDebitIncomeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.AiUndressDebitIncomeGold),
|
||||
Desc: fmt.Sprintf("生成AI脱衣收益金币-%d", debitIncomeCoins),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
SysType: user.SysType,
|
||||
IsRepurchase: isRepurchase,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(tl) > 0 {
|
||||
txnErr := txnmod.InsertManyTransactionLog(tool, tl)
|
||||
if txnErr != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate txnmod.InsertTransactionLog error:%+v:", txnErr), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
// 新增AI脱衣记录
|
||||
orderId = primitive.NewObjectID()
|
||||
orderCreatedAt = time.Now()
|
||||
if err := aiUnDressmod.InsertMany(tool, req.GenerateMany(orderId, orderCreatedAt, uid, privilegeFreeCount, debitFreeCount, debitAmountCoins, debitIncomeCoins, req.Coin, req.ShareTitle, req.ShareStatus)); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate InsertOnes error:%+v:", err), log.Any("uid", uid))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress Generate Trans error:%+v;uid:%v;", err, uid))
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
common.Go(func() {
|
||||
if err := aiautoser.SubmitUndress(orderId); err != nil {
|
||||
log.Error("AI脱衣自动处理失败", log.Any("id", orderId.Hex()), log.E(err))
|
||||
}
|
||||
})
|
||||
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func AiUndressHide(uid uint64, req *aiUnDressmod.DelRequest) stderr.Code {
|
||||
acf, err := aiUnDressmod.FindByID(nil, req.ID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if acf.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
if acf.UID != uid {
|
||||
return stderr.Failure
|
||||
}
|
||||
if acf.Status == aiUnDressmod.Processing || acf.Status == aiUnDressmod.SubmitOrder {
|
||||
return stderr.AiGenningDelForbidden
|
||||
}
|
||||
if err = aiUnDressmod.Edit(nil, req.Filter(), req.Update()); err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func TotalDebit(price int64, count int64, w walletmod.Wallet) (fCount, amountCoins, incomeCoins int64) {
|
||||
var (
|
||||
debitFreeTimes int64
|
||||
debitAmountCoins int64
|
||||
debitIncomeCoins int64
|
||||
)
|
||||
|
||||
// 当日剩余ai免费脱衣次数
|
||||
todayAiUndressFreeTimes := GetTodayRemainingAiFreeUndressTimes(w)
|
||||
// 实际使用的免费脱衣次数
|
||||
if todayAiUndressFreeTimes >= count {
|
||||
return count, 0, 0
|
||||
}
|
||||
debitFreeTimes = todayAiUndressFreeTimes
|
||||
// 计算当日实际扣减的金额
|
||||
debitCoins := (count - debitFreeTimes) * price
|
||||
if w.Amount >= debitCoins {
|
||||
debitAmountCoins = debitCoins
|
||||
} else {
|
||||
debitAmountCoins = w.Amount
|
||||
debitIncomeCoins = debitCoins - w.Amount
|
||||
}
|
||||
|
||||
return debitFreeTimes, debitAmountCoins, debitIncomeCoins
|
||||
}
|
||||
|
||||
// GetTodayRemainingAiFreeUndressTimes 获取当日剩余的免费ai脱衣的次数
|
||||
func GetTodayRemainingAiFreeUndressTimes(w walletmod.Wallet) (num int64) {
|
||||
var todayUseAiUndressFreeTimes int // 当日已使用的免费ai脱衣次数
|
||||
//获取当日已经使用的ai免费脱衣次数
|
||||
str, err := appg.Redis.Get(redisconst.AiFreeUndressTodayUseTimesKey(w.UID))
|
||||
if err != nil && err != redis.Nil {
|
||||
log.Error(fmt.Sprintf("get AiUndressTodayUseTimes err%v", err))
|
||||
// redis报错返回已经当日最大限制次数,避免被刷
|
||||
todayUseAiUndressFreeTimes = 0
|
||||
} else if str != nil {
|
||||
todayUseAiUndressFreeTimes, _ = strconv.Atoi(*str)
|
||||
}
|
||||
num = int64(appg.Conf.AiFreeUnDressDailyLimit - todayUseAiUndressFreeTimes)
|
||||
// 如果用户剩余的免费次数不足,以剩余次数为准
|
||||
if num >= w.AiUndressFreeTimes {
|
||||
num = w.AiUndressFreeTimes
|
||||
}
|
||||
if num < 0 {
|
||||
num = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CheckBalance(price int64, count int64, w walletmod.Wallet) bool {
|
||||
var checkStats bool
|
||||
var totalCoins = w.Amount
|
||||
if w.Income != 0 {
|
||||
totalCoins += w.Income
|
||||
}
|
||||
if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes >= count {
|
||||
checkStats = true
|
||||
}
|
||||
|
||||
if w.AiUndressFreeTimes > 0 && w.AiUndressFreeTimes < count && totalCoins >= price*(count-w.AiUndressFreeTimes) {
|
||||
checkStats = true
|
||||
}
|
||||
|
||||
if w.AiUndressFreeTimes <= 0 && totalCoins >= price*count {
|
||||
checkStats = true
|
||||
}
|
||||
return checkStats
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package aiautoser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aiUnDressmod"
|
||||
"91porn-server/models/v/aichangefaceimgmod"
|
||||
"91porn-server/models/v/aichangefacemod"
|
||||
"91porn-server/models/v/backpackmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const autoOperator = "AI_AUTO"
|
||||
|
||||
type changeFaceRequest struct {
|
||||
AppID int `json:"app_id"`
|
||||
OrderNumApp string `json:"order_num_app"`
|
||||
Title string `json:"title"`
|
||||
SourceImageURL string `json:"source_image_url"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
}
|
||||
|
||||
type changeFaceResponse struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
// SubmitVideo automatically approves and submits an AI video face-swap order.
|
||||
// Submission failures are handled with the same refund policy as the existing
|
||||
// Web callback flow.
|
||||
func SubmitVideo(id primitive.ObjectID) error {
|
||||
order, err := aichangefacemod.FindByID(nil, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transitioned, err := aichangefacemod.TransitionStatus(nil, id, aichangefacemod.StatusGenning, aichangefacemod.StatusSubmit, bson.M{
|
||||
"operator": autoOperator,
|
||||
"remark": "已自动提交",
|
||||
})
|
||||
if err != nil || !transitioned {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyURL, notifyErr := appg.AICallbackURL("/api/web/admin/ai/changeface/callback")
|
||||
if notifyErr != nil || len(order.Pic) == 0 || order.Pic[0] == "" || appg.Conf.URL.AiServer == "" {
|
||||
return refundVideo(id, "自动提交参数不完整")
|
||||
}
|
||||
req := changeFaceRequest{
|
||||
AppID: int(commod.KFK_APPID),
|
||||
OrderNumApp: id.Hex(),
|
||||
Title: order.ModTitle,
|
||||
SourceImageURL: order.Pic[0],
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
if err := postChangeFace(req); err != nil {
|
||||
log.Error("AI视频换脸自动提交失败", log.Any("id", id.Hex()), log.E(err))
|
||||
return refundVideo(id, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitImage automatically approves and submits an AI image face-swap order.
|
||||
func SubmitImage(id primitive.ObjectID) error {
|
||||
order, err := aichangefaceimgmod.FindByID(nil, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transitioned, err := aichangefaceimgmod.TransitionStatus(nil, id, aichangefaceimgmod.Processing, aichangefaceimgmod.StatusSubmit, bson.M{
|
||||
"updateAct": autoOperator,
|
||||
"remark": "已自动提交",
|
||||
})
|
||||
if err != nil || !transitioned {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyURL, notifyErr := appg.AICallbackURL("/api/web/admin/ai/change_face_img/callback")
|
||||
if notifyErr != nil || order.OriginPic == "" || appg.Conf.URL.AiServer == "" {
|
||||
return refundImage(id, "自动提交参数不完整")
|
||||
}
|
||||
req := changeFaceRequest{
|
||||
AppID: int(commod.KFK_APPID),
|
||||
OrderNumApp: id.Hex(),
|
||||
Title: order.ModTitle,
|
||||
SourceImageURL: order.OriginPic,
|
||||
NotifyURL: notifyURL,
|
||||
}
|
||||
if err := postChangeFace(req); err != nil {
|
||||
log.Error("AI图片换脸自动提交失败", log.Any("id", id.Hex()), log.E(err))
|
||||
return refundImage(id, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitUndress automatically approves and submits an AI undress order.
|
||||
func SubmitUndress(id primitive.ObjectID) error {
|
||||
order, err := aiUnDressmod.FindByID(nil, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transitioned, err := aiUnDressmod.TransitionStatus(nil, id, aiUnDressmod.Processing, aiUnDressmod.SubmitOrder, bson.M{
|
||||
"updateAct": autoOperator,
|
||||
"remark": "已自动提交",
|
||||
})
|
||||
if err != nil || !transitioned {
|
||||
return err
|
||||
}
|
||||
|
||||
notifyURL, notifyErr := appg.AICallbackURL("/api/web/admin/ai/undress/callback")
|
||||
if notifyErr != nil || len(order.OriginPics) == 0 || appg.Conf.URL.AiUndressServer == "" {
|
||||
return refundUndress(id, "自动提交参数不完整")
|
||||
}
|
||||
req := aiUnDressmod.AiUndressOrderReq{
|
||||
AppId: int(commod.KFK_APPID),
|
||||
FileUrl: order.OriginPics,
|
||||
UserId: strconv.FormatUint(order.UID, 10),
|
||||
AppOrderNum: id.Hex(),
|
||||
NotifyUrl: notifyURL,
|
||||
}
|
||||
var resp aiUnDressmod.AiUndressOrderResp
|
||||
statusCode, err := httputil.DefaultClientPostJsonWithResp(
|
||||
&resp,
|
||||
common.BindUrl(appg.Conf.URL.AiUndressServer, "/api/undress/prd/create_job"),
|
||||
nil,
|
||||
req,
|
||||
)
|
||||
if err != nil || statusCode != 200 {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("third-party HTTP status %d", statusCode)
|
||||
}
|
||||
log.Error("AI脱衣自动提交失败", log.Any("id", id.Hex()), log.E(err))
|
||||
return refundUndress(id, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func postChangeFace(req changeFaceRequest) error {
|
||||
var resp changeFaceResponse
|
||||
statusCode, err := httputil.DefaultClientPostJsonWithResp(
|
||||
&resp,
|
||||
common.BindUrl(appg.Conf.URL.AiServer, "/api/changeface/prd/changeFaceByVideo"),
|
||||
nil,
|
||||
req,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if statusCode != 200 {
|
||||
return fmt.Errorf("third-party HTTP status %d", statusCode)
|
||||
}
|
||||
if resp.Code == 40000 {
|
||||
return errors.New("third-party template is unavailable")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func refundVideo(id primitive.ObjectID, remark string) error {
|
||||
return appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
order, err := aichangefacemod.FindByID(tool, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transitioned, err := aichangefacemod.TransitionStatus(tool, id, aichangefacemod.StatusSubmit, aichangefacemod.StatusRefund, bson.M{
|
||||
"operator": autoOperator,
|
||||
"remark": remark,
|
||||
})
|
||||
if err != nil || !transitioned {
|
||||
return err
|
||||
}
|
||||
if len(order.Discount) > 0 {
|
||||
if _, err = backpackmod.UnuseManyGoods(tool, order.Discount); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if order.Coin == 0 && order.IncomeCoin == 0 {
|
||||
return nil
|
||||
}
|
||||
wallet, err := walletmod.ReturnAmountAndIncome(tool, -order.Coin, -order.IncomeCoin, order.Uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
totalCoin := order.Coin + order.IncomeCoin
|
||||
return txnmod.InsertTransactionLog(tool, &txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: order.Uid,
|
||||
Amount: totalCoin,
|
||||
ActualAmount: float64(totalCoin),
|
||||
TranType: txnmod.AiChangefaceDebitGoldReturn.Key(),
|
||||
TranTypeInt: int64(txnmod.AiChangefaceDebitGoldReturn),
|
||||
Desc: fmt.Sprintf("AI换脸金币退返-%d", totalCoin),
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func refundImage(id primitive.ObjectID, remark string) error {
|
||||
return appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
order, err := aichangefaceimgmod.FindByID(tool, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transitioned, err := aichangefaceimgmod.TransitionStatus(tool, id, aichangefaceimgmod.StatusSubmit, aichangefaceimgmod.REFUND, bson.M{
|
||||
"updateAct": autoOperator,
|
||||
"remark": remark,
|
||||
})
|
||||
if err != nil || !transitioned {
|
||||
return err
|
||||
}
|
||||
return refundImageBalance(tool, order)
|
||||
})
|
||||
}
|
||||
|
||||
func refundImageBalance(tool *db.MongoTool, order *aichangefaceimgmod.AiChangeFaceImg) error {
|
||||
logs := make([]txnmod.TransactionLog, 0, 3)
|
||||
if order.Count > 0 || order.IsFreeTimes {
|
||||
count := order.Count
|
||||
if count == 0 {
|
||||
count = 1
|
||||
}
|
||||
wallet, err := walletmod.DebitAiFreeTimes(tool, -count, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(), UID: order.UID, Amount: count, ActualAmount: float64(count),
|
||||
TranType: txnmod.AiChangeFaceImgDebitFreeTimesReturn.Key(), TranTypeInt: int64(txnmod.AiChangeFaceImgDebitFreeTimesReturn),
|
||||
Desc: fmt.Sprintf("AI图片换脸免费次数退返[%d次]", count), RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
if order.DebitAmountCoin > 0 || order.DebitIncomeCoin > 0 {
|
||||
wallet, err := walletmod.ReturnAmountAndIncome(tool, -order.DebitAmountCoin, -order.DebitIncomeCoin, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if order.DebitIncomeCoin > 0 {
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(), UID: order.UID, Amount: order.DebitIncomeCoin, ActualAmount: float64(order.DebitIncomeCoin),
|
||||
TranType: txnmod.AiChangeFaceImgReturnIncomeGold.Key(), TranTypeInt: int64(txnmod.AiChangeFaceImgReturnIncomeGold),
|
||||
Desc: fmt.Sprintf("AI图片换脸收益金币退返-%d", order.DebitIncomeCoin), RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
if order.DebitAmountCoin > 0 {
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(), UID: order.UID, Amount: order.DebitAmountCoin, ActualAmount: float64(order.DebitAmountCoin),
|
||||
TranType: txnmod.AiChangeFaceImgReturnGold.Key(), TranTypeInt: int64(txnmod.AiChangeFaceImgReturnGold),
|
||||
Desc: fmt.Sprintf("AI图片换脸金币退返-%d", order.DebitAmountCoin), RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return txnmod.InsertManyTransactionLog(tool, logs)
|
||||
}
|
||||
|
||||
func refundUndress(id primitive.ObjectID, remark string) error {
|
||||
return appg.VideoDB.Trans(func(tool *db.MongoTool) error {
|
||||
order, err := aiUnDressmod.FindByID(tool, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transitioned, err := aiUnDressmod.TransitionStatus(tool, id, aiUnDressmod.SubmitOrder, aiUnDressmod.REFUND, bson.M{
|
||||
"updateAct": autoOperator,
|
||||
"remark": remark,
|
||||
})
|
||||
if err != nil || !transitioned {
|
||||
return err
|
||||
}
|
||||
return refundUndressBalance(tool, order)
|
||||
})
|
||||
}
|
||||
|
||||
func refundUndressBalance(tool *db.MongoTool, order *aiUnDressmod.AiUnDress) error {
|
||||
logs := make([]txnmod.TransactionLog, 0, 3)
|
||||
if order.Count > 0 || order.IsFreeTimes {
|
||||
count := order.Count
|
||||
if count == 0 {
|
||||
count = 1
|
||||
}
|
||||
wallet, err := walletmod.DebitAiFreeTimes(tool, -count, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(), UID: order.UID, Amount: count, ActualAmount: float64(count),
|
||||
TranType: txnmod.AiUndressDebitFreeTimesReturn.Key(), TranTypeInt: int64(txnmod.AiUndressDebitFreeTimesReturn),
|
||||
Desc: fmt.Sprintf("AI脱衣免费次数退返[%d次]", count), RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
debitAmount := order.DebitAmountCoin
|
||||
debitIncome := order.DebitIncomeCoin
|
||||
if !order.IsFreeTimes && debitAmount == 0 && debitIncome == 0 {
|
||||
debitAmount = order.Coin
|
||||
}
|
||||
if debitAmount > 0 || debitIncome > 0 {
|
||||
wallet, err := walletmod.ReturnAmountAndIncome(tool, -debitAmount, -debitIncome, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debitIncome > 0 {
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(), UID: order.UID, Amount: debitIncome, ActualAmount: float64(debitIncome),
|
||||
TranType: txnmod.AiUndressDebitIncomeGoldReturn.Key(), TranTypeInt: int64(txnmod.AiUndressDebitIncomeGoldReturn),
|
||||
Desc: fmt.Sprintf("AI脱衣收益金币退返-%d", debitIncome), RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
if debitAmount > 0 {
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(), UID: order.UID, Amount: debitAmount, ActualAmount: float64(debitAmount),
|
||||
TranType: txnmod.AiUndressDebitGoldReturn.Key(), TranTypeInt: int64(txnmod.AiUndressDebitGoldReturn),
|
||||
Desc: fmt.Sprintf("AI脱衣金币退返-%d", debitAmount), RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(logs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return txnmod.InsertManyTransactionLog(tool, logs)
|
||||
}
|
||||
Executable
+325
@@ -0,0 +1,325 @@
|
||||
package aiplazaser
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/cache/aiplazadata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aiUnDressmod"
|
||||
"91porn-server/models/v/aichangefaceimgmod"
|
||||
"91porn-server/models/v/aichangefacemod"
|
||||
"91porn-server/models/v/aiimagetovideomod"
|
||||
"91porn-server/models/v/aiplazamod"
|
||||
"91porn-server/models/v/aitexttoimagemod"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
Type *int `json:"type" form:"type"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画 不传则查看全部
|
||||
SortType int `json:"sortType" form:"sortType"` // 1-推荐 2-最新上架 3-最多观看 4-最多收藏
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*aiplazamod.AiPlazaInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
filter := bson.M{
|
||||
"status": 1,
|
||||
}
|
||||
if p.Type != nil {
|
||||
filter["type"] = p.Type
|
||||
}
|
||||
sort := bson.D{{"reviewAt", -1}}
|
||||
switch p.SortType {
|
||||
case 2:
|
||||
// 最新上架
|
||||
sort = bson.D{{"reviewAt", -1}}
|
||||
case 3:
|
||||
// 最多观看
|
||||
sort = bson.D{{"viewCount", -1}, {"reviewAt", -1}}
|
||||
case 4:
|
||||
// 最多收藏
|
||||
sort = bson.D{{"collectCount", -1}, {"reviewAt", -1}}
|
||||
default: // 1
|
||||
// 推荐
|
||||
//filter["reviewAt"] = bson.M{"$gt": carbon.NewCarbon().StartOfDay().SubDays(30).StdTime()}
|
||||
sort = bson.D{{"sortCode", -1}, {"reviewAt", -1}}
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
var data []aiplazamod.AiPlaza
|
||||
data, res.Total, res.HasNext, err = aiplazadata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取ai广场帖子列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = aiplazadata.FormatAppDataList(data)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type AppQueryInfoReq struct {
|
||||
ID string `json:"id" form:"id"` // id
|
||||
}
|
||||
|
||||
type AppQueryInfoResp struct {
|
||||
Detail *aiplazamod.AiPlazaInfo
|
||||
}
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *AppQueryInfoReq) GetInfo(uid uint64) (res AppQueryInfoResp, err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var item aiplazamod.AiPlaza
|
||||
item, err = aiplazadata.GetInfoFromCache(oid)
|
||||
if err != nil {
|
||||
log.Error("获取ai广场帖子详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
res.Detail = aiplazadata.FormatAppData(uid, item)
|
||||
return
|
||||
}
|
||||
|
||||
type ShareReq struct {
|
||||
Type int `json:"type" form:"type" binding:"required"` // 1-ai图片换脸 2-ai视频换脸 3-ai脱衣 4-ai图生视频 5-ai绘画
|
||||
OrderId primitive.ObjectID `json:"orderId" form:"orderId" binding:"required"`
|
||||
Title string `json:"title" form:"title"`
|
||||
}
|
||||
|
||||
// Create 发布数据
|
||||
func (p *ShareReq) Create(uid uint64) error {
|
||||
// 根据类型获取订单
|
||||
switch p.Type {
|
||||
case 1:
|
||||
// ai图片换脸
|
||||
return p.shareAiImageChangeFace(uid)
|
||||
case 2:
|
||||
// ai视频换脸
|
||||
return p.shareAiVideoChangeFace(uid)
|
||||
case 3:
|
||||
// ai脱衣
|
||||
return p.shareAiUndress(uid)
|
||||
case 4:
|
||||
// ai图生视频
|
||||
return p.shareAiImageToVideo(uid)
|
||||
case 5:
|
||||
// ai绘画
|
||||
return p.shareAiTextToImage(uid)
|
||||
default:
|
||||
// 其他类型的不处理
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ShareReq) shareAiImageChangeFace(uid uint64) (err error) {
|
||||
// ai图片换脸
|
||||
aichangeFaceInfo, err := aichangefaceimgmod.FindByID(nil, p.OrderId)
|
||||
if aichangeFaceInfo == nil && err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if aichangeFaceInfo.UID != uid || aichangeFaceInfo.Status != aichangefaceimgmod.SUCCESS {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
// 修改状态
|
||||
if err = aichangefaceimgmod.Update(nil, p.OrderId, bson.M{
|
||||
"shareStatus": 1,
|
||||
"shareTitle": p.Title,
|
||||
}); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
// 新增一个分享
|
||||
alPlaza := aiplazamod.AiPlaza{
|
||||
Type: 1,
|
||||
OrderId: aichangeFaceInfo.ID,
|
||||
Template: aichangeFaceInfo.MId,
|
||||
Uid: aichangeFaceInfo.UID,
|
||||
Title: p.Title,
|
||||
OriginalImage: aichangeFaceInfo.ModPic,
|
||||
OriginalVideo: "",
|
||||
OriginContent: "",
|
||||
GenerateVideo: "",
|
||||
Status: aiplazamod.DefaultStatus,
|
||||
}
|
||||
if len(aichangeFaceInfo.NewPic) > 0 {
|
||||
alPlaza.GenerateImage = aichangeFaceInfo.NewPic[0]
|
||||
}
|
||||
_, err = aiplazamod.Insert(nil, alPlaza)
|
||||
if err != nil {
|
||||
log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ShareReq) shareAiVideoChangeFace(uid uint64) (err error) {
|
||||
//查询该笔订单是否存在
|
||||
aichangeFaceInfo, err := aichangefacemod.FindByID(nil, p.OrderId)
|
||||
if aichangeFaceInfo == nil && err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if aichangeFaceInfo.Uid != uid || aichangeFaceInfo.Status != aichangefacemod.StatusComplete {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if err = aichangefacemod.Update(nil, p.OrderId, bson.M{"$set": bson.M{
|
||||
"shareStatus": 1,
|
||||
"shareTitle": p.Title,
|
||||
}}); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
// 新增一个分享
|
||||
alPlaza := aiplazamod.AiPlaza{
|
||||
Type: 2,
|
||||
OrderId: aichangeFaceInfo.ID,
|
||||
Template: aichangeFaceInfo.VidID.Hex(),
|
||||
Uid: aichangeFaceInfo.Uid,
|
||||
Title: p.Title,
|
||||
OriginalImage: "",
|
||||
OriginalVideo: aichangeFaceInfo.ModVideo,
|
||||
OriginalVideoCover: aichangeFaceInfo.ModCover,
|
||||
OriginContent: "",
|
||||
GenerateVideo: aichangeFaceInfo.Url,
|
||||
Status: aiplazamod.DefaultStatus,
|
||||
}
|
||||
if len(aichangeFaceInfo.Pic) > 0 {
|
||||
alPlaza.GenerateVideoCover = aichangeFaceInfo.Pic[0]
|
||||
}
|
||||
_, err = aiplazamod.Insert(nil, alPlaza)
|
||||
if err != nil {
|
||||
log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ShareReq) shareAiUndress(uid uint64) (err error) {
|
||||
data, err := aiUnDressmod.FindDataByID(p.OrderId.Hex())
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if data.UID != uid || data.Status != aiUnDressmod.SUCCESS {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
// 修改订单状态
|
||||
if err = aiUnDressmod.Edit(nil, bson.M{"_id": p.OrderId}, bson.M{"$set": bson.M{
|
||||
"shareStatus": 1,
|
||||
"shareTitle": p.Title,
|
||||
}}); err != nil {
|
||||
log.Error(fmt.Sprintf("Handle AiUnDress CallBackOrder aiUnDressmod.Edit error:%+v:", err), log.Any("uid", uid))
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
// 新增一个分享
|
||||
alPlaza := aiplazamod.AiPlaza{
|
||||
Type: 3,
|
||||
OrderId: data.ID,
|
||||
Template: "",
|
||||
Uid: data.UID,
|
||||
Title: p.Title,
|
||||
OriginalImage: data.OriginPic,
|
||||
OriginalVideo: "",
|
||||
OriginContent: "",
|
||||
//GenerateImage: data.NewPic[0],
|
||||
GenerateVideo: "",
|
||||
Status: aiplazamod.DefaultStatus,
|
||||
}
|
||||
if len(data.NewPic) > 0 {
|
||||
alPlaza.GenerateImage = data.NewPic[0]
|
||||
}
|
||||
_, err = aiplazamod.Insert(nil, alPlaza)
|
||||
if err != nil {
|
||||
log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ShareReq) shareAiImageToVideo(uid uint64) (err error) {
|
||||
// 查询该笔订单是否存在
|
||||
data, err := aiimagetovideomod.GetInfo(p.OrderId)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if data.UID != uid || data.Status != int(aiimagetovideomod.StatusGenerationSuccess) {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
// 修改订单状态
|
||||
if _, err = aiimagetovideomod.UpdateByID(nil, p.OrderId, bson.M{
|
||||
"shareStatus": 1,
|
||||
"shareTitle": p.Title,
|
||||
}); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
// 新增一个分享
|
||||
alPlaza := aiplazamod.AiPlaza{
|
||||
Type: 4,
|
||||
OrderId: data.ID,
|
||||
Template: "",
|
||||
Uid: data.UID,
|
||||
Title: p.Title,
|
||||
OriginalImage: data.ImgUrl,
|
||||
OriginalVideo: "",
|
||||
OriginContent: "",
|
||||
GenerateImage: data.NewImgUrl,
|
||||
GenerateVideo: "",
|
||||
Status: aiplazamod.DefaultStatus,
|
||||
}
|
||||
_, err = aiplazamod.Insert(nil, alPlaza)
|
||||
if err != nil {
|
||||
log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ShareReq) shareAiTextToImage(uid uint64) (err error) {
|
||||
// 查询该笔订单是否存在
|
||||
data, err := aitexttoimagemod.GetInfo(p.OrderId)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if data.UID != uid || data.Status != int(aitexttoimagemod.StatusGenerationSuccess) {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
// 修改订单状态
|
||||
if _, err = aitexttoimagemod.UpdateByID(nil, p.OrderId, bson.M{
|
||||
"shareStatus": 1,
|
||||
"shareTitle": p.Title,
|
||||
}); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
// 新增一个分享
|
||||
alPlaza := aiplazamod.AiPlaza{
|
||||
Type: 5,
|
||||
OrderId: data.ID,
|
||||
Template: "",
|
||||
Uid: data.UID,
|
||||
Title: p.Title,
|
||||
OriginalImage: "",
|
||||
OriginalVideo: "",
|
||||
OriginContent: data.Text,
|
||||
GenerateImage: data.NewImgUrl,
|
||||
GenerateVideo: "",
|
||||
Status: aiplazamod.DefaultStatus,
|
||||
}
|
||||
_, err = aiplazamod.Insert(nil, alPlaza)
|
||||
if err != nil {
|
||||
log.Error("新增ai广场帖子失败", log.Any("alPlaza", alPlaza), log.E(err))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package aiser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/laosiji_app"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/fundtransferlogmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const aiFundLockExpiry = 30 * time.Second
|
||||
|
||||
type GetAuthURLResp struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type transferDependencies struct {
|
||||
findLatest func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error)
|
||||
findLatestOut func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error)
|
||||
getWallet func(uint64) (*walletmod.Wallet, error)
|
||||
findUser func(uint64) (*usermod.User, error)
|
||||
getAuthURL func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error)
|
||||
bringOut func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error)
|
||||
debitAndLog func(uint64, int64, time.Time) error
|
||||
creditAndLog func(uint64, int64, float64, string, time.Time) error
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func defaultTransferDependencies() transferDependencies {
|
||||
return transferDependencies{
|
||||
findLatest: fundtransferlogmod.FindLatestByUIDAndCategory,
|
||||
findLatestOut: fundtransferlogmod.FindLatestOutByUIDAndCategory,
|
||||
getWallet: walletmod.GetWallet,
|
||||
findUser: usermod.FindUserByUID,
|
||||
getAuthURL: laosiji_app.GetAiMateURL,
|
||||
bringOut: laosiji_app.AiMateBringOut,
|
||||
debitAndLog: debitAndLog,
|
||||
creditAndLog: creditAndLog,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func acquireLock(uid uint64) (func(), error) {
|
||||
if appg.Redis == nil {
|
||||
return nil, errors.New("redis is unavailable")
|
||||
}
|
||||
lock := redis.BuildLock(
|
||||
appg.Redis,
|
||||
redisconst.GetAiFundLockKey(uid),
|
||||
redsync.WithExpiry(aiFundLockExpiry),
|
||||
redsync.WithTries(1),
|
||||
)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() {
|
||||
if _, err := lock.Unlock(); err != nil {
|
||||
log.Warn("unlock AI fund lock failed", log.Any("uid", uid), log.E(err))
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAuthURL 先回收上一次第三方余额,再将主钱包金币上分并返回AI女友授权地址。
|
||||
func GetAuthURL(ctx context.Context, uid uint64) (GetAuthURLResp, error) {
|
||||
unlock, err := acquireLock(uid)
|
||||
if err != nil {
|
||||
return GetAuthURLResp{}, errors.New("操作频繁,请稍后再试")
|
||||
}
|
||||
defer unlock()
|
||||
return getAuthURL(ctx, uid, defaultTransferDependencies())
|
||||
}
|
||||
|
||||
func getAuthURL(ctx context.Context, uid uint64, deps transferDependencies) (GetAuthURLResp, error) {
|
||||
if err := settleDown(ctx, uid, deps); err != nil {
|
||||
return GetAuthURLResp{}, fmt.Errorf("下分失败: %w", err)
|
||||
}
|
||||
|
||||
wallet, err := deps.getWallet(uid)
|
||||
if err != nil {
|
||||
return GetAuthURLResp{}, fmt.Errorf("获取钱包失败: %w", err)
|
||||
}
|
||||
var amount int64
|
||||
if wallet != nil {
|
||||
amount = wallet.Amount
|
||||
}
|
||||
|
||||
var remainder float64
|
||||
lastOut, err := deps.findLatestOut(uid, fundtransferlogmod.CategoryAiGirlfriend)
|
||||
if err != nil {
|
||||
return GetAuthURLResp{}, fmt.Errorf("获取下分余数失败: %w", err)
|
||||
}
|
||||
if lastOut != nil {
|
||||
remainder = lastOut.Remainder
|
||||
}
|
||||
|
||||
nickname, avatar := "用户", ""
|
||||
if user, findErr := deps.findUser(uid); findErr == nil && user != nil {
|
||||
if user.Name != "" {
|
||||
nickname = user.Name
|
||||
}
|
||||
avatar = user.Portrait
|
||||
}
|
||||
username := laosiji_app.GetUserName(appg.Conf.Base.Env, commod.KFK_APPID, uid)
|
||||
authResp, err := deps.getAuthURL(ctx, laosiji_app.GetAiMateURLReq{
|
||||
Username: username,
|
||||
Nickname: nickname,
|
||||
Asset: assetAmount(amount, remainder),
|
||||
Currency: "CNY",
|
||||
Theme: "dark",
|
||||
UserAvatar: avatar,
|
||||
})
|
||||
if err != nil {
|
||||
return GetAuthURLResp{}, fmt.Errorf("获取AI女友地址失败: %w", err)
|
||||
}
|
||||
if authResp.AuthURL == "" {
|
||||
return GetAuthURLResp{}, errors.New("AI女友地址为空")
|
||||
}
|
||||
|
||||
if amount > 0 {
|
||||
if err = deps.debitAndLog(uid, amount, deps.now()); err != nil {
|
||||
return GetAuthURLResp{}, fmt.Errorf("上分扣除金币失败: %w", err)
|
||||
}
|
||||
}
|
||||
return GetAuthURLResp{URL: authResp.AuthURL}, nil
|
||||
}
|
||||
|
||||
// TrySettleDown 在钱包查询前尝试将AI女友剩余余额下分回主钱包。
|
||||
func TrySettleDown(ctx context.Context, uid uint64) error {
|
||||
unlock, err := acquireLock(uid)
|
||||
if err != nil {
|
||||
return errors.New("操作频繁,请稍后再试")
|
||||
}
|
||||
defer unlock()
|
||||
return settleDown(ctx, uid, defaultTransferDependencies())
|
||||
}
|
||||
|
||||
func settleDown(ctx context.Context, uid uint64, deps transferDependencies) error {
|
||||
latest, err := deps.findLatest(uid, fundtransferlogmod.CategoryAiGirlfriend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if latest == nil || latest.FundType != fundtransferlogmod.FundTypeIn {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := deps.bringOut(ctx, laosiji_app.AiMateBringOutReq{
|
||||
Username: laosiji_app.GetUserName(appg.Conf.Base.Env, commod.KFK_APPID, uid),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
returnAmount, remainder, err := balanceToWallet(resp.Balance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc := "AI女友下分"
|
||||
if returnAmount == 0 {
|
||||
desc += "-金币为0"
|
||||
}
|
||||
return deps.creditAndLog(uid, returnAmount, remainder, desc, deps.now())
|
||||
}
|
||||
|
||||
func assetAmount(amount int64, remainder float64) string {
|
||||
amountDecimal := decimal.NewFromInt(amount).Div(decimal.NewFromInt(10))
|
||||
return amountDecimal.Add(decimal.NewFromFloat(remainder)).Round(2).StringFixed(2)
|
||||
}
|
||||
|
||||
func balanceToWallet(balance string) (int64, float64, error) {
|
||||
value, err := decimal.NewFromString(balance)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("无效的第三方余额: %w", err)
|
||||
}
|
||||
if value.IsNegative() {
|
||||
return 0, 0, errors.New("第三方余额不能为负数")
|
||||
}
|
||||
coins := value.Mul(decimal.NewFromInt(10)).Floor()
|
||||
if coins.GreaterThan(decimal.NewFromInt(math.MaxInt64)) {
|
||||
return 0, 0, errors.New("第三方余额超出范围")
|
||||
}
|
||||
returnAmount := coins.IntPart()
|
||||
remainderDecimal := value.Sub(decimal.NewFromInt(returnAmount).Div(decimal.NewFromInt(10))).Round(2)
|
||||
remainder, _ := remainderDecimal.Float64()
|
||||
return returnAmount, remainder, nil
|
||||
}
|
||||
|
||||
func debitAndLog(uid uint64, amount int64, now time.Time) error {
|
||||
if appg.VideoDB == nil {
|
||||
return errors.New("video database is unavailable")
|
||||
}
|
||||
return appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
wallet, err := walletmod.DebitAmount(t, amount, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fundtransferlogmod.Insert(t, &fundtransferlogmod.FundTransferLog{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Category: fundtransferlogmod.CategoryAiGirlfriend,
|
||||
FundType: fundtransferlogmod.FundTypeIn,
|
||||
Amount: amount,
|
||||
Balance: wallet.Amount,
|
||||
Desc: "AI女友上分",
|
||||
CreatedAt: now,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func creditAndLog(uid uint64, amount int64, remainder float64, desc string, now time.Time) error {
|
||||
if appg.VideoDB == nil {
|
||||
return errors.New("video database is unavailable")
|
||||
}
|
||||
return appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
wallet, err := walletmod.CreditAmount(t, amount, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fundtransferlogmod.Insert(t, &fundtransferlogmod.FundTransferLog{
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Category: fundtransferlogmod.CategoryAiGirlfriend,
|
||||
FundType: fundtransferlogmod.FundTypeOut,
|
||||
Amount: amount,
|
||||
Balance: wallet.Amount,
|
||||
Remainder: remainder,
|
||||
Desc: desc,
|
||||
CreatedAt: now,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package aiser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/laosiji_app"
|
||||
"91porn-server/models/v/fundtransferlogmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
func TestGetAuthURLTransfersWalletAmount(t *testing.T) {
|
||||
originalConfig := appg.Conf
|
||||
appg.Conf = &appg.GlobalConfig{}
|
||||
appg.Conf.Base.Env = "test"
|
||||
t.Cleanup(func() { appg.Conf = originalConfig })
|
||||
|
||||
var (
|
||||
gotAsset string
|
||||
gotDebit int64
|
||||
gotUser string
|
||||
debitTime time.Time
|
||||
)
|
||||
now := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC)
|
||||
deps := transferDependencies{
|
||||
findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
return nil, nil
|
||||
},
|
||||
findLatestOut: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
return &fundtransferlogmod.FundTransferLog{Remainder: 0.04}, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{Amount: 123}, nil
|
||||
},
|
||||
findUser: func(uint64) (*usermod.User, error) {
|
||||
return &usermod.User{Name: "测试", Portrait: "avatar"}, nil
|
||||
},
|
||||
getAuthURL: func(_ context.Context, req laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) {
|
||||
gotAsset = req.Asset
|
||||
gotUser = req.Username
|
||||
return laosiji_app.GetAiMateURLResp{AuthURL: "https://example.com/auth"}, nil
|
||||
},
|
||||
bringOut: func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) {
|
||||
t.Fatal("bringOut must not be called without pending transfer")
|
||||
return laosiji_app.AiMateBringOutResp{}, nil
|
||||
},
|
||||
debitAndLog: func(_ uint64, amount int64, at time.Time) error {
|
||||
gotDebit, debitTime = amount, at
|
||||
return nil
|
||||
},
|
||||
creditAndLog: func(uint64, int64, float64, string, time.Time) error {
|
||||
t.Fatal("creditAndLog must not be called")
|
||||
return nil
|
||||
},
|
||||
now: func() time.Time { return now },
|
||||
}
|
||||
|
||||
resp, err := getAuthURL(context.Background(), 99, deps)
|
||||
if err != nil {
|
||||
t.Fatalf("getAuthURL() error = %v", err)
|
||||
}
|
||||
if resp.URL != "https://example.com/auth" {
|
||||
t.Fatalf("URL = %q", resp.URL)
|
||||
}
|
||||
if gotAsset != "12.34" {
|
||||
t.Fatalf("asset = %q, want 12.34", gotAsset)
|
||||
}
|
||||
if gotUser != "TEST-204_99" {
|
||||
t.Fatalf("username = %q", gotUser)
|
||||
}
|
||||
if gotDebit != 123 || !debitTime.Equal(now) {
|
||||
t.Fatalf("debit = %d at %s", gotDebit, debitTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthURLDoesNotDebitWhenThirdPartyFails(t *testing.T) {
|
||||
originalConfig := appg.Conf
|
||||
appg.Conf = &appg.GlobalConfig{}
|
||||
t.Cleanup(func() { appg.Conf = originalConfig })
|
||||
|
||||
deps := transferDependencies{
|
||||
findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
return nil, nil
|
||||
},
|
||||
findLatestOut: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
return nil, nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
return &walletmod.Wallet{Amount: 100}, nil
|
||||
},
|
||||
findUser: func(uint64) (*usermod.User, error) { return nil, nil },
|
||||
getAuthURL: func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) {
|
||||
return laosiji_app.GetAiMateURLResp{}, errors.New("remote failed")
|
||||
},
|
||||
debitAndLog: func(uint64, int64, time.Time) error {
|
||||
t.Fatal("debitAndLog must not be called after remote failure")
|
||||
return nil
|
||||
},
|
||||
now: time.Now,
|
||||
}
|
||||
if _, err := getAuthURL(context.Background(), 99, deps); err == nil {
|
||||
t.Fatal("getAuthURL() error = nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAuthURLSettlesPendingTransferBeforeNewTransfer(t *testing.T) {
|
||||
originalConfig := appg.Conf
|
||||
appg.Conf = &appg.GlobalConfig{}
|
||||
appg.Conf.Base.Env = "test"
|
||||
t.Cleanup(func() { appg.Conf = originalConfig })
|
||||
|
||||
steps := make([]string, 0, 5)
|
||||
deps := transferDependencies{
|
||||
findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
steps = append(steps, "find-pending")
|
||||
return &fundtransferlogmod.FundTransferLog{FundType: fundtransferlogmod.FundTypeIn}, nil
|
||||
},
|
||||
bringOut: func(context.Context, laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) {
|
||||
steps = append(steps, "bring-out")
|
||||
return laosiji_app.AiMateBringOutResp{Balance: "1.00"}, nil
|
||||
},
|
||||
creditAndLog: func(uint64, int64, float64, string, time.Time) error {
|
||||
steps = append(steps, "credit")
|
||||
return nil
|
||||
},
|
||||
getWallet: func(uint64) (*walletmod.Wallet, error) {
|
||||
steps = append(steps, "wallet")
|
||||
return &walletmod.Wallet{Amount: 20}, nil
|
||||
},
|
||||
findLatestOut: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
return nil, nil
|
||||
},
|
||||
findUser: func(uint64) (*usermod.User, error) { return nil, nil },
|
||||
getAuthURL: func(context.Context, laosiji_app.GetAiMateURLReq) (laosiji_app.GetAiMateURLResp, error) {
|
||||
steps = append(steps, "auth")
|
||||
return laosiji_app.GetAiMateURLResp{AuthURL: "https://example.com/auth"}, nil
|
||||
},
|
||||
debitAndLog: func(uint64, int64, time.Time) error { return nil },
|
||||
now: time.Now,
|
||||
}
|
||||
|
||||
if _, err := getAuthURL(context.Background(), 99, deps); err != nil {
|
||||
t.Fatalf("getAuthURL() error = %v", err)
|
||||
}
|
||||
want := []string{"find-pending", "bring-out", "credit", "wallet", "auth"}
|
||||
if len(steps) != len(want) {
|
||||
t.Fatalf("steps = %v, want %v", steps, want)
|
||||
}
|
||||
for index := range want {
|
||||
if steps[index] != want[index] {
|
||||
t.Fatalf("steps = %v, want %v", steps, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettleDownCreditsConvertedBalance(t *testing.T) {
|
||||
originalConfig := appg.Conf
|
||||
appg.Conf = &appg.GlobalConfig{}
|
||||
appg.Conf.Base.Env = "prod"
|
||||
t.Cleanup(func() { appg.Conf = originalConfig })
|
||||
|
||||
var (
|
||||
gotAmount int64
|
||||
gotRemainder float64
|
||||
gotDesc string
|
||||
gotUsername string
|
||||
)
|
||||
deps := transferDependencies{
|
||||
findLatest: func(uint64, fundtransferlogmod.Category) (*fundtransferlogmod.FundTransferLog, error) {
|
||||
return &fundtransferlogmod.FundTransferLog{FundType: fundtransferlogmod.FundTypeIn}, nil
|
||||
},
|
||||
bringOut: func(_ context.Context, req laosiji_app.AiMateBringOutReq) (laosiji_app.AiMateBringOutResp, error) {
|
||||
gotUsername = req.Username
|
||||
return laosiji_app.AiMateBringOutResp{Balance: "1.23"}, nil
|
||||
},
|
||||
creditAndLog: func(_ uint64, amount int64, remainder float64, desc string, _ time.Time) error {
|
||||
gotAmount, gotRemainder, gotDesc = amount, remainder, desc
|
||||
return nil
|
||||
},
|
||||
now: time.Now,
|
||||
}
|
||||
if err := settleDown(context.Background(), 88, deps); err != nil {
|
||||
t.Fatalf("settleDown() error = %v", err)
|
||||
}
|
||||
if gotUsername != "JHA-204_88" {
|
||||
t.Fatalf("username = %q", gotUsername)
|
||||
}
|
||||
if gotAmount != 12 || gotRemainder != 0.03 || gotDesc != "AI女友下分" {
|
||||
t.Fatalf("credit amount=%d remainder=%v desc=%q", gotAmount, gotRemainder, gotDesc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBalanceToWallet(t *testing.T) {
|
||||
tests := []struct {
|
||||
balance string
|
||||
amount int64
|
||||
remainder float64
|
||||
wantErr bool
|
||||
}{
|
||||
{balance: "0", amount: 0, remainder: 0},
|
||||
{balance: "1.20", amount: 12, remainder: 0},
|
||||
{balance: "1.29", amount: 12, remainder: 0.09},
|
||||
{balance: "-1", wantErr: true},
|
||||
{balance: "invalid", wantErr: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
amount, remainder, err := balanceToWallet(test.balance)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("balanceToWallet(%q) error = %v", test.balance, err)
|
||||
}
|
||||
if amount != test.amount || remainder != test.remainder {
|
||||
t.Fatalf("balanceToWallet(%q) = (%d,%v), want (%d,%v)", test.balance, amount, remainder, test.amount, test.remainder)
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package aitemplatemoduleser
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/cache/aitemplatemoduledata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aitemplatemodulemod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*aitemplatemodulemod.AiTemplateModuleInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
filter := bson.M{}
|
||||
|
||||
sort := bson.D{{"_id", -1}}
|
||||
// 获取列表
|
||||
var data []aitemplatemodulemod.AiTemplateModule
|
||||
data, res.Total, res.HasNext, err = aitemplatemoduledata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取AI模版模块列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = aitemplatemoduledata.FormatAppDataList(data)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type AppQueryAllListReq struct {
|
||||
}
|
||||
|
||||
type AppAllListRes struct {
|
||||
List []*aitemplatemodulemod.AiTemplateModuleInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryAllListReq) GetList() AppAllListRes {
|
||||
var res AppAllListRes
|
||||
var err error
|
||||
filter := bson.M{"status": 1}
|
||||
sort := bson.D{{"sortNum", -1}}
|
||||
// 获取列表
|
||||
data, err := aitemplatemodulemod.GetAll(filter, sort)
|
||||
if err != nil {
|
||||
log.Error("获取AI模版模块列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
|
||||
res.List = aitemplatemoduledata.FormatAppDataList(data)
|
||||
return res
|
||||
}
|
||||
|
||||
type AppQueryInfoReq struct {
|
||||
ID string `json:"id" form:"id"` // id
|
||||
}
|
||||
type AppQueryInfoRes = *aitemplatemodulemod.AiTemplateModuleInfo
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var item aitemplatemodulemod.AiTemplateModule
|
||||
item, err = aitemplatemoduledata.GetInfoFromCache(oid)
|
||||
if err != nil {
|
||||
log.Error("获取AI模版模块列表详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
res = aitemplatemoduledata.FormatAppData(item)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package annouser
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/localcache"
|
||||
"91porn-server/models/v/adsmod"
|
||||
"91porn-server/models/v/annoumod"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
// GetAnnouList 获取指定类型的公告
|
||||
func GetAnnouList(t int) (out *Annou, err error) {
|
||||
log.Info(fmt.Sprintf("[Params-%s]", "GetAnnouList"), log.Any("type", t))
|
||||
cfg, ok := localcache.C.Get(adsmod.GetAnnounceByTypeList)
|
||||
if ok {
|
||||
return cfg.(*Annou), nil
|
||||
}
|
||||
if out, err = getAnnouList(); err != nil {
|
||||
log.Error(fmt.Sprintf("[HTTP-%s]==> request fail error:%v:", "GetAnnouList", err), log.Any("type", t))
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
localcache.C.Set(adsmod.GetAnnounceByTypeList, out, 10*time.Minute)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// AnnouList 公告列表
|
||||
func AnnouList() (out []*Annou, err error) {
|
||||
log.Info(fmt.Sprintf("[Params-%s]", "GetAnnouList"))
|
||||
cfg, ok := localcache.C.Get(adsmod.GetAnnounceList)
|
||||
if ok {
|
||||
return cfg.([]*Annou), nil
|
||||
}
|
||||
if out, err = annouList(); err != nil {
|
||||
log.Error(fmt.Sprintf("[HTTP-%s]==> request fail error:%v:", "GetAnnouList", err))
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
localcache.C.Set(adsmod.GetAnnounceList, out, 5*time.Minute)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// MsgAnnouList 消息模块公告列表
|
||||
func MsgAnnouList(req annoumod.MsgListReq) (out *MsgAnnousResp, err error) {
|
||||
log.Info(fmt.Sprintf("[Params-%s]", "MsgAnnouList"))
|
||||
cfg, ok := localcache.C.Get(adsmod.GetMsgAnnounceList)
|
||||
if ok {
|
||||
return cfg.(*MsgAnnousResp), nil
|
||||
}
|
||||
if out, err = annouLists(req.PageNumber, req.PageSize); err != nil {
|
||||
log.Error(fmt.Sprintf("[HTTP-%s]==> request fail error:%v:", "MsgAnnouList", err))
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
localcache.C.Set(adsmod.GetMsgAnnounceList, out, 10*time.Minute)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package annouser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/httputil"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 从数据中心获取数据
|
||||
func getAnnouList() (*Annou, error) {
|
||||
var (
|
||||
url string = appg.Conf.URL.DataCentreURL + GetAnnouListRouter
|
||||
params QueryAllCond = QueryAllCond{AppId}
|
||||
out CommonResponse
|
||||
)
|
||||
if code, err := httputil.DefaultClientGetWithResp(&out, url, nil, params); err != nil {
|
||||
return nil, err
|
||||
} else if code != http.StatusOK {
|
||||
return nil, errors.New(NetWorkError)
|
||||
}
|
||||
if len(out.Data) > 0 {
|
||||
return out.Data[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 从数据中心获取数据
|
||||
func annouList() ([]*Annou, error) {
|
||||
var (
|
||||
url string = appg.Conf.URL.DataCentreURL + GetAnnouListRouter
|
||||
params QueryAllCond = QueryAllCond{AppId}
|
||||
out CommonResponse
|
||||
)
|
||||
if code, err := httputil.DefaultClientGetWithResp(&out, url, nil, params); err != nil {
|
||||
return nil, err
|
||||
} else if code != http.StatusOK {
|
||||
return nil, errors.New(NetWorkError)
|
||||
}
|
||||
return out.Data, nil
|
||||
}
|
||||
|
||||
// 从数据中心分页获取数据
|
||||
func annouLists(pageNumber, pageSize int) (*MsgAnnousResp, error) {
|
||||
var (
|
||||
url string = appg.Conf.URL.DataCentreURL + GetAnnouListsRouter
|
||||
params QueryByPageCond = QueryByPageCond{AppId, pageNumber, pageSize}
|
||||
out MsgAnnousResponse
|
||||
)
|
||||
if code, err := httputil.DefaultClientGetWithResp(&out, url, nil, params); err != nil {
|
||||
return nil, err
|
||||
} else if code != http.StatusOK {
|
||||
return nil, errors.New(NetWorkError)
|
||||
}
|
||||
return out.Data, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package annouser
|
||||
|
||||
const (
|
||||
GetAnnouListRouter = "/api/stat/announce/get"
|
||||
GetAnnouListsRouter = "/api/stat/announce/out/list"
|
||||
NetWorkError = "网络错误"
|
||||
AppId = 31
|
||||
)
|
||||
|
||||
type QueryAllCond struct {
|
||||
AppId int32 `json:"appId" binding:"required"`
|
||||
}
|
||||
|
||||
type QueryByPageCond struct {
|
||||
AppId int32 `json:"appId" binding:"required"`
|
||||
PageNumber int `form:"pageNum" json:"pageNum"`
|
||||
PageSize int `form:"pageSize" json:"pageSize"`
|
||||
}
|
||||
|
||||
// Annou 公告模型
|
||||
type Annou struct {
|
||||
//id
|
||||
ID string `json:"id"`
|
||||
//名字
|
||||
Title string `json:"title"`
|
||||
//内容
|
||||
Content string `json:"content"`
|
||||
//广告图
|
||||
Cover string `json:"cover"`
|
||||
//链接地址
|
||||
Href string `json:"href"`
|
||||
//类型
|
||||
Type int64 `json:"type"`
|
||||
}
|
||||
|
||||
type CommonResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Data []*Annou `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
Time string `json:"time"`
|
||||
Tip string `json:"tip"`
|
||||
}
|
||||
|
||||
type MsgAnnousResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Data *MsgAnnousResp `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
Time string `json:"time"`
|
||||
Tip string `json:"tip"`
|
||||
}
|
||||
|
||||
type MsgAnnousResp struct {
|
||||
Total int64 `json:"total"`
|
||||
List []*Annou `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package backpackser
|
||||
|
||||
import (
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/backpackmod"
|
||||
)
|
||||
|
||||
// 获取优惠卷列表
|
||||
func GetCouponList(uid uint64, types int, status int, limit, page int64) (interface{}, stderr.Code) {
|
||||
list, err := backpackmod.GetCouponList(uid, types, status, limit, page)
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
return list, stderr.Success
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
package checkinser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/common/timeutil"
|
||||
"91porn-server/models/v/checkinconfigmod"
|
||||
"91porn-server/models/v/checkinprizemod"
|
||||
"91porn-server/models/v/prizemod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usercheckinmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
checkinLimitKeyPrefix = "checkin::limit::"
|
||||
checkinLimitExpire = 3 * time.Second
|
||||
)
|
||||
|
||||
// CheckinInfo 签到信息
|
||||
type CheckinInfo struct {
|
||||
TodayChecked bool `json:"todayChecked"`
|
||||
ContinuouslyDays int64 `json:"continuouslyDays"`
|
||||
CumulativeDays int64 `json:"cumulativeDays"`
|
||||
DoubleReward bool `json:"doubleReward"`
|
||||
}
|
||||
|
||||
// CheckinConfigResp 签到配置返回
|
||||
type CheckinConfigResp struct {
|
||||
Enable bool `json:"enable"`
|
||||
Description string `json:"description"`
|
||||
BackgroundImage string `json:"backgroundImage"`
|
||||
IntegerExchangeList []checkinconfigmod.GiftItem `json:"integerExchangeList"`
|
||||
}
|
||||
|
||||
// PrizeBrief 奖品摘要
|
||||
type PrizeBrief struct {
|
||||
PrizeImage string `json:"prizeImage"`
|
||||
PrizeType prizemod.PrizeType `json:"prizeType"`
|
||||
PrizeCount int64 `json:"prizeCount"`
|
||||
PrizeTitle string `json:"prizeTitle"`
|
||||
}
|
||||
|
||||
type PrizeBriefList []*PrizeBrief
|
||||
|
||||
// UserCheckinResp 用户签到返回
|
||||
type UserCheckinResp struct {
|
||||
Message string `json:"message"`
|
||||
Checkin CheckinInfo `json:"checkin"`
|
||||
Prizes PrizeBriefList `json:"prizes"`
|
||||
PrizeVideo string `json:"prizeVideo"`
|
||||
}
|
||||
|
||||
// CheckinDto 签到奖品DTO
|
||||
type CheckinDto struct {
|
||||
checkinprizemod.CheckinPrize `bson:",inline"`
|
||||
Score int64 `json:"score"`
|
||||
PrizeType prizemod.PrizeType `json:"prizeType"`
|
||||
IsReceive bool `json:"isReceive"`
|
||||
IsCheckedIn bool `json:"isCheckedIn"`
|
||||
CanClaim bool `json:"canClaim"`
|
||||
IsExpired bool `json:"isExpired"`
|
||||
}
|
||||
|
||||
// GetCheckinPrizeResp 获取签到奖品返回
|
||||
type GetCheckinPrizeResp struct {
|
||||
Prizes []CheckinDto `json:"prizes"`
|
||||
BigPrizes []CheckinDto `json:"bigPrizes"`
|
||||
Checkin CheckinInfo `json:"checkin"`
|
||||
Config CheckinConfigResp `json:"config"`
|
||||
}
|
||||
|
||||
// getCheckinConfig 获取签到配置
|
||||
func getCheckinConfig() *checkinconfigmod.CheckinConfig {
|
||||
cfg, _ := checkinconfigmod.FindOne(bson.M{})
|
||||
return cfg
|
||||
}
|
||||
|
||||
// giveCheckinPrizeById 发放签到奖品
|
||||
func giveCheckinPrizeById(uid uint64, checkInPrize *checkinprizemod.CheckinPrize, isVip bool) (*PrizeBrief, error) {
|
||||
if checkInPrize == nil || checkInPrize.PrizeId.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
prizes, err := prizemod.GetPrizeListByIDs([]primitive.ObjectID{checkInPrize.PrizeId})
|
||||
if err != nil || len(prizes) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
prize := *prizes[0]
|
||||
// 非VIP专属奖励 && 是VIP用户 && 积分类型 → 积分+2
|
||||
if isVip && !checkInPrize.BigPrize && prize.Type == prizemod.Integral {
|
||||
prize.Price += 2
|
||||
}
|
||||
|
||||
handler, err := prizemod.Run(uid, prize, txnmod.SignBoon)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if handler == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if err := handler.Run(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return txnmod.InsertManyTransactionLog(t, handler.GetTransactionLog())
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prizeBrief := &PrizeBrief{
|
||||
PrizeImage: prize.Image,
|
||||
PrizeType: prize.Type,
|
||||
PrizeCount: prize.Price,
|
||||
PrizeTitle: checkInPrize.PrizeName,
|
||||
}
|
||||
return prizeBrief, nil
|
||||
}
|
||||
|
||||
// AddCheckin 添加一次签到记录
|
||||
func AddCheckin(uid uint64) (*UserCheckinResp, stderr.Code) {
|
||||
// 3秒限速
|
||||
limitKey := checkinLimitKeyPrefix + strconv.FormatUint(uid, 10)
|
||||
if ok, _ := appg.Redis.Setnx_NewOK(limitKey, 1, checkinLimitExpire); !ok {
|
||||
return nil, stderr.ErrLoginTooFrequently
|
||||
}
|
||||
|
||||
now := time.Now().Local()
|
||||
cfg := getCheckinConfig()
|
||||
if cfg == nil || !cfg.Enable {
|
||||
return nil, stderr.FunctionNotEnabled
|
||||
}
|
||||
|
||||
today := timeutil.BeginOfTime(now)
|
||||
// 获取今天签到记录
|
||||
checkinToday, err := usercheckinmod.FindOne(bson.M{
|
||||
"userId": uid,
|
||||
"date": today,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("AddCheckin:FindOne:today", log.E(err))
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
if checkinToday != nil {
|
||||
resp := &UserCheckinResp{
|
||||
Message: "今日已经签到,请勿重复签到",
|
||||
Checkin: CheckinInfo{
|
||||
TodayChecked: true,
|
||||
ContinuouslyDays: checkinToday.ContinuouslyDays,
|
||||
CumulativeDays: checkinToday.CumulativeDays,
|
||||
},
|
||||
}
|
||||
return resp, stderr.Success
|
||||
}
|
||||
|
||||
// 插入签到记录
|
||||
checkin := usercheckinmod.UserCheckin{
|
||||
ID: primitive.NewObjectID(),
|
||||
Date: today,
|
||||
UserId: uid,
|
||||
Prizes: nil,
|
||||
Gave: false,
|
||||
CreatedAt: now,
|
||||
IsReset: false,
|
||||
}
|
||||
if err := usercheckinmod.InsertOne(&checkin); err != nil {
|
||||
log.Error("AddCheckin:InsertOne failed", log.E(err))
|
||||
return nil, stderr.ErrDbInsertError
|
||||
}
|
||||
|
||||
// 查询本月签到记录
|
||||
limit := int64(now.Day())
|
||||
checkinOpt := options.Find()
|
||||
checkinOpt.SetSort(bson.D{{Key: "date", Value: -1}}).SetLimit(limit)
|
||||
checkinList, err := usercheckinmod.FindMany(bson.M{"userId": uid}, checkinOpt)
|
||||
if err != nil {
|
||||
log.Error("AddCheckin:FindMany", log.E(err))
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
ContinuouslyDays := int64(1)
|
||||
CumulativeDays := int64(1)
|
||||
doubleBonusReceived := false
|
||||
beginOfCurrentMonth := timeutil.BeginningOfMonth(now)
|
||||
yesterday := today.Add(-24 * time.Hour)
|
||||
|
||||
for i, item := range checkinList {
|
||||
if i == 0 {
|
||||
continue // 跳过今天刚插入的记录
|
||||
}
|
||||
if item.Date.Before(beginOfCurrentMonth) {
|
||||
continue
|
||||
}
|
||||
CumulativeDays += 1
|
||||
if i == 1 {
|
||||
if item.Date.Equal(yesterday) {
|
||||
ContinuouslyDays = item.ContinuouslyDays + 1
|
||||
}
|
||||
} else if item.ContinuouslyDays == 7 {
|
||||
doubleBonusReceived = true
|
||||
}
|
||||
}
|
||||
|
||||
// 查找签到奖励
|
||||
prizeBriefList := make(PrizeBriefList, 0)
|
||||
bgMediaUrl := ""
|
||||
continuouslyPrizes, err := checkinprizemod.FindMany(bson.M{
|
||||
"status": true,
|
||||
"checkinType": checkinprizemod.CheckinTypeContinuously,
|
||||
"checkinDays": ContinuouslyDays,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("AddCheckin:FindMany prizes", log.E(err))
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
prizeIds := make([]primitive.ObjectID, 0)
|
||||
giveVipPrize := false
|
||||
isVip := u != nil && u.IsPaidVIP()
|
||||
continuousGiveTimes := int64(1)
|
||||
|
||||
if len(continuouslyPrizes) > 0 {
|
||||
if ContinuouslyDays == 7 && !doubleBonusReceived {
|
||||
continuousGiveTimes = 2
|
||||
}
|
||||
for _, item := range continuouslyPrizes {
|
||||
if item.BigPrize && !isVip {
|
||||
continue
|
||||
}
|
||||
for i := int64(0); i < continuousGiveTimes; i++ {
|
||||
prizeBrief, err := giveCheckinPrizeById(uid, item, isVip)
|
||||
if err != nil {
|
||||
log.Error("giveCheckinPrizeById", log.E(err))
|
||||
continue
|
||||
}
|
||||
if prizeBrief == nil {
|
||||
continue
|
||||
}
|
||||
prizeBriefList = append(prizeBriefList, prizeBrief)
|
||||
prizeIds = append(prizeIds, item.PrizeId)
|
||||
giveVipPrize = giveVipPrize || item.BigPrize
|
||||
|
||||
// 7天翻倍,根据非VIP的奖励类型来返回奖励背景视频
|
||||
if !item.BigPrize && continuousGiveTimes == 2 && cfg.RewardBgVideos != nil {
|
||||
for _, vItem := range cfg.RewardBgVideos {
|
||||
if vItem.PrizeType == prizeBrief.PrizeType {
|
||||
bgMediaUrl = vItem.BgMediaUrl
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新签到记录
|
||||
update := bson.M{
|
||||
"continuouslyDays": ContinuouslyDays,
|
||||
"cumulativeDays": CumulativeDays,
|
||||
"prizes": prizeIds,
|
||||
"gave": true,
|
||||
"vipPrizeGave": giveVipPrize,
|
||||
}
|
||||
if err = usercheckinmod.UpdateOne(bson.M{"_id": checkin.ID}, bson.M{"$set": update}); err != nil {
|
||||
log.Error("AddCheckin:UpdateOne", log.E(err))
|
||||
}
|
||||
|
||||
resp := &UserCheckinResp{
|
||||
Message: "签到成功",
|
||||
Checkin: CheckinInfo{
|
||||
TodayChecked: true,
|
||||
ContinuouslyDays: ContinuouslyDays,
|
||||
CumulativeDays: CumulativeDays,
|
||||
DoubleReward: continuousGiveTimes == 2,
|
||||
},
|
||||
Prizes: prizeBriefList,
|
||||
PrizeVideo: bgMediaUrl,
|
||||
}
|
||||
return resp, stderr.Success
|
||||
}
|
||||
|
||||
// GetCheckinPrizes 获取签到奖品
|
||||
func GetCheckinPrizes(uid uint64) (*GetCheckinPrizeResp, stderr.Code) {
|
||||
now := time.Now().Local()
|
||||
cfg := getCheckinConfig()
|
||||
cfgResp := CheckinConfigResp{}
|
||||
if cfg != nil {
|
||||
cfgResp.Enable = cfg.Enable
|
||||
cfgResp.Description = cfg.Description
|
||||
cfgResp.BackgroundImage = cfg.BackgroundImage
|
||||
cfgResp.IntegerExchangeList = cfg.IntegerExchangeList
|
||||
}
|
||||
|
||||
// 获取所有启用的连续签到奖品
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{{Key: "checkinDays", Value: 1}})
|
||||
var info []CheckinDto
|
||||
err := checkinprizemod.FindManyWithBind(&info, bson.M{
|
||||
"status": true,
|
||||
"checkinType": checkinprizemod.CheckinTypeContinuously,
|
||||
}, opts)
|
||||
if err != nil {
|
||||
log.Error("GetCheckinPrizes:FindMany", log.E(err))
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
// 查询奖品信息,填充积分数据
|
||||
prizeIdSet := make(map[primitive.ObjectID]struct{})
|
||||
for _, item := range info {
|
||||
prizeIdSet[item.PrizeId] = struct{}{}
|
||||
}
|
||||
prizeIds := make([]primitive.ObjectID, 0, len(prizeIdSet))
|
||||
for id := range prizeIdSet {
|
||||
prizeIds = append(prizeIds, id)
|
||||
}
|
||||
if len(prizeIds) > 0 {
|
||||
prizeList, _ := prizemod.GetPrizeListByIDs(prizeIds)
|
||||
prizeMap := make(map[primitive.ObjectID]*prizemod.Prize)
|
||||
for _, p := range prizeList {
|
||||
prizeMap[p.ID] = p
|
||||
}
|
||||
for i, v := range info {
|
||||
if prize, ok := prizeMap[v.PrizeId]; ok {
|
||||
info[i].Score = int64(prize.Count)
|
||||
info[i].PrizeType = prize.Type
|
||||
if prize.Type == prizemod.Gold {
|
||||
info[i].Score = int64(prize.Count) / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分组
|
||||
normalPrizes := make([]CheckinDto, 0)
|
||||
bigPrizes := make([]CheckinDto, 0)
|
||||
for _, item := range info {
|
||||
if item.BigPrize {
|
||||
bigPrizes = append(bigPrizes, item)
|
||||
} else {
|
||||
normalPrizes = append(normalPrizes, item)
|
||||
}
|
||||
}
|
||||
|
||||
// 签到记录
|
||||
limit := int64(now.Day())
|
||||
checkinOpt := options.Find()
|
||||
checkinOpt.SetSort(bson.D{{Key: "date", Value: -1}}).SetLimit(limit)
|
||||
checkinList, err := usercheckinmod.FindMany(bson.M{"userId": uid}, checkinOpt)
|
||||
if err != nil {
|
||||
log.Error("GetCheckinPrizes:FindMany checkins", log.E(err))
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
ContinuouslyDays := int64(0)
|
||||
CumulativeDays := int64(0)
|
||||
lastCheckinDay := timeutil.BeginOfTime(now)
|
||||
doubleRewardsReceived := false
|
||||
isTodayCheckin := false
|
||||
|
||||
// 查询用户VIP状态
|
||||
isVip := false
|
||||
if u, uErr := usermod.FindUserByUID(uid); uErr == nil && u != nil {
|
||||
isVip = u.IsPaidVIP()
|
||||
}
|
||||
|
||||
if len(checkinList) > 0 {
|
||||
if checkinList[0].Date.Equal(lastCheckinDay) {
|
||||
isTodayCheckin = true
|
||||
} else if checkinList[0].Date.Equal(lastCheckinDay.Add(-24 * time.Hour)) {
|
||||
lastCheckinDay = lastCheckinDay.Add(-24 * time.Hour)
|
||||
}
|
||||
beginOfCurrentMonth := timeutil.BeginningOfMonth(now)
|
||||
streakBroken := false
|
||||
for _, item := range checkinList {
|
||||
CumulativeDays += 1
|
||||
if lastCheckinDay.Before(beginOfCurrentMonth) {
|
||||
lastCheckinDay = beginOfCurrentMonth
|
||||
}
|
||||
if !streakBroken && item.Date.Equal(lastCheckinDay) {
|
||||
ContinuouslyDays += 1
|
||||
lastCheckinDay = item.Date.Add(-24 * time.Hour)
|
||||
} else {
|
||||
streakBroken = true
|
||||
}
|
||||
if ContinuouslyDays >= 7 {
|
||||
doubleRewardsReceived = true
|
||||
}
|
||||
}
|
||||
// 只对当前连续签到的记录映射IsReceive和IsCheckedIn
|
||||
for i := int64(0); i < ContinuouslyDays; i++ {
|
||||
record := checkinList[ContinuouslyDays-1-i]
|
||||
idx := int(i)
|
||||
if idx < len(normalPrizes) {
|
||||
normalPrizes[idx].IsReceive = record.Gave
|
||||
normalPrizes[idx].IsCheckedIn = true
|
||||
}
|
||||
if idx < len(bigPrizes) {
|
||||
bigPrizes[idx].IsReceive = record.VipPrizeGave
|
||||
bigPrizes[idx].IsCheckedIn = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标记可补领的VIP奖励
|
||||
if isVip && isTodayCheckin && len(checkinList) > 0 {
|
||||
for _, item := range checkinList {
|
||||
if !item.Date.Equal(timeutil.BeginOfTime(now)) {
|
||||
continue
|
||||
}
|
||||
if item.VipPrizeGave {
|
||||
continue
|
||||
}
|
||||
for i := range bigPrizes {
|
||||
if bigPrizes[i].CheckinDays == item.ContinuouslyDays {
|
||||
bigPrizes[i].CanClaim = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标记已过期的大会员奖励:已签到、未领取、不能补领、且不是今天签到对应的那天
|
||||
todayPrizeIdx := int(ContinuouslyDays - 1) // 今天签到对应的bigPrizes索引
|
||||
for i := range bigPrizes {
|
||||
if i == todayPrizeIdx && isTodayCheckin {
|
||||
continue
|
||||
}
|
||||
if bigPrizes[i].IsCheckedIn && !bigPrizes[i].IsReceive && !bigPrizes[i].CanClaim {
|
||||
bigPrizes[i].IsExpired = true
|
||||
}
|
||||
}
|
||||
|
||||
doubleReward := !doubleRewardsReceived && ContinuouslyDays == 6 && !isTodayCheckin
|
||||
resp := &GetCheckinPrizeResp{
|
||||
Prizes: normalPrizes,
|
||||
BigPrizes: bigPrizes,
|
||||
Checkin: CheckinInfo{
|
||||
TodayChecked: isTodayCheckin,
|
||||
DoubleReward: doubleReward,
|
||||
ContinuouslyDays: ContinuouslyDays,
|
||||
CumulativeDays: CumulativeDays,
|
||||
},
|
||||
Config: cfgResp,
|
||||
}
|
||||
return resp, stderr.Success
|
||||
}
|
||||
|
||||
// ClaimVipCheckinPrizes 补领VIP签到奖励
|
||||
func ClaimVipCheckinPrizes(uid uint64) (*UserCheckinResp, stderr.Code) {
|
||||
// 3秒限速
|
||||
limitKey := checkinLimitKeyPrefix + strconv.FormatUint(uid, 10)
|
||||
if ok, _ := appg.Redis.Setnx_NewOK(limitKey, 1, checkinLimitExpire); !ok {
|
||||
return nil, stderr.ErrLoginTooFrequently
|
||||
}
|
||||
|
||||
now := time.Now().Local()
|
||||
cfg := getCheckinConfig()
|
||||
if cfg == nil || !cfg.Enable {
|
||||
return nil, stderr.FunctionNotEnabled
|
||||
}
|
||||
|
||||
// 校验用户是否是VIP
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
return nil, stderr.UserIsNotExists
|
||||
}
|
||||
if !u.IsPaidVIP() {
|
||||
return nil, stderr.NoVipPrivilege
|
||||
}
|
||||
|
||||
// 查询当天未领取VIP奖励的签到记录
|
||||
today := timeutil.BeginOfTime(now)
|
||||
checkinOpt := options.Find()
|
||||
checkinOpt.SetSort(bson.D{{Key: "date", Value: 1}})
|
||||
unclaimed, err := usercheckinmod.FindMany(bson.M{
|
||||
"userId": uid,
|
||||
"vipPrizeGave": false,
|
||||
"date": today,
|
||||
}, checkinOpt)
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(unclaimed) == 0 {
|
||||
return nil, stderr.Failure
|
||||
}
|
||||
|
||||
// 获取所有启用的连续签到VIP奖励配置
|
||||
vipPrizes, err := checkinprizemod.FindMany(bson.M{
|
||||
"status": true,
|
||||
"checkinType": checkinprizemod.CheckinTypeContinuously,
|
||||
"bigPrize": true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
vipPrizeMap := make(map[int64]*checkinprizemod.CheckinPrize)
|
||||
for _, p := range vipPrizes {
|
||||
vipPrizeMap[p.CheckinDays] = p
|
||||
}
|
||||
|
||||
// 逐条补领
|
||||
prizeBriefList := make(PrizeBriefList, 0)
|
||||
for _, record := range unclaimed {
|
||||
prize, ok := vipPrizeMap[record.ContinuouslyDays]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// 原子更新防并发
|
||||
if err = usercheckinmod.UpdateOne(bson.M{
|
||||
"_id": record.ID,
|
||||
"vipPrizeGave": false,
|
||||
}, bson.M{
|
||||
"$set": bson.M{"vipPrizeGave": true},
|
||||
}); err != nil {
|
||||
continue
|
||||
}
|
||||
prizeBrief, gErr := giveCheckinPrizeById(uid, prize, true)
|
||||
if gErr != nil {
|
||||
log.Error("ClaimVipCheckinPrizes:giveCheckinPrizeById", log.E(gErr), log.Any("recordId", record.ID))
|
||||
continue
|
||||
}
|
||||
if prizeBrief != nil {
|
||||
prizeBriefList = append(prizeBriefList, prizeBrief)
|
||||
}
|
||||
}
|
||||
|
||||
if len(prizeBriefList) == 0 {
|
||||
return nil, stderr.Failure
|
||||
}
|
||||
|
||||
resp := &UserCheckinResp{
|
||||
Message: "补领成功",
|
||||
Prizes: prizeBriefList,
|
||||
}
|
||||
return resp, stderr.Success
|
||||
}
|
||||
|
||||
// GetCheckinConfig 获取签到配置(给后台用)
|
||||
func GetCheckinConfig() (*checkinconfigmod.CheckinConfig, error) {
|
||||
cfg, err := checkinconfigmod.FindOne(bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg == nil {
|
||||
return &checkinconfigmod.CheckinConfig{}, nil
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// UpdateCheckinConfig 更新签到配置
|
||||
func UpdateCheckinConfig(update bson.M) error {
|
||||
count, err := checkinconfigmod.Count(bson.M{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
newConfig := &checkinconfigmod.CheckinConfig{}
|
||||
return checkinconfigmod.InsertOne(newConfig)
|
||||
}
|
||||
// 查找并更新第一条
|
||||
cfg, err := checkinconfigmod.FindOne(bson.M{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkinconfigmod.UpdateOne(bson.M{"_id": cfg.ID}, update)
|
||||
}
|
||||
|
||||
// GetCheckinPrizeList 获取签到奖品列表(后台)
|
||||
func GetCheckinPrizeList(pageNumber, pageSize int64) (checkinprizemod.CheckinPrizeList, int64, error) {
|
||||
filter := bson.M{}
|
||||
total, err := checkinprizemod.Count(filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return make(checkinprizemod.CheckinPrizeList, 0), 0, nil
|
||||
}
|
||||
opts := options.Find()
|
||||
opts.SetLimit(pageSize)
|
||||
opts.SetSkip((pageNumber - 1) * pageSize)
|
||||
opts.SetSort(bson.D{{Key: "checkinDays", Value: 1}})
|
||||
data, err := checkinprizemod.FindMany(filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return data, total, nil
|
||||
}
|
||||
|
||||
// AddCheckinPrize 添加签到奖品
|
||||
func AddCheckinPrize(prize *checkinprizemod.CheckinPrize) error {
|
||||
now := time.Now()
|
||||
prize.CreatedAt = now
|
||||
prize.UpdatedAt = now
|
||||
return checkinprizemod.InsertOne(prize)
|
||||
}
|
||||
|
||||
// UpdateCheckinPrize 更新签到奖品
|
||||
func UpdateCheckinPrize(id primitive.ObjectID, update bson.M) error {
|
||||
return checkinprizemod.UpdateOne(bson.M{"_id": id}, bson.M{"$set": update})
|
||||
}
|
||||
|
||||
// DeleteCheckinPrize 删除签到奖品
|
||||
func DeleteCheckinPrize(id primitive.ObjectID) error {
|
||||
_, err := checkinprizemod.DeleteOne(bson.M{"_id": id})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUserCheckinList 获取用户签到历史(后台)
|
||||
func GetUserCheckinList(filter bson.M, pageNumber, pageSize int64) (usercheckinmod.CheckinList, int64, error) {
|
||||
total, err := usercheckinmod.Count(filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return make(usercheckinmod.CheckinList, 0), 0, nil
|
||||
}
|
||||
opts := options.Find()
|
||||
opts.SetSort(bson.D{{Key: "createdAt", Value: -1}})
|
||||
opts.SetSkip((pageNumber - 1) * pageSize)
|
||||
opts.SetLimit(pageSize)
|
||||
data, err := usercheckinmod.FindMany(filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return data, total, nil
|
||||
}
|
||||
|
||||
// DeleteUserCheckin 删除用户签到记录
|
||||
func DeleteUserCheckin(id primitive.ObjectID) error {
|
||||
_, err := usercheckinmod.DeleteOne(bson.M{"_id": id})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package collectser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/taskser"
|
||||
"91porn-server/app/service/vidhelpser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/cache/aiplazadata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/aiplazamod"
|
||||
"91porn-server/models/v/collectmod"
|
||||
"91porn-server/models/v/dailytaskmod"
|
||||
"91porn-server/models/v/tagmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// DoCollect 收藏标签
|
||||
func DoCollect(uid uint64, cType string, objID primitive.ObjectID, isCollect bool, ua ua.UA, ip string) (code stderr.Code, err error) {
|
||||
coll, err := collectmod.CollFindOneByUidAndObjID(uid, objID)
|
||||
if err != nil {
|
||||
code = stderr.CollectErrGetDataFailed
|
||||
log.Error("app service collect DoCollect CollFindOneByUidAndObjID err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
if isCollect && !coll.ID.IsZero() {
|
||||
code = stderr.CollectHasCollected
|
||||
return
|
||||
} else if !isCollect && coll.ID.IsZero() {
|
||||
code = stderr.CollectHasNotCollected
|
||||
return
|
||||
}
|
||||
cMod := collectmod.Collect{
|
||||
ObjID: objID,
|
||||
UID: uid,
|
||||
Type: cType,
|
||||
}
|
||||
if isCollect {
|
||||
if err = collectmod.CollInsertOne(&cMod); err != nil {
|
||||
code = stderr.CollectErrCollectFailed
|
||||
log.Error("app service collect DoCollect CollInsertOne err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
if incErr := vidmod.IncrementRecommendInteraction(objID, vidmod.RecommendInteractionCollect); incErr != nil {
|
||||
// 推荐累计失败不回滚已成功的收藏,记录后由监控补偿。
|
||||
log.Error("increment recommend collect failed", log.E(incErr), log.Any("objID", objID))
|
||||
}
|
||||
} else {
|
||||
if err = collectmod.CollDeleteOne(uid, cType, objID); err != nil {
|
||||
code = stderr.CollectErrCancelFailed
|
||||
log.Error("app service collect DoCollect CollDeleteOne err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
inc := -1
|
||||
if isCollect {
|
||||
inc = 1
|
||||
// 每日任务完成
|
||||
taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyDoCollect)
|
||||
}
|
||||
switch cType {
|
||||
case constant.CollectTypeAiPlaza:
|
||||
err = aiplazamod.IncCollectCount(objID)
|
||||
case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC, constant.SEED_LINK:
|
||||
err = vidmod.IncForwardCount(inc, objID)
|
||||
case constant.CollectTypeTag:
|
||||
err = tagmod.IncreaseTagCollectCount(inc, objID)
|
||||
}
|
||||
if err != nil {
|
||||
code = stderr.CollectErrCollectFailed
|
||||
log.Error("app service collect DoCollect IncreaseTagCollectCount err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
reportVideoCollect(uid, cType, objID, !isCollect, ua, ip)
|
||||
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// DoBatchCancelCollect 批量取消收藏
|
||||
func DoBatchCancelCollect(uid uint64, cType string, objIds []primitive.ObjectID, ua ua.UA, ip string) (code stderr.Code, err error) {
|
||||
colls, err := collectmod.CollFindOneByUidAndObjIds(uid, objIds)
|
||||
if err != nil {
|
||||
code = stderr.CollectErrGetDataFailed
|
||||
log.Error("app service collect DoBatchCollect CollFindOneByUidAndObjIds err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
var uniqueIds []string
|
||||
var oids []primitive.ObjectID
|
||||
for _, coll := range colls {
|
||||
if !coll.ID.IsZero() {
|
||||
oids = append(oids, coll.ObjID)
|
||||
uniqueIds = append(uniqueIds, collectmod.Unique(uid, cType, coll.ObjID))
|
||||
}
|
||||
}
|
||||
if len(uniqueIds) == 0 {
|
||||
code = stderr.CollectHasNotCollected
|
||||
return
|
||||
}
|
||||
|
||||
if err = collectmod.CollDeleteMany(uniqueIds); err != nil {
|
||||
code = stderr.CollectErrCancelFailed
|
||||
log.Error("app service collect DoCollect CollDeleteMany err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
switch cType {
|
||||
case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC, constant.CollectTypeSEED_LINK:
|
||||
err = vidmod.IncForwardCount(-1, oids...)
|
||||
case constant.CollectTypeTag:
|
||||
err = tagmod.IncreaseTagCollectCount(-1, oids...)
|
||||
}
|
||||
if err != nil {
|
||||
code = stderr.CollectErrCollectFailed
|
||||
log.Error("app service collect DoCollect IncreaseTagCollectCount err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
for _, oid := range oids {
|
||||
reportVideoCollect(uid, cType, oid, true, ua, ip)
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
func reportVideoCollect(uid uint64, cType string, objID primitive.ObjectID, cancel bool, uaInfo ua.UA, ip string) {
|
||||
if !shouldReportVideoCollect(cType) || objID.IsZero() {
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || user == nil {
|
||||
return
|
||||
}
|
||||
video, err := vidmod.GetVideoInfo(objID.Hex())
|
||||
if err != nil || video.ID.IsZero() {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func shouldReportVideoCollect(cType string) bool {
|
||||
switch cType {
|
||||
case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC, constant.CollectTypeSEED_LINK:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfoList 收藏详情列表
|
||||
func GetInfoList(uid uint64, cType string, page commod.Page) (code stderr.Code, hasNext bool, data interface{}, err error) {
|
||||
stdQuery := commod.StdQuery{Page: &commod.PageBy{Num: page.PageNumber, Size: page.PageSize + 1, CheckNext: true}, Order: &[]commod.OrderBy{}}
|
||||
modList, err := collectmod.CollFindCollects(uid, cType, stdQuery)
|
||||
if err != nil {
|
||||
code = stderr.CollectErrGetDataFailed
|
||||
log.Error("app service collect GetInfoList CollFindCollects err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
cIDs := make([]primitive.ObjectID, len(modList)) // 收藏对象id
|
||||
for i := range modList {
|
||||
cIDs[i] = modList[i].ObjID
|
||||
}
|
||||
|
||||
if len(cIDs) <= 0 {
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
if len(cIDs) > int(page.PageSize) {
|
||||
hasNext = true
|
||||
cIDs = cIDs[:page.PageSize]
|
||||
}
|
||||
switch cType {
|
||||
case constant.AiPlaza:
|
||||
// 获取ai广场
|
||||
// 获取ai广场帖子
|
||||
list, err := aiplazamod.GetAll(bson.M{"_id": bson.M{"$in": cIDs}}, nil)
|
||||
if err != nil {
|
||||
log.Error("获取收藏的ai广场帖子失败", log.Any("uid", uid), log.E(err))
|
||||
break
|
||||
}
|
||||
data = aiplazadata.FormatAppDataList(list)
|
||||
case constant.CollectTypeShort, constant.CollectTypeSP, constant.CollectTypeCover, constant.CollectTypePIC, constant.CollectTypeSEED_LINK:
|
||||
vidList := vidhelpser.GetVideosByIDsNoCache(uid, cIDs)
|
||||
data = vidList
|
||||
if len(vidList) > int(page.PageSize) {
|
||||
hasNext = true
|
||||
data = vidList[:page.PageSize]
|
||||
}
|
||||
break
|
||||
case constant.CollectTypeTag:
|
||||
tags, err := getTagsInfo(cIDs)
|
||||
if err != nil {
|
||||
code = stderr.CollectErrGetDataFailed
|
||||
return code, hasNext, data, err
|
||||
}
|
||||
data = tags
|
||||
if len(tags) > int(page.PageSize) {
|
||||
hasNext = true
|
||||
data = tags[:page.PageSize]
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
func getTagsInfo(ids []primitive.ObjectID) ([]tagmod.RespInfo, error) {
|
||||
tList, err := tagmod.FindTagsByIDS(ids)
|
||||
if err != nil {
|
||||
log.Error("app service collect getTagsInfo FindTagsByIDS err", log.Any("err", err))
|
||||
return nil, err
|
||||
}
|
||||
var tInfoList []tagmod.RespInfo
|
||||
for _, v := range tList {
|
||||
tTmp := tagmod.RespInfo{
|
||||
ID: v.ID,
|
||||
Name: v.TagName,
|
||||
CoverImg: v.CoverImg,
|
||||
Description: v.Description,
|
||||
PlayCount: v.TPlayCount,
|
||||
HasCollected: true,
|
||||
VideoCount: v.VideoCount,
|
||||
}
|
||||
tInfoList = append(tInfoList, tTmp)
|
||||
}
|
||||
if tInfoList == nil || len(tInfoList) <= 0 {
|
||||
tInfoList = []tagmod.RespInfo{}
|
||||
}
|
||||
return tInfoList, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetChildCmtList 获取子评论列表
|
||||
func GetChildCmtList(uid uint64, objID ObjectID, cid ObjectID, fstID ObjectID, curTime time.Time, page commod.Page) (code stderr.Code, data []cmtmod.ChildRespList, err error) {
|
||||
// 获取作者
|
||||
//videoInfo, err := vidmod.GetVideoInfo(objID.Hex())
|
||||
//if err != nil {
|
||||
// code = stderr.CommentErrGetDataFail
|
||||
// return
|
||||
//}
|
||||
skip, limit := getNewPageInfo(0, int64(page.PageNumber), int64(page.PageSize))
|
||||
aTotal, aList, err := getAuthorChildCmtList(objID, cid, fstID, skip, limit, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
if len(aList) > int(page.PageSize) {
|
||||
data, err = getChildCommentTackInfo(uid, aList)
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
// 获取热评及其回复
|
||||
skip, limit = getNewPageInfo(aTotal, int64(page.PageNumber), int64(page.PageSize))
|
||||
_, hList, err := getHotChildCmtList(objID, cid, fstID, skip, limit, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
aList = append(aList, hList...)
|
||||
//if len(aList) > page.PageSize {
|
||||
data, err = getChildCommentTackInfo(uid, aList)
|
||||
code = stderr.Success
|
||||
return
|
||||
//}
|
||||
// 获取普通评论
|
||||
//skip, limit = getNewPageInfo(aTotal+hTotal, int64(page.PageNumber), int64(page.PageSize))
|
||||
//cList, err := getChildCmtList(objID, cid, fstID, skip, limit, curTime)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//aList = append(aList, cList...)
|
||||
//data, err = getChildCommentTackInfo(uid, aList)
|
||||
//if err != nil {
|
||||
// code = stderr.CommentErrGetDataFail
|
||||
// return
|
||||
//}
|
||||
//code = stderr.Success
|
||||
//return
|
||||
}
|
||||
|
||||
// 获取作者的子评论及其回复和数量
|
||||
func getAuthorChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (total int64, data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetAuthorChildCmtList(objID, cid, fstId, skip, limit, curTime)
|
||||
}
|
||||
|
||||
// 获取热门的子评论及其回复和数量
|
||||
func getHotChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (total int64, data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetHotChildCmtList(objID, cid, fstId, skip, limit, curTime)
|
||||
}
|
||||
|
||||
// 获取普通子评论
|
||||
func getChildCmtList(objID ObjectID, cid ObjectID, fstId ObjectID, skip int64, limit int64, curTime time.Time) (data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetChildCmtList(objID, cid, fstId, skip, limit, curTime)
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/userser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"91porn-server/models/v/followmod"
|
||||
"91porn-server/models/v/likemod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectID = primitive.ObjectID
|
||||
|
||||
// 获取父评论的附加信息--用户头像/是否点赞/是否是关注
|
||||
func getParentCommentTackInfo(uid uint64, cModList []cmtmod.Comment) (data []cmtmod.ParentRespList, err error) {
|
||||
now := time.Now()
|
||||
if cModList == nil {
|
||||
return []cmtmod.ParentRespList{}, err
|
||||
}
|
||||
var CIDs []ObjectID
|
||||
var UIDs []uint64
|
||||
for _, value := range cModList {
|
||||
if uid != value.UserID && value.IsAdvertiser {
|
||||
continue
|
||||
}
|
||||
tmp := cmtmod.ParentRespList{
|
||||
ID: value.ID.Hex(),
|
||||
ObjID: value.ObjID.Hex(),
|
||||
UserID: value.UserID,
|
||||
LikeCount: value.LikeCount,
|
||||
LinkStr: value.LinkStr,
|
||||
IsFollow: false,
|
||||
IsAuthor: value.IsAuthor,
|
||||
IsLike: false,
|
||||
IsDelete: value.IsDelete,
|
||||
IsGodComment: value.IsGodComment,
|
||||
CommCount: value.CommentCount,
|
||||
CommCountBurden: value.CommentCount,
|
||||
Content: value.Content,
|
||||
Image: value.Image,
|
||||
Status: value.Status,
|
||||
City: value.City,
|
||||
QuoteType: value.QuoteType,
|
||||
QuoteID: value.QuoteID,
|
||||
QuoteTitle: value.QuoteTitle,
|
||||
QuoteImg: value.QuoteImg,
|
||||
CreatedAt: value.CreatedAt,
|
||||
Info: []cmtmod.ChildRespList{}, // 空值处理
|
||||
}
|
||||
CIDs = append(CIDs, value.ID)
|
||||
UIDs = append(UIDs, value.UserID)
|
||||
data = append(data, tmp)
|
||||
}
|
||||
var wg = sync.WaitGroup{}
|
||||
var errs []error
|
||||
wg.Add(3)
|
||||
// 获取点赞记录信息
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
likedList, err1 := likemod.FindLikesByObjIDS(constant.LikeTypeComment, uid, CIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
for k, v := range data {
|
||||
for _, ls := range likedList {
|
||||
if v.ID == ls.ObjID.Hex() {
|
||||
data[k].IsLike = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// 获取用户昵称/性别/头像地址
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
users, err1 := usermod.FindUsersByUID(UIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range data {
|
||||
for _, u := range users {
|
||||
if v.UserID == u.UID {
|
||||
data[k].UserName = u.Name
|
||||
data[k].UserPortrait = u.Portrait
|
||||
data[k].Gender = u.Gender
|
||||
data[k].Age = u.Age()
|
||||
data[k].Level = u.VipLevel
|
||||
data[k].VipLevel = u.VipLevel
|
||||
data[k].SuperUser = u.GetSuperUser(now)
|
||||
data[k].Awards = u.Awards
|
||||
data[k].AwardsExpire = userser.HandleUserAwardsExpire(u)
|
||||
data[k].VipExpireDate = u.VipExpireDate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// 是否关注
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
fMap, err1 := followmod.IsFollowUsers(uid, UIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
for k, v := range data {
|
||||
if fMap[v.UserID] {
|
||||
data[k].IsFollow = true
|
||||
} else {
|
||||
data[k].IsFollow = false
|
||||
}
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
if len(errs) > 0 {
|
||||
err = errs[0]
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取父评论的作者或者热门的首条子评论
|
||||
func getFstChildCmtOfParent(isAdvertiser bool, objID ObjectID, list []cmtmod.ParentRespList, ids []ObjectID, uid uint64, author uint64, curTime time.Time) (data []cmtmod.ParentRespList, err error) {
|
||||
if list == nil || ids == nil {
|
||||
return
|
||||
}
|
||||
// 先获取作者的子评论
|
||||
aList, err := cmtmod.GetFstAuthorCmtOfParentCmt(isAdvertiser, objID, ids, author, curTime)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var cmtMap = make(map[ObjectID]*cmtmod.Comment)
|
||||
for _, v := range aList {
|
||||
_v := v
|
||||
cmtMap[v.CID] = &_v
|
||||
}
|
||||
// 获取其余的子评论
|
||||
cList, err := cmtmod.GetFstCmtOfParentCmt(isAdvertiser, objID, ids, curTime)
|
||||
for _, v := range cList {
|
||||
_v := v
|
||||
if cmtMap[v.CID] == nil {
|
||||
cmtMap[v.CID] = &_v
|
||||
}
|
||||
}
|
||||
var childList []cmtmod.Comment
|
||||
for _, v := range cmtMap {
|
||||
childList = append(childList, *v)
|
||||
}
|
||||
cResList, err := getChildCommentTackInfo(uid, childList)
|
||||
var childMap = make(map[string]*cmtmod.ChildRespList)
|
||||
for _, v := range cResList {
|
||||
_v := v
|
||||
childMap[v.CID] = &_v
|
||||
}
|
||||
for _, v := range list {
|
||||
if childMap[v.ID] != nil {
|
||||
v.Info = []cmtmod.ChildRespList{*childMap[v.ID]}
|
||||
data = append(data, v)
|
||||
} else {
|
||||
data = append(data, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取子评论的附加信息--用户头像/是否点赞/是否是关注
|
||||
func getChildCommentTackInfo(uid uint64, cModList []cmtmod.Comment) (data []cmtmod.ChildRespList, err error) {
|
||||
now := time.Now()
|
||||
if len(cModList) == 0 {
|
||||
return
|
||||
}
|
||||
var CIDs []ObjectID
|
||||
var UIDs []uint64
|
||||
var toUIDs []uint64
|
||||
|
||||
for _, value := range cModList {
|
||||
if uid != value.UserID && value.IsAdvertiser {
|
||||
continue
|
||||
}
|
||||
tmp := cmtmod.ChildRespList{
|
||||
ID: value.ID.Hex(),
|
||||
ObjID: value.ObjID.Hex(),
|
||||
CID: value.CID.Hex(),
|
||||
UserID: value.UserID,
|
||||
ToUserID: value.ToUserID,
|
||||
LikeCount: value.LikeCount,
|
||||
LinkStr: value.LinkStr,
|
||||
Level: value.Level,
|
||||
IsFollow: false,
|
||||
IsAuthor: value.IsAuthor,
|
||||
IsLike: false,
|
||||
IsDelete: value.IsDelete,
|
||||
Content: value.Content,
|
||||
Image: value.Image,
|
||||
Status: value.Status,
|
||||
City: value.City,
|
||||
QuoteType: value.QuoteType,
|
||||
QuoteID: value.QuoteID,
|
||||
QuoteTitle: value.QuoteTitle,
|
||||
QuoteImg: value.QuoteImg,
|
||||
CreatedAt: value.CreatedAt,
|
||||
}
|
||||
CIDs = append(CIDs, value.ID)
|
||||
UIDs = append(UIDs, value.UserID)
|
||||
data = append(data, tmp)
|
||||
if value.ToUserID != 0 {
|
||||
toUIDs = append(UIDs, value.ToUserID)
|
||||
}
|
||||
}
|
||||
var wg = sync.WaitGroup{}
|
||||
var errs []error
|
||||
wg.Add(4)
|
||||
// 是否点赞
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
likedList, err1 := likemod.FindLikesByObjIDS(constant.LikeTypeComment, uid, CIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
for k, v := range data {
|
||||
for _, ls := range likedList {
|
||||
if v.ID == ls.ObjID.Hex() {
|
||||
data[k].IsLike = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// 获取用户昵称/性别/头像地址
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
users, err1 := usermod.FindUsersByUID(UIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range data {
|
||||
for _, u := range users {
|
||||
if v.UserID == u.UID {
|
||||
data[k].UserName = u.Name
|
||||
data[k].UserPortrait = u.Portrait
|
||||
data[k].Gender = u.Gender
|
||||
data[k].Age = u.Age()
|
||||
data[k].VipLevel = u.VipLevel
|
||||
data[k].Awards = u.Awards
|
||||
data[k].AwardsExpire = userser.HandleUserAwardsExpire(u)
|
||||
data[k].SuperUser = u.GetSuperUser(now)
|
||||
data[k].VipExpireDate = u.VipExpireDate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// 获取回复用户
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
if toUIDs == nil || len(toUIDs) == 0 {
|
||||
return
|
||||
}
|
||||
toUsers, err1 := usermod.FindUsersByUID(toUIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
for k, v := range data {
|
||||
for _, u := range toUsers {
|
||||
if v.ToUserID == u.UID {
|
||||
data[k].ToUserName = u.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
// 是否关注
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
followers, err1 := followmod.IsFollowUsers(uid, UIDs)
|
||||
if err1 != nil {
|
||||
errs = append(errs, err1)
|
||||
return
|
||||
}
|
||||
for k, v := range data {
|
||||
data[k].IsFollow = followers[v.UserID]
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
if len(errs) > 0 {
|
||||
err = errs[0]
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 计算分页信息
|
||||
func getNewPageInfo(total int64, pageNumber int64, pageSize int64) (int64, int64) {
|
||||
var curPage = pageNumber - total/pageSize
|
||||
var skip, limit int64
|
||||
if curPage == 1 {
|
||||
skip = 0
|
||||
limit = (pageSize - total%pageSize) + 1
|
||||
} else {
|
||||
skip = pageSize - total%pageSize + (curPage-2)*pageSize
|
||||
limit = (pageSize) + 1
|
||||
}
|
||||
return skip, limit
|
||||
}
|
||||
|
||||
// 排序
|
||||
func sortCmtList(ids []ObjectID, cmtList []cmtmod.Comment) (list []cmtmod.Comment) {
|
||||
var cMap = make(map[ObjectID]cmtmod.Comment)
|
||||
for _, v := range cmtList {
|
||||
cMap[v.ID] = v
|
||||
}
|
||||
for _, v := range ids {
|
||||
list = append(list, cMap[v])
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 评论发表限制
|
||||
func canPublishComment(uid uint64) (isCan bool, err error) {
|
||||
var LimitExpire = time.Second * time.Duration(appg.Conf.CmtLimit.CmtLimitExpire)
|
||||
var key = redisconst.AutoBlockKey(constant.FrequencyComment, uid)
|
||||
cnt := appg.Redis.Incr(key)
|
||||
if cnt == 1 {
|
||||
_, _ = appg.Redis.ExpireKey(key, LimitExpire)
|
||||
}
|
||||
|
||||
if cnt > appg.Conf.CmtLimit.CommentCount {
|
||||
// err = usermod.UpdateById(nil, uid, time.Now().Add(12*time.Hour))
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
return
|
||||
}
|
||||
//code := CheckCountLimit(uid)
|
||||
//if code != stderr.Success {
|
||||
// return
|
||||
//}
|
||||
isCan = true
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/cache/quicksearchdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// GetParentCmtList 获取父评论列表
|
||||
func GetParentCmtList(uid uint64, objID ObjectID, objType cmtmod.OType, curTime time.Time, page commod.Page) (code stderr.Code, data []cmtmod.ParentRespList, hasNext bool, err error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
code = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if user == nil || user.ID.IsZero() || user.HasBanned || user.HasLocked {
|
||||
code = stderr.UserIsNotExists
|
||||
return
|
||||
}
|
||||
var publisherID uint64
|
||||
if objType == cmtmod.OTypeCartoon || objType == cmtmod.OTypeDrama {
|
||||
media, mediaErr := mediamod.GetInfo(objID)
|
||||
if mediaErr != nil || media.ID.IsZero() || media.Status != 1 || media.IsDelete ||
|
||||
(objType == cmtmod.OTypeDrama && media.MediaType != mediamod.MediaTypeDrama) {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
err = mediaErr
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 获取视频作者
|
||||
videoInfo, videoErr := vidmod.GetVideoInfo(objID.Hex())
|
||||
if videoErr != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
err = videoErr
|
||||
return
|
||||
}
|
||||
publisherID = videoInfo.PublisherID
|
||||
}
|
||||
skip, limit := getNewPageInfo(0, int64(page.PageNumber), int64(page.PageSize))
|
||||
var (
|
||||
isGodList = make([]cmtmod.Comment, 0)
|
||||
aList = make([]cmtmod.Comment, 0)
|
||||
hLikeList = make([]cmtmod.Comment, 0)
|
||||
hReplyList = make([]cmtmod.Comment, 0)
|
||||
)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(3)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
// 获神评论
|
||||
isGodList, err = getIsGodCommentParentCmtList(user.IsAdvertiser, objID, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
// 获取作者的评论
|
||||
aList, err = getAuthorParentCmtList(user.IsAdvertiser, objID, publisherID, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
// 获取热门评论(回复数)
|
||||
hReplyList, err = getHotReplyParentCmeList(user.UID, user.IsAdvertiser, objID, 0, 3, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
noIds := []primitive.ObjectID{}
|
||||
for _, v := range hReplyList {
|
||||
noIds = append(noIds, v.ID)
|
||||
}
|
||||
// 获取热门评论(点赞数)
|
||||
hLikeList, err = getHotParentCmeList(user.UID, user.IsAdvertiser, noIds, objID, 0, 5, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
aList = append(isGodList, aList...) // 神评论+作者评论
|
||||
aList = append(aList, hLikeList...)
|
||||
aList = append(aList, hReplyList...)
|
||||
var mList []cmtmod.Comment
|
||||
if len(aList) > int(page.PageNumber*page.PageSize) {
|
||||
mList = aList[skip : skip+limit]
|
||||
} else if len(aList) > int((page.PageNumber-1)*page.PageSize) && len(aList) < int(page.PageNumber*page.PageSize) {
|
||||
mList = aList[skip:]
|
||||
}
|
||||
var aIds []ObjectID // 作者+热评的id
|
||||
for _, v := range aList {
|
||||
aIds = append(aIds, v.ID)
|
||||
}
|
||||
if aIds == nil {
|
||||
aIds = []ObjectID{}
|
||||
}
|
||||
if len(mList) <= int(page.PageSize) {
|
||||
skip, limit = getNewPageInfo(int64(len(aList)), int64(page.PageNumber), int64(page.PageSize))
|
||||
//获取普通评论
|
||||
cList, err := getParentCmtList(user.IsAdvertiser, objID, aIds, skip, limit, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return code, data, false, err
|
||||
}
|
||||
mList = append(mList, cList...)
|
||||
}
|
||||
list, err := getParentCommentTackInfo(uid, mList)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
var ids []ObjectID
|
||||
for _, v := range mList {
|
||||
ids = append(ids, v.ID)
|
||||
}
|
||||
// 获取首条子评论
|
||||
data, err = getFstChildCmtOfParent(user.IsAdvertiser, objID, list, ids, uid, publisherID, curTime)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
if len(data) > int(page.PageSize) {
|
||||
hasNext = true
|
||||
data = data[:page.PageSize]
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
func QuickSearch(objID string, data []cmtmod.ParentRespList) []cmtmod.ParentRespList {
|
||||
//获取置顶评论,走缓存
|
||||
quickSearches, err := quicksearchdata.GetAllFromCache(bson.M{"enabled": true, "type": 1})
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
count := len(quickSearches)
|
||||
if count > 0 {
|
||||
var (
|
||||
info = make([]cmtmod.ChildRespList, 0)
|
||||
s = rand.Intn(count)
|
||||
q = quickSearches[s]
|
||||
)
|
||||
cmts := []cmtmod.ParentRespList{
|
||||
cmtmod.ParentRespList{
|
||||
Info: info,
|
||||
ID: q.ID.Hex(),
|
||||
ObjID: objID,
|
||||
UserID: 100001,
|
||||
UserName: "91PORN运营",
|
||||
UserPortrait: "cf230705/image/cg/227/fg/1bw/a86065d7ab4cbb11ebbebaefc0ee4444.png",
|
||||
Content: q.Title,
|
||||
LinkStr: q.Link,
|
||||
LinkType: q.LinkType,
|
||||
SearchKeyword: q.SearchKeyword,
|
||||
Level: 1,
|
||||
Status: 1,
|
||||
City: "香港",
|
||||
CreatedAt: time.Now(),
|
||||
}}
|
||||
user, err := usermod.FindUserByUID(100001)
|
||||
if err == nil && user != nil {
|
||||
cmts[0].UserName = user.Name
|
||||
cmts[0].UserPortrait = user.Portrait
|
||||
}
|
||||
data = append(cmts, data...)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 获取作者的父评论及其数量
|
||||
func getAuthorParentCmtList(isAdvertiser bool, objID ObjectID, author uint64, curTime time.Time) (data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetAuthorParentCmt(isAdvertiser, objID, author, curTime)
|
||||
}
|
||||
|
||||
// 获取神评论及其数量
|
||||
func getIsGodCommentParentCmtList(isAdvertiser bool, objID ObjectID, curTime time.Time) (data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetIsGodCommentParentCmt(isAdvertiser, objID, curTime)
|
||||
}
|
||||
|
||||
// 获取热门评论(点赞数)
|
||||
func getHotParentCmeList(uid uint64, isAdvertiser bool, noIds []primitive.ObjectID, objID ObjectID, skip int64, limit int64, cutTime time.Time) (data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetHotParentCmtList(uid, isAdvertiser, noIds, objID, skip, limit, cutTime)
|
||||
}
|
||||
|
||||
// 获取热门评论(回复数)
|
||||
func getHotReplyParentCmeList(uid uint64, isAdvertiser bool, objID ObjectID, skip int64, limit int64, cutTime time.Time) (data []cmtmod.Comment, err error) {
|
||||
return cmtmod.GetHotReplyParentCmtList(uid, isAdvertiser, objID, skip, limit, cutTime)
|
||||
}
|
||||
|
||||
// 获取其余评论
|
||||
func getParentCmtList(isAdvertiser bool, objID ObjectID, ids []ObjectID, skip int64, limit int64, cutTime time.Time) (data []cmtmod.Comment, err error) {
|
||||
// 获取作者+热评
|
||||
return cmtmod.GetParentCmtList(isAdvertiser, objID, ids, skip, limit, cutTime)
|
||||
}
|
||||
|
||||
// GetMyCmtList 获取我的评论列表
|
||||
func GetMyCmtList(uid uint64, page commod.Page) (list []cmtmod.MyCmt, hasNext bool, err error) {
|
||||
skip := int64(page.Skip())
|
||||
list, hasNext, err = cmtmod.GetMyCmtList(uid, skip, int64(page.PageSize))
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("cmtmod.GetMyCmtList err:%v", err))
|
||||
return
|
||||
}
|
||||
if len(list) == 0 {
|
||||
list = []cmtmod.MyCmt{}
|
||||
return
|
||||
}
|
||||
vids := []primitive.ObjectID{}
|
||||
for _, v := range list {
|
||||
vids = append(vids, v.ObjID)
|
||||
}
|
||||
vidList, err := vidmod.GetVideoListByIDsNoStatus(vids)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("vidmod.GetVideoListByIDsNoStatus err:%v", err))
|
||||
return
|
||||
}
|
||||
videoMap := make(map[primitive.ObjectID]*vidmod.VideoModel)
|
||||
for _, v := range vidList {
|
||||
videoMap[v.ID] = v
|
||||
}
|
||||
for i, v := range list {
|
||||
// 从map中取出video对象
|
||||
vid, ok := videoMap[v.ObjID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
v.VidTitle = vid.Title
|
||||
v.VidCover = vid.Cover
|
||||
list[i] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/vidhelpser"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/aiplazamod"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// 发布评论后获取返回结果
|
||||
func getRespInfoAfterPublish(uid uint64, id ObjectID, level int) (code stderr.Code, data interface{}, err error) {
|
||||
var cmt cmtmod.Comment
|
||||
if cmt, err = cmtmod.CmtFindOneByID(id); err != nil {
|
||||
code = stderr.CommentErrGetDataFail
|
||||
return
|
||||
}
|
||||
if level == 1 {
|
||||
var info []cmtmod.ParentRespList
|
||||
if info, err = getParentCommentTackInfo(uid, []cmtmod.Comment{cmt}); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return
|
||||
}
|
||||
data = info[0]
|
||||
} else {
|
||||
var info []cmtmod.ChildRespList
|
||||
info, err = getChildCommentTackInfo(uid, []cmtmod.Comment{cmt})
|
||||
if code = stderr.CommentErrSendFail; err != nil {
|
||||
return
|
||||
}
|
||||
data = info[0]
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
func checkUserStatus(uid uint64) (code stderr.Code, data *usermod.User, err error) {
|
||||
// 获取用户状态
|
||||
data, err = usermod.FindUserByUID(uid)
|
||||
if err != nil || data == nil {
|
||||
code = stderr.CommentUserInfoErr
|
||||
return code, data, err
|
||||
}
|
||||
if data.HasBanned {
|
||||
code = stderr.CommentUserHasBanned
|
||||
return
|
||||
}
|
||||
|
||||
if data.BannedTime.After(time.Now()) {
|
||||
code = stderr.CommentUserHasBanned
|
||||
return
|
||||
}
|
||||
// if data.Mobile == "" {
|
||||
// code = stderr.CommentUserNotBind
|
||||
// return code, data, nil
|
||||
// }
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// 检查视频状态
|
||||
func checkVideoStatus(videoID string) (code stderr.Code, videoInfo vidmod.VideoModel, err error) {
|
||||
videoInfo, err = vidmod.GetVideoInfo(videoID)
|
||||
if err != nil {
|
||||
code = stderr.CommentVideoInfoErr
|
||||
return code, videoInfo, err
|
||||
}
|
||||
if videoInfo.ID.IsZero() {
|
||||
code = stderr.CommentErrVideoIsInvalid
|
||||
return code, videoInfo, err
|
||||
}
|
||||
// 视频未通过审核/未审核
|
||||
if videoInfo.Status != 1 && videoInfo.Status != 3 {
|
||||
code = stderr.CommentErrVideoIsInvalid
|
||||
return code, videoInfo, err
|
||||
}
|
||||
code = stderr.Success
|
||||
return code, videoInfo, err
|
||||
}
|
||||
|
||||
// 获取相关信息
|
||||
func getTrackInfo(uid uint64, publisher uint64, isAuthor bool, ip string, param cmtmod.PublishReqInfo, cmtType cmtmod.CmtType) (code stderr.Code, data cmtmod.Comment, err error) {
|
||||
//var waitGroup sync.WaitGroup
|
||||
var city = "-"
|
||||
//var replacedContent string
|
||||
var replacedContent = param.Content
|
||||
var cid ObjectID
|
||||
var err1 error
|
||||
if param.CID != "" {
|
||||
if cid, err = primitive.ObjectIDFromHex(param.CID); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
}
|
||||
//waitGroup.Add(1)
|
||||
//// 过滤评论
|
||||
//common.Go(func() {
|
||||
// defer waitGroup.Done()
|
||||
// replacedContent, err1 = filterser.TextFilter(param.Content)
|
||||
//})
|
||||
|
||||
// 获取城市城市
|
||||
// common.Go(func() {
|
||||
// defer waitGroup.Done()
|
||||
// city = vidhelpser.GetLocationByIP(ip)
|
||||
// })
|
||||
//waitGroup.Wait()
|
||||
if err = err1; err1 != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return
|
||||
}
|
||||
isRobot := false
|
||||
if uid < constant.RobotUIDLimit {
|
||||
isRobot = true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
data = cmtmod.Comment{
|
||||
ObjType: param.ObjType,
|
||||
ObjID: param.ObjID,
|
||||
CID: cid,
|
||||
PublisherID: publisher,
|
||||
UserID: uid,
|
||||
SortCnt: 0,
|
||||
ToUserID: param.ToUserID,
|
||||
Content: replacedContent,
|
||||
OrgContent: param.Content,
|
||||
Image: param.Image,
|
||||
Status: 2,
|
||||
Level: param.Level,
|
||||
AuthorMark: isAuthor,
|
||||
IsAuthor: isAuthor,
|
||||
IsDelete: false,
|
||||
CommentCount: 0,
|
||||
LikeCount: 0,
|
||||
IPAddr: ip,
|
||||
City: city,
|
||||
IsRobot: isRobot,
|
||||
QuoteType: param.QuoteType,
|
||||
QuoteID: param.QuoteID,
|
||||
QuoteImg: param.QuoteImg,
|
||||
QuoteTitle: param.QuoteTitle,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if param.RID != "" {
|
||||
rid, err := primitive.ObjectIDFromHex(param.RID)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
rCmt, err := cmtmod.CmtFindOneByID(rid)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
user, err := usermod.FindUserByUID(rCmt.UserID)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
data.RID = rid
|
||||
data.GID = rCmt.GID
|
||||
data.AuthorMark = rCmt.AuthorMark
|
||||
data.SortCnt = rCmt.SortCnt
|
||||
data.Type = cmtmod.CmtTypeReply
|
||||
data.PublisherID = rCmt.UserID
|
||||
data.ToUserName = user.Name
|
||||
data.ToUserID = rCmt.UserID
|
||||
data.CID = rCmt.CID
|
||||
} else if param.RID == "" && !cid.IsZero() {
|
||||
data.Type = cmtmod.CmtTypeComment
|
||||
cCmt, err := cmtmod.CmtFindOneByID(cid)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
data.PublisherID = cCmt.UserID
|
||||
} else {
|
||||
data.PublisherID = publisher
|
||||
data.Type = cmtType
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// 设置分组id
|
||||
func setGid(cid string, rid string, id ObjectID) (err error) {
|
||||
if cid != "" && rid == "" {
|
||||
return cmtmod.SetGid(id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 增加评论数
|
||||
func incCommentCount(cid string, oType cmtmod.OType, objID ObjectID) (code stderr.Code, err error) {
|
||||
if cid != "" {
|
||||
var objCid ObjectID
|
||||
if objCid, err = primitive.ObjectIDFromHex(cid); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, err
|
||||
}
|
||||
// 根据cid获取评论信息
|
||||
if toComment, err := cmtmod.CmtFindOneByID(objCid); err != nil || (toComment.Level != 1 && toComment.Level != 2) {
|
||||
code = stderr.CommentErrSendFail
|
||||
log.Error("app service comment CmtFindOneByID err", log.E(err), log.Any("toComment level", toComment.Level))
|
||||
return code, err
|
||||
}
|
||||
if err = cmtmod.CmtIncreaseCommentCountByID(objCid); err != nil {
|
||||
code = stderr.CommentErrIncCountFailed
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
if oType == cmtmod.OTypeCartoon || oType == cmtmod.OTypeDrama {
|
||||
if err = mediamod.IncrCommentCountByID(objID, 1); err != nil {
|
||||
code = stderr.CommentErrIncCountFailed
|
||||
return
|
||||
}
|
||||
} else if oType == cmtmod.OTypeAiPlaza {
|
||||
aiplazamod.IncCommentCount(objID)
|
||||
} else {
|
||||
// 增加视频评论数
|
||||
if err = vidhelpser.IncCommentCount(objID); err != nil {
|
||||
code = stderr.CommentErrIncCountFailed
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否为vip,非vip用户只能发表5个字
|
||||
func checkVip(vipLevel int, content string) stderr.Code {
|
||||
var charCnt int
|
||||
for _, v := range content {
|
||||
if unicode.Is(unicode.Scripts["Han"], v) {
|
||||
charCnt++
|
||||
} else {
|
||||
charCnt++
|
||||
}
|
||||
if charCnt > 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if vipLevel == 0 && charCnt > 5 {
|
||||
return stderr.CommentNotVip
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func CheckCountLimit(uid uint64) stderr.Code {
|
||||
var key = redisconst.AutoBlockKey(constant.BlockComment, uid)
|
||||
cnt := appg.Redis.Incr(key)
|
||||
if cnt == 1 {
|
||||
_, _ = appg.Redis.ExpireKey(key, 12*time.Hour)
|
||||
}
|
||||
if cnt > appg.Conf.CmtLimit.CommentCount {
|
||||
err := usermod.UpdateById(nil, uid, time.Now().Add(12*time.Hour))
|
||||
if err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
return stderr.CommentCountOverLimit
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/messageser"
|
||||
"91porn-server/app/service/taskser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/common/truthutil"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"91porn-server/models/v/dailytaskmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/messagemod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// GetVideoTotalComments 获取视频的评论数
|
||||
func GetVideoTotalComments(objID primitive.ObjectID) (code stderr.Code, data int64, err error) {
|
||||
return GetTotalComments(objID, cmtmod.OTypeVideo)
|
||||
}
|
||||
|
||||
func GetTotalComments(objID primitive.ObjectID, objType cmtmod.OType) (code stderr.Code, data int64, err error) {
|
||||
if objType == "" {
|
||||
objType = cmtmod.OTypeVideo
|
||||
}
|
||||
params := bson.M{
|
||||
"objID": objID,
|
||||
"objType": objType,
|
||||
"level": 1,
|
||||
"status": 1,
|
||||
"isDelete": false,
|
||||
"isAdvertiser": bson.M{"$in": []any{nil, false}},
|
||||
}
|
||||
data, err = cmtmod.GetCount(params)
|
||||
if err != nil {
|
||||
code = stderr.ErrNetWorkBusy
|
||||
return code, 0, err
|
||||
}
|
||||
code = stderr.Success
|
||||
return code, data, err
|
||||
}
|
||||
|
||||
// 获取二级评论列表
|
||||
func GetSndCommentsList(uid uint64, objID primitive.ObjectID, cid primitive.ObjectID, fstID primitive.ObjectID, curTime time.Time, page commod.Page) (code stderr.Code, data []cmtmod.ChildRespList, err error) {
|
||||
// 获取评论信息
|
||||
stdQuery := commod.StdQuery{Page: &commod.PageBy{Num: page.PageNumber, Size: page.PageSize, CheckNext: true}, Order: &[]commod.OrderBy{}}
|
||||
modList, err := cmtmod.CmtFindSecondLevelComments(objID, cid, fstID, curTime, stdQuery)
|
||||
if err != nil {
|
||||
code = stderr.ErrNetWorkBusy
|
||||
log.Error("app service comment GetSndCommentsList CmtFindSecondLevelComments err", log.E(err))
|
||||
return
|
||||
}
|
||||
// 空值直接返回
|
||||
if modList == nil || len(modList) <= 0 {
|
||||
code = stderr.Success
|
||||
data = []cmtmod.ChildRespList{}
|
||||
return
|
||||
}
|
||||
// 获取评论附加信息
|
||||
cList, err := getChildCommentTackInfo(uid, modList)
|
||||
if err != nil {
|
||||
code = stderr.ErrNetWorkBusy
|
||||
log.Error("app service comment GetSndCommentsList getSndCommentTackInfo err", log.E(err))
|
||||
return
|
||||
}
|
||||
code = stderr.Success
|
||||
data = cList
|
||||
return
|
||||
}
|
||||
|
||||
// PublishComment 发表评论
|
||||
func PublishComment(uid uint64, ua ua.UA, ip string, param cmtmod.PublishReqInfo) (code stderr.Code, data interface{}, err error) {
|
||||
// 获取用户状态
|
||||
var user, publisher *usermod.User
|
||||
if code, user, err = checkUserStatus(uid); err != nil || code != stderr.Success || user == nil {
|
||||
return code, data, err
|
||||
}
|
||||
|
||||
// 评论限制
|
||||
if isCan, err := canPublishComment(uid); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
} else if !isCan {
|
||||
code = stderr.CommentPublishTooFrequent
|
||||
return code, data, err
|
||||
}
|
||||
|
||||
//检测评论是否包含网址,包含网址则发布不予处理
|
||||
if uid > 200000 && !truthutil.CheckIsValid(param.Content, 1) {
|
||||
log.Warn("comment send CheckIsValid fail", log.Any("uid", uid), log.Any("content", param.Content))
|
||||
return stderr.Success, stderr.Success.Msg(), nil
|
||||
}
|
||||
|
||||
// 校验视频是否通过审核
|
||||
var videoInfo vidmod.VideoModel
|
||||
var mediaInfo mediamod.Media
|
||||
|
||||
var isAuthor bool
|
||||
var isOneself bool
|
||||
if param.ObjType == "" || param.ObjType == cmtmod.OTypeVideo {
|
||||
if code, videoInfo, err = checkVideoStatus(param.ObjID.Hex()); err != nil || code != stderr.Success || videoInfo.ID.IsZero() {
|
||||
return code, data, err
|
||||
}
|
||||
|
||||
// 获取视频作者信息
|
||||
if publisher, err = usermod.FindUserByUID(videoInfo.PublisherID); err != nil {
|
||||
log.Error("获取评论视频作者信息", log.Any("uid", uid), log.Any("ip", ip), log.Any("param", param), log.E(err))
|
||||
return stderr.CommentErrSendFail, nil, err
|
||||
}
|
||||
|
||||
if publisher == nil || publisher.ID.IsZero() {
|
||||
return stderr.UserIsNotExists, nil, err
|
||||
}
|
||||
if uid == videoInfo.PublisherID {
|
||||
isAuthor = true
|
||||
}
|
||||
}
|
||||
if param.ObjType == cmtmod.OTypeCartoon || param.ObjType == cmtmod.OTypeDrama {
|
||||
mediaInfo, err = mediamod.GetInfo(param.ObjID)
|
||||
if err != nil || mediaInfo.ID.IsZero() || mediaInfo.IsDelete || mediaInfo.Status != 1 {
|
||||
return stderr.CommentErrVideoIsInvalid, nil, err
|
||||
}
|
||||
if param.ObjType == cmtmod.OTypeDrama && mediaInfo.MediaType != mediamod.MediaTypeDrama {
|
||||
return stderr.CommentErrVideoIsInvalid, nil, errors.New("invalid drama")
|
||||
}
|
||||
}
|
||||
|
||||
var cmtMod cmtmod.Comment
|
||||
if code, cmtMod, err = getTrackInfo(uid, videoInfo.PublisherID, isAuthor, ip, param, cmtmod.CmtTypeVideo); code != stderr.Success || err != nil {
|
||||
return code, data, err
|
||||
}
|
||||
cmtMod.IsAdvertiser = user.IsAdvertiser
|
||||
if !mediaInfo.ID.IsZero() {
|
||||
cmtMod.Title = mediaInfo.Title
|
||||
}
|
||||
var sUid uint64
|
||||
if param.CID != "" {
|
||||
// 根据回复ID获取用户信息
|
||||
oid, _ := primitive.ObjectIDFromHex(param.CID)
|
||||
cmt, err := cmtmod.CmtFindOneByID(oid)
|
||||
if err != nil {
|
||||
code = stderr.ErrDbQueryError
|
||||
return code, data, err
|
||||
}
|
||||
// 校验是否是回复自己的评论
|
||||
if cmt.UserID != uid {
|
||||
sUid = cmt.UserID
|
||||
}
|
||||
if cmt.UserID == uid {
|
||||
isOneself = true
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
id, err := cmtmod.CmtInsertOne(&cmtMod)
|
||||
fmt.Println(id.Hex())
|
||||
if err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
if err = setGid(param.CID, param.RID, id); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
// 推荐互动不按用户/账号身份过滤;运营后台直接配置的 fake* 展示值
|
||||
// 不经过该真实评论落库链路,因此仍不会进入推荐累计分。
|
||||
if param.ObjType == "" || param.ObjType == cmtmod.OTypeVideo {
|
||||
if incErr := vidmod.IncrementRecommendInteraction(param.ObjID, vidmod.RecommendInteractionComment); incErr != nil {
|
||||
// 评论已落库,推荐累计失败只记录,不能向客户端返回发布失败。
|
||||
log.Error("increment recommend comment failed", log.E(incErr), log.Any("objID", param.ObjID))
|
||||
}
|
||||
}
|
||||
if param.ObjType == cmtmod.OTypeDrama {
|
||||
if incErr := mediamod.IncrCommentCountByID(param.ObjID, 1); incErr != nil {
|
||||
log.Error("increment drama comment count failed", log.E(incErr), log.Any("objID", param.ObjID))
|
||||
}
|
||||
}
|
||||
// 增加评论数(审核只统计已审核通过的评论)
|
||||
//if code, err = incCommentCount(param.CID, param.ObjType, param.ObjID); code != stderr.Success || err != nil {
|
||||
// return code, data, err
|
||||
//}
|
||||
//存动态
|
||||
common.Go(func() {
|
||||
if param.Content != "" && utf8.RuneCountInString(param.Content) >= 10 {
|
||||
_ = taskser.CompleteDailyTask(nil, uid, dailytaskmod.DailyComment)
|
||||
}
|
||||
|
||||
if !isAuthor && videoInfo.Title != "" {
|
||||
dynamicContent := fmt.Sprintf("%s 于 %s 评论了你的作品", user.Name, time.Now().Format("15:04:05"))
|
||||
messageser.PublicAddMsg(messagemod.PublicAddMsg{
|
||||
SendUid: uid,
|
||||
TakeUid: videoInfo.PublisherID,
|
||||
MsgType: messagemod.CommentMsg,
|
||||
ObjId: videoInfo.ID,
|
||||
ObjName: videoInfo.Title,
|
||||
ObjCover: videoInfo.Cover,
|
||||
Content: dynamicContent, //param.Content,
|
||||
})
|
||||
}
|
||||
if !isOneself && videoInfo.Title != "" && param.CID != "" && sUid != 0 {
|
||||
dynamicContent := fmt.Sprintf("%s 于 %s 回复了你的评论", user.Name, time.Now().Format("15:04:05"))
|
||||
messageser.PublicAddMsg(messagemod.PublicAddMsg{
|
||||
SendUid: uid,
|
||||
TakeUid: sUid,
|
||||
MsgType: messagemod.ReplyCommentMsg,
|
||||
ObjId: videoInfo.ID,
|
||||
ObjName: videoInfo.Title,
|
||||
ObjCover: videoInfo.Cover,
|
||||
Content: dynamicContent, //param.Content,
|
||||
})
|
||||
}
|
||||
})
|
||||
//if (user.VipLevel == 0 || user.VipExpireDate.Before(now)) && !user.GetSuperUser(now) {
|
||||
// if err = appg.Redis.Set(redisconst.CommentCacheKey(uid), data, 1*time.Minute); err != nil {
|
||||
// log.Warn(fmt.Sprintf("用户ID:%d;IP:%s;保存缓存数据异常:%v", uid, ip, err))
|
||||
// }
|
||||
//}
|
||||
|
||||
// 获取该评论
|
||||
return getRespInfoAfterPublish(uid, id, param.Level)
|
||||
}
|
||||
|
||||
//func DailyComment(uid uint64) {
|
||||
// key := redisconst.DailyTaskCacheKey(uid, dailytaskmod.DailyComment, timeutil.BeginOfToday())
|
||||
// if appg.Redis.Exists(key) {
|
||||
// return
|
||||
// }
|
||||
// tasks, err := dailytaskmod.GetUserDailyTaskByType(nil, dailytaskmod.DailyComment)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if tasks == nil || tasks.ID.IsZero() {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // 获取任务记录
|
||||
// today := timeutil.BeginningOfDay(time.Now())
|
||||
// item, err := taskrecordmod.GetUserRecordByTime(uid, tasks.ID, today)
|
||||
// if item == nil || item.ID.IsZero() {
|
||||
// now := time.Now()
|
||||
// // 更新今日登录记录
|
||||
// filter := bson.M{"uid": uid, "recordTime": today, "taskId": tasks.ID}
|
||||
// cond := bson.M{
|
||||
// "uid": uid,
|
||||
// "recordTime": today,
|
||||
// "taskId": tasks.ID,
|
||||
// "type": taskrecordmod.DailyTask,
|
||||
// "taskType": taskrecordmod.DailyComment,
|
||||
// "status": taskrecordmod.UnReceived,
|
||||
// "count": 1,
|
||||
// "updateTime": now,
|
||||
// "createTime": now,
|
||||
// }
|
||||
// if err := taskrecordmod.InsertTask(nil, filter, cond); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// // 判断每日评论只可三次
|
||||
// if item != nil && !item.ID.IsZero() && item.Count < 3 {
|
||||
// filter := bson.M{"uid": uid, "recordTime": today, "taskId": tasks.ID}
|
||||
// if err := taskrecordmod.CurrentCountUpdate(nil, filter); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// // 每日超过三次评论放入缓存
|
||||
// if item != nil && !item.ID.IsZero() && item.Count > 2 {
|
||||
// err = appg.Redis.Set(key, "_", time.Hour*24)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// return
|
||||
//}
|
||||
|
||||
// NoVidPublishComment 非视频帖子发表评论
|
||||
func NoVidPublishComment(uid uint64, ip, cmtType string, param cmtmod.PublishReqInfo) (code stderr.Code, data interface{}, err error) {
|
||||
var publisherID uint64
|
||||
// 获取用户状态
|
||||
var user *usermod.User
|
||||
if code, user, err = checkUserStatus(uid); err != nil || code != stderr.Success || user == nil {
|
||||
return code, data, err
|
||||
}
|
||||
|
||||
// 评论限制
|
||||
if isCan, err := canPublishComment(uid); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
} else if !isCan {
|
||||
code = stderr.CommentPublishTooFrequent
|
||||
return code, data, err
|
||||
}
|
||||
|
||||
//检测评论是否包含网址,包含网址则发布不予处理
|
||||
if uid > 200000 && !truthutil.CheckIsValid(param.Content, 1) {
|
||||
log.Warn("comment send CheckIsValid fail", log.Any("uid", uid), log.Any("content", param.Content))
|
||||
return stderr.Success, stderr.Success.Msg(), nil
|
||||
}
|
||||
|
||||
var isAuthor bool
|
||||
if uid == publisherID {
|
||||
isAuthor = true
|
||||
}
|
||||
var cmtMod cmtmod.Comment
|
||||
if code, cmtMod, err = getTrackInfo(uid, publisherID, isAuthor, ip, param, cmtmod.CmtNoVideo); code != stderr.Success || err != nil {
|
||||
return code, data, err
|
||||
}
|
||||
cmtMod.IsAdvertiser = user.IsAdvertiser
|
||||
// 保存到数据库
|
||||
id, err := cmtmod.CmtInsertOne(&cmtMod)
|
||||
if err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
if err = setGid(param.CID, param.RID, id); err != nil {
|
||||
code = stderr.CommentErrSendFail
|
||||
return code, data, err
|
||||
}
|
||||
common.Go(func() {
|
||||
// 增加评论数
|
||||
if param.Level == 2 {
|
||||
_id, err := primitive.ObjectIDFromHex(param.CID)
|
||||
if err != nil {
|
||||
log.Warn("NoVidPublishComment primitive.ObjectIDFromHex err,", log.Any("CID", param.CID), log.Any("ToUserID", param.ToUserID))
|
||||
}
|
||||
if err = cmtmod.CmtIncreaseCommentCountByID(_id); err != nil {
|
||||
log.Warn("NoVidPublishComment cmtmod.CmtIncreaseCommentCountByID err,", log.Any("err", err.Error()), log.Any("CID", param.CID), log.Any("ToUserID", param.ToUserID))
|
||||
}
|
||||
}
|
||||
})
|
||||
//存动态
|
||||
//common.Go(func() {
|
||||
// if objName != "" {
|
||||
//messageser.PublicAddMsg(messagemod.PublicAddMsg{
|
||||
// SendUid: uid,
|
||||
// TakeUid: publisherID,
|
||||
// MsgType: messagemod.DesireMsg,
|
||||
// ObjId: objID,
|
||||
// ObjName: objName,
|
||||
//})
|
||||
// }
|
||||
//})
|
||||
// 获取该评论
|
||||
return getRespInfoAfterPublish(uid, id, param.Level)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package commentser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/searcher"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VideoRes = searcher.VideoRes
|
||||
|
||||
type UserBInfo = usermod.BaseInfo
|
||||
|
||||
type Comment = cmtmod.Comment
|
||||
|
||||
type ReplyInfo struct {
|
||||
RUID uint64 `json:"rUID"` //回复者UID
|
||||
RName string `json:"rName"` //回复者名字
|
||||
RPortrait string `json:"rPortrait"` //回复者头像
|
||||
RContent string `json:"rContent"` //回复内容
|
||||
RTime time.Time `json:"rTime"` //回复时间
|
||||
RType cmtmod.CmtType `json:"rType"` //回复类型
|
||||
Video VideoRes `json:"video"` //视屏信息
|
||||
}
|
||||
|
||||
type ReplyPage struct {
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []ReplyInfo `json:"list"`
|
||||
}
|
||||
|
||||
func ReplyPages(uid uint64, skip, limit int64) (ReplyPage, error) {
|
||||
limitEx := limit + 1
|
||||
cmtList, err := cmtmod.ListByPublisherID(skip, limitEx, uid)
|
||||
if err != nil {
|
||||
return ReplyPage{}, err
|
||||
}
|
||||
hasNext := false
|
||||
if len(cmtList) > int(limit) {
|
||||
hasNext = true
|
||||
cmtList = cmtList[:limit]
|
||||
}
|
||||
vids := make([]ObjectID, 0, len(cmtList))
|
||||
uids := make([]uint64, 0, len(cmtList))
|
||||
for _, v := range cmtList {
|
||||
vids = append(vids, v.ObjID) //视屏ID
|
||||
uids = append(uids, v.UserID)
|
||||
}
|
||||
var (
|
||||
wg = sync.WaitGroup{}
|
||||
videoResList []VideoRes
|
||||
userInfos []*UserBInfo
|
||||
)
|
||||
wg.Add(2)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
list, err := searcher.GetVideoResList(uid, vids)
|
||||
if err != nil {
|
||||
log.Error("commentser GetVideoResList faild", log.E(err))
|
||||
}
|
||||
videoResList = list
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
list, err := usermod.GetUsersBaseInfo(uids)
|
||||
if err != nil {
|
||||
log.Error("commentser GetUsersBaseInfo faild", log.E(err))
|
||||
}
|
||||
userInfos = list
|
||||
})
|
||||
wg.Wait()
|
||||
videoResMap := make(map[ObjectID]VideoRes, len(videoResList))
|
||||
for _, v := range videoResList {
|
||||
vidp := v.VideoInfo.ID
|
||||
if vidp != nil {
|
||||
videoResMap[*vidp] = v
|
||||
}
|
||||
}
|
||||
userInfoMap := make(map[uint64]*usermod.BaseInfo, len(userInfos))
|
||||
for _, v := range userInfos {
|
||||
userInfoMap[v.UID] = v
|
||||
}
|
||||
replyInfos := toReplyInfoList(cmtList, videoResMap, userInfoMap)
|
||||
return ReplyPage{
|
||||
hasNext,
|
||||
replyInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toReplyInfoList(cmtList []Comment,
|
||||
videoResMap map[ObjectID]VideoRes,
|
||||
userInfoMap map[uint64]*usermod.BaseInfo,
|
||||
) []ReplyInfo {
|
||||
replyInfos := make([]ReplyInfo, 0, len(cmtList))
|
||||
for _, v := range cmtList {
|
||||
userInfo, ok := userInfoMap[v.UserID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
videoRes, ok := videoResMap[v.ObjID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
replyInfo := ReplyInfo{
|
||||
RUID: userInfo.UID,
|
||||
RName: userInfo.Name,
|
||||
RPortrait: userInfo.Portrait,
|
||||
RContent: v.Content,
|
||||
RType: v.Type,
|
||||
RTime: v.CreatedAt,
|
||||
Video: videoRes,
|
||||
}
|
||||
replyInfos = append(replyInfos, replyInfo)
|
||||
}
|
||||
return replyInfos
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/taskmod"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/service/productser"
|
||||
"91porn-server/app/service/taskser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/goldextramod"
|
||||
"91porn-server/models/v/oncetaskmod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// BuyGold 购买金币 payMoney单位为分
|
||||
func BuyGold(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, int64, error) {
|
||||
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, 0, 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)
|
||||
}
|
||||
w, err := walletmod.Credit(t, walletmod.CreditPlan{
|
||||
Amount: &gold,
|
||||
Consumption: &payMoney,
|
||||
}, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 完成一次性任务
|
||||
//if err = taskser.CompleteOnceTask(t, order.UID, oncetaskmod.OnceTaskTypeUserBuyCoin); err != nil {
|
||||
// fmt.Println("buy coin complete once task err", order.UID, err.Error())
|
||||
// return err
|
||||
//}
|
||||
|
||||
desc := "官方充值-到账" + strconv.FormatInt(gold, 10) + "金币"
|
||||
if goldExtra != 0 {
|
||||
desc = "官方充值-到账" + strconv.FormatInt(originGold, 10) + "金币, 消耗金币加购券增加" + strconv.FormatUint(goldExtra, 10) + "金币"
|
||||
}
|
||||
err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: order.ID,
|
||||
UID: order.UID,
|
||||
Amount: gold,
|
||||
ActualAmount: float64(gold),
|
||||
TranType: txnmod.OfficialRech.Key(),
|
||||
TranTypeInt: int64(txnmod.OfficialRech),
|
||||
ChannelType: order.RechargeType,
|
||||
Desc: desc,
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
common.Go(func() {
|
||||
// 完成任务
|
||||
_ = taskser.CompleteOnceTask(nil, order.UID, oncetaskmod.OnceTaskTypeUserBuyCoin)
|
||||
taskser.CompleteGrowthTask(order.UID, taskmod.GrowthBuyGold, w.Consumption)
|
||||
})
|
||||
return nil
|
||||
}, gold, nil
|
||||
}
|
||||
|
||||
// BuyFruitCoin 购买果币
|
||||
func BuyFruitCoin(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, error) {
|
||||
fruitCoin := payMoney / 100
|
||||
// 查询货币币配置
|
||||
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.GiveFruitCoin > 0 {
|
||||
fruitCoin += currency.GiveFruitCoin
|
||||
}
|
||||
}
|
||||
return func(t *db.MongoTool) error {
|
||||
// 增加果币与总充值金额
|
||||
w, err := walletmod.Credit(t, walletmod.CreditPlan{
|
||||
FruitCoin: &fruitCoin,
|
||||
Consumption: &payMoney,
|
||||
}, order.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: order.ID,
|
||||
UID: order.UID,
|
||||
FruitCoin: fruitCoin,
|
||||
TranType: txnmod.OfficialRech.Key(),
|
||||
TranTypeInt: int64(txnmod.OfficialRech),
|
||||
ChannelType: order.RechargeType,
|
||||
Desc: "官方充值-到账" + strconv.FormatInt(fruitCoin, 10) + "果币",
|
||||
DiscDoc: order.DiscDoc,
|
||||
SysType: order.DevType,
|
||||
FruitCoinBalance: w.FruitCoin,
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuyGameCoin 购买游戏币
|
||||
func BuyGameCoin(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, error) {
|
||||
var (
|
||||
currency currencymod.Currency
|
||||
u *usermod.User
|
||||
err error
|
||||
wg sync.WaitGroup
|
||||
creditPlan = walletmod.CreditPlan{Consumption: &payMoney}
|
||||
txnLogs = make([]txnmod.TransactionLog, 0)
|
||||
productId = order.ProductID.Hex()
|
||||
isFirstPay bool
|
||||
)
|
||||
wg.Add(2)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
// 查询货币信息
|
||||
currency, err = currencymod.Get(order.ProductID)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%s] 产品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))
|
||||
}
|
||||
})
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !u.HasFirstChargeVID {
|
||||
item, err := rchgordmod.FindOne(nil, bson.M{"uid": u.UID, "productType": 1, "status": rchgordmod.SUCCESS})
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("货币列表==>棋牌首次充值查询异常[%v]", err))
|
||||
}
|
||||
if item == nil {
|
||||
isFirstPay = true
|
||||
} else {
|
||||
payTrue := true
|
||||
_, err = usermod.UpdateTrans(nil, u.UID, usermod.UserSelector{HasFirstChargeWaLi: &payTrue})
|
||||
if err != nil {
|
||||
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 {
|
||||
// 赠送金币
|
||||
if currency.GiveGold > 0 {
|
||||
creditPlan.Amount = ¤cy.GiveGold
|
||||
}
|
||||
// 赠送果币
|
||||
if currency.GiveFruitCoin > 0 {
|
||||
creditPlan.FruitCoin = ¤cy.GiveFruitCoin
|
||||
}
|
||||
}
|
||||
}
|
||||
if u == nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%s] 用户ID[%d]不存在", order.ID, order.UID))
|
||||
return nil, fmt.Errorf("充值游戏币-订单号[%s] 用户ID[%d]不存在", order.ID, order.UID)
|
||||
}
|
||||
return func(t *db.MongoTool) error {
|
||||
// 赠送会员天数
|
||||
if currency.GiveVipDays > 0 || (currency.FirstGiveVipDays > 0 && isFirstPay) {
|
||||
level, expire := currency.HandlerGiveVip(u.VipLevel, u.VipExpireDate, isFirstPay)
|
||||
if err := usermod.VipInfoChange(t, order.UID, level, expire); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 会员卡变更异常[%v]",
|
||||
order.ID, order.ProductID, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 赠送楼凤解锁次数
|
||||
if currency.LouFengUnlockTimes > 0 {
|
||||
if err := usermod.LouFengUnlockTimesIncr(t, order.UID, currency.LouFengUnlockTimes); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 楼凤解锁次数变更异常[%v]",
|
||||
order.ID, order.ProductID, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 钱包变更
|
||||
w, err := walletmod.Credit(t, creditPlan, order.UID)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 钱包变更异常[%v]",
|
||||
order.ID, order.ProductID, err))
|
||||
return err
|
||||
}
|
||||
// 赠送金币记录
|
||||
if (payMoney+100-currency.Price) > 0 && currency.GiveGold > 0 {
|
||||
txnLogs = append(txnLogs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: u.UID,
|
||||
TranType: txnmod.CurrencyGive.Key(),
|
||||
TranTypeInt: int64(txnmod.CurrencyGive),
|
||||
Desc: fmt.Sprintf("官方充值赠送-到账%d金币", currency.GiveGold),
|
||||
DiscDoc: u.DiscDoc,
|
||||
SysType: u.SysType,
|
||||
Amount: currency.GiveGold,
|
||||
ActualAmount: float64(currency.GiveGold),
|
||||
ProductID: &productId,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
}
|
||||
// 赠送果币记录
|
||||
if (payMoney+100-currency.Price) > 0 && currency.GiveFruitCoin > 0 {
|
||||
txnLogs = append(txnLogs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: u.UID,
|
||||
TranType: txnmod.CurrencyGive.Key(),
|
||||
TranTypeInt: int64(txnmod.CurrencyGive),
|
||||
Desc: fmt.Sprintf("官方充值赠送-到账%d果币", currency.GiveFruitCoin),
|
||||
DiscDoc: u.DiscDoc,
|
||||
SysType: u.SysType,
|
||||
FruitCoin: currency.GiveFruitCoin,
|
||||
ProductID: &productId,
|
||||
FruitCoinBalance: w.FruitCoin,
|
||||
})
|
||||
}
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 记录资金流水记录异常[%v]",
|
||||
order.ID, order.ProductID, err))
|
||||
return err
|
||||
}
|
||||
if order.RechargeType == "usdt" {
|
||||
// 增加usdt可提现额度
|
||||
if err = walletmod.AddUsdtAmount(t, order.UID, payMoney); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值游戏币-订单号[%v] 产品ID[%v] 增加usdt可提现额度异常[%v]",
|
||||
order.ID, order.ProductID, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if isFirstPay {
|
||||
_, err = usermod.UpdateTrans(t, u.UID, usermod.UserSelector{HasFirstChargeWaLi: &isFirstPay})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuyProduct 购买商品
|
||||
func BuyProduct(ctx context.Context, order rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, int, time.Time, error) {
|
||||
var (
|
||||
creditPlan = walletmod.CreditPlan{Consumption: &payMoney}
|
||||
u *usermod.User
|
||||
p *productmod.Product
|
||||
err error
|
||||
wg sync.WaitGroup
|
||||
csm int
|
||||
w *walletmod.Wallet
|
||||
vipExpireTime time.Time
|
||||
)
|
||||
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))
|
||||
}
|
||||
|
||||
// 检查用户VIP升级
|
||||
productser.CheckUserUpgrade(order.UID, p)
|
||||
})
|
||||
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, 0, vipExpireTime, fmt.Errorf("充值购买会员卡-订单号[%v] 产品ID[%v]不存在", order.ID, order.ProductID)
|
||||
}
|
||||
// 正常分成
|
||||
if p.ChanSplitMod == 1 {
|
||||
csm = rchgordmod.ChanShareModVIP
|
||||
}
|
||||
if u == nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID))
|
||||
return nil, 0, vipExpireTime, fmt.Errorf("充值购买会员卡-订单号[%v] 用户ID[%d]不存在", order.ID, order.UID)
|
||||
}
|
||||
// 充值金额低于会员卡售价1元 转化为充值对应金额金币
|
||||
// 会员抵扣券抵扣后,期望实付 = 原价 - 实际抵扣额;否则折价支付会被误判为"低于售价"而全额转金币,导致VIP不发放
|
||||
expectedPrice := p.DiscountedPrice*10 - order.DeductAmount
|
||||
if expectedPrice < 0 {
|
||||
expectedPrice = 0
|
||||
}
|
||||
var (
|
||||
disPay = payMoney - expectedPrice
|
||||
fn func(t *db.MongoTool) error
|
||||
)
|
||||
if (p.ProductType != commod.AdvanceCard) && disPay < -100 {
|
||||
var (
|
||||
gold = payMoney / 10
|
||||
)
|
||||
creditPlan.Amount = &gold
|
||||
// 金币分成
|
||||
csm = rchgordmod.ChanShareCoin
|
||||
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.OfficialRech.Key(),
|
||||
TranTypeInt: int64(txnmod.OfficialRech),
|
||||
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
|
||||
}, csm, vipExpireTime, nil
|
||||
}
|
||||
// 购买商品处理
|
||||
switch p.ProductType {
|
||||
case commod.MeetingCard:
|
||||
fn = productser.NewBuyMeetingCard(order, p, "官方充值-购买[%s]", w)
|
||||
case commod.OTHER, commod.PhysicalGoods:
|
||||
fn = productser.NewBuyOtherCard(order, p, "官方充值-购买[%s]", w)
|
||||
case commod.VIP, commod.NEWUSERCard:
|
||||
if fn, vipExpireTime, err = productser.NewBuyVIP(order, p, "官方充值-购买[%s]", w); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
case commod.AdvanceCard:
|
||||
if err = handleAdvanceOrderPre(order, p, u); err != nil {
|
||||
log.ErrorX(ctx, fmt.Sprintf("官方充值-订单号[%s] 预处理预售卡异常[%v]", order.ID, err))
|
||||
}
|
||||
if fn, vipExpireTime, err = productser.BuyAdvanceVIP(order, p, "官方充值-购买[%s]", w, payMoney); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
case commod.GameAdvanceCard:
|
||||
if fn, vipExpireTime, err = productser.BuyGameAdvanceVIP(order, p, "官方充值-购买[%s]", w); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
case commod.VideoDiscount:
|
||||
if fn, err = productser.NewBuyVideoDiscountCard(order, p, "官方充值-购买[%s]", w); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
case commod.VideoFreeCard:
|
||||
if fn, err = productser.NewBuyVideoFreeCard(order, p, "官方充值-购买[%s]", w); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
case commod.CoinMonthCard:
|
||||
if fn, err = productser.NewBuyCoinMonthCard(order, p, "官方充值-购买[%s]", w); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
case commod.WhoringCard:
|
||||
if fn, err = productser.NewBuyWhoringCard(order, p, "官方充值-购买[%s]", w); err != nil {
|
||||
return nil, 0, vipExpireTime, err
|
||||
}
|
||||
default:
|
||||
return nil, 0, vipExpireTime, 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.OfficialRech.Key(),
|
||||
TranTypeInt: int64(txnmod.OfficialRech),
|
||||
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
|
||||
}
|
||||
if (p.ProductType == commod.VIP || p.ProductType == commod.NEWUSERCard || p.ProductType == commod.AdvanceCard) && p.Duration > 0 {
|
||||
common.Go(func() {
|
||||
fmt.Println("buyvip complete once task", u.UID)
|
||||
if err = taskser.CompleteOnceTask(t, order.UID, oncetaskmod.OnceTaskTypeUserBuyVip); err != nil {
|
||||
fmt.Println("buyvip complete once task err", u.UID, err.Error())
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}, csm, vipExpireTime, nil
|
||||
}
|
||||
|
||||
func handleAdvanceOrderPre(order rchgordmod.RechargeOrder, product *productmod.Product, u *usermod.User) error {
|
||||
data, err := advanceordermod.IsExist(bson.M{"productID": product.ID, "uid": u.UID})
|
||||
if err != nil {
|
||||
return errors.New("advance order query IsExist err")
|
||||
}
|
||||
|
||||
// 预付订单
|
||||
var advanceOrder advanceordermod.AdvanceOrder
|
||||
// 如果没有预售单,直接写入一笔
|
||||
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,
|
||||
}
|
||||
}
|
||||
// 创建预售订单
|
||||
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.AdvOid = order.ID.Hex()
|
||||
advanceOrder.PrepaidPrivilege = prepaidPrivilege
|
||||
advanceOrder.TodayUse = advanceordermod.DayUse{}
|
||||
_, err = advanceordermod.CreditOrder(nil, &advanceOrder)
|
||||
if err != nil {
|
||||
log.Error("handleAdvanceOrderPre 创建预售订单失败", log.Any("advanceOrder", advanceOrder), log.E(err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if data.Status != advanceordermod.AdvanceSUCCESS {
|
||||
return nil
|
||||
}
|
||||
data.Status = advanceordermod.BalanceProcessing
|
||||
err = advanceordermod.Update(nil, data.ID, bson.M{"status": advanceordermod.BalanceProcessing, "balOid": order.ID.Hex()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package contentmarkerser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/moduleconfmod"
|
||||
"91porn-server/models/v/tagmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type ModuleMarker struct {
|
||||
ModuleID string `json:"moduleId"`
|
||||
LatestAt *time.Time `json:"latestAt"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
HomeLatestAt *time.Time `json:"homeLatestAt"`
|
||||
Modules []ModuleMarker `json:"modules"`
|
||||
}
|
||||
|
||||
// MarkerCacheReader is the Redis JSON subset used to share marker snapshots.
|
||||
type MarkerCacheReader interface {
|
||||
GetWithJson2Any(key string, val any) error
|
||||
}
|
||||
|
||||
// MarkerCache adds atomic cache population to the marker snapshot reader.
|
||||
type MarkerCache interface {
|
||||
MarkerCacheReader
|
||||
SetNX(key string, value interface{}, expiration time.Duration) (bool, error)
|
||||
}
|
||||
|
||||
// GetCached returns the shared marker snapshot, loading and caching it on a miss.
|
||||
func GetCached(now time.Time, cache MarkerCache) (Response, error) {
|
||||
data, err := getCached(now, cache, Get)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
if latestAt, cacheHit := homeLatestAtFromCachedList(cache); cacheHit {
|
||||
data.HomeLatestAt = latestAt
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func getCached(now time.Time, cache MarkerCache, load func(time.Time) (Response, error)) (Response, error) {
|
||||
if data, ok := ReadCached(cache); ok {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
data, err := load(now)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
if cache != nil {
|
||||
payload, marshalErr := json.Marshal(data)
|
||||
if marshalErr != nil {
|
||||
log.Warn("marshal content update markers failed", log.E(marshalErr))
|
||||
return data, nil
|
||||
}
|
||||
stored, cacheErr := cache.SetNX(
|
||||
redisconst.ContentUpdateMarkersCache,
|
||||
string(payload),
|
||||
redisconst.ContentUpdateMarkersCacheExpire,
|
||||
)
|
||||
if cacheErr != nil {
|
||||
log.Warn("cache content update markers failed", log.E(cacheErr))
|
||||
} else if !stored {
|
||||
if current, ok := ReadCached(cache); ok {
|
||||
return current, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ReadCached returns only an existing marker snapshot and never queries MongoDB.
|
||||
func ReadCached(cache MarkerCacheReader) (Response, bool) {
|
||||
if cache == nil {
|
||||
return Response{}, false
|
||||
}
|
||||
var data Response
|
||||
if err := cache.GetWithJson2Any(redisconst.ContentUpdateMarkersCache, &data); err != nil {
|
||||
return Response{}, false
|
||||
}
|
||||
return data, true
|
||||
}
|
||||
|
||||
type cachedHomeLatestList struct {
|
||||
Videos []struct {
|
||||
ReviewAt time.Time `json:"reviewAt"`
|
||||
} `json:"videos"`
|
||||
}
|
||||
|
||||
const (
|
||||
homeLatestListSortType = 1
|
||||
homeLatestListPageNumber = 1
|
||||
homeLatestListPageSize = 10
|
||||
)
|
||||
|
||||
// homeLatestAtFromCachedList makes the "latest" red dot describe the same
|
||||
// first-page snapshot that the client can currently read from the list cache.
|
||||
func homeLatestAtFromCachedList(cache MarkerCacheReader) (*time.Time, bool) {
|
||||
if cache == nil {
|
||||
return nil, false
|
||||
}
|
||||
var data cachedHomeLatestList
|
||||
key := redisconst.GetMostNewModuleVideoListKey(
|
||||
homeLatestListSortType,
|
||||
homeLatestListPageNumber,
|
||||
homeLatestListPageSize,
|
||||
)
|
||||
if err := cache.GetWithJson2Any(key, &data); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if len(data.Videos) == 0 || data.Videos[0].ReviewAt.IsZero() {
|
||||
return nil, true
|
||||
}
|
||||
latestAt := data.Videos[0].ReviewAt
|
||||
return &latestAt, true
|
||||
}
|
||||
|
||||
func Get(now time.Time) (Response, error) {
|
||||
modules, err := moduleconfmod.GetAllActiveModule(now)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
excluded, err := moduleconfmod.ExcludedVideoModuleIDs(now, false)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
baseFilter := bson.M{"status": vidmod.CheckPass, "newsType": vidmod.SP}
|
||||
if len(excluded) > 0 {
|
||||
baseFilter["mId"] = bson.M{"$nin": excluded}
|
||||
}
|
||||
if darkTag, findErr := tagmod.FindOneTagByName("暗网"); findErr == nil && !darkTag.ID.IsZero() {
|
||||
baseFilter["tags"] = bson.M{"$ne": darkTag.ID}
|
||||
}
|
||||
|
||||
homeLatestAt, err := vidmod.LatestReviewAt(baseFilter)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
|
||||
moduleIDs := make([]string, 0, len(modules))
|
||||
moduleObjectIDs := make([]primitive.ObjectID, 0, len(modules))
|
||||
for _, module := range modules {
|
||||
moduleIDs = append(moduleIDs, module.ID.Hex())
|
||||
moduleObjectIDs = append(moduleObjectIDs, module.ID)
|
||||
}
|
||||
moduleTimes, err := vidmod.LatestReviewAtByModules(moduleIDs)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
mediaModuleTimes, err := mediamod.LatestContentAtByModules(moduleObjectIDs)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
markers := make([]ModuleMarker, 0, len(modules))
|
||||
for _, module := range modules {
|
||||
var latest *time.Time
|
||||
if value, ok := moduleTimes[module.ID.Hex()]; ok {
|
||||
valueCopy := value
|
||||
latest = &valueCopy
|
||||
}
|
||||
if value, ok := mediaModuleTimes[module.ID.Hex()]; ok && (latest == nil || value.After(*latest)) {
|
||||
valueCopy := value
|
||||
latest = &valueCopy
|
||||
}
|
||||
markers = append(markers, ModuleMarker{ModuleID: module.ID.Hex(), LatestAt: latest})
|
||||
}
|
||||
return Response{
|
||||
HomeLatestAt: homeLatestAt,
|
||||
Modules: markers,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package contentmarkerser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/constant/redisconst"
|
||||
)
|
||||
|
||||
type markerCacheStub struct {
|
||||
data Response
|
||||
getErr error
|
||||
setData Response
|
||||
setCalls int
|
||||
|
||||
latestListCached bool
|
||||
latestListReviewAt *time.Time
|
||||
}
|
||||
|
||||
type markerRaceCache struct {
|
||||
mu sync.Mutex
|
||||
payload string
|
||||
exists bool
|
||||
}
|
||||
|
||||
func (c *markerRaceCache) GetWithJson2Any(_ string, val any) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.exists {
|
||||
return errors.New("cache miss")
|
||||
}
|
||||
return json.Unmarshal([]byte(c.payload), val)
|
||||
}
|
||||
|
||||
func (c *markerRaceCache) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.exists {
|
||||
return false, nil
|
||||
}
|
||||
payload, ok := val.(string)
|
||||
if !ok {
|
||||
return false, errors.New("cache value is not JSON text")
|
||||
}
|
||||
c.payload = payload
|
||||
c.exists = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *markerCacheStub) GetWithJson2Any(key string, val any) error {
|
||||
if key == redisconst.GetMostNewModuleVideoListKey(
|
||||
homeLatestListSortType,
|
||||
homeLatestListPageNumber,
|
||||
homeLatestListPageSize,
|
||||
) {
|
||||
if !c.latestListCached {
|
||||
return errors.New("latest list cache miss")
|
||||
}
|
||||
data := cachedHomeLatestList{}
|
||||
if c.latestListReviewAt != nil {
|
||||
data.Videos = append(data.Videos, struct {
|
||||
ReviewAt time.Time `json:"reviewAt"`
|
||||
}{ReviewAt: *c.latestListReviewAt})
|
||||
}
|
||||
payload, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(payload, val)
|
||||
}
|
||||
if c.getErr != nil {
|
||||
return c.getErr
|
||||
}
|
||||
payload, err := json.Marshal(c.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(payload, val)
|
||||
}
|
||||
|
||||
func (c *markerCacheStub) SetNX(_ string, val interface{}, _ time.Duration) (bool, error) {
|
||||
c.setCalls++
|
||||
payload, ok := val.(string)
|
||||
if !ok {
|
||||
return false, errors.New("cache value is not JSON text")
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payload), &c.setData); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func TestResponseOmitsTodayLatestAt(t *testing.T) {
|
||||
payload, err := json.Marshal(Response{})
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal(Response{}) error = %v", err)
|
||||
}
|
||||
if bytes.Contains(payload, []byte("todayLatestAt")) {
|
||||
t.Fatalf("response still contains removed todayLatestAt field: %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCachedReturnsCachedMarkers(t *testing.T) {
|
||||
latest := time.Date(2026, 8, 4, 11, 30, 0, 123000000, time.FixedZone("CST", 8*60*60))
|
||||
cache := &markerCacheStub{data: Response{HomeLatestAt: &latest}}
|
||||
|
||||
got, err := GetCached(time.Now(), cache)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCached() error = %v", err)
|
||||
}
|
||||
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) {
|
||||
t.Fatalf("GetCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest)
|
||||
}
|
||||
if cache.setCalls != 0 {
|
||||
t.Fatalf("GetCached() cache writes = %d, want 0", cache.setCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCachedReportsMiss(t *testing.T) {
|
||||
cache := &markerCacheStub{getErr: errors.New("cache miss")}
|
||||
if _, ok := ReadCached(cache); ok {
|
||||
t.Fatal("ReadCached() hit = true, want false")
|
||||
}
|
||||
if _, ok := ReadCached(nil); ok {
|
||||
t.Fatal("ReadCached(nil) hit = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCachedUsesHomeLatestAtFromCachedList(t *testing.T) {
|
||||
markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
|
||||
listLatest := markerLatest.Add(-2 * time.Minute)
|
||||
cache := &markerCacheStub{
|
||||
data: Response{HomeLatestAt: &markerLatest},
|
||||
latestListCached: true,
|
||||
latestListReviewAt: &listLatest,
|
||||
}
|
||||
|
||||
got, err := GetCached(time.Now(), cache)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCached() error = %v", err)
|
||||
}
|
||||
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(listLatest) {
|
||||
t.Fatalf("GetCached() homeLatestAt = %v, want cached list time %v", got.HomeLatestAt, listLatest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCachedClearsHomeLatestAtForCachedEmptyList(t *testing.T) {
|
||||
markerLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
|
||||
cache := &markerCacheStub{
|
||||
data: Response{HomeLatestAt: &markerLatest},
|
||||
latestListCached: true,
|
||||
}
|
||||
|
||||
got, err := GetCached(time.Now(), cache)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCached() error = %v", err)
|
||||
}
|
||||
if got.HomeLatestAt != nil {
|
||||
t.Fatalf("GetCached() homeLatestAt = %v, want nil for cached empty list", got.HomeLatestAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCachedLoadsAndWritesCacheBeforeReturning(t *testing.T) {
|
||||
latest := time.Date(2026, 8, 4, 11, 31, 0, 0, time.UTC)
|
||||
cache := &markerCacheStub{getErr: errors.New("cache miss")}
|
||||
loadCalls := 0
|
||||
load := func(time.Time) (Response, error) {
|
||||
loadCalls++
|
||||
return Response{HomeLatestAt: &latest}, nil
|
||||
}
|
||||
|
||||
got, err := getCached(time.Now(), cache, load)
|
||||
if err != nil {
|
||||
t.Fatalf("getCached() error = %v", err)
|
||||
}
|
||||
if loadCalls != 1 {
|
||||
t.Fatalf("getCached() loader calls = %d, want 1", loadCalls)
|
||||
}
|
||||
if cache.setCalls != 1 || cache.setData.HomeLatestAt == nil {
|
||||
t.Fatalf("getCached() cache write = (%d, %v), want one marker write", cache.setCalls, cache.setData.HomeLatestAt)
|
||||
}
|
||||
if got.HomeLatestAt == nil || !got.HomeLatestAt.Equal(latest) {
|
||||
t.Fatalf("getCached() homeLatestAt = %v, want %v", got.HomeLatestAt, latest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCachedConcurrentMissReturnsWinningSnapshot(t *testing.T) {
|
||||
oldLatest := time.Date(2026, 8, 4, 11, 30, 0, 0, time.UTC)
|
||||
newLatest := oldLatest.Add(time.Minute)
|
||||
cache := &markerRaceCache{}
|
||||
oldLoaderStarted := make(chan struct{})
|
||||
releaseOldLoader := make(chan struct{})
|
||||
type outcome struct {
|
||||
data Response
|
||||
err error
|
||||
}
|
||||
oldOutcome := make(chan outcome, 1)
|
||||
|
||||
go func() {
|
||||
data, err := getCached(time.Now(), cache, func(time.Time) (Response, error) {
|
||||
close(oldLoaderStarted)
|
||||
<-releaseOldLoader
|
||||
return Response{HomeLatestAt: &oldLatest}, nil
|
||||
})
|
||||
oldOutcome <- outcome{data: data, err: err}
|
||||
}()
|
||||
|
||||
<-oldLoaderStarted
|
||||
newData, err := getCached(time.Now(), cache, func(time.Time) (Response, error) {
|
||||
return Response{HomeLatestAt: &newLatest}, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("new getCached() error = %v", err)
|
||||
}
|
||||
close(releaseOldLoader)
|
||||
oldResult := <-oldOutcome
|
||||
if oldResult.err != nil {
|
||||
t.Fatalf("old getCached() error = %v", oldResult.err)
|
||||
}
|
||||
for name, data := range map[string]Response{"new": newData, "old": oldResult.data} {
|
||||
if data.HomeLatestAt == nil || !data.HomeLatestAt.Equal(newLatest) {
|
||||
t.Fatalf("%s getCached() homeLatestAt = %v, want winning %v", name, data.HomeLatestAt, newLatest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
package couponser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/backpackmod"
|
||||
"91porn-server/models/v/coupon_record_mod"
|
||||
"91porn-server/models/v/prize_record_mod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"91porn-server/web/service/userser"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// List 优惠券列表
|
||||
func List(uid uint64, in *coupon_record_mod.AppListReq) (*coupon_record_mod.QueryAllRes, error) {
|
||||
var out = &coupon_record_mod.QueryAllRes{}
|
||||
count, err := coupon_record_mod.QueryAllCount(in.Filter(uid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
out.List = []*coupon_record_mod.AppCouponRecordRes{}
|
||||
out.Total = 0
|
||||
return out, nil
|
||||
}
|
||||
out.Total = count
|
||||
out.List, err = coupon_record_mod.QueryAllList(in.Filter(uid), in.Options())
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("coupon_record_mod.QueryAllList err:%v", err), log.Any("uid", uid), log.Any("params", in))
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Upload 上传优惠券列表
|
||||
func Upload(uid uint64, in *prize_record_mod.AppUploadReq) error {
|
||||
var (
|
||||
prizes []*prize_record_mod.Prize
|
||||
transactionLogs []txnmod.TransactionLog
|
||||
)
|
||||
// 获取用户信息
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
|
||||
prizes = in.Prizes
|
||||
if len(prizes) == 0 {
|
||||
return stderr.PrizeRecordNoExists
|
||||
}
|
||||
|
||||
// 优先使用预售卡权益免费次数
|
||||
hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid)
|
||||
if hasPrivilege {
|
||||
// 获取可用免费次数
|
||||
validCount := advance.PrepaidPrivilege.LuckyDrawLimitPerDay - advance.TodayUse.LuckyDrawCount
|
||||
if validCount >= in.Count {
|
||||
// 扣除免费次数
|
||||
debitPlan := advanceordermod.DebitPlan{}
|
||||
debitPlan.LuckyDrawCount = &in.Count
|
||||
if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
in.Count = 0
|
||||
} else if validCount > 0 {
|
||||
// 扣除免费次数
|
||||
debitPlan := advanceordermod.DebitPlan{}
|
||||
debitPlan.LuckyDrawCount = &validCount
|
||||
if err := advanceordermod.Debit(nil, uid, debitPlan); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
in.Count -= validCount
|
||||
}
|
||||
}
|
||||
wal, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
totalMoney := wal.Amount + wal.Income
|
||||
if totalMoney <= 0 && wal.LotteryTimes < in.Count {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;用户余额不足,当前剩余次数:%v;抽奖扣除次数:%v", uid, wal.LotteryTimes, in.Count))
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
|
||||
if in.Gold > 0 && in.Gold > totalMoney {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;用户余额不足,当前剩余总余额:%v;抽奖扣除金额:%v", uid, totalMoney, in.Gold))
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
|
||||
if wal.LotteryTimes < in.Count && in.Gold == 0 {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;用户余额不足,当前剩余次数:%v;抽奖扣除次数:%v", uid, wal.LotteryTimes, in.Count))
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
if in.Count != 0 || in.Gold != 0 {
|
||||
var creditPlan = walletmod.CreditPlan{}
|
||||
// 处理扣钱
|
||||
if in.Count > 0 && in.Gold == 0 {
|
||||
inc := -in.Count
|
||||
creditPlan.LotteryTimes = &inc
|
||||
}
|
||||
if in.Gold != 0 {
|
||||
desc := -in.Gold
|
||||
if wal.Amount < in.Gold {
|
||||
descAmt := -wal.Amount
|
||||
creditPlan.Amount = &descAmt
|
||||
descInc := -(in.Gold - wal.Amount)
|
||||
creditPlan.Income = &descInc
|
||||
log.Info(fmt.Sprintf("用户ID:%d;抽奖进入这里扣出income:%v;amount:%v", uid, descInc, descAmt))
|
||||
} else if wal.Amount >= in.Gold {
|
||||
creditPlan.Amount = &desc
|
||||
log.Info(fmt.Sprintf("用户ID:%d;抽奖进入这里扣出;amount:%v", uid, in.Gold))
|
||||
}
|
||||
}
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
// 增加金币与总充值金额
|
||||
wal, err = walletmod.DescCredit(t, creditPlan, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Gold > 0 {
|
||||
out := txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
Amount: in.Gold,
|
||||
ActualAmount: -float64(in.Gold),
|
||||
TranType: txnmod.RaffleDeduction.Key(),
|
||||
TranTypeInt: int64(txnmod.RaffleDeduction),
|
||||
Desc: fmt.Sprintf("%s-金币[%d个]", txnmod.RaffleDeduction.Key(), in.Gold),
|
||||
RealAmount: wal.RealAmount(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
transactionLogs = append(transactionLogs, out)
|
||||
}
|
||||
if len(transactionLogs) > 0 {
|
||||
err = txnmod.InsertManyTransactionLog(t, transactionLogs)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发资金流水日志插入异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return stderr.ErrDbTransError
|
||||
}
|
||||
}
|
||||
common.Go(func() {
|
||||
_ = HandlePrize(uid, in, u, wal)
|
||||
})
|
||||
log.Info(fmt.Sprintf("用户ID:%d;业务处理完成", uid))
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandlePrize(uid uint64, in *prize_record_mod.AppUploadReq, u *usermod.User, wal *walletmod.Wallet) error {
|
||||
var (
|
||||
prizes []*prize_record_mod.Prize
|
||||
transactionLogs []txnmod.TransactionLog
|
||||
couponRecords []coupon_record_mod.CouponRecord
|
||||
goodsList []backpackmod.Backpack
|
||||
totalInc int64
|
||||
totalAIUndressInc int64
|
||||
err error
|
||||
)
|
||||
prizes = in.Prizes
|
||||
vipStatus := false
|
||||
// 处理同类型
|
||||
for _, p := range prizes {
|
||||
log.Info(fmt.Sprintf("--------------开始任务:%+v", p))
|
||||
|
||||
if p.PrizeType == prize_record_mod.PrizeTypeVIPCard {
|
||||
id, _ := primitive.ObjectIDFromHex(p.Param1)
|
||||
// 查询会员卡详情
|
||||
productDetail, err := productmod.FindProduct(id, "android")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
vipExpire, vipLevel, payVidDiscount := prize_record_mod.CheckVipRenew(u, productDetail)
|
||||
u.VipExpireDate = vipExpire
|
||||
u.VipLevel = vipLevel
|
||||
u.PayVidDiscount = payVidDiscount
|
||||
vipStatus = true
|
||||
}
|
||||
if p.PrizeType == prize_record_mod.PrizeTypeVIP {
|
||||
if u.VipLevel == 0 {
|
||||
u.VipLevel = 1
|
||||
}
|
||||
vipExpire, vipLevel := userser.CheckVipRenew(u, int(p.Price), u.VipLevel)
|
||||
u.VipExpireDate = vipExpire
|
||||
u.VipLevel = vipLevel
|
||||
vipStatus = true
|
||||
}
|
||||
if p.PrizeType == prize_record_mod.PrizeTypeGold {
|
||||
totalInc += p.Price
|
||||
transactionLogs = append(transactionLogs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: u.UID,
|
||||
Amount: p.Price,
|
||||
ActualAmount: float64(p.Price),
|
||||
TranType: txnmod.PrizeRecord.Key(),
|
||||
TranTypeInt: int64(txnmod.PrizeRecord),
|
||||
Desc: fmt.Sprintf("%s-金币[%d个]", txnmod.PrizeRecord.Key(), p.Price),
|
||||
RealAmount: wal.RealAmount().Add(decimal.NewFromInt(totalInc)),
|
||||
})
|
||||
}
|
||||
if p.PrizeType == prize_record_mod.PrizeTypeGoldCoinBonus {
|
||||
cId := prize_record_mod.HandleCId(p.ID)
|
||||
// 获取观影券是否存在
|
||||
c, err := coupon_record_mod.FindOneByCId(cId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !c.ID.IsZero() {
|
||||
return stderr.New(400, "coupon_record is exist")
|
||||
}
|
||||
|
||||
// 保存用户优惠券
|
||||
now := time.Now()
|
||||
newCo := coupon_record_mod.CouponRecord{
|
||||
CID: cId,
|
||||
PID: p.ID,
|
||||
UID: u.UID,
|
||||
UserName: u.Name,
|
||||
Name: p.Name,
|
||||
Count: p.Count,
|
||||
Price: p.Price,
|
||||
Type: coupon_record_mod.PrizeTypeGoldCoinBonus,
|
||||
Used: false,
|
||||
IsDelete: false,
|
||||
ExpireTime: now.Add(time.Hour * 24 * time.Duration(p.ValidDate)),
|
||||
}
|
||||
couponRecords = append(couponRecords, newCo)
|
||||
}
|
||||
// AI脱衣
|
||||
if p.PrizeType == prize_record_mod.PrizeTypeAiUndress {
|
||||
totalAIUndressInc += p.Price
|
||||
aiLog := txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: p.Price,
|
||||
ActualAmount: float64(p.Price),
|
||||
TranType: txnmod.PrizeRecord.Key(),
|
||||
TranTypeInt: int64(txnmod.PrizeRecord),
|
||||
TransNo: primitive.NewObjectID(),
|
||||
Desc: fmt.Sprintf("%s-赠送AI脱衣免费次数[%v次]", txnmod.PrizeRecord.Key(), p.Price),
|
||||
DiscDoc: u.DiscDoc,
|
||||
SysType: u.SysType,
|
||||
}
|
||||
transactionLogs = append(transactionLogs, aiLog)
|
||||
}
|
||||
// AI金币抵扣券
|
||||
if p.PrizeType == prize_record_mod.PrizeTypeGoldDiscountCoupon {
|
||||
nowTime := time.Now()
|
||||
goodsList = append(goodsList, backpackmod.Backpack{
|
||||
UID: uid,
|
||||
GoodsName: p.Name,
|
||||
GoodsType: backpackmod.AiChangeFaceDiscount,
|
||||
GoodsValue: p.Price,
|
||||
GoodsOrigin: "抽奖获得AI金币抵扣券",
|
||||
Status: backpackmod.Unused,
|
||||
ExpiredTime: nowTime.Add(time.Hour * 24 * time.Duration(p.ValidDate)),
|
||||
CreateTime: nowTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
set := usermod.UserSelector{VipExpireDate: &u.VipExpireDate, VipLevel: &u.VipLevel, PayVidDiscount: &u.PayVidDiscount}
|
||||
|
||||
// 根据类型处理业务
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if vipStatus {
|
||||
if err = usermod.UpdateUserById(t, uid, set); err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发VIP更新异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(couponRecords) > 0 {
|
||||
err = coupon_record_mod.InsertManyCouponRecords(t, couponRecords)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发加赠券日志插入异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if totalInc > 0 {
|
||||
_, err = walletmod.CreditAmount(t, totalInc, uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发金币流水异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(transactionLogs) > 0 {
|
||||
err = txnmod.InsertManyTransactionLog(t, transactionLogs)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发资金流水日志插入异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if totalAIUndressInc > 0 {
|
||||
_, err := walletmod.DebitAiFreeTimes(t, -totalAIUndressInc, uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发AI免费次数异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(goodsList) > 0 {
|
||||
if err := backpackmod.AddGoodsMany(t, uid, goodsList); err != nil {
|
||||
log.Error(fmt.Sprintf("用户ID:%d;奖品派发AI抵扣券插入异常:%v", uid, err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return stderr.ErrDbTransError
|
||||
}
|
||||
log.Info(fmt.Sprintf("用户ID:%d;抽奖派发成功", uid))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除优惠券列表
|
||||
func Delete(uid uint64, in *prize_record_mod.AppDeleteReq) error {
|
||||
// 获取用户信息
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
// 删除成功
|
||||
if err = coupon_record_mod.Remove(in.Filter()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("用户ID:%d;优惠券删除发成功", uid))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gain 获取用户信息
|
||||
func Gain(c *gin.Context, uid uint64) (data *coupon_record_mod.AppGainUserInfoRes, err error) {
|
||||
info := coupon_record_mod.AppGainUserInfoRes{UID: uid, LotteryTimes: 0}
|
||||
var conAmt int64 = 0
|
||||
// 获取用户信息
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
return nil, stderr.UserIsNotExists
|
||||
}
|
||||
|
||||
w, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return nil, stderr.ErrDbQueryError
|
||||
}
|
||||
if w.Consumption != 0 {
|
||||
//conAmt = w.Consumption / 10
|
||||
}
|
||||
|
||||
if w.LotteryTimes != 0 {
|
||||
info.LotteryTimes = w.LotteryTimes
|
||||
}
|
||||
// 检查预售权益免费次数
|
||||
hasPrivilege, advance := advanceordermod.GetUserValidOrder(uid)
|
||||
if hasPrivilege {
|
||||
// 获取可用免费次数
|
||||
validCount := advance.PrepaidPrivilege.LuckyDrawLimitPerDay - advance.TodayUse.LuckyDrawCount
|
||||
if validCount > 0 {
|
||||
info.LotteryTimes += validCount
|
||||
}
|
||||
}
|
||||
t := c.Request.Header.Get("authorization")
|
||||
sReq := coupon_record_mod.AppGainUserInfoReq{
|
||||
AppID: commod.KFK_APPID,
|
||||
Token: t,
|
||||
UserName: u.Name,
|
||||
UID: uid,
|
||||
TotalRecharge: conAmt,
|
||||
Balance: w.Amount + w.Income,
|
||||
}
|
||||
|
||||
ct, _ := json.Marshal(sReq)
|
||||
log.Info(fmt.Sprintf("Gain userInfo:%+v:", string(ct)))
|
||||
|
||||
aesKey := []byte("nU7cLOX7t3yJHq8yeIMCfO9emiOWtdlN")
|
||||
encrypt, err := AesPCK5Encrypt(ct, aesKey)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Gain userInfo aes err:%+v:", err))
|
||||
return nil, err
|
||||
}
|
||||
log.Info(fmt.Sprintf("Gain userInfo aes string:%+v:", encrypt))
|
||||
info.Data = *encrypt
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func PKCS5Padding(plaintext []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(plaintext)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(plaintext, padtext...)
|
||||
}
|
||||
|
||||
func PKCS5UnPadding(origData []byte) []byte {
|
||||
length := len(origData)
|
||||
unpadding := int(origData[length-1])
|
||||
return origData[:(length - unpadding)]
|
||||
}
|
||||
|
||||
// AesPCK5Encrypt 加密
|
||||
func AesPCK5Encrypt(origData, key []byte) (*string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockSize := block.BlockSize()
|
||||
origData = PKCS5Padding(origData, blockSize)
|
||||
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
|
||||
encrypted := make([]byte, len(origData))
|
||||
blockMode.CryptBlocks(encrypted, origData)
|
||||
enData := base64.StdEncoding.EncodeToString(encrypted)
|
||||
return &enData, nil
|
||||
}
|
||||
|
||||
// 解密
|
||||
func AesPCK5Decrypt(crypted, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
|
||||
origData := make([]byte, len(crypted))
|
||||
blockMode.CryptBlocks(origData, crypted)
|
||||
origData = PKCS5UnPadding(origData)
|
||||
return origData, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package currencyser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/currencymod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
func BuyGold(r rchgordmod.RechargeOrder, payMoney int64) (func(*db.MongoTool) error, error) {
|
||||
var gold = payMoney / 10
|
||||
// 查询金币配置
|
||||
currency, err := currencymod.Get(r.ProductID)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("充值金币-产品ID[%v]查询异常[%v]", r.ProductID, err))
|
||||
return nil, err
|
||||
}
|
||||
if currency.ID.IsZero() {
|
||||
log.Error(fmt.Sprintf("充值金币-产品ID[%v]不存在", r.ProductID))
|
||||
return nil, fmt.Errorf("充值金币-产品ID[%v]不存在", r.ProductID)
|
||||
}
|
||||
if currency.GiveGold > 0 {
|
||||
gold += currency.GiveGold
|
||||
}
|
||||
return func(t *db.MongoTool) error {
|
||||
w, err := walletmod.Credit(t, walletmod.CreditPlan{
|
||||
Amount: &gold,
|
||||
Consumption: &payMoney,
|
||||
}, r.UID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
TransNo: r.ID,
|
||||
UID: r.UID,
|
||||
FruitCoin: gold,
|
||||
TranType: txnmod.FruitCoinRecharge.Key(),
|
||||
TranTypeInt: int64(txnmod.FruitCoinRecharge),
|
||||
ChannelType: r.RechargeType,
|
||||
Desc: "官方充值-到账" + strconv.FormatInt(gold, 10) + "金币",
|
||||
DiscDoc: r.DiscDoc,
|
||||
SysType: r.DevType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package customerser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/l/visitlogmod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GetUrlResp struct {
|
||||
Code int `json:"code"` // 状态码
|
||||
Msg string `json:"msg"` // 状态描述
|
||||
Data struct {
|
||||
Url string `json:"url"`
|
||||
Params string `json:"params"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func GetUrl(uid uint64, ua ua.UA) (resp GetUrlResp, err error) {
|
||||
reqUrl := appg.Conf.Customer.Url + "/app/customer/user/getUrl"
|
||||
ul := url.Values{}
|
||||
ul.Add("userId", fmt.Sprintf("%v", uid))
|
||||
ul.Add("appId", appg.Conf.Customer.AppId)
|
||||
ul.Add("deviceType", ua.DevType)
|
||||
ul.Add("systemType", ua.SysType)
|
||||
ul.Add("systemVersion", ua.Ver)
|
||||
dcodeurl, _ := url.QueryUnescape(ul.Encode())
|
||||
sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Customer.Secret)
|
||||
if err != nil {
|
||||
log.Warn("GetUrl fail", log.E(err))
|
||||
return
|
||||
}
|
||||
reqUrl = reqUrl + "?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Customer.AppId
|
||||
//请求对应产品
|
||||
httpCode, err := httputil.DefaultClientGetWithResp(&resp, reqUrl, nil)
|
||||
if err != nil {
|
||||
log.Warn("GetUrl DefaultClientGetWithResp fail", log.Any("reqUrl", reqUrl), log.E(err))
|
||||
return
|
||||
}
|
||||
if httpCode != http.StatusOK {
|
||||
log.Warn("GetUrl DefaultClientGetWithResp fail httpCode != http.StatusOK ", log.Any("reqUrl", reqUrl), log.E(err))
|
||||
err = errors.New("获取失败")
|
||||
return
|
||||
}
|
||||
if resp.Code != 200 {
|
||||
log.Warn("GetUrl DefaultClientGetWithResp fail resp.Code != 200", log.Any("reqUrl", reqUrl), log.Any("resp.Msg", resp.Msg), log.E(err))
|
||||
err = errors.New("获取失败")
|
||||
return
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type BackpackReq struct {
|
||||
UserID int `form:"userId"`
|
||||
Account string `form:"account"`
|
||||
Phone string `form:"phone"`
|
||||
InviteCode string `form:"inviteCode"` // 邀请码
|
||||
}
|
||||
|
||||
type BackpackResp struct {
|
||||
UserId string `json:"userId"` // 用户Id
|
||||
Account string `json:"account"` // 账号
|
||||
Email string `json:"email"` // 邮箱
|
||||
UserName string `json:"userName"` // 用户姓名
|
||||
UserIp string `json:"userIp"` // 用户IP
|
||||
UserIpLocation string `json:"userIpLocation"` // 用户IP属地
|
||||
DevType string `json:"devType"` // 设备类型
|
||||
DevId string `json:"devId"` // 设备id
|
||||
SystemType string `json:"systemType" bson:"systemType"` // 系统类型
|
||||
SystemVersion string `json:"systemVersion" bson:"systemVersion"` // 系统版本
|
||||
AppId string `json:"appId"` // appId
|
||||
AppName string `json:"appName"` // appName
|
||||
VipExpiredTime time.Time `json:"vipExpiredTime"` // VIP过期时间
|
||||
VipLevel string `json:"vipLevel"` // VIP等级
|
||||
VipName string `json:"vipName"` // VIP名
|
||||
Balance float64 `json:"balance"` // 金币余额
|
||||
UserRights string `json:"userRights"` // 用户权益,eg:金币抵扣券*3,AI抵扣券*4
|
||||
InviteCode string `json:"inviteCode"` // 邀请码
|
||||
}
|
||||
|
||||
func (p *BackpackReq) GetData(ctx *gin.Context, uid uint64, account, phone, inviteCode string) (resp BackpackResp, err error) {
|
||||
var user *usermod.User
|
||||
if uid > 0 {
|
||||
// 获取用户信息
|
||||
user, err = usermod.FindUserByUID(uid)
|
||||
|
||||
} else if account != "" {
|
||||
user, err = usermod.FindUserByAccount(account)
|
||||
} else if phone != "" {
|
||||
if !strings.HasPrefix(phone, "+86") {
|
||||
phone = "+86" + phone
|
||||
}
|
||||
user, err = usermod.FindUserByMobile(phone)
|
||||
} else if inviteCode != "" {
|
||||
user, err = usermod.FindUserPromotionCode(inviteCode)
|
||||
} else {
|
||||
return resp, nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("BackpackReq.GetData fail", log.Any("uid", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
// 获取用户余额
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
log.Error("BackpackReq.GetData fail", log.Any("uid", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
filter := bson.M{
|
||||
"uid": user.UID,
|
||||
}
|
||||
// 获取用户的最后的登录信息
|
||||
visit, err := visitlogmod.GetInfoByCond(filter, options.FindOne().SetSort(bson.D{{Key: "_id", Value: -1}}))
|
||||
if err != nil {
|
||||
log.Error("BackpackReq.GetData visitlogmod.GetInfoByCond fail", log.Any("uid", uid), log.E(err))
|
||||
}
|
||||
resp = BackpackResp{
|
||||
UserId: fmt.Sprintf("%v", user.UID),
|
||||
UserName: user.Name,
|
||||
Account: user.Account,
|
||||
Email: user.Email,
|
||||
UserIp: visit.IP,
|
||||
DevType: user.LastSysType,
|
||||
DevId: user.DevID,
|
||||
SystemType: user.SysType,
|
||||
SystemVersion: user.LastVer,
|
||||
AppId: appg.Conf.Customer.AppId,
|
||||
AppName: commod.KFK_APP_NAME,
|
||||
VipExpiredTime: user.VipExpireDate,
|
||||
VipName: user.VipName,
|
||||
VipLevel: fmt.Sprintf("%v", user.VipLevel),
|
||||
//UserRights :user.
|
||||
InviteCode: user.PromCode,
|
||||
}
|
||||
userRights := []string{}
|
||||
if wallet != nil {
|
||||
resp.Balance = float64(wallet.Amount)
|
||||
//if wallet.AiChangeFaceImgFreeTimes > 0 {
|
||||
// userRights = append(userRights, fmt.Sprintf("AI图片换脸免费次数*%v", wallet.AiChangeFaceImgFreeTimes))
|
||||
//}
|
||||
if wallet.AiUndressFreeTimes > 0 {
|
||||
userRights = append(userRights, fmt.Sprintf("AI脱衣免费次数*%v", wallet.AiUndressFreeTimes))
|
||||
}
|
||||
if wallet.DownloadCount > 0 {
|
||||
userRights = append(userRights, fmt.Sprintf("下载次数*%v", wallet.DownloadCount))
|
||||
}
|
||||
if wallet.LotteryTimes > 0 {
|
||||
userRights = append(userRights, fmt.Sprintf("抽奖次数*%v", wallet.LotteryTimes))
|
||||
}
|
||||
|
||||
userRights = append(userRights, fmt.Sprintf("AI女友余额=%v", wallet.AiMateBalance))
|
||||
userRights = append(userRights, fmt.Sprintf("金币余额=%v", wallet.Amount))
|
||||
userRights = append(userRights, fmt.Sprintf("金币收益=%v", wallet.Income))
|
||||
}
|
||||
resp.UserRights = strings.Join(userRights, ",")
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type RechargeReq struct {
|
||||
UserID int `form:"userId"`
|
||||
}
|
||||
|
||||
type RechargeResp []RechargeItem
|
||||
|
||||
type RechargeItem struct {
|
||||
UserId string `json:"userId"` // 用户Id
|
||||
UserName string `json:"userName"` // 用户姓名
|
||||
UserIp string `json:"userIp"` // 用户IP
|
||||
DevType string `json:"devType"` // 设备类型
|
||||
AppId string `json:"appId"` // appId
|
||||
AppName string `json:"appName"` // appName
|
||||
PayType string `json:"payType"` // 支付方式
|
||||
OrderId string `json:"orderId"` // 订单号
|
||||
ChannelOid string `json:"channelOid"` // 渠道订单号
|
||||
Money float64 `json:"money"` // 订单金额
|
||||
PayMoney float64 `json:"payMoney"` // 支付金额
|
||||
Status string `json:"status"` // 订单状态
|
||||
ProductName string `json:"productName"` // 商品名称
|
||||
Remark string `json:"remark"` // 备注
|
||||
CreateTime time.Time `json:"createTime"` // 下单时间
|
||||
PayTime time.Time `json:"payTime"` // 支付时间
|
||||
NotifyTime time.Time `json:"notifyTime"` // 回调时间
|
||||
}
|
||||
|
||||
func (p *RechargeReq) GetData(ctx context.Context, uid uint64) (resp RechargeResp, err error) {
|
||||
_, list, err := rchgordmod.FindRechargesOrders(bson.M{
|
||||
"uid": uid,
|
||||
}, options.Find().SetLimit(200).SetSort(bson.M{"_id": -1}))
|
||||
if err != nil {
|
||||
log.Error("RechargeReq.GetData fail", log.Any("uid", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
productIds := []primitive.ObjectID{}
|
||||
for _, v := range list {
|
||||
if v.BuyType == commod.BuyProduct {
|
||||
productIds = append(productIds, v.ProductID)
|
||||
}
|
||||
}
|
||||
productMap := getProductMap(productIds)
|
||||
for _, v := range list {
|
||||
var status string
|
||||
switch v.Status {
|
||||
case 2:
|
||||
//2付款失败
|
||||
status = "付款失败"
|
||||
case 3:
|
||||
// 3付款成功
|
||||
status = "付款成功"
|
||||
case 4:
|
||||
// 4已经退款
|
||||
status = "已经退款"
|
||||
default:
|
||||
status = "进行中"
|
||||
}
|
||||
item := RechargeItem{
|
||||
UserId: fmt.Sprintf("%v", v.UID),
|
||||
UserName: v.Name,
|
||||
UserIp: v.UserIP,
|
||||
DevType: v.DevType,
|
||||
AppId: appg.Conf.Customer.AppId,
|
||||
AppName: commod.KFK_APP_NAME,
|
||||
PayType: v.RechargeType,
|
||||
OrderId: v.ID.Hex(),
|
||||
ChannelOid: v.OID,
|
||||
Money: float64(v.Money) / 100,
|
||||
PayMoney: float64(v.PayMoney) / 100,
|
||||
Status: status,
|
||||
ProductName: v.ProductID.Hex(),
|
||||
Remark: v.Remark,
|
||||
CreateTime: v.CreatedAt,
|
||||
PayTime: v.PaymentAt,
|
||||
NotifyTime: v.SuccessAt,
|
||||
}
|
||||
switch v.BuyType {
|
||||
case commod.BuyProduct:
|
||||
product, ok := productMap[v.ProductID]
|
||||
if ok {
|
||||
item.ProductName = product.Name
|
||||
}
|
||||
case commod.BuyGold:
|
||||
item.ProductName = "金币"
|
||||
case commod.BuyFruitCoin:
|
||||
item.ProductName = "果币"
|
||||
case commod.BuyGameCoin:
|
||||
item.ProductName = "游戏币"
|
||||
}
|
||||
resp = append(resp, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getProductMap(productIds []primitive.ObjectID) map[primitive.ObjectID]productmod.Product {
|
||||
productMap := make(map[primitive.ObjectID]productmod.Product)
|
||||
if len(productIds) == 0 {
|
||||
return productMap
|
||||
}
|
||||
list, _ := productmod.FindByProductIDs(productIds)
|
||||
for _, v := range list {
|
||||
productMap[v.ID] = v
|
||||
}
|
||||
return productMap
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/models/v/eventmod"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const dramaEventDedupTTL = 30 * 24 * time.Hour
|
||||
|
||||
var analyticsEventNames = map[string]struct{}{
|
||||
"DRAMA_EXPOSURE": {}, "DRAMA_CLICK": {}, "DRAMA_PLAY_START": {},
|
||||
"DRAMA_PLAY_PROGRESS": {}, "DRAMA_PLAY_FINISH": {}, "DRAMA_PAYWALL_EXPOSURE": {},
|
||||
"DRAMA_UNLOCK_CLICK": {}, "DRAMA_CARD_CLICK": {}, "DRAMA_RECHARGE_SUCCESS": {},
|
||||
"DRAMA_UNLOCK_SUCCESS": {}, "DRAMA_CARD_PURCHASE_SUCCESS": {},
|
||||
}
|
||||
|
||||
var dramaScenes = map[string]struct{}{
|
||||
"ai_feed": {}, "hot_drama": {}, "detail": {}, "favorite": {},
|
||||
"like": {}, "history": {}, "purchased": {}, "share": {},
|
||||
}
|
||||
|
||||
func SaveEvents(uid uint64, requestID string, req EventsRequest) (EventsResponse, error) {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" || len(requestID) > 128 {
|
||||
return EventsResponse{}, fmt.Errorf("X-Request-ID is required and must not exceed 128 characters")
|
||||
}
|
||||
if len(req.Events) == 0 || len(req.Events) > 50 {
|
||||
return EventsResponse{}, fmt.Errorf("events must contain 1 to 50 items")
|
||||
}
|
||||
response := EventsResponse{}
|
||||
mediaCache := make(map[primitive.ObjectID]mediamod.Media)
|
||||
contentCache := make(map[primitive.ObjectID]mediacontentmod.MediaContent)
|
||||
for _, event := range req.Events {
|
||||
if err := validateAnalyticsEvent(event); err != nil {
|
||||
return EventsResponse{}, err
|
||||
}
|
||||
if err := validateAnalyticsObjects(event, mediaCache, contentCache); err != nil {
|
||||
return EventsResponse{}, err
|
||||
}
|
||||
key := dramaIdempotencyKey("event", uid, event.EventID)
|
||||
acquired := true
|
||||
if appg.Redis != nil {
|
||||
var err error
|
||||
acquired, err = appg.Redis.SetNX(key, "1", dramaEventDedupTTL)
|
||||
if err != nil {
|
||||
return EventsResponse{}, err
|
||||
}
|
||||
} else {
|
||||
exists, err := eventmod.Exists(event.EventID)
|
||||
if err != nil {
|
||||
return EventsResponse{}, err
|
||||
}
|
||||
acquired = !exists
|
||||
}
|
||||
if !acquired {
|
||||
response.Duplicated++
|
||||
continue
|
||||
}
|
||||
payload, err := json.Marshal(struct {
|
||||
UID uint64 `json:"uid"`
|
||||
AnalyticsEvent
|
||||
}{UID: uid, AnalyticsEvent: event})
|
||||
if err != nil {
|
||||
return EventsResponse{}, err
|
||||
}
|
||||
now := time.Now()
|
||||
if err = eventmod.Insert(nil, &eventmod.Event{
|
||||
EventId: event.EventID, EventType: event.EventName, Data: string(payload),
|
||||
Status: eventmod.Default, CreatedAt: now, UpdatedAt: now,
|
||||
}); err != nil {
|
||||
if appg.Redis != nil {
|
||||
_, _ = appg.Redis.Del(key)
|
||||
}
|
||||
return EventsResponse{}, err
|
||||
}
|
||||
response.Accepted++
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func validateAnalyticsObjects(
|
||||
event AnalyticsEvent,
|
||||
mediaCache map[primitive.ObjectID]mediamod.Media,
|
||||
contentCache map[primitive.ObjectID]mediacontentmod.MediaContent,
|
||||
) error {
|
||||
media, ok := mediaCache[event.MediaID]
|
||||
if !ok {
|
||||
var err error
|
||||
media, err = mediamod.GetInfo(event.MediaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mediaCache[event.MediaID] = media
|
||||
}
|
||||
if media.ID.IsZero() || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete {
|
||||
return fmt.Errorf("invalid drama event mediaId")
|
||||
}
|
||||
if event.ContentID.IsZero() {
|
||||
return nil
|
||||
}
|
||||
content, ok := contentCache[event.ContentID]
|
||||
if !ok {
|
||||
var err error
|
||||
content, err = mediacontentmod.GetInfo(event.ContentID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentCache[event.ContentID] = content
|
||||
}
|
||||
if content.MediaID != event.MediaID || content.MediaType != mediamod.MediaTypeDrama || content.IsDelete {
|
||||
return fmt.Errorf("event contentId does not belong to mediaId")
|
||||
}
|
||||
if event.EpisodeNumber > 0 && content.EpisodeNumber != event.EpisodeNumber {
|
||||
return fmt.Errorf("event episodeNumber does not match contentId")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAnalyticsEvent(event AnalyticsEvent) error {
|
||||
event.EventID = strings.TrimSpace(event.EventID)
|
||||
if event.EventID == "" || len(event.EventID) > 128 {
|
||||
return fmt.Errorf("eventId is required and must not exceed 128 characters")
|
||||
}
|
||||
if _, ok := analyticsEventNames[event.EventName]; !ok {
|
||||
return fmt.Errorf("unsupported eventName: %s", event.EventName)
|
||||
}
|
||||
if _, ok := dramaScenes[event.Scene]; !ok {
|
||||
return fmt.Errorf("unsupported scene: %s", event.Scene)
|
||||
}
|
||||
if event.MediaID.IsZero() || event.OccurredAt.IsZero() || strings.TrimSpace(event.SessionID) == "" {
|
||||
return fmt.Errorf("sessionId, occurredAt and mediaId are required")
|
||||
}
|
||||
requireContent := event.EventName != "DRAMA_EXPOSURE" && event.EventName != "DRAMA_CLICK"
|
||||
if requireContent && event.ContentID.IsZero() {
|
||||
return fmt.Errorf("contentId is required for %s", event.EventName)
|
||||
}
|
||||
if event.EventName == "DRAMA_EXPOSURE" && event.Scene == "ai_feed" && (event.QueueVersion == "" || event.Position < 0) {
|
||||
return fmt.Errorf("queueVersion and position are required for feed exposure")
|
||||
}
|
||||
if strings.HasPrefix(event.EventName, "DRAMA_PLAY_") {
|
||||
if event.EpisodeNumber < 1 || (event.PlayMode != "feed" && event.PlayMode != "continuous") {
|
||||
return fmt.Errorf("episodeNumber and playMode are required for play events")
|
||||
}
|
||||
}
|
||||
if event.EventName == "DRAMA_PLAY_PROGRESS" && (event.Progress < 0 || event.Duration <= 0) {
|
||||
return fmt.Errorf("progressSeconds and durationSeconds are required for progress events")
|
||||
}
|
||||
if event.EventName == "DRAMA_PLAY_FINISH" && event.Duration <= 0 {
|
||||
return fmt.Errorf("durationSeconds is required for finish events")
|
||||
}
|
||||
if strings.Contains(event.EventName, "PAYWALL") || strings.Contains(event.EventName, "UNLOCK") ||
|
||||
strings.Contains(event.EventName, "CARD") || strings.Contains(event.EventName, "RECHARGE") {
|
||||
if strings.TrimSpace(event.CheckoutContextID) == "" {
|
||||
return fmt.Errorf("checkoutContextId is required for %s", event.EventName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dramaIdempotencyKey(scope string, uid uint64, id string) string {
|
||||
sum := sha256.Sum256([]byte(fmt.Sprintf("%s:%d:%s", scope, uid, strings.TrimSpace(id))))
|
||||
return "drama:" + scope + ":dedup:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/models/v/media_buy_record_mod"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDramaEntitlementRequired = errors.New("active drama entitlement is required")
|
||||
ErrDownloadResourceInvalid = errors.New("short drama episode is unavailable")
|
||||
)
|
||||
|
||||
const downloadTicketTTL = 6 * time.Hour
|
||||
|
||||
type DownloadAuthorizeRequest struct {
|
||||
MediaID primitive.ObjectID `json:"mediaId" binding:"required"`
|
||||
ContentID primitive.ObjectID `json:"contentId" binding:"required"`
|
||||
}
|
||||
|
||||
type DownloadAuthorizeResponse struct {
|
||||
MediaID primitive.ObjectID `json:"mediaId"`
|
||||
ContentID primitive.ObjectID `json:"contentId"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Name string `json:"name"`
|
||||
Cover string `json:"cover"`
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
H265DownloadURL string `json:"h265DownloadUrl"`
|
||||
MediaSize int64 `json:"mediaSize"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
RemainingDownloadCount int64 `json:"remainingDownloadCount"`
|
||||
Charged bool `json:"charged"`
|
||||
}
|
||||
|
||||
func AuthorizeDownload(ctx context.Context, uid uint64, requestID string, req DownloadAuthorizeRequest, now time.Time) (DownloadAuthorizeResponse, error) {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if len(requestID) < 1 || len(requestID) > 128 {
|
||||
return DownloadAuthorizeResponse{}, errors.New("X-Request-ID长度必须为1到128")
|
||||
}
|
||||
media, err := mediamod.GetInfo(req.MediaID)
|
||||
if err != nil || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete {
|
||||
return DownloadAuthorizeResponse{}, ErrDownloadResourceInvalid
|
||||
}
|
||||
content, err := mediacontentmod.GetInfo(req.ContentID)
|
||||
if err != nil || content.MediaID != req.MediaID || content.MediaType != mediamod.MediaTypeDrama || content.Status != 1 || !content.IsActive || content.IsDelete {
|
||||
return DownloadAuthorizeResponse{}, ErrDownloadResourceInvalid
|
||||
}
|
||||
requestKey := downloadRequestKey(uid, requestID)
|
||||
fingerprint := req.MediaID.Hex() + ":" + req.ContentID.Hex()
|
||||
var wallet *walletmod.Wallet
|
||||
var charged bool
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
err = appg.VideoDB.TransCtx(ctx, func(t *db.MongoTool) error {
|
||||
user, userErr := usermod.FindUserByUIDTrans(t, uid)
|
||||
if userErr != nil {
|
||||
return userErr
|
||||
}
|
||||
bought := false
|
||||
if user != nil && !user.HasLocked && !user.DramaExpire.After(now) {
|
||||
bought, userErr = media_buy_record_mod.IsBuyWithTool(t, uid, req.MediaID, req.ContentID)
|
||||
if userErr != nil {
|
||||
return userErr
|
||||
}
|
||||
}
|
||||
if !canAuthorizeDramaDownload(user, bought, now) {
|
||||
return ErrDramaEntitlementRequired
|
||||
}
|
||||
wallet, charged, userErr = walletmod.AuthorizeDramaDownload(t, uid, requestKey, fingerprint, now)
|
||||
return userErr
|
||||
})
|
||||
if !errors.Is(err, walletmod.ErrDownloadAuthorizationRace) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return DownloadAuthorizeResponse{}, err
|
||||
}
|
||||
name := strings.TrimSpace(content.Name)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("第%d集", content.EpisodeNumber)
|
||||
}
|
||||
return DownloadAuthorizeResponse{
|
||||
MediaID: req.MediaID, ContentID: req.ContentID, EpisodeNumber: content.EpisodeNumber,
|
||||
Name: name, Cover: content.Cover, DownloadURL: content.VideoUrl,
|
||||
H265DownloadURL: appg.H265URLForApp(content.H265Url), MediaSize: content.MediaSize,
|
||||
ExpiresAt: now.Add(downloadTicketTTL), RemainingDownloadCount: wallet.DownloadCount, Charged: charged,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func downloadRequestKey(uid uint64, requestID string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d:%s", uid, requestID))))
|
||||
}
|
||||
|
||||
func canAuthorizeDramaDownload(user *usermod.User, bought bool, now time.Time) bool {
|
||||
return user != nil && !user.HasLocked && (user.DramaExpire.After(now) || bought)
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/mediacontentser"
|
||||
"91porn-server/app/service/mediaser"
|
||||
recommendqueue "91porn-server/common/shortrecommend"
|
||||
"91porn-server/models/cache/mediadata"
|
||||
"91porn-server/models/v/media_buy_record_mod"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/moduleconfmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const dramaFeedCandidateLimit = 500
|
||||
|
||||
func GetChannelConfig(now time.Time) (ChannelConfig, error) {
|
||||
modules, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Drama)
|
||||
if err != nil {
|
||||
return ChannelConfig{}, err
|
||||
}
|
||||
active := make([]moduleconfmod.ModuleConf, 0, len(modules))
|
||||
for _, module := range modules {
|
||||
if module.IsActiveAt(now) {
|
||||
active = append(active, module)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(active, func(i, j int) bool { return active[i].SortNum > active[j].SortNum })
|
||||
var aiModuleID, hotModuleID primitive.ObjectID
|
||||
for _, module := range active {
|
||||
name := strings.ToLower(strings.TrimSpace(module.ModuleName))
|
||||
switch {
|
||||
case strings.Contains(name, "热门") || strings.Contains(name, "hot"):
|
||||
if hotModuleID.IsZero() {
|
||||
hotModuleID = module.ID
|
||||
}
|
||||
case strings.Contains(name, "ai"):
|
||||
if aiModuleID.IsZero() {
|
||||
aiModuleID = module.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
if aiModuleID.IsZero() && len(active) > 0 {
|
||||
aiModuleID = active[0].ID
|
||||
}
|
||||
if hotModuleID.IsZero() && len(active) > 1 {
|
||||
hotModuleID = active[1].ID
|
||||
}
|
||||
return ChannelConfig{
|
||||
BottomTabTitle: "短剧",
|
||||
DefaultTab: TabAIDrama,
|
||||
AdEnabled: false,
|
||||
Tabs: []ChannelTab{
|
||||
{Key: TabLive, Title: "直播", Enabled: true, Sort: 1},
|
||||
{Key: TabDouyin, Title: "抖音", Enabled: true, Sort: 2},
|
||||
{Key: TabAIDrama, Title: "AI短剧", Enabled: !aiModuleID.IsZero(), Sort: 3, ModuleID: objectIDString(aiModuleID)},
|
||||
{Key: TabHotDrama, Title: "热门短剧", Enabled: !hotModuleID.IsZero(), Sort: 4, ModuleID: objectIDString(hotModuleID)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func objectIDString(id primitive.ObjectID) string {
|
||||
if id.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return id.Hex()
|
||||
}
|
||||
|
||||
func GetList(uid uint64, req ListRequest) (ListResponse, error) {
|
||||
filter := bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false}
|
||||
sortBy := dramaListSort(req.SortType)
|
||||
total, err := mediamod.QueryAllCount(filter)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
}
|
||||
list, hasNext, err := mediadata.GetListFromCache(filter, req.Skip64(), req.Limit64(), sortBy)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
}
|
||||
return ListResponse{Total: total, HasNext: hasNext, List: mediaser.FillMedias(list, uid, true)}, nil
|
||||
}
|
||||
|
||||
func dramaListSort(sortType int) bson.D {
|
||||
sortBy := bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
switch sortType {
|
||||
case 2:
|
||||
sortBy = bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
case 3:
|
||||
sortBy = bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
case 4:
|
||||
sortBy = bson.D{{Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
}
|
||||
return sortBy
|
||||
}
|
||||
|
||||
func GetFeed(ctx context.Context, uid uint64, pageSize uint64, now time.Time) (FeedResponse, error) {
|
||||
filter := bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false}
|
||||
candidates, _, err := mediadata.GetListFromCache(
|
||||
filter, 0, dramaFeedCandidateLimit,
|
||||
bson.D{{Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}},
|
||||
)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
ordered := orderFeedCandidates(candidates, now)
|
||||
if len(ordered) == 0 {
|
||||
return FeedResponse{QueueVersion: "drama-" + recommendqueue.VersionAt(now), List: []FeedItem{}}, nil
|
||||
}
|
||||
queueIDs := make([]string, 0, len(ordered))
|
||||
mediaByID := make(map[string]*mediamod.Media, len(ordered))
|
||||
for _, media := range ordered {
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
id := media.ID.Hex()
|
||||
queueIDs = append(queueIDs, id)
|
||||
mediaByID[id] = media
|
||||
}
|
||||
queueVersion := dramaFeedQueueVersion(now, queueIDs)
|
||||
reservation, err := reserveDramaFeedPage(
|
||||
ctx, appg.Redis, uid, int(pageSize)*dramaFeedScanMultiplier, queueVersion, queueIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
abortCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = abortDramaFeedPage(abortCtx, appg.Redis, uid, reservation)
|
||||
}
|
||||
}()
|
||||
reservedMedias := make([]*mediamod.Media, 0, len(reservation.IDs))
|
||||
for _, id := range reservation.IDs {
|
||||
if media := mediaByID[id]; media != nil {
|
||||
reservedMedias = append(reservedMedias, media)
|
||||
}
|
||||
}
|
||||
hasCard, err := mediacontentser.ActiveDramaCardStatus(uid)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
mediaResponses := mediaser.FillMedias(reservedMedias, uid, true, hasCard)
|
||||
episodes, err := firstEpisodeMap(uid, reservedMedias, hasCard)
|
||||
if err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
responseByID := make(map[string]*mediamod.AppMediaBase, len(mediaResponses))
|
||||
for _, media := range mediaResponses {
|
||||
if media != nil {
|
||||
responseByID[media.ID.Hex()] = media
|
||||
}
|
||||
}
|
||||
items := make([]FeedItem, 0, pageSize)
|
||||
scanned := 0
|
||||
for _, id := range reservation.IDs {
|
||||
scanned++
|
||||
media := responseByID[id]
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
episode, ok := episodes[media.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, FeedItem{Media: media, Content: episode})
|
||||
if len(items) >= int(pageSize) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := commitDramaFeedPage(ctx, appg.Redis, uid, reservation, scanned); err != nil {
|
||||
return FeedResponse{}, err
|
||||
}
|
||||
committed = true
|
||||
return FeedResponse{
|
||||
QueueVersion: "drama-" + reservation.Version,
|
||||
HasNext: reservation.Length > len(items),
|
||||
List: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dramaFeedQueueVersion 将当天候选集快照写入版本。上架、下架导致候选集变化时,
|
||||
// 新请求会切换到独立队列;ID 排序后再计算,避免仅推荐顺序变化造成队列频繁重建。
|
||||
func dramaFeedQueueVersion(now time.Time, queueIDs []string) string {
|
||||
ids := append([]string(nil), queueIDs...)
|
||||
sort.Strings(ids)
|
||||
h := fnv.New64a()
|
||||
for _, id := range ids {
|
||||
_, _ = h.Write([]byte(id))
|
||||
_, _ = h.Write([]byte{0})
|
||||
}
|
||||
return recommendqueue.VersionWithRevision(now, fmt.Sprintf("%016x", h.Sum64()))
|
||||
}
|
||||
|
||||
func orderFeedCandidates(candidates []*mediamod.Media, now time.Time) []*mediamod.Media {
|
||||
recommended := make([]*mediamod.Media, 0)
|
||||
regular := make([]vidmod.RecommendCandidate, 0)
|
||||
mediaByID := make(map[primitive.ObjectID]*mediamod.Media, len(candidates))
|
||||
for _, media := range candidates {
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
mediaByID[media.ID] = media
|
||||
candidate := vidmod.RecommendCandidate{
|
||||
ID: media.ID, ReviewAt: dramaReviewAt(media), RecommendScore: dramaInteractionScore(media),
|
||||
}
|
||||
if media.SortCode > 0 || !media.Choice.IsZero() {
|
||||
recommended = append(recommended, media)
|
||||
continue
|
||||
}
|
||||
regular = append(regular, candidate)
|
||||
}
|
||||
rng := rand.New(rand.NewSource(dramaQueueSeed(now)))
|
||||
rng.Shuffle(len(recommended), func(i, j int) {
|
||||
recommended[i], recommended[j] = recommended[j], recommended[i]
|
||||
})
|
||||
// 直接复用91P短视频队列的17条互动高分+3条24小时新内容组装算法。
|
||||
regularIDs := recommendqueue.Assemble(regular, now)
|
||||
ordered := make([]*mediamod.Media, 0, len(candidates))
|
||||
ordered = append(ordered, recommended...)
|
||||
ordered = appendDramaIDs(ordered, regularIDs, mediaByID)
|
||||
return ordered
|
||||
}
|
||||
|
||||
func dramaQueueSeed(now time.Time) int64 {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte("drama:" + recommendqueue.VersionAt(now)))
|
||||
return int64(h.Sum64())
|
||||
}
|
||||
|
||||
func dramaInteractionScore(media *mediamod.Media) int64 {
|
||||
if media == nil {
|
||||
return 0
|
||||
}
|
||||
return vidmod.CalculateRecommendScore(media.CountLike, media.CountCollect, media.CountComment, media.CountShare)
|
||||
}
|
||||
|
||||
func dramaReviewAt(media *mediamod.Media) time.Time {
|
||||
if media != nil && !media.LatestPublishedAt.IsZero() {
|
||||
return media.LatestPublishedAt
|
||||
}
|
||||
if media == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return media.CreatedAt
|
||||
}
|
||||
|
||||
func appendDramaIDs(
|
||||
ordered []*mediamod.Media,
|
||||
ids []string,
|
||||
mediaByID map[primitive.ObjectID]*mediamod.Media,
|
||||
) []*mediamod.Media {
|
||||
for _, rawID := range ids {
|
||||
id, err := primitive.ObjectIDFromHex(rawID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if media := mediaByID[id]; media != nil {
|
||||
ordered = append(ordered, media)
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
func firstEpisodeMap(uid uint64, medias []*mediamod.Media, hasCard bool) (map[primitive.ObjectID]mediacontentser.AppMediaContent, error) {
|
||||
result := make(map[primitive.ObjectID]mediacontentser.AppMediaContent, len(medias))
|
||||
if len(medias) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
mediaIDs := make([]primitive.ObjectID, 0, len(medias))
|
||||
mediaMap := make(map[primitive.ObjectID]*mediamod.Media, len(medias))
|
||||
for _, media := range medias {
|
||||
if media != nil {
|
||||
mediaIDs = append(mediaIDs, media.ID)
|
||||
mediaMap[media.ID] = media
|
||||
}
|
||||
}
|
||||
contents, err := mediacontentmod.QueryAllList(
|
||||
bson.M{"mediaId": bson.M{"$in": mediaIDs}, "mediaType": mediamod.MediaTypeDrama, "episodeNumber": 1, "isActive": true, "isDelete": false},
|
||||
options.Find().SetSort(bson.D{{Key: "createdAt", Value: 1}}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
contentIDs := make([]primitive.ObjectID, 0, len(contents))
|
||||
for _, content := range contents {
|
||||
if content != nil {
|
||||
contentIDs = append(contentIDs, content.ID)
|
||||
}
|
||||
}
|
||||
bought, err := media_buy_record_mod.IsContentBuyBatch(uid, contentIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, content := range contents {
|
||||
if content == nil || result[content.MediaID].ID != primitive.NilObjectID {
|
||||
continue
|
||||
}
|
||||
media := mediaMap[content.MediaID]
|
||||
if media == nil {
|
||||
continue
|
||||
}
|
||||
isFree, canPlay, accessType := mediacontentser.ResolveDramaAccess(*content, *media, bought[content.ID], hasCard)
|
||||
name := strings.TrimSpace(content.Name)
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("第%d集", content.EpisodeNumber)
|
||||
}
|
||||
item := mediacontentser.AppMediaContent{
|
||||
ID: content.ID, MediaID: content.MediaID, MediaType: content.MediaType,
|
||||
EpisodeNumber: content.EpisodeNumber, ListenPermission: content.ListenPermission,
|
||||
VideoUrl: content.VideoUrl, H265Url: appg.H265URLForApp(content.H265Url), AudioUrl: content.AudioUrl,
|
||||
PlayTime: content.PlayTime, Price: content.Price, Name: name, Cover: content.Cover,
|
||||
HasBuy: bought[content.ID], IsFree: isFree, HasDramaCard: hasCard,
|
||||
CanPlay: canPlay, AccessType: accessType, Ratio: content.Ratio,
|
||||
PreviewEnabled: content.PreviewEnabled, PreviewStart: content.PreviewStart, PreviewSeconds: content.PreviewSeconds,
|
||||
CreatedAt: content.CreatedAt, UpdateTime: content.UpdateTime,
|
||||
}
|
||||
if !item.CanPlay {
|
||||
if item.PreviewEnabled {
|
||||
item.PreviewVideoUrl, item.PreviewH265Url = item.VideoUrl, item.H265Url
|
||||
}
|
||||
item.VideoUrl, item.H265Url, item.AudioUrl = "", "", ""
|
||||
}
|
||||
result[content.MediaID] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/v/mediamod"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestDramaListSortMatchesMediaLibraryEnum(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sortType int
|
||||
want bson.D
|
||||
}{
|
||||
{name: "热门推荐", sortType: 1, want: bson.D{{Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}},
|
||||
{name: "最新上架", sortType: 2, want: bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}},
|
||||
{name: "最热", sortType: 3, want: bson.D{{Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}},
|
||||
{name: "最多收藏", sortType: 4, want: bson.D{{Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
request := httptest.NewRequest("GET", fmt.Sprintf("/?pageNumber=1&pageSize=20&sortType=%d", test.sortType), nil)
|
||||
var params ListRequest
|
||||
if err := binding.Query.Bind(request, ¶ms); err != nil {
|
||||
t.Fatalf("bind sortType=%d: %v", test.sortType, err)
|
||||
}
|
||||
if got := dramaListSort(params.SortType); !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("dramaListSort(%d) = %#v, want %#v", test.sortType, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
legacyRequest := httptest.NewRequest("GET", "/?pageNumber=1&pageSize=20&sortType=7", nil)
|
||||
if err := binding.Query.Bind(legacyRequest, &ListRequest{}); err == nil {
|
||||
t.Fatal("bind legacy sortType=7 succeeded, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDramaFeedQueueVersionChangesWithCandidateSet(t *testing.T) {
|
||||
now := time.Date(2026, 8, 28, 10, 0, 0, 0, time.FixedZone("CST", 8*60*60))
|
||||
first := dramaFeedQueueVersion(now, []string{"media-b", "media-a"})
|
||||
sameSet := dramaFeedQueueVersion(now.Add(time.Hour), []string{"media-a", "media-b"})
|
||||
withNewDrama := dramaFeedQueueVersion(now.Add(time.Hour), []string{"media-a", "media-b", "media-c"})
|
||||
nextDay := dramaFeedQueueVersion(now.Add(24*time.Hour), []string{"media-a", "media-b"})
|
||||
|
||||
if first != sameSet {
|
||||
t.Fatalf("same-day candidate set produced different versions: %q != %q", first, sameSet)
|
||||
}
|
||||
if first == withNewDrama {
|
||||
t.Fatalf("new candidate did not change queue version: %q", first)
|
||||
}
|
||||
if first == nextDay {
|
||||
t.Fatalf("next day did not change queue version: %q", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderFeedCandidatesPrioritizesRecommendedAndIsStable(t *testing.T) {
|
||||
now := time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC)
|
||||
recommended := &mediamod.Media{ID: primitive.NewObjectID(), SortCode: 10, CreatedAt: now.Add(-time.Hour)}
|
||||
high := &mediamod.Media{ID: primitive.NewObjectID(), CountLike: 100, CreatedAt: now.Add(-48 * time.Hour)}
|
||||
recent := &mediamod.Media{ID: primitive.NewObjectID(), CreatedAt: now.Add(-time.Hour)}
|
||||
|
||||
candidates := []*mediamod.Media{high, recent, recommended}
|
||||
ordered := orderFeedCandidates(candidates, now)
|
||||
again := orderFeedCandidates(candidates, now.Add(time.Minute))
|
||||
if len(ordered) != 3 || len(again) != 3 {
|
||||
t.Fatalf("ordered lengths = %d/%d, want 3/3", len(ordered), len(again))
|
||||
}
|
||||
if ordered[0].ID != recommended.ID {
|
||||
t.Fatalf("first item = %s, want recommended %s", ordered[0].ID, recommended.ID)
|
||||
}
|
||||
for i := range ordered {
|
||||
if ordered[i].ID != again[i].ID {
|
||||
t.Fatalf("same-day queue changed at %d: %s != %s", i, ordered[i].ID, again[i].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAnalyticsEventRequiresCheckoutContextForPaywall(t *testing.T) {
|
||||
event := AnalyticsEvent{
|
||||
EventID: "event-1", EventName: "DRAMA_PAYWALL_EXPOSURE", SessionID: "session-1",
|
||||
OccurredAt: time.Now(), MediaID: primitive.NewObjectID(), ContentID: primitive.NewObjectID(),
|
||||
Scene: "detail",
|
||||
}
|
||||
if err := validateAnalyticsEvent(event); err == nil {
|
||||
t.Fatal("validateAnalyticsEvent() should reject missing checkoutContextId")
|
||||
}
|
||||
event.CheckoutContextID = "drama-checkout-test"
|
||||
if err := validateAnalyticsEvent(event); err != nil {
|
||||
t.Fatalf("validateAnalyticsEvent() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDramaInteractionScoreUses91PWeights(t *testing.T) {
|
||||
media := &mediamod.Media{
|
||||
CountLike: 1, CountCollect: 2, CountComment: 3, CountShare: 4, CountBrowse: 999,
|
||||
}
|
||||
if got, want := dramaInteractionScore(media), int64(34); got != want {
|
||||
t.Fatalf("dramaInteractionScore() = %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectIDStringReturnsEmptyForZeroID(t *testing.T) {
|
||||
if got := objectIDString(primitive.NilObjectID); got != "" {
|
||||
t.Fatalf("objectIDString(zero) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
redisutil "91porn-server/common/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
dramaFeedScanMultiplier = 5
|
||||
dramaFeedQueueTTL = 72 * time.Hour
|
||||
dramaFeedLeaseTTL = 15 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
errDramaFeedQueueMissing = errors.New("drama feed queue is missing")
|
||||
errDramaFeedCursorBusy = errors.New("drama feed cursor is busy")
|
||||
)
|
||||
|
||||
type dramaFeedReservation struct {
|
||||
Version string
|
||||
Length int
|
||||
Offset int
|
||||
Reserved int
|
||||
Token string
|
||||
IDs []string
|
||||
}
|
||||
|
||||
type dramaFeedScriptClient interface {
|
||||
RunScriptContext(context.Context, *redisutil.Script, []string, ...interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
const publishDramaFeedQueueScriptSource = `
|
||||
if redis.call('EXISTS', KEYS[1]) == 1 then
|
||||
return 0
|
||||
end
|
||||
for i = 2, #ARGV do
|
||||
redis.call('RPUSH', KEYS[1], ARGV[i])
|
||||
end
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
return 1
|
||||
`
|
||||
|
||||
const reserveDramaFeedPageScriptSource = `
|
||||
local length = redis.call('LLEN', KEYS[1])
|
||||
if length <= 0 then
|
||||
return {'QUEUE_MISSING'}
|
||||
end
|
||||
if redis.call('EXISTS', KEYS[3]) == 1 then
|
||||
return {'BUSY'}
|
||||
end
|
||||
local offset = tonumber(redis.call('HGET', KEYS[2], ARGV[1]) or '0') % length
|
||||
local reserved = math.min(tonumber(ARGV[2]), length)
|
||||
redis.call('HSET', KEYS[3],
|
||||
'token', ARGV[3], 'length', length, 'offset', offset, 'reserved', reserved)
|
||||
redis.call('EXPIRE', KEYS[3], ARGV[4])
|
||||
local result = {'RESERVED', ARGV[5], tostring(length), tostring(offset), tostring(reserved), ARGV[3]}
|
||||
for i = 0, reserved - 1 do
|
||||
table.insert(result, redis.call('LINDEX', KEYS[1], (offset + i) % length))
|
||||
end
|
||||
return result
|
||||
`
|
||||
|
||||
const commitDramaFeedPageScriptSource = `
|
||||
local lease = redis.call('HMGET', KEYS[3], 'token', 'length', 'offset', 'reserved')
|
||||
if not lease[1] then return -1 end
|
||||
if lease[1] ~= ARGV[2] or tonumber(lease[2]) ~= tonumber(ARGV[3]) or
|
||||
tonumber(lease[3]) ~= tonumber(ARGV[4]) or tonumber(lease[4]) ~= tonumber(ARGV[5]) then
|
||||
return -2
|
||||
end
|
||||
local length = redis.call('LLEN', KEYS[1])
|
||||
if length <= 0 or length ~= tonumber(ARGV[3]) then return -3 end
|
||||
local consumed = tonumber(ARGV[6])
|
||||
if not consumed or consumed <= 0 or consumed > tonumber(ARGV[5]) then return -2 end
|
||||
local currentOffset = tonumber(redis.call('HGET', KEYS[2], ARGV[1]) or '0') % length
|
||||
if currentOffset ~= tonumber(ARGV[4]) then return -2 end
|
||||
redis.call('HSET', KEYS[2], ARGV[1], (currentOffset + consumed) % length)
|
||||
redis.call('EXPIRE', KEYS[2], ARGV[7])
|
||||
redis.call('DEL', KEYS[3])
|
||||
return 1
|
||||
`
|
||||
|
||||
const abortDramaFeedPageScriptSource = `
|
||||
if redis.call('HGET', KEYS[1], 'token') ~= ARGV[1] then
|
||||
return 0
|
||||
end
|
||||
return redis.call('DEL', KEYS[1])
|
||||
`
|
||||
|
||||
var (
|
||||
publishDramaFeedQueueScript = redisutil.NewScript(publishDramaFeedQueueScriptSource)
|
||||
reserveDramaFeedPageScript = redisutil.NewScript(reserveDramaFeedPageScriptSource)
|
||||
commitDramaFeedPageScript = redisutil.NewScript(commitDramaFeedPageScriptSource)
|
||||
abortDramaFeedPageScript = redisutil.NewScript(abortDramaFeedPageScriptSource)
|
||||
)
|
||||
|
||||
func dramaFeedQueueKey(version string) string {
|
||||
return "recommend:drama:queue:" + version
|
||||
}
|
||||
|
||||
func dramaFeedOffsetKey(version string) string {
|
||||
return "recommend:drama:offset:" + version
|
||||
}
|
||||
|
||||
func dramaFeedLeaseKey(version string, uid uint64) string {
|
||||
return "recommend:drama:reservation:" + version + ":" + strconv.FormatUint(uid, 10)
|
||||
}
|
||||
|
||||
func reserveDramaFeedPage(
|
||||
ctx context.Context,
|
||||
client dramaFeedScriptClient,
|
||||
uid uint64,
|
||||
size int,
|
||||
version string,
|
||||
queueIDs []string,
|
||||
) (dramaFeedReservation, error) {
|
||||
if ctx == nil {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed context must not be nil")
|
||||
}
|
||||
if client == nil {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed Redis client must not be nil")
|
||||
}
|
||||
if uid == 0 || size <= 0 || version == "" || len(queueIDs) == 0 {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reservation arguments are invalid")
|
||||
}
|
||||
token, err := newDramaFeedToken()
|
||||
if err != nil {
|
||||
return dramaFeedReservation{}, err
|
||||
}
|
||||
reservation, err := runReserveDramaFeedPage(ctx, client, uid, size, version, token)
|
||||
if !errors.Is(err, errDramaFeedQueueMissing) {
|
||||
return reservation, err
|
||||
}
|
||||
args := make([]interface{}, 1, len(queueIDs)+1)
|
||||
args[0] = int64(dramaFeedQueueTTL / time.Second)
|
||||
for _, id := range queueIDs {
|
||||
args = append(args, id)
|
||||
}
|
||||
if _, err = client.RunScriptContext(
|
||||
ctx, publishDramaFeedQueueScript, []string{dramaFeedQueueKey(version)}, args...,
|
||||
); err != nil {
|
||||
return dramaFeedReservation{}, err
|
||||
}
|
||||
return runReserveDramaFeedPage(ctx, client, uid, size, version, token)
|
||||
}
|
||||
|
||||
func runReserveDramaFeedPage(
|
||||
ctx context.Context,
|
||||
client dramaFeedScriptClient,
|
||||
uid uint64,
|
||||
size int,
|
||||
version, token string,
|
||||
) (dramaFeedReservation, error) {
|
||||
raw, err := client.RunScriptContext(
|
||||
ctx,
|
||||
reserveDramaFeedPageScript,
|
||||
[]string{
|
||||
dramaFeedQueueKey(version),
|
||||
dramaFeedOffsetKey(version),
|
||||
dramaFeedLeaseKey(version, uid),
|
||||
},
|
||||
strconv.FormatUint(uid, 10),
|
||||
size,
|
||||
token,
|
||||
int64(dramaFeedLeaseTTL/time.Second),
|
||||
version,
|
||||
)
|
||||
if err != nil {
|
||||
return dramaFeedReservation{}, err
|
||||
}
|
||||
return parseDramaFeedReservation(raw)
|
||||
}
|
||||
|
||||
func parseDramaFeedReservation(raw interface{}) (dramaFeedReservation, error) {
|
||||
items, ok := raw.([]interface{})
|
||||
if !ok || len(items) == 0 {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned malformed result")
|
||||
}
|
||||
switch fmt.Sprint(items[0]) {
|
||||
case "QUEUE_MISSING":
|
||||
return dramaFeedReservation{}, errDramaFeedQueueMissing
|
||||
case "BUSY":
|
||||
return dramaFeedReservation{}, errDramaFeedCursorBusy
|
||||
case "RESERVED":
|
||||
default:
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned unknown status")
|
||||
}
|
||||
if len(items) < 7 {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned incomplete result")
|
||||
}
|
||||
result := dramaFeedReservation{Version: fmt.Sprint(items[1]), Token: fmt.Sprint(items[5])}
|
||||
var err error
|
||||
if result.Length, err = strconv.Atoi(fmt.Sprint(items[2])); err != nil {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid length: %w", err)
|
||||
}
|
||||
if result.Offset, err = strconv.Atoi(fmt.Sprint(items[3])); err != nil {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid offset: %w", err)
|
||||
}
|
||||
if result.Reserved, err = strconv.Atoi(fmt.Sprint(items[4])); err != nil {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid size: %w", err)
|
||||
}
|
||||
if result.Version == "" || result.Token == "" || result.Length <= 0 ||
|
||||
result.Offset < 0 || result.Offset >= result.Length || result.Reserved <= 0 ||
|
||||
result.Reserved > result.Length || len(items)-6 != result.Reserved {
|
||||
return dramaFeedReservation{}, fmt.Errorf("drama feed reserve returned invalid bounds")
|
||||
}
|
||||
for _, item := range items[6:] {
|
||||
result.IDs = append(result.IDs, fmt.Sprint(item))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func commitDramaFeedPage(
|
||||
ctx context.Context,
|
||||
client dramaFeedScriptClient,
|
||||
uid uint64,
|
||||
reservation dramaFeedReservation,
|
||||
consumed int,
|
||||
) error {
|
||||
if client == nil {
|
||||
return fmt.Errorf("drama feed Redis client must not be nil")
|
||||
}
|
||||
raw, err := client.RunScriptContext(
|
||||
ctx,
|
||||
commitDramaFeedPageScript,
|
||||
[]string{
|
||||
dramaFeedQueueKey(reservation.Version),
|
||||
dramaFeedOffsetKey(reservation.Version),
|
||||
dramaFeedLeaseKey(reservation.Version, uid),
|
||||
},
|
||||
strconv.FormatUint(uid, 10), reservation.Token, reservation.Length,
|
||||
reservation.Offset, reservation.Reserved, consumed,
|
||||
int64(dramaFeedQueueTTL/time.Second),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fmt.Sprint(raw) != "1" {
|
||||
return fmt.Errorf("drama feed commit rejected: %v", raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func abortDramaFeedPage(
|
||||
ctx context.Context,
|
||||
client dramaFeedScriptClient,
|
||||
uid uint64,
|
||||
reservation dramaFeedReservation,
|
||||
) error {
|
||||
if client == nil || reservation.Version == "" || reservation.Token == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := client.RunScriptContext(
|
||||
ctx,
|
||||
abortDramaFeedPageScript,
|
||||
[]string{dramaFeedLeaseKey(reservation.Version, uid)},
|
||||
reservation.Token,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func newDramaFeedToken() (string, error) {
|
||||
value := make([]byte, 16)
|
||||
if _, err := cryptorand.Read(value); err != nil {
|
||||
return "", fmt.Errorf("generate drama feed reservation token: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(value), nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
redisutil "91porn-server/common/redis"
|
||||
)
|
||||
|
||||
type fakeDramaFeedRedis struct {
|
||||
calls int
|
||||
run func(int, *redisutil.Script, []string, []interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
func (f *fakeDramaFeedRedis) RunScriptContext(
|
||||
_ context.Context,
|
||||
script *redisutil.Script,
|
||||
keys []string,
|
||||
args ...interface{},
|
||||
) (interface{}, error) {
|
||||
f.calls++
|
||||
return f.run(f.calls, script, keys, args)
|
||||
}
|
||||
|
||||
func TestReserveDramaFeedPagePublishesOnceAndReturnsRingSegment(t *testing.T) {
|
||||
ids := []string{"a", "b", "c", "d"}
|
||||
client := &fakeDramaFeedRedis{}
|
||||
client.run = func(call int, script *redisutil.Script, keys []string, args []interface{}) (interface{}, error) {
|
||||
switch call {
|
||||
case 1:
|
||||
if script != reserveDramaFeedPageScript {
|
||||
t.Fatal("first call should reserve existing queue")
|
||||
}
|
||||
return []interface{}{"QUEUE_MISSING"}, nil
|
||||
case 2:
|
||||
if script != publishDramaFeedQueueScript || keys[0] != "recommend:drama:queue:20260826" {
|
||||
t.Fatalf("unexpected publish call: keys=%v", keys)
|
||||
}
|
||||
if len(args) != len(ids)+1 {
|
||||
t.Fatalf("publish args = %d, want %d", len(args), len(ids)+1)
|
||||
}
|
||||
return int64(1), nil
|
||||
case 3:
|
||||
if script != reserveDramaFeedPageScript {
|
||||
t.Fatal("third call should reserve published queue")
|
||||
}
|
||||
token := fmt.Sprint(args[2])
|
||||
return []interface{}{"RESERVED", "20260826", "4", "2", "2", token, "c", "d"}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected Redis call %d", call)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
reservation, err := reserveDramaFeedPage(context.Background(), client, 7, 2, "20260826", ids)
|
||||
if err != nil {
|
||||
t.Fatalf("reserveDramaFeedPage() error = %v", err)
|
||||
}
|
||||
if reservation.Offset != 2 || reservation.Reserved != 2 ||
|
||||
fmt.Sprint(reservation.IDs) != "[c d]" {
|
||||
t.Fatalf("reservation = %+v", reservation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitDramaFeedPageAdvancesOnlyConsumedPrefix(t *testing.T) {
|
||||
reservation := dramaFeedReservation{
|
||||
Version: "20260826", Length: 20, Offset: 5, Reserved: 10,
|
||||
Token: "token", IDs: []string{"a", "b"},
|
||||
}
|
||||
client := &fakeDramaFeedRedis{}
|
||||
client.run = func(call int, script *redisutil.Script, keys []string, args []interface{}) (interface{}, error) {
|
||||
if script != commitDramaFeedPageScript {
|
||||
t.Fatal("expected commit script")
|
||||
}
|
||||
if keys[1] != "recommend:drama:offset:20260826" || fmt.Sprint(args[5]) != "4" {
|
||||
t.Fatalf("commit keys/args mismatch: keys=%v args=%v", keys, args)
|
||||
}
|
||||
return int64(1), nil
|
||||
}
|
||||
if err := commitDramaFeedPage(context.Background(), client, 7, reservation, 4); err != nil {
|
||||
t.Fatalf("commitDramaFeedPage() error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/mediaser"
|
||||
"91porn-server/common/dramatopic"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/modulesectionmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const everyoneLikesCacheTTL = 30 * time.Second
|
||||
|
||||
type TopicSummary struct {
|
||||
TopicID string `json:"topicId"`
|
||||
Name string `json:"name"`
|
||||
TopicType string `json:"topicType"`
|
||||
SystemKey string `json:"systemKey"`
|
||||
Sort int `json:"sort"`
|
||||
WorkCount int64 `json:"workCount"`
|
||||
}
|
||||
|
||||
type TopicListResponse struct {
|
||||
List []TopicSummary `json:"list"`
|
||||
}
|
||||
|
||||
type TopicWorksRequest struct {
|
||||
TopicID string `form:"topicId" json:"topicId" binding:"required"`
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type TopicWorksResponse struct {
|
||||
Topic TopicSummary `json:"topic"`
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*mediamod.AppMediaBase `json:"list"`
|
||||
}
|
||||
|
||||
func GetTopics(now time.Time) (TopicListResponse, error) {
|
||||
moduleID, ok, err := dramatopic.ModuleID(now)
|
||||
if err != nil {
|
||||
return TopicListResponse{}, err
|
||||
}
|
||||
total, err := mediamod.QueryAllCount(activeDramaFilter())
|
||||
if err != nil {
|
||||
return TopicListResponse{}, err
|
||||
}
|
||||
sections := make([]modulesectionmod.Section, 0)
|
||||
if ok {
|
||||
sections, err = modulesectionmod.ListBySubModule(moduleID, 1000)
|
||||
if err != nil {
|
||||
return TopicListResponse{}, err
|
||||
}
|
||||
}
|
||||
sectionIDs := make([]primitive.ObjectID, 0, len(sections))
|
||||
for _, section := range sections {
|
||||
if section.Status != nil && *section.Status == 1 {
|
||||
sectionIDs = append(sectionIDs, section.ID)
|
||||
}
|
||||
}
|
||||
counts, err := mediamod.CountActiveDramaBySectionIDs(sectionIDs)
|
||||
if err != nil {
|
||||
return TopicListResponse{}, err
|
||||
}
|
||||
items := make([]topicSortItem, 0, 3+len(sections))
|
||||
for _, system := range dramatopic.SystemTopics() {
|
||||
items = append(items, topicSortItem{summary: TopicSummary{
|
||||
TopicID: system.ID, Name: system.Name, TopicType: dramatopic.TypeSystem,
|
||||
SystemKey: system.SystemKey, Sort: system.Sort, WorkCount: total,
|
||||
}, systemOrder: system.TieOrder})
|
||||
}
|
||||
for _, section := range sections {
|
||||
if section.Status == nil || *section.Status != 1 {
|
||||
continue
|
||||
}
|
||||
items = append(items, topicSortItem{summary: TopicSummary{
|
||||
TopicID: section.ID.Hex(), Name: section.SectionName, TopicType: dramatopic.TypeCustom,
|
||||
Sort: valueOrZero(section.Sort), WorkCount: counts[section.ID],
|
||||
}, updatedAt: section.UpdatedAt})
|
||||
}
|
||||
sortTopicItems(items)
|
||||
response := TopicListResponse{List: make([]TopicSummary, 0, len(items))}
|
||||
for _, item := range items {
|
||||
response.List = append(response.List, item.summary)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
type topicSortItem struct {
|
||||
summary TopicSummary
|
||||
systemOrder int
|
||||
updatedAt time.Time
|
||||
}
|
||||
|
||||
func sortTopicItems(items []topicSortItem) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].summary.Sort != items[j].summary.Sort {
|
||||
return items[i].summary.Sort > items[j].summary.Sort
|
||||
}
|
||||
if items[i].systemOrder != items[j].systemOrder {
|
||||
return items[i].systemOrder > items[j].systemOrder
|
||||
}
|
||||
return items[i].updatedAt.After(items[j].updatedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func GetTopicWorks(uid uint64, req TopicWorksRequest, now time.Time) (TopicWorksResponse, error) {
|
||||
if system, ok := dramatopic.FindSystem(req.TopicID); ok {
|
||||
return getSystemTopicWorks(uid, req, system)
|
||||
}
|
||||
moduleID, ok, err := dramatopic.ModuleID(now)
|
||||
if err != nil || !ok {
|
||||
if err == nil {
|
||||
err = errors.New("短剧专题模块未配置")
|
||||
}
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
topicID, err := primitive.ObjectIDFromHex(req.TopicID)
|
||||
if err != nil {
|
||||
return TopicWorksResponse{}, errors.New("无效的专题ID")
|
||||
}
|
||||
section, err := modulesectionmod.GetBySectionByID(topicID)
|
||||
if err != nil || section.SubModuleID != moduleID || section.DeletedAt != nil || section.Status == nil || *section.Status != 1 {
|
||||
if err == nil {
|
||||
err = errors.New("专题不存在或已停用")
|
||||
}
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
filter := activeDramaFilter()
|
||||
filter["sId"] = topicID
|
||||
total, err := mediamod.QueryAllCount(filter)
|
||||
if err != nil {
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
list, err := mediamod.QueryAllList(filter, options.Find().
|
||||
SetSkip(req.Skip64()).SetLimit(req.Limit64()+1).
|
||||
SetSort(bson.D{{Key: "sectionSort", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}}))
|
||||
if err != nil {
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
hasNext := len(list) > int(req.PageSize)
|
||||
if hasNext {
|
||||
list = list[:req.PageSize]
|
||||
}
|
||||
return TopicWorksResponse{
|
||||
Topic: TopicSummary{TopicID: req.TopicID, Name: section.SectionName, TopicType: dramatopic.TypeCustom, Sort: valueOrZero(section.Sort), WorkCount: total},
|
||||
Total: total, HasNext: hasNext, List: fillTopicMedias(list, uid),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getSystemTopicWorks(uid uint64, req TopicWorksRequest, topic dramatopic.SystemTopic) (TopicWorksResponse, error) {
|
||||
filter := activeDramaFilter()
|
||||
total, err := mediamod.QueryAllCount(filter)
|
||||
if err != nil {
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
var list []*mediamod.Media
|
||||
var hasNext bool
|
||||
if topic.ID == dramatopic.IDEveryone {
|
||||
list, err = listEveryoneLikes(req.Skip64(), req.Limit64()+1)
|
||||
if err != nil {
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
hasNext = len(list) > int(req.PageSize)
|
||||
if hasNext {
|
||||
list = list[:req.PageSize]
|
||||
}
|
||||
} else {
|
||||
sortBy := bson.D{{Key: "sortCode", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
if topic.ID == dramatopic.IDLatest {
|
||||
sortBy = bson.D{{Key: "latestPublishedAt", Value: -1}, {Key: "createdAt", Value: -1}, {Key: "_id", Value: -1}}
|
||||
}
|
||||
list, err = mediamod.QueryAllList(filter, options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()+1).SetSort(sortBy))
|
||||
if err != nil {
|
||||
return TopicWorksResponse{}, err
|
||||
}
|
||||
hasNext = len(list) > int(req.PageSize)
|
||||
if hasNext {
|
||||
list = list[:req.PageSize]
|
||||
}
|
||||
}
|
||||
return TopicWorksResponse{
|
||||
Topic: TopicSummary{TopicID: topic.ID, Name: topic.Name, TopicType: dramatopic.TypeSystem, SystemKey: topic.SystemKey, Sort: topic.Sort, WorkCount: total},
|
||||
Total: total, HasNext: hasNext, List: fillTopicMedias(list, uid),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fillTopicMedias(list []*mediamod.Media, uid uint64) []*mediamod.AppMediaBase {
|
||||
filled := mediaser.FillMedias(list, uid, true)
|
||||
if filled == nil {
|
||||
return []*mediamod.AppMediaBase{}
|
||||
}
|
||||
return filled
|
||||
}
|
||||
|
||||
func listEveryoneLikes(skip, limit int64) ([]*mediamod.Media, error) {
|
||||
key := fmt.Sprintf("drama:topic:everyone-likes:v1:%d:%d", skip, limit)
|
||||
if appg.Redis != nil {
|
||||
cached, err := appg.Redis.Get(key)
|
||||
if err == nil && cached != nil {
|
||||
var list []*mediamod.Media
|
||||
if jsonErr := json.Unmarshal([]byte(*cached), &list); jsonErr == nil {
|
||||
return list, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
list, err := mediamod.ListActiveDramaByInteractionScore(skip, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if appg.Redis != nil {
|
||||
if data, marshalErr := json.Marshal(list); marshalErr == nil {
|
||||
if cacheErr := appg.Redis.Set(key, data, everyoneLikesCacheTTL); cacheErr != nil {
|
||||
log.Warn("缓存大家爱看短剧列表失败", log.E(cacheErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func activeDramaFilter() bson.M {
|
||||
return bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false}
|
||||
}
|
||||
|
||||
func valueOrZero(value *int) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package dramaser
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"91porn-server/app/service/mediacontentser"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
TabLive = "live"
|
||||
TabDouyin = "douyin"
|
||||
TabAIDrama = "ai_drama"
|
||||
TabHotDrama = "hot_drama"
|
||||
)
|
||||
|
||||
type ChannelTab struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Sort int `json:"sort"`
|
||||
ModuleID string `json:"moduleId"`
|
||||
}
|
||||
|
||||
type ChannelConfig struct {
|
||||
BottomTabTitle string `json:"bottomTabTitle"`
|
||||
DefaultTab string `json:"defaultTab"`
|
||||
AdEnabled bool `json:"adEnabled"`
|
||||
Tabs []ChannelTab `json:"tabs"`
|
||||
}
|
||||
|
||||
type FeedRequest struct {
|
||||
PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=50"`
|
||||
}
|
||||
|
||||
type FeedItem struct {
|
||||
Media *mediamod.AppMediaBase `json:"media"`
|
||||
Content mediacontentser.AppMediaContent `json:"content"`
|
||||
}
|
||||
|
||||
type FeedResponse struct {
|
||||
QueueVersion string `json:"queueVersion"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []FeedItem `json:"list"`
|
||||
}
|
||||
|
||||
type ListRequest struct {
|
||||
commod.Page
|
||||
SortType int `form:"sortType" json:"sortType" binding:"required,oneof=1 2 3 4"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*mediamod.AppMediaBase `json:"list"`
|
||||
}
|
||||
|
||||
type AnalyticsEvent struct {
|
||||
EventID string `json:"eventId" binding:"required,max=128"`
|
||||
EventName string `json:"eventName" binding:"required"`
|
||||
SessionID string `json:"sessionId" binding:"required,max=128"`
|
||||
OccurredAt time.Time `json:"occurredAt" binding:"required"`
|
||||
MediaID primitive.ObjectID `json:"mediaId" binding:"required"`
|
||||
ContentID primitive.ObjectID `json:"contentId"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Scene string `json:"scene" binding:"required"`
|
||||
PlayMode string `json:"playMode"`
|
||||
Progress int `json:"progressSeconds"`
|
||||
Duration int `json:"durationSeconds"`
|
||||
QueueVersion string `json:"queueVersion"`
|
||||
Position int `json:"position"`
|
||||
CheckoutContextID string `json:"checkoutContextId"`
|
||||
}
|
||||
|
||||
type EventsRequest struct {
|
||||
Events []AnalyticsEvent `json:"events" binding:"required,min=1,max=50,dive"`
|
||||
}
|
||||
|
||||
type EventsResponse struct {
|
||||
Accepted int `json:"accepted"`
|
||||
Duplicated int `json:"duplicated"`
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package emailser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
|
||||
"github.com/badoux/checkmail"
|
||||
)
|
||||
|
||||
// CheckEmail 校验邮件地址是否合法
|
||||
func CheckEmail(ctx context.Context, emailStr string) stderr.Code {
|
||||
if emailStr == "" {
|
||||
return stderr.ErrInvalidEmail
|
||||
}
|
||||
if err := checkmail.ValidateFormat(emailStr); err != nil {
|
||||
log.ErrorX(ctx, "invalid email", log.Any("email", emailStr), log.E(err))
|
||||
return stderr.ErrInvalidEmail
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// VerifyCaptchaCode 核对验证码
|
||||
func VerifyCaptchaCode(ctx context.Context, emailAddr, code string) stderr.Code {
|
||||
emailAddr = strings.TrimSpace(emailAddr)
|
||||
if strings.HasSuffix(emailAddr, "@dsp.xyz") { // pass the test email
|
||||
log.InfoX(ctx, "VerifyCaptchaCode dismiss test email", log.Any("email", emailAddr))
|
||||
return stderr.Success
|
||||
}
|
||||
if code := CheckEmail(ctx, emailAddr); code != stderr.Success {
|
||||
return code
|
||||
}
|
||||
redisKey := redisconst.GetEmailCaptchaKey(emailAddr)
|
||||
redisCode, err := appg.Redis.Get(redisKey)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "VerifyCaptchaCode redis get err", log.Any("emailAddr", emailAddr),
|
||||
log.Any("redisKey", redisKey), log.E(err))
|
||||
return stderr.ErrNetWorkBusy
|
||||
}
|
||||
if redisCode == nil {
|
||||
log.InfoX(ctx, "VerifyCaptchaCode nil code in redis", log.Any("emailAddr", emailAddr), log.Any("code", code))
|
||||
return stderr.ErrCaptcha
|
||||
}
|
||||
if *redisCode != code {
|
||||
log.InfoX(ctx, "VerifyCaptchaCode invalid", log.Any("emailAddr", emailAddr), log.Any("code", code),
|
||||
log.Any("redisCode", redisCode))
|
||||
return stderr.ErrCaptchaInvalid
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package exchcodeser
|
||||
|
||||
import (
|
||||
"91porn-server/models/v/backpackmod"
|
||||
"91porn-server/models/v/videocoupon"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/l/exchlogmod"
|
||||
"91porn-server/models/v/exchcodemod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
)
|
||||
|
||||
// 兑换码兑换
|
||||
func CodeExchange(uid uint64, exchangeCode string) (code stderr.Code, data interface{}, err error) {
|
||||
// 获取该兑换码
|
||||
cMod, err := exchcodemod.GetExchangeCodeByCode(exchangeCode)
|
||||
if err != nil || cMod == nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
return
|
||||
}
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
code = stderr.UserIsNotExists
|
||||
return
|
||||
}
|
||||
if cMod.Channel == "XUA01" && cMod.Channel != u.DistrictCode {
|
||||
code = stderr.ExchangeCodeInvalidAuthority
|
||||
return
|
||||
}
|
||||
// 无该兑换码或者不是此app的兑换码
|
||||
if cMod == nil || cMod.App != exchcodemod.AppTypeYSVideo || cMod.Authority == exchcodemod.AuthorityFilmVip {
|
||||
code = stderr.ExchangeCodeInvalidCode
|
||||
return
|
||||
}
|
||||
var now = time.Now()
|
||||
// 未生效
|
||||
if cMod.EffectiveAt.After(now) {
|
||||
code = stderr.ExchangeCodeIsNotEffective
|
||||
return
|
||||
}
|
||||
// 已过期
|
||||
if cMod.InvalidAt.Before(now) {
|
||||
code = stderr.ExchangeCodeIsExpired
|
||||
return
|
||||
}
|
||||
//使用次数已满
|
||||
if cMod.UsableNum <= 0 {
|
||||
code = stderr.ExchangeCodeInvalidCode
|
||||
return
|
||||
}
|
||||
// 同一个的兑换码不能兑换两次
|
||||
lMod, err := exchlogmod.GetLogByUIDAndCode(uid, cMod.Code)
|
||||
if err != nil || (!lMod.ID.IsZero() && cMod.BatchNum != "YS-dzwj") {
|
||||
code = stderr.ExchangeCodeRepeatExchange
|
||||
return
|
||||
}
|
||||
// 兑换
|
||||
if code, err = exchange(uid, cMod); err != nil || code != stderr.Success {
|
||||
return
|
||||
}
|
||||
return doReward(uid, cMod.Authority, cMod.Reward, cMod.RewardCount)
|
||||
}
|
||||
|
||||
// 兑换并记录日志
|
||||
func exchange(uid uint64, cMod *exchcodemod.ExchCode) (code stderr.Code, err error) {
|
||||
if err = exchcodemod.Exchange(cMod.Code); err != nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
return
|
||||
}
|
||||
// 插入日志
|
||||
var logMod = exchlogmod.ExchangeLog{
|
||||
Code: cMod.Code,
|
||||
UserID: uid,
|
||||
BatchNum: cMod.BatchNum,
|
||||
Channel: cMod.Channel,
|
||||
Authority: cMod.Authority,
|
||||
Reward: cMod.Reward,
|
||||
RewardCount: cMod.RewardCount,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err = exchlogmod.Insert(logMod); err != nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
return
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// 发放奖品
|
||||
func doReward(uid uint64, eType exchcodemod.AuthorityType, reward int, rewardCount int) (code stderr.Code, data interface{}, err error) {
|
||||
// 兼容之前的数据
|
||||
if rewardCount == 0 {
|
||||
rewardCount = 1
|
||||
}
|
||||
now := time.Now()
|
||||
// 获取用户信息
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
code = stderr.UserIsNotExists
|
||||
return
|
||||
}
|
||||
switch eType {
|
||||
case exchcodemod.AuthorityGold:
|
||||
if _, err := walletmod.CreditAmount(nil, int64(reward*rewardCount), uid); err != nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
return code, nil, err
|
||||
}
|
||||
case exchcodemod.AuthorityShortVideoVip, exchcodemod.AuthorityFilmVip, exchcodemod.AuthoritySuper: // vip增加天数
|
||||
var now = time.Now()
|
||||
var vipExpireDate = time.Time{}
|
||||
if u.VipExpireDate.Before(now) {
|
||||
vipExpireDate = now.AddDate(0, 0, reward*rewardCount)
|
||||
} else {
|
||||
vipExpireDate = u.VipExpireDate.AddDate(0, 0, reward*rewardCount)
|
||||
}
|
||||
vipLevel := usermod.RenewVIPLevel(u.VipExpireDate, now, u.VipLevel, 1)
|
||||
var update = usermod.UserSelector{VipExpireDate: &vipExpireDate, VipLevel: &vipLevel}
|
||||
if u, err = usermod.Update(uid, update); err != nil || u == nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
return code, nil, err
|
||||
}
|
||||
data = u
|
||||
case exchcodemod.Authority3dPermanentVIP:
|
||||
s := usermod.ThreeDayVIP_SnapVip
|
||||
permanentVIPLevel := 2
|
||||
permanentVIPExpireDate := now.AddDate(30, 0, 0)
|
||||
if u, err := usermod.UpdateSnapVip(u.UID, usermod.UserSelector{SnapVip: &s, VipLevel: &permanentVIPLevel, VipExpireDate: &permanentVIPExpireDate, OriginVipLevel: &u.VipLevel, OriginVipExpire: &u.VipExpireDate}); err != nil || u == nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
return code, nil, err
|
||||
}
|
||||
case exchcodemod.AuthorityVideoCoupon: // 观影券
|
||||
for i := 0; i < rewardCount; i++ {
|
||||
videoCoupon := videocoupon.UserGoldVideoCoupon{
|
||||
UID: uid,
|
||||
Num: reward,
|
||||
Used: false,
|
||||
Source: videocoupon.GoldVideoCouponSourceExchange,
|
||||
UpdatedAt: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := videocoupon.InsertOne(videoCoupon); err != nil {
|
||||
code = stderr.ExchangeCodeExchangeFailed
|
||||
continue
|
||||
}
|
||||
backpack := backpackmod.Backpack{
|
||||
UID: uid,
|
||||
GoodsType: backpackmod.ExchangeCoupon,
|
||||
GoodsValue: int64(videoCoupon.Num),
|
||||
GoodsOrigin: string(videocoupon.GoldVideoCouponSourceExchange),
|
||||
Status: backpackmod.Unused,
|
||||
ExpiredTime: now.AddDate(0, 0, 7),
|
||||
CreateTime: now,
|
||||
}
|
||||
backpackmod.AddGoods(nil, uid, backpack)
|
||||
}
|
||||
default:
|
||||
code = stderr.ExchangeCodeInvalidCode
|
||||
return
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
// 查询用户兑换记录
|
||||
func UserExchageRecord(uid uint64, page commod.Page) (stderr.Code, interface{}, error) {
|
||||
total, data, err := exchlogmod.GetExchangeLogList(exchlogmod.FilterDoc{
|
||||
UserID: &uid,
|
||||
}, page)
|
||||
if err != nil {
|
||||
return stderr.ExchangeCodeExchangeFailed, nil, nil
|
||||
}
|
||||
for i := range data {
|
||||
// 兼容兑换量
|
||||
if data[i].RewardCount == 0 {
|
||||
data[i].RewardCount = 1
|
||||
}
|
||||
switch data[i].Authority {
|
||||
case exchcodemod.AuthorityShortVideoVip:
|
||||
data[i].Desc = "视频VIPx" + strconv.Itoa(data[i].RewardCount)
|
||||
case exchcodemod.AuthorityFilmVip:
|
||||
data[i].Desc = "影视VIP(影院)" + strconv.Itoa(data[i].RewardCount)
|
||||
case exchcodemod.AuthoritySuper:
|
||||
data[i].Desc = "超级VIPx" + strconv.Itoa(data[i].RewardCount)
|
||||
case exchcodemod.AuthorityGold:
|
||||
data[i].Desc = "兑换金币x" + strconv.Itoa(data[i].Reward*data[i].RewardCount)
|
||||
case exchcodemod.Authority3dPermanentVIP:
|
||||
data[i].Desc = "3天VIPx" + strconv.Itoa(data[i].Reward*data[i].RewardCount)
|
||||
case exchcodemod.AuthorityVideoCoupon:
|
||||
data[i].Desc = "观影券" + strconv.Itoa(data[i].Reward) + "x" + strconv.Itoa(data[i].RewardCount) + "张"
|
||||
}
|
||||
}
|
||||
return stderr.Success, struct {
|
||||
Data []exchlogmod.ExchangeLog `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
}{
|
||||
Data: data,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package feedbackser
|
||||
|
||||
import (
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/feedbackmod"
|
||||
)
|
||||
|
||||
type FeedbackReq struct {
|
||||
CateInfo string `json:"cateInfo" binding:"lte=20"` // 问题分类 限制10个字
|
||||
Desc string `json:"desc" binding:"required,lte=100"` // 描述 限制50个字
|
||||
Region string `json:"region" binding:"lte=30"` // 地区 限制10个字
|
||||
DevInfo string `json:"devInfo" binding:"lte=20"` // 设备信息 限制10个字
|
||||
ISP string `json:"ISP" binding:"lte=20"` // 运营商 限制10个字
|
||||
Contact string `json:"contact" binding:"lte=30"` // 联系方式 限制20个字
|
||||
Images []string `json:"images"` // 图片信息
|
||||
}
|
||||
|
||||
func (q FeedbackReq) Submit(ua ua.UA, uid uint64) (code stderr.Code, err error) {
|
||||
var cInfo = feedbackmod.ContentInfo{
|
||||
CateInfo: q.CateInfo,
|
||||
Desc: q.Desc,
|
||||
Region: q.Region,
|
||||
DevInfo: q.DevInfo,
|
||||
ISP: q.ISP,
|
||||
Contact: q.Contact,
|
||||
Images: q.Images,
|
||||
}
|
||||
return feedbackmod.FeedBackInsert(feedbackmod.FeedBack{UID: uid, Content: cInfo, Version: ua.Ver, DevType: ua.DevType})
|
||||
}
|
||||
|
||||
// GetFeedBackList 获取反馈列表
|
||||
func GetFeedBackList(page commod.Page, uid uint64) (data commod.ListResp, err error) {
|
||||
var feedbacks []feedbackmod.FeedBack
|
||||
feedbacks, data.HasNext, err = feedbackmod.GetFeedBackList(page.PageSize, page.PageNumber, uid)
|
||||
data.List = feedbacks
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package filterser
|
||||
|
||||
import (
|
||||
"91porn-server/common/filter"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
func TextFilter(content string) (string, error) {
|
||||
if filter.WordFilter == nil {
|
||||
return content, nil
|
||||
}
|
||||
dst, err := filter.WordFilter.Replace(content, filter.ReplaceMark)
|
||||
if err != nil {
|
||||
log.Error("service filter TextFilter Replace error", log.E(err))
|
||||
}
|
||||
return dst, err
|
||||
}
|
||||
|
||||
// CheckIllegalWords checks if user's input has illegal words.
|
||||
// Return non-zero when there are illegal words, return err when something unexpected happened.
|
||||
func CheckIllegalWords(content string) (int, error) {
|
||||
if filter.WordFilter == nil {
|
||||
return 1, nil
|
||||
}
|
||||
illegalWords, err := filter.WordFilter.Filter(content)
|
||||
return len(illegalWords), err
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package followser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/vidhelpser"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/services/follow"
|
||||
"91porn-server/common/stderr"
|
||||
topser "91porn-server/common/top"
|
||||
"91porn-server/common/top/weektop"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/followmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
goredis "github.com/go-redis/redis"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func encodeUserBase(uids []uint64, uInfo []*usermod.BaseInfoVip, mModel map[uint64]followmod.FollowModel, mFollow map[uint64]bool) []*followmod.BaseInfo {
|
||||
var infos []*followmod.BaseInfo
|
||||
mInfo := make(map[uint64]usermod.BaseInfoVip)
|
||||
for _, u := range uInfo {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
mInfo[u.UID] = *u
|
||||
}
|
||||
for _, u := range uids {
|
||||
bi := followmod.BaseInfo{
|
||||
UID: u,
|
||||
Name: mInfo[u].Name,
|
||||
Gender: mInfo[u].Gender,
|
||||
Portrait: mInfo[u].Portrait,
|
||||
HasLocked: mInfo[u].HasLocked,
|
||||
HasBanned: mInfo[u].HasBanned,
|
||||
VipLevel: mInfo[u].VipLevel,
|
||||
IsVip: mInfo[u].IsVip,
|
||||
HasFollow: true,
|
||||
Via: mModel[u].Via,
|
||||
Fans: mInfo[u].Fans,
|
||||
Summary: mInfo[u].Summary,
|
||||
CreatedAt: mModel[u].CreatedAt,
|
||||
SuperUser: mInfo[u].SuperUser,
|
||||
Awards: mInfo[u].Awards,
|
||||
TotalWorks: mInfo[u].TotalWorks,
|
||||
}
|
||||
infos = append(infos, &bi)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// GetHisFollowList 获取他关注列表
|
||||
func GetHisFollowList(uid, oid uint64, page int, size int) (code stderr.Code, data interface{}) {
|
||||
uids, mModel, hasNext, err := followmod.GetFollowList(oid, page, size, false)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
uInfos, err := usermod.GetUsersBaseInfoWithVip(uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
mFollow, err := followmod.IsFollowedByUsers(uid, uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
datas := encodeUserBase(uids, uInfos, mModel, mFollow)
|
||||
data = followmod.ListResp{List: datas, HasNext: hasNext}
|
||||
return stderr.Success, data
|
||||
}
|
||||
|
||||
// GetFollowList 获取关注列表
|
||||
func GetFollowList(uid uint64, page int, size int, isShort bool) (code stderr.Code, data interface{}) {
|
||||
uids, mModel, hasNext, err := followmod.GetFollowList(uid, page, size, isShort)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
uInfos, err := usermod.GetUsersBaseInfoWithVip(uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
mFollow, err := followmod.IsFollowedByUsers(uid, uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
datas := encodeUserBase(uids, uInfos, mModel, mFollow)
|
||||
data = followmod.ListResp{List: datas, HasNext: hasNext}
|
||||
return stderr.Success, data
|
||||
}
|
||||
|
||||
// GetHisFansList 获取他的粉丝列表
|
||||
func GetHisFansList(uid, oid uint64, page int, size int) (code stderr.Code, data interface{}) {
|
||||
uids, mModel, hasNext, err := followmod.GetFansList(oid, page, size)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
uInfos, err := usermod.GetUsersBaseInfoWithVip(uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
mFollow, err := followmod.IsFollowUsers(uid, uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
datas := encodeUserBase(uids, uInfos, mModel, mFollow)
|
||||
data = followmod.ListResp{List: datas, HasNext: hasNext}
|
||||
return stderr.Success, data
|
||||
}
|
||||
|
||||
// GetFansList 获取粉丝列表
|
||||
func GetFansList(uid uint64, page int, size int) (code stderr.Code, data interface{}) {
|
||||
uids, mModel, hasNext, err := followmod.GetFansList(uid, page, size)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
uInfos, err := usermod.GetUsersBaseInfoWithVip(uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
mFollow, err := followmod.IsFollowUsers(uid, uids)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
datas := encodeUserBase(uids, uInfos, mModel, mFollow)
|
||||
data = followmod.ListResp{List: datas, HasNext: hasNext}
|
||||
return stderr.Success, data
|
||||
}
|
||||
|
||||
// GetDynamicsList 获取关注用户动态
|
||||
func GetDynamicsList(uid uint64, req followmod.AppDynamicsListReq) (data followmod.DynamicsResp, code stderr.Code) {
|
||||
// 处理用户关注消息状态
|
||||
_ = follow.CancelTip(uid)
|
||||
var infos []*vidmod.VideoModel
|
||||
|
||||
// 获取官方帖子
|
||||
//vid, err := vidmod.FindOneByUid(bson.M{"publisherID": 100000, "newsType": "COVER", "status": 1})
|
||||
//if err != nil {
|
||||
// return stderr.ErrDbQueryError, nil
|
||||
//}
|
||||
//
|
||||
//if vid != nil && !vid.ID.IsZero() {
|
||||
// infos = append(infos, vid)
|
||||
//}
|
||||
|
||||
uids, err := followmod.GetTotalFollowList(uid)
|
||||
if err != nil {
|
||||
return data, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
//if len(infos) > 0 {
|
||||
// list := vidhelpser.EncodeVideoInfo(uid, infos)
|
||||
// data = followmod.DynamicsResp{VInfos: list, List: list, TotalPages: 1, HasNext: false}
|
||||
//} else {
|
||||
//data = followmod.DynamicsResp{VInfos: nil, List: nil, HasNext: false, TotalPages: 0}
|
||||
//}
|
||||
return data, stderr.Success
|
||||
}
|
||||
filter := bson.M{
|
||||
"publisherID": bson.M{"$in": uids},
|
||||
"newsType": vidmod.SHORT, // 默认短视频
|
||||
"status": vidmod.CheckPass,
|
||||
}
|
||||
if req.NewsType != "" {
|
||||
filter["newsType"] = req.NewsType
|
||||
}
|
||||
sort := bson.D{{Key: "reviewAt", Value: -1}}
|
||||
opt := options.Find().SetSkip(req.Skip64()).SetLimit(req.Limit64()).SetSort(sort)
|
||||
videos, hasNext, err := vidmod.FindList(filter, opt)
|
||||
if err != nil {
|
||||
return data, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(videos) == 0 {
|
||||
//if len(infos) > 0 {
|
||||
// list := vidhelpser.EncodeVideoInfo(uid, infos)
|
||||
// data = followmod.DynamicsResp{VInfos: list, List: list, TotalPages: 1, HasNext: false}
|
||||
//} else {
|
||||
//data = followmod.DynamicsResp{VInfos: nil, List: nil, HasNext: false, TotalPages: 0}
|
||||
//}
|
||||
return data, stderr.Success
|
||||
}
|
||||
infos = append(infos, videos...)
|
||||
list := vidhelpser.EncodeVideoInfo(uid, infos)
|
||||
data = followmod.DynamicsResp{VInfos: list, List: list, TotalPages: 0, HasNext: hasNext}
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
// 短视频-获取关注用户和作品,若无关注,则获取推荐用户及作品
|
||||
type (
|
||||
GetFollowUpUsersWithShortReq struct {
|
||||
commod.Page
|
||||
ShowType int `json:"showType" form:"showType"` //0:优先获取已关注没有则获取推荐 1:已关注 2:推荐
|
||||
}
|
||||
GetFollowUpUsersWithShortRep struct {
|
||||
RecoUsers []*RecoUser `json:"recoUsers"` // 官方推荐(没有关注用户则使用此数据)
|
||||
//FollowUsers []*UserInfo `json:"followUsers"` // 已关注的博主列表
|
||||
List []*vidmod.VideoInfo `json:"list"` // 已关注的博主发布的视频列表
|
||||
ShowType int `json:"showType"` // 1:已关注 2:推荐
|
||||
HasNext bool `json:"hasNext"` // 是否有下一页
|
||||
}
|
||||
UserInfo struct {
|
||||
Uid uint64 `json:"uid" bson:"uid"` //UID
|
||||
Gender string `json:"gender" bson:"gender"` //性别
|
||||
Name string `json:"name" bson:"name"` //姓名
|
||||
Portrait string `json:"portrait" bson:"portrait"` //头像
|
||||
Fans int64 `json:"fans" bson:"fans"` //粉丝数
|
||||
Summary string `json:"summary" bson:"summary"` //简介
|
||||
OfficialCert bool `json:"officialCert" bson:"officialCert"` //是否官方认证
|
||||
|
||||
IsFollow bool `json:"isFollow"` //是否关注
|
||||
TotalWorks int64 `json:"totalWorks" bson:"totalWorks"` // 总作品数
|
||||
}
|
||||
RecoUser struct {
|
||||
UserInfo UserInfo `json:"userInfo"` // 用户信息
|
||||
List []*vidmod.VideoBase `json:"list"` // 用户发布的视频信息
|
||||
}
|
||||
)
|
||||
|
||||
// GetFollowUpUserListWithShort 获取用户关注的UP主短视频 或 短视频推荐UP主以及UP主发布的短视频
|
||||
func (req *GetFollowUpUsersWithShortReq) GetFollowUpUserListWithShort(uid uint64) (data GetFollowUpUsersWithShortRep, code stderr.Code) {
|
||||
if req.ShowType <= 1 {
|
||||
data, code = req.GetFollowUpUserListAndShortVideo(uid)
|
||||
if code != stderr.Success || data.ShowType == 1 {
|
||||
return data, code
|
||||
}
|
||||
}
|
||||
// 短视频推荐
|
||||
data, code = req.getRecoUserByShortVideo()
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
// GetFollowUpUserListAndShortVideo 获取关注用户和用户下的短视频
|
||||
func (req *GetFollowUpUsersWithShortReq) GetFollowUpUserListAndShortVideo(uid uint64) (data GetFollowUpUsersWithShortRep, code stderr.Code) {
|
||||
// 已关注的用户列表不需要展示
|
||||
var uids []uint64
|
||||
uids, err := followmod.GetTotalFollowListLimit(uid, 30)
|
||||
if err != nil {
|
||||
return data, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
//users, err := usermod.FindUsersMapByUID(uids)
|
||||
//if err != nil {
|
||||
// return data, stderr.Failure
|
||||
//}
|
||||
//
|
||||
//if len(users) == 0 {
|
||||
// return data, stderr.Success
|
||||
//}
|
||||
//
|
||||
//for _, user := range users {
|
||||
// data.FollowUsers = append(data.FollowUsers, &UserInfo{
|
||||
// Uid: user.UID,
|
||||
// Gender: user.Gender,
|
||||
// Name: user.Name,
|
||||
// Portrait: user.Portrait,
|
||||
// Fans: user.Fans,
|
||||
// OfficialCert: user.OfficialCert,
|
||||
// IsFollow: true,
|
||||
// Summary: user.Summary,
|
||||
// TotalWorks: user.TotalWorks,
|
||||
// })
|
||||
//}
|
||||
cond := bson.M{"newsType": vidmod.SHORT, "publisherID": bson.M{"$in": uids}, "status": vidmod.CheckPass} // 下层函数已经设置过滤status
|
||||
sort := bson.D{{Key: "reviewAt", Value: -1}}
|
||||
videos, hasNext, err := vidmod.FindList(cond, options.Find().SetSort(sort).SetLimit(req.Limit64()).SetSkip(req.Skip64()))
|
||||
if err != nil {
|
||||
return data, stderr.Failure
|
||||
}
|
||||
|
||||
list := vidhelpser.EncodeVideoInfo(uid, videos)
|
||||
data.List = list
|
||||
data.HasNext = hasNext
|
||||
data.ShowType = 1 // 当前是关注用户
|
||||
|
||||
return data, stderr.Success
|
||||
}
|
||||
|
||||
// getRecoUserByShortVideo 获取推荐用户
|
||||
func (req *GetFollowUpUsersWithShortReq) getRecoUserByShortVideo() (data GetFollowUpUsersWithShortRep, code stderr.Code) {
|
||||
|
||||
req.PageSize = 6 // 短视频推荐每页固定条数
|
||||
key := fmt.Sprintf("recoUserAndShortVideo:%d_%d", req.PageSize, req.PageNumber)
|
||||
err := appg.Redis.GetWithJson2Any(key, &data)
|
||||
if err == nil {
|
||||
return data, stderr.Success
|
||||
}
|
||||
if !errors.Is(err, goredis.Nil) {
|
||||
log.Warn("GetFollowUpUserPublishVideosReq.getRecoVideoList error", log.E(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = appg.Redis.SetWithAny2Json(key, data, time.Minute*5)
|
||||
}()
|
||||
|
||||
recoUsers, _, hasNext := weektop.GetTopByPage(topser.TypeRecommendUser, req.Limit64(), req.Limit64()+1)
|
||||
if len(recoUsers) > int(req.Limit64()) {
|
||||
recoUsers = recoUsers[:req.Limit64()]
|
||||
hasNext = true
|
||||
}
|
||||
if len(recoUsers) == 0 {
|
||||
return data, stderr.Success
|
||||
}
|
||||
var (
|
||||
dataMap = map[uint64]*RecoUser{}
|
||||
uids []uint64
|
||||
)
|
||||
|
||||
for _, v := range recoUsers {
|
||||
uid, e := strconv.ParseUint(v, 10, 64)
|
||||
if e != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
users, err := usermod.FindUsersByUID(uids)
|
||||
if err != nil {
|
||||
return data, stderr.Failure
|
||||
}
|
||||
|
||||
uids = uids[:0]
|
||||
for _, user := range users {
|
||||
recoUser := &RecoUser{
|
||||
UserInfo: UserInfo{
|
||||
Uid: user.UID,
|
||||
Gender: user.Gender,
|
||||
Name: user.Name,
|
||||
Portrait: user.Portrait,
|
||||
Fans: user.Fans,
|
||||
Summary: user.Summary,
|
||||
OfficialCert: user.OfficialCert,
|
||||
TotalWorks: user.TotalWorks,
|
||||
},
|
||||
}
|
||||
dataMap[user.UID] = recoUser
|
||||
// 获取用户的视频
|
||||
filterTmp := bson.M{"newsType": vidmod.SP, "publisherID": user.UID, "status": vidmod.CheckPass}
|
||||
optTmp := options.Find().SetSort(bson.D{{"reviewAt", -1}}).SetLimit(6)
|
||||
list, _, err := vidmod.FindList(filterTmp, optTmp)
|
||||
if err != nil {
|
||||
return data, stderr.Failure
|
||||
}
|
||||
for _, model := range list {
|
||||
baseVid := vidhelpser.ToVideoBaseInfo(model, nil, 0)
|
||||
recoUser.List = append(recoUser.List, baseVid)
|
||||
}
|
||||
data.RecoUsers = append(data.RecoUsers, recoUser)
|
||||
}
|
||||
|
||||
data.HasNext = hasNext
|
||||
data.ShowType = 2 // 当前是推荐用户
|
||||
|
||||
return data, stderr.Success
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
package health_check_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PingReq struct {
|
||||
GroupId string `json:"groupId"` // 平台分组ID
|
||||
Timestamp int64 `json:"timestamp"` // Unix时间戳(秒)
|
||||
Sign string `json:"sign"` // HMAC-SHA256签名
|
||||
}
|
||||
|
||||
// Ping 服务健康检测
|
||||
func (p *PingReq) Ping() (ret SystemStatus, err error) {
|
||||
str, err := appg.Redis.Get(redisconst.MonitorCacheKey)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("缓存获取服务健康检测信息异常:%v", err))
|
||||
}
|
||||
if str != nil {
|
||||
var resp SystemStatus
|
||||
if err = json.Unmarshal([]byte(*str), &resp); err == nil {
|
||||
resp.Timestamp = time.Now().Unix()
|
||||
return resp, nil
|
||||
}
|
||||
log.Warn(fmt.Sprintf("解析缓存服务健康检测信息数据异常:%v", err))
|
||||
}
|
||||
|
||||
// 校验请求信息
|
||||
if err = p.checkInfo(&ret); err != nil {
|
||||
return ret, fmt.Errorf("校验请求信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取服务器基本信息
|
||||
if err = p.getServerInfo(&ret); err != nil {
|
||||
return ret, fmt.Errorf("获取服务器信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取硬件资源信息
|
||||
if err = p.getHardwareInfo(&ret); err != nil {
|
||||
return ret, fmt.Errorf("获取硬件信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查各项服务状态
|
||||
p.getServiceStatus(&ret)
|
||||
|
||||
common.Go(func() {
|
||||
bytes, _ := json.Marshal(ret)
|
||||
if err = appg.Redis.Set(redisconst.MonitorCacheKey, bytes, redisconst.GetMonitorCacheExpired()); err != nil {
|
||||
log.Warn(fmt.Sprintf("保存缓存数据异常:%v", err))
|
||||
}
|
||||
})
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// checkInfo 获取服务器请求信息
|
||||
func (p *PingReq) checkInfo(status *SystemStatus) error {
|
||||
// 1、验证时间戳有效性(允许±5分钟误差)
|
||||
currentTime := time.Now().Unix()
|
||||
timeDiff := absInt64(currentTime - p.Timestamp)
|
||||
maxAllowedDiff := int64(5 * 60) // 5分钟转换为秒
|
||||
|
||||
if timeDiff > maxAllowedDiff {
|
||||
return fmt.Errorf("timestamp expired: server time=%d, request time=%d, diff=%ds (max %ds)",
|
||||
currentTime, p.Timestamp, timeDiff, maxAllowedDiff)
|
||||
}
|
||||
|
||||
// 2、验证签名
|
||||
if err := p.verifySignature(); err != nil {
|
||||
return fmt.Errorf("signature verification failed: %v", err)
|
||||
}
|
||||
|
||||
// 返回赋值
|
||||
status.Timestamp = time.Now().Unix()
|
||||
return nil
|
||||
}
|
||||
|
||||
// absInt64 计算int64绝对值
|
||||
func absInt64(n int64) int64 {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// verifySignature 验证HMAC-SHA256签名
|
||||
func (p *PingReq) verifySignature() error {
|
||||
// 构建签名:groupId + ":" + timestamp
|
||||
signatureBase := fmt.Sprintf("%s:%d", p.GroupId, p.Timestamp)
|
||||
|
||||
// 计算HMAC-SHA256
|
||||
mac := hmac.New(sha256.New, []byte(appg.Conf.MonitorReport.SecretKey))
|
||||
mac.Write([]byte(signatureBase))
|
||||
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
// 比较签名(使用常量时间比较防止时序攻击)
|
||||
if !hmac.Equal([]byte(p.Sign), []byte(expectedSignature)) {
|
||||
log.Error(fmt.Sprintf("verifySignature err,sign:%v, chenckSign:%v", p.Sign, expectedSignature))
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServerInfo 获取服务器基本信息
|
||||
func (p *PingReq) getServerInfo(status *SystemStatus) error {
|
||||
// 获取项目名称
|
||||
status.Name = commod.KFK_APP_NAME
|
||||
|
||||
// 获取本机IP地址
|
||||
ip, err := getLocalIP()
|
||||
if err != nil {
|
||||
status.Ip = "127.0.0.1"
|
||||
} else {
|
||||
status.Ip = ip
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLocalIP 获取本机非回环IP地址
|
||||
func getLocalIP() (string, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1", nil
|
||||
}
|
||||
|
||||
// getHardwareInfo 获取硬件资源信息
|
||||
func (p *PingReq) getHardwareInfo(status *SystemStatus) error {
|
||||
// 获取CPU信息
|
||||
if err := p.getCPUInfo(&status.Cpu); err != nil {
|
||||
status.Cpu.Cores = runtime.NumCPU()
|
||||
status.Cpu.Used = 0.0
|
||||
}
|
||||
|
||||
// 获取内存信息
|
||||
if err := p.getMemoryInfo(&status.Memory); err != nil {
|
||||
// 设置默认值或使用runtime信息
|
||||
status.Memory.Total = 0
|
||||
status.Memory.Used = 0
|
||||
}
|
||||
|
||||
// 获取磁盘信息
|
||||
if err := p.getDiskInfo(&status.Disk); err != nil {
|
||||
status.Disk.Total = 0
|
||||
status.Disk.Used = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCPUInfo 获取CPU信息(跨平台实现)
|
||||
func (p *PingReq) getCPUInfo(cpu *CpuStatus) error {
|
||||
cpu.Cores = runtime.NumCPU()
|
||||
|
||||
// 根据操作系统获取CPU使用率
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
cpu.Used, err = getLinuxCPUUsage()
|
||||
case "darwin": // macOS
|
||||
cpu.Used, err = getDarwinCPUUsage()
|
||||
case "windows":
|
||||
cpu.Used, err = getWindowsCPUUsage()
|
||||
default:
|
||||
cpu.Used = 0.0
|
||||
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cpu.Used = 0.0
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLinuxCPUUsage 获取Linux系统CPU使用率
|
||||
func getLinuxCPUUsage() (float64, error) {
|
||||
// 读取/proc/stat文件获取CPU信息
|
||||
data, err := ioutil.ReadFile("/proc/stat")
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "cpu ") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 8 {
|
||||
var total, idle uint64
|
||||
for i := 1; i < len(fields); i++ {
|
||||
val, _ := strconv.ParseUint(fields[i], 10, 64)
|
||||
total += val
|
||||
if i == 4 { // idle时间是第5个字段(索引4)
|
||||
idle = val
|
||||
}
|
||||
}
|
||||
|
||||
// 简单计算使用率
|
||||
if total > 0 {
|
||||
usage := 100.0 * (float64(total-idle) / float64(total))
|
||||
return usage, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
// getDarwinCPUUsage 获取macOS系统CPU使用率
|
||||
func getDarwinCPUUsage() (float64, error) {
|
||||
// 使用top命令获取CPU使用率
|
||||
cmd := exec.Command("top", "-l", "1", "-n", "0")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "CPU usage") {
|
||||
// 解析CPU使用率,例如:CPU usage: 16.34% user, 16.98% sys, 66.66% idle
|
||||
usage, err := parseTopCPUUsageEnhanced(line)
|
||||
if err != nil {
|
||||
return 30, nil
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
func parseTopCPUUsageEnhanced(line string) (float64, error) {
|
||||
// 移除多余的空格
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// 处理不同格式:
|
||||
// 1. "CPU usage: 16.34% user, 16.98% sys, 66.66% idle"
|
||||
// 2. "CPU Usage: 25% user, 10% sys, 65% idle"
|
||||
// 3. "CPU: 10.5% user, 5.2% sys, 84.3% idle"
|
||||
|
||||
// 查找user和sys的百分比
|
||||
userRe := regexp.MustCompile(`(\d+\.?\d*)%\s*user`)
|
||||
sysRe := regexp.MustCompile(`(\d+\.?\d*)%\s*sys`)
|
||||
|
||||
var user, sys float64
|
||||
var err error
|
||||
|
||||
if userMatch := userRe.FindStringSubmatch(line); userMatch != nil {
|
||||
user, err = strconv.ParseFloat(userMatch[1], 64)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
}
|
||||
|
||||
if sysMatch := sysRe.FindStringSubmatch(line); sysMatch != nil {
|
||||
sys, err = strconv.ParseFloat(sysMatch[1], 64)
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了user和sys,计算总和
|
||||
if user > 0 || sys > 0 {
|
||||
cpuUsage := user + sys
|
||||
|
||||
// 验证合理性
|
||||
if cpuUsage >= 0 && cpuUsage <= 100 {
|
||||
return cpuUsage, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 备选:尝试匹配idle,然后计算100-idle
|
||||
idleRe := regexp.MustCompile(`(\d+\.?\d*)%\s*idle`)
|
||||
if idleMatch := idleRe.FindStringSubmatch(line); idleMatch != nil {
|
||||
idle, err := strconv.ParseFloat(idleMatch[1], 64)
|
||||
if err == nil && idle >= 0 && idle <= 100 {
|
||||
return 100 - idle, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, fmt.Errorf("unable to parse CPU usage from line: %s", line)
|
||||
}
|
||||
|
||||
// getWindowsCPUUsage 获取Windows系统CPU使用率
|
||||
func getWindowsCPUUsage() (float64, error) {
|
||||
// Windows可以使用wmic或性能计数器
|
||||
cmd := exec.Command("wmic", "cpu", "get", "loadpercentage")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) >= 2 {
|
||||
usageStr := strings.TrimSpace(lines[1])
|
||||
if usage, err := strconv.ParseFloat(usageStr, 64); err == nil {
|
||||
return usage, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
// getMemoryInfo 获取内存信息
|
||||
func (p *PingReq) getMemoryInfo(memory *MemoryStatus) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
return getLinuxMemoryInfo(memory)
|
||||
case "darwin":
|
||||
return getDarwinMemoryInfo(memory)
|
||||
case "windows":
|
||||
return getWindowsMemoryInfo(memory)
|
||||
default:
|
||||
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// getLinuxMemoryInfo 获取Linux内存信息
|
||||
func getLinuxMemoryInfo(memory *MemoryStatus) error {
|
||||
data, err := ioutil.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var memTotal, memAvailable int
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
val, _ := strconv.Atoi(fields[1])
|
||||
memTotal = val / 1024 / 1024 // 转换为GB
|
||||
}
|
||||
} else if strings.HasPrefix(line, "MemAvailable:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
val, _ := strconv.Atoi(fields[1])
|
||||
memAvailable = val / 1024 / 1024 // 转换为GB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if memTotal > 0 {
|
||||
memory.Total = memTotal
|
||||
memory.Used = memTotal - memAvailable
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDarwinMemoryInfo 获取macOS内存信息
|
||||
func getDarwinMemoryInfo(memory *MemoryStatus) error {
|
||||
cmd := exec.Command("sysctl", "-n", "hw.memsize")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalStr := strings.TrimSpace(string(output))
|
||||
if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil {
|
||||
memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
||||
memory.Used = memory.Total / 3 // 示例值,实际需要更准确的获取方式
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getWindowsMemoryInfo 获取Windows内存信息
|
||||
func getWindowsMemoryInfo(memory *MemoryStatus) error {
|
||||
cmd := exec.Command("wmic", "ComputerSystem", "get", "TotalPhysicalMemory")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) >= 2 {
|
||||
totalStr := strings.TrimSpace(lines[1])
|
||||
if total, err := strconv.ParseInt(totalStr, 10, 64); err == nil {
|
||||
memory.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
||||
memory.Used = memory.Total / 4 // 示例值
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDiskInfo 获取磁盘信息
|
||||
func (p *PingReq) getDiskInfo(disk *DiskStatus) error {
|
||||
// 这里简化处理,获取根目录磁盘使用情况
|
||||
var stat syscall.Statfs_t
|
||||
err := syscall.Statfs("/", &stat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算总空间和已用空间
|
||||
total := stat.Blocks * uint64(stat.Bsize)
|
||||
free := stat.Bfree * uint64(stat.Bsize)
|
||||
|
||||
disk.Total = int(total / 1024 / 1024 / 1024) // 转换为GB
|
||||
disk.Used = int((total - free) / 1024 / 1024 / 1024) // 转换为GB
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getServiceStatus 检查各项服务状态
|
||||
func (p *PingReq) getServiceStatus(status *SystemStatus) {
|
||||
// 检查MySQL
|
||||
status.Mysql = p.checkService(ServiceMySQL)
|
||||
|
||||
// 检查MongoDB
|
||||
status.Mongodb = p.checkService(ServiceMongoDB)
|
||||
|
||||
// 检查Redis
|
||||
status.Redis = p.checkService(ServiceRedis)
|
||||
|
||||
// 检查Elasticsearch
|
||||
status.Elasticsearch = p.checkService(ServiceElasticsearch)
|
||||
|
||||
// 检查Kafka
|
||||
status.Kafka = p.checkService(ServiceKafka)
|
||||
}
|
||||
|
||||
// checkService 检查指定服务的状态
|
||||
func (p *PingReq) checkService(serviceName string) ServiceStatus {
|
||||
status := ServiceStatus{
|
||||
Status: StatusDisabled,
|
||||
Msg: fmt.Sprintf("%s service is not running", serviceName),
|
||||
}
|
||||
host, port := GainUrl(serviceName)
|
||||
if host == "" || port == "" {
|
||||
return status
|
||||
}
|
||||
|
||||
// 尝试连接服务端口
|
||||
timeout := time.Second
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout)
|
||||
if err != nil {
|
||||
// 尝试通过进程检查
|
||||
if p.isProcessRunning(serviceName) {
|
||||
status.Status = StatusOK
|
||||
status.Msg = fmt.Sprintf("%s process is running", serviceName)
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
status.Status = StatusOK
|
||||
status.Msg = fmt.Sprintf("%s service is running on port %s", serviceName, port)
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func GainUrl(serviceName string) (host, port string) {
|
||||
switch serviceName {
|
||||
case ServiceMySQL:
|
||||
// todo
|
||||
case ServiceMongoDB:
|
||||
cs, err := connstring.Parse(appg.Conf.Mongo.VideoDbUrl)
|
||||
if err != nil {
|
||||
return host, port
|
||||
}
|
||||
randomIndex := rand.Intn(len(cs.Hosts))
|
||||
mongoUrl := cs.Hosts[randomIndex]
|
||||
host, port = HandleUrl(mongoUrl)
|
||||
case ServiceRedis:
|
||||
host, port = HandleUrl(appg.Conf.Redis.URL)
|
||||
case ServiceElasticsearch:
|
||||
host, port = HandleUrl(appg.Conf.Elastic.VideoUrl)
|
||||
case ServiceKafka:
|
||||
host, port = HandleUrl(appg.Conf.Kafka.Url)
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
func HandleUrl(urlStr string) (host, port string) {
|
||||
var err error
|
||||
if strings.Contains(urlStr, "://") {
|
||||
// 有协议头,需要解析
|
||||
if u, parseErr := url.Parse(urlStr); parseErr == nil {
|
||||
if host, port, err = net.SplitHostPort(u.Host); err != nil {
|
||||
return host, port
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 没有协议头,直接拆分
|
||||
if host, port, err = net.SplitHostPort(urlStr); err != nil {
|
||||
return host, port
|
||||
}
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// isProcessRunning 检查进程是否在运行
|
||||
func (p *PingReq) isProcessRunning(processName string) bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
cmd = exec.Command("pgrep", "-f", processName)
|
||||
case "windows":
|
||||
cmd = exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s.exe", processName))
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package health_check_ser
|
||||
|
||||
// 校验服务类型常量定义
|
||||
const (
|
||||
ServiceMySQL = "mysql" // MySQL数据库服务
|
||||
ServiceMongoDB = "mongodb" // MongoDB数据库服务
|
||||
ServiceRedis = "redis" // Redis缓存服务
|
||||
ServiceElasticsearch = "elasticsearch" // Elasticsearch搜索服务
|
||||
ServiceKafka = "kafka" // Kafka消息队列服务
|
||||
)
|
||||
|
||||
// SystemStatus 表示服务器的完整状态监控信息
|
||||
type SystemStatus struct {
|
||||
Name string `json:"serverName"` // 服务器名称
|
||||
Ip string `json:"serverIp"` // 服务器IP地址
|
||||
Cpu CpuStatus `json:"cpu"` // CPU状态
|
||||
Memory MemoryStatus `json:"memory"` // 内存状态
|
||||
Disk DiskStatus `json:"disk"` // 磁盘状态
|
||||
Mysql ServiceStatus `json:"mysql"` // MySQL数据库
|
||||
Mongodb ServiceStatus `json:"mongodb"` // MongoDB数据库
|
||||
Redis ServiceStatus `json:"redis"` // Redis缓存
|
||||
Elasticsearch ServiceStatus `json:"elasticsearch"` // Elasticsearch服务
|
||||
Kafka ServiceStatus `json:"kafka"` // Kafka消息队列
|
||||
Timestamp int64 `json:"timestamp"` // 时间戳(Unix时间戳)
|
||||
}
|
||||
|
||||
// CpuStatus CPU状态
|
||||
type CpuStatus struct {
|
||||
Cores int `json:"cores"` // CPU核心数
|
||||
Used float64 `json:"used"` // 当前负载(使用中的核心数)
|
||||
}
|
||||
|
||||
// MemoryStatus 内存状态
|
||||
type MemoryStatus struct {
|
||||
Total int `json:"total"` // 内存总量(单位建议统一,如GB)
|
||||
Used int `json:"used"` // 已使用内存(单位建议统一,如GB)
|
||||
}
|
||||
|
||||
// DiskStatus 磁盘状态
|
||||
type DiskStatus struct {
|
||||
Total int `json:"total"` // 磁盘总量(单位建议统一,如GB)
|
||||
Used int `json:"used"` // 已使用磁盘空间(单位建议统一,如GB)
|
||||
}
|
||||
|
||||
// ServiceStatus 通用服务状态
|
||||
type ServiceStatus struct {
|
||||
Status string `json:"status"` // 状态:ok-正常,err-异常,disabled-未启用
|
||||
Msg string `json:"msg"` // 异常时的错误信息
|
||||
}
|
||||
|
||||
const (
|
||||
StatusOK = "ok" // 服务状态开启
|
||||
StatusDisabled = "disabled" // 服务状态关闭
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
package imadser
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"91porn-server/app/service/adser"
|
||||
"91porn-server/common/enum/imad"
|
||||
)
|
||||
|
||||
const maxAdsPerPosition = 10
|
||||
|
||||
type IMAdItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Cover string `json:"cover"`
|
||||
Href string `json:"href"`
|
||||
Position string `json:"position"`
|
||||
PositionName string `json:"positionName"`
|
||||
SortCode int `json:"sortCode"`
|
||||
CoverImgSize string `json:"coverImgSize"`
|
||||
WatchTime int `json:"watchTime"`
|
||||
}
|
||||
|
||||
type AdPositionGroup struct {
|
||||
Position string `json:"position"`
|
||||
PositionName string `json:"positionName"`
|
||||
List []IMAdItem `json:"list"`
|
||||
}
|
||||
|
||||
func GetAdsByPositionCode(positionCode string) (positionName string, list []IMAdItem) {
|
||||
slots, err := adser.JtAdvertiseThreeServer()
|
||||
if err != nil {
|
||||
return "", []IMAdItem{}
|
||||
}
|
||||
return SelectAdsByPositionCode(slots, positionCode)
|
||||
}
|
||||
|
||||
func GetAdsByPositionCodes(positionCodes []string) []AdPositionGroup {
|
||||
slots, err := adser.JtAdvertiseThreeServer()
|
||||
if err != nil {
|
||||
return []AdPositionGroup{}
|
||||
}
|
||||
return SelectAdsByPositionCodes(slots, positionCodes)
|
||||
}
|
||||
|
||||
func SelectAdsByPositionCode(slots []adser.AdSlot, positionCode string) (string, []IMAdItem) {
|
||||
positionCode = strings.TrimSpace(positionCode)
|
||||
matches := make([]IMAdItem, 0)
|
||||
positionName := ""
|
||||
for _, slot := range slots {
|
||||
if strings.TrimSpace(slot.AdvertiseLocationCode) != positionCode {
|
||||
continue
|
||||
}
|
||||
positionName = slot.AdvertiseLocationName
|
||||
for _, ad := range slot.AdDetailInfoList {
|
||||
extra := ad.GetExtraData()
|
||||
matches = append(matches, IMAdItem{
|
||||
ID: ad.AdvertiseCode,
|
||||
Title: ad.AdvertiseName,
|
||||
Description: ad.AdvertiseDesc,
|
||||
Cover: ad.GetCoverLsj(),
|
||||
Href: ad.GetRealLink(nil, nil),
|
||||
Position: positionCode,
|
||||
PositionName: slot.AdvertiseLocationName,
|
||||
SortCode: ad.Sort,
|
||||
CoverImgSize: extra.CoverImgSize,
|
||||
WatchTime: extra.WatchTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return matches[i].SortCode < matches[j].SortCode
|
||||
})
|
||||
if len(matches) > maxAdsPerPosition {
|
||||
matches = matches[:maxAdsPerPosition]
|
||||
}
|
||||
return positionName, matches
|
||||
}
|
||||
|
||||
func SelectAdsByPositionCodes(slots []adser.AdSlot, positionCodes []string) []AdPositionGroup {
|
||||
normalized := normalizePositionCodes(positionCodes)
|
||||
groups := make([]AdPositionGroup, 0, len(normalized))
|
||||
for _, positionCode := range normalized {
|
||||
positionName, list := SelectAdsByPositionCode(slots, positionCode)
|
||||
groups = append(groups, AdPositionGroup{
|
||||
Position: positionCode,
|
||||
PositionName: positionName,
|
||||
List: list,
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func ListIMAdPositionCodesWithAds() []string {
|
||||
slots, err := adser.JtAdvertiseThreeServer()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
positions := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, slot := range slots {
|
||||
code := strings.TrimSpace(slot.AdvertiseLocationCode)
|
||||
if !imad.IsPositionCode(code) || len(slot.AdDetailInfoList) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[code]; ok {
|
||||
continue
|
||||
}
|
||||
seen[code] = struct{}{}
|
||||
positions = append(positions, code)
|
||||
}
|
||||
sort.Strings(positions)
|
||||
return positions
|
||||
}
|
||||
|
||||
func normalizePositionCodes(positionCodes []string) []string {
|
||||
seen := make(map[string]struct{}, len(positionCodes))
|
||||
normalized := make([]string, 0, len(positionCodes))
|
||||
for _, positionCode := range positionCodes {
|
||||
positionCode = strings.TrimSpace(positionCode)
|
||||
if positionCode == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[positionCode]; ok {
|
||||
continue
|
||||
}
|
||||
seen[positionCode] = struct{}{}
|
||||
normalized = append(normalized, positionCode)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package imgroupmemberser
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/cache/imgroupmemberdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/imgroupmembermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*imgroupmembermod.ImGroupMemberInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
filter := bson.M{}
|
||||
|
||||
sort := bson.D{{"_id", -1}}
|
||||
// 获取列表
|
||||
var data []imgroupmembermod.ImGroupMember
|
||||
data, res.Total, res.HasNext, err = imgroupmemberdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取im群组成员列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = imgroupmemberdata.FormatAppDataList(data)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type HasJoinReq struct {
|
||||
GroupId int64 `json:"groupId" form:"groupId" binding:"required"`
|
||||
}
|
||||
type HasJoinRes struct {
|
||||
Status int `json:"status"` // 0-已被禁言 1-可发言 2-未加入
|
||||
}
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *HasJoinReq) GetInfo(uid uint64) (res HasJoinRes, err error) {
|
||||
item, err := imgroupmembermod.GetInfoByCond(bson.M{"groupId": p.GroupId, "uid": uid})
|
||||
if err != nil {
|
||||
log.Error("获取im群组成员详情数据错误", log.Any("groupId", p.GroupId), log.Any("uid", uid), log.E(err))
|
||||
return
|
||||
}
|
||||
if item.ID.IsZero() || item.Status == 2 {
|
||||
res.Status = 2 //.未加入
|
||||
} else {
|
||||
res.Status = item.Status
|
||||
}
|
||||
return
|
||||
}
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
package imgroupser
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/cache/imgroupdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/imgroupmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*imgroupmod.ImGroupInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
filter := bson.M{"status": 1}
|
||||
|
||||
sort := bson.D{{"_id", -1}}
|
||||
// 获取列表
|
||||
var data []imgroupmod.ImGroup
|
||||
data, res.Total, res.HasNext, err = imgroupdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取im群组列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = imgroupdata.FormatAppDataList(data)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type AppQueryInfoReq struct {
|
||||
ID string `json:"id" form:"id"` // id
|
||||
}
|
||||
type AppQueryInfoRes = *imgroupmod.ImGroupInfo
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *AppQueryInfoReq) GetInfo() (res AppQueryInfoRes, err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var item imgroupmod.ImGroup
|
||||
item, err = imgroupdata.GetInfoFromCache(oid)
|
||||
if err != nil {
|
||||
log.Error("获取im群组详情数据错误", log.Any("ID", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
res = imgroupdata.FormatAppData(item)
|
||||
return
|
||||
}
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
package immessageser
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/cache/immessagedata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/imgroupmembermod"
|
||||
"91porn-server/models/v/immessagemod"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
GroupId int64 `json:"groupId" form:"groupId" binding:"required"`
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*immessagemod.ImMessageInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
filter := bson.M{
|
||||
"groupId": p.GroupId,
|
||||
}
|
||||
|
||||
sort := bson.D{{"_id", -1}}
|
||||
// 获取列表
|
||||
var data []immessagemod.ImMessage
|
||||
data, res.Total, res.HasNext, err = immessagemod.GetList(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取im消息列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return res
|
||||
}
|
||||
res.List = immessagedata.FormatAppDataList(data)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type SendReq struct {
|
||||
GroupId int64 `json:"groupId" form:"groupId" binding:"required"` // 群组id
|
||||
Content string `json:"content" form:"content"` // 消息内容
|
||||
Image string `json:"image" form:"image"` // 图片
|
||||
}
|
||||
|
||||
// Create 发布数据
|
||||
func (p *SendReq) Create(uid uint64) error {
|
||||
// 判断是否是群成员
|
||||
member, err := imgroupmembermod.GetInfoByCond(bson.M{"groupId": p.GroupId, "uid": uid})
|
||||
if err != nil {
|
||||
log.Error("发送群组消息失败", log.Any("params", *p), log.E(err))
|
||||
return stderr.NewCustomErr(stderr.Failure, "发送失败")
|
||||
}
|
||||
if member.ID.IsZero() || member.Status == 2 {
|
||||
return stderr.NewCustomErr(stderr.Failure, "发送失败,您还未加入群组")
|
||||
}
|
||||
if member.Status == 0 {
|
||||
return stderr.NewCustomErr(stderr.Failure, "发送失败,您已经被该群禁言")
|
||||
}
|
||||
data := immessagemod.ImMessage{
|
||||
GroupId: p.GroupId,
|
||||
Uid: uid,
|
||||
Content: p.Content,
|
||||
Image: p.Image,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 创建数据
|
||||
if _, err = immessagemod.Insert(nil, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package imser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/imclient"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/models/v/usermod"
|
||||
)
|
||||
|
||||
const friendListFirstPageNow int64 = 1
|
||||
|
||||
type IMUserMapping struct {
|
||||
UID uint64
|
||||
IMUserID int64
|
||||
}
|
||||
|
||||
type IMFriendItem struct {
|
||||
FriendIMUserID int64 `json:"friendImUserId"`
|
||||
UID uint64 `json:"uid,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt int64 `json:"createdAt,omitempty"`
|
||||
UpdatedAt int64 `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type IMHistoryMessage struct {
|
||||
MessageID string `json:"messageId"`
|
||||
SenderID int64 `json:"senderId"`
|
||||
ReceiverID int64 `json:"receiverId"`
|
||||
SenderUserID uint64 `json:"senderUserId,omitempty"`
|
||||
SenderNickName string `json:"senderNickName,omitempty"`
|
||||
SenderAvatar string `json:"senderAvatar,omitempty"`
|
||||
ReceiverUserID uint64 `json:"receiverUserId,omitempty"`
|
||||
ReceiverNickName string `json:"receiverNickName,omitempty"`
|
||||
ReceiverAvatar string `json:"receiverAvatar,omitempty"`
|
||||
Content string `json:"content"`
|
||||
MessageType string `json:"messageType"`
|
||||
Seq int64 `json:"seq"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
FromSelf bool `json:"fromSelf"`
|
||||
}
|
||||
|
||||
// ResolveStoredIMUserID 解析用户的 IM 平台 user_id:
|
||||
// 优先 usermod.ImUserID → imusermod 映射表 → 都没有则调用 IM 注册接口创建。
|
||||
// 保证返回的 imUserId > 0,或返回错误,调用方无需再判 0。
|
||||
func ResolveStoredIMUserID(uid uint64) (int64, error) {
|
||||
// 直接复用 EnsureSDKUser:它的"查→兜底注册"流程跟这里需要的语义一致。
|
||||
return EnsureSDKUser(uid)
|
||||
}
|
||||
|
||||
func ResolveUIDByIMUserID(imUserID int64) (uint64, error) {
|
||||
if imUserID <= 0 {
|
||||
return 0, fmt.Errorf("imUserId is empty")
|
||||
}
|
||||
// imUserId → uid 以 imusermod 为唯一真源
|
||||
mapping, err := imusermod.FindByIMUserIDQuiet(imUserID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if mapping.UID <= 0 {
|
||||
return 0, fmt.Errorf("im user not found")
|
||||
}
|
||||
return mapping.UID, nil
|
||||
}
|
||||
|
||||
func BusinessUsersByIMUserIDs(imUserIDs []int64) (map[int64]*usermod.User, error) {
|
||||
out := make(map[int64]*usermod.User)
|
||||
ids := uniquePositiveInt64(imUserIDs)
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
// imUserId → uid 走 imusermod(唯一真源),再按 uid 批量取业务用户
|
||||
mappings, err := imusermod.FindByIMUserIDsQuiet(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uids := make([]uint64, 0, len(mappings))
|
||||
uidByIM := make(map[int64]uint64, len(mappings))
|
||||
for _, mapping := range mappings {
|
||||
if mapping.UID <= 0 || mapping.IMUserID <= 0 {
|
||||
continue
|
||||
}
|
||||
uids = append(uids, mapping.UID)
|
||||
uidByIM[mapping.IMUserID] = mapping.UID
|
||||
}
|
||||
if len(uids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
usersByUID, err := usermod.FindUsersMapByUID(uids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for imUserID, uid := range uidByIM {
|
||||
if u := usersByUID[uid]; u != nil {
|
||||
out[imUserID] = u
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func EnsureFriendsBidirectional(uid, peerUID uint64) (selfIMUserID, peerIMUserID int64, added bool, err error) {
|
||||
if uid == 0 || peerUID == 0 {
|
||||
return 0, 0, false, fmt.Errorf("uid is empty")
|
||||
}
|
||||
if uid == peerUID {
|
||||
return 0, 0, false, fmt.Errorf("cannot add self")
|
||||
}
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return 0, 0, false, nil
|
||||
}
|
||||
selfIMUserID, err = EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
peerIMUserID, err = EnsureSDKUser(peerUID)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
addedForward, err := ensureFriendDirect(c, selfIMUserID, peerIMUserID)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
addedReverse, err := ensureFriendDirect(c, peerIMUserID, selfIMUserID)
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
return selfIMUserID, peerIMUserID, addedForward || addedReverse, nil
|
||||
}
|
||||
|
||||
func FriendList(uid uint64, now int64) (imUserID int64, usedNow int64, nextNow int64, friends []IMFriendItem, err error) {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return 0, 0, 0, []IMFriendItem{}, nil
|
||||
}
|
||||
imUserID, err = EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
if now <= 0 {
|
||||
now = friendListFirstPageNow
|
||||
}
|
||||
var relations []imclient.Friend
|
||||
err = withAppToken(c, func(token string) error {
|
||||
var listErr error
|
||||
relations, listErr = c.FriendList(imUserID, now, token)
|
||||
return listErr
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
friendIMIDs := make([]int64, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.FriendID > 0 {
|
||||
friendIMIDs = append(friendIMIDs, relation.FriendID)
|
||||
}
|
||||
}
|
||||
userByIM, err := BusinessUsersByIMUserIDs(friendIMIDs)
|
||||
if err != nil {
|
||||
return 0, 0, 0, nil, err
|
||||
}
|
||||
nextNow = now
|
||||
items := make([]IMFriendItem, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
if relation.UpdatedAt > nextNow {
|
||||
nextNow = relation.UpdatedAt
|
||||
}
|
||||
item := IMFriendItem{
|
||||
FriendIMUserID: relation.FriendID,
|
||||
Remark: relation.Remark,
|
||||
Status: relation.Status,
|
||||
CreatedAt: relation.CreatedAt,
|
||||
UpdatedAt: relation.UpdatedAt,
|
||||
}
|
||||
if u := userByIM[relation.FriendID]; u != nil {
|
||||
item.UID = u.UID
|
||||
item.NickName = u.Name
|
||||
item.Avatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return imUserID, now, nextNow, items, nil
|
||||
}
|
||||
|
||||
func HistoryMessages(uid, peerUID uint64, req imclient.HistoryMessageRequest) (selfIMUserID, peerIMUserID int64, messages []IMHistoryMessage, err error) {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return 0, 0, []IMHistoryMessage{}, nil
|
||||
}
|
||||
selfIMUserID, err = EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
peerIMUserID, err = ResolveStoredIMUserID(peerUID)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
if peerIMUserID <= 0 {
|
||||
return 0, 0, nil, fmt.Errorf("peer im user not found")
|
||||
}
|
||||
req.UserID1 = selfIMUserID
|
||||
req.UserID2 = peerIMUserID
|
||||
var history []imclient.MessageDetail
|
||||
err = withAppToken(c, func(token string) error {
|
||||
var historyErr error
|
||||
history, historyErr = c.HistoryMessages(req, token)
|
||||
return historyErr
|
||||
})
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
messageIMIDs := make([]int64, 0, len(history)*2)
|
||||
for _, msg := range history {
|
||||
if msg.SenderID > 0 {
|
||||
messageIMIDs = append(messageIMIDs, msg.SenderID)
|
||||
}
|
||||
if msg.ReceiverID > 0 {
|
||||
messageIMIDs = append(messageIMIDs, msg.ReceiverID)
|
||||
}
|
||||
}
|
||||
userByIM, err := BusinessUsersByIMUserIDs(messageIMIDs)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
out := make([]IMHistoryMessage, 0, len(history))
|
||||
for _, msg := range history {
|
||||
item := IMHistoryMessage{
|
||||
MessageID: msg.MessageID,
|
||||
SenderID: msg.SenderID,
|
||||
ReceiverID: msg.ReceiverID,
|
||||
Content: msg.Content,
|
||||
MessageType: msg.MessageType,
|
||||
Seq: msg.Seq,
|
||||
CreatedAt: msg.CreatedAt,
|
||||
FromSelf: msg.SenderID == selfIMUserID,
|
||||
}
|
||||
if u := userByIM[msg.SenderID]; u != nil {
|
||||
item.SenderUserID = u.UID
|
||||
item.SenderNickName = u.Name
|
||||
item.SenderAvatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)
|
||||
}
|
||||
if u := userByIM[msg.ReceiverID]; u != nil {
|
||||
item.ReceiverUserID = u.UID
|
||||
item.ReceiverNickName = u.Name
|
||||
item.ReceiverAvatar = common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait)
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return selfIMUserID, peerIMUserID, out, nil
|
||||
}
|
||||
|
||||
func ensureFriendDirect(c *imclient.Client, userIMID, friendIMID int64) (bool, error) {
|
||||
var added bool
|
||||
err := withAppToken(c, func(token string) error {
|
||||
relations, err := c.FriendList(userIMID, friendListFirstPageNow, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, relation := range relations {
|
||||
if relation.FriendID == friendIMID && relation.Status == 0 {
|
||||
added = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = c.DirectAddFriend(imclient.DirectAddFriendRequest{
|
||||
UserID: userIMID,
|
||||
FriendID: friendIMID,
|
||||
}, token)
|
||||
if err == nil {
|
||||
added = true
|
||||
}
|
||||
return err
|
||||
})
|
||||
if isDuplicateFriendError(err) {
|
||||
return false, nil
|
||||
}
|
||||
return added, err
|
||||
}
|
||||
|
||||
func isDuplicateFriendError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "already") ||
|
||||
strings.Contains(msg, "exists") ||
|
||||
strings.Contains(msg, "重复") ||
|
||||
strings.Contains(msg, "已存在") ||
|
||||
strings.Contains(msg, "好友关系已存在")
|
||||
}
|
||||
|
||||
func uniquePositiveInt64(in []int64) []int64 {
|
||||
out := make([]int64, 0, len(in))
|
||||
seen := make(map[int64]struct{}, len(in))
|
||||
for _, v := range in {
|
||||
if v <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package imser
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/v/usermod"
|
||||
)
|
||||
|
||||
// GetSign 获取IM sign
|
||||
func GetSign(uid uint64, ua ua.UA) string {
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
return ""
|
||||
}
|
||||
// IsVip := false
|
||||
// if userModel.VipExpireDate.After(time.Now()) {
|
||||
// IsVip = true
|
||||
// }
|
||||
var voiceChatNotify string
|
||||
if common.IsSpecifyVerBaseOnUa(ua, constant.Ver3_1_0) {
|
||||
voiceChatNotify = "支持语音聊天"
|
||||
} else {
|
||||
voiceChatNotify = "不支持语音聊天"
|
||||
}
|
||||
ul := url.Values{}
|
||||
ul.Add("id", strconv.FormatUint(uid, 10))
|
||||
ul.Add("appId", appg.Conf.Kefu.AppID)
|
||||
ul.Add("platName", appg.Conf.Kefu.PlatName)
|
||||
ul.Add("userName", u.Name+"**"+ua.SysType+"**"+ua.DevType+"**"+ua.Ver+"**"+voiceChatNotify)
|
||||
ul.Add("isVip", "false")
|
||||
ul.Add("avatar", common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait))
|
||||
dcodeurl, _ := url.QueryUnescape(ul.Encode())
|
||||
sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Kefu.AppSecret)
|
||||
if err != nil {
|
||||
log.Warn("create im sign wrong", log.Any("warn", err))
|
||||
return ""
|
||||
}
|
||||
return "/kefu/?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Kefu.AppID + "&theme=" + appg.Conf.Kefu.Theme
|
||||
}
|
||||
|
||||
// GetWhiteSign 获取White IM sign
|
||||
func GetWhiteSign() string {
|
||||
return "/kefu/?" + "appId=" + appg.Conf.Kefu.AppID + "&anonymous=true"
|
||||
}
|
||||
|
||||
// GetSignNew 新获取IM sign
|
||||
func GetSignNew(uid uint64, ua ua.UA) string {
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || u == nil {
|
||||
return ""
|
||||
}
|
||||
// IsVip := false
|
||||
// if userModel.VipExpireDate.After(time.Now()) {
|
||||
// IsVip = true
|
||||
// }
|
||||
var voiceChatNotify string
|
||||
if common.IsSpecifyVerBaseOnUa(ua, constant.Ver3_1_0) {
|
||||
voiceChatNotify = "支持语音聊天"
|
||||
} else {
|
||||
voiceChatNotify = "不支持语音聊天"
|
||||
}
|
||||
ul := url.Values{}
|
||||
ul.Add("id", strconv.FormatUint(uid, 10))
|
||||
ul.Add("appId", appg.Conf.Kefu.AppID)
|
||||
ul.Add("platName", appg.Conf.Kefu.PlatName)
|
||||
ul.Add("userName", u.Name+"**"+ua.SysType+"**"+ua.DevType+"**"+ua.Ver+"**"+voiceChatNotify)
|
||||
ul.Add("isVip", "false")
|
||||
ul.Add("avatar", common.BindUrl(appg.Conf.URL.OriginUrl, u.Portrait))
|
||||
dcodeurl, _ := url.QueryUnescape(ul.Encode())
|
||||
sign, err := crypt.AesEncrypt(dcodeurl, appg.Conf.Kefu.AppSecret)
|
||||
if err != nil {
|
||||
log.Warn("create im sign wrong", log.Any("warn", err))
|
||||
return ""
|
||||
}
|
||||
return "/kefu/customer/im?sign=" + hex.EncodeToString(sign) + "&appId=" + appg.Conf.Kefu.AppID + "&theme=" + appg.Conf.Kefu.Theme
|
||||
}
|
||||
|
||||
// GetWhiteSignNew 新获取White IM sign
|
||||
func GetWhiteSignNew() string {
|
||||
return "/kefu/customer/im?appId=" + appg.Conf.Kefu.AppID + "&anonymous=true"
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package imser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/imclient"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/models/v/usermod"
|
||||
|
||||
"github.com/go-redsync/redsync/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultIMV2DynamicConfigDomain = "https://sdk.api.cdflow.cn"
|
||||
defaultIMV2SocketURL = "wss://wss.cdflow.cn:32214"
|
||||
|
||||
// ensureSDKUser 抢锁失败后,轮询 imUser 表的间隔与最大次数(10 × 200ms = 最多等 2s)
|
||||
imSDKEnsureWaitInterval = 200 * time.Millisecond
|
||||
imSDKEnsureWaitTries = 10
|
||||
)
|
||||
|
||||
var appTokenCache = struct {
|
||||
sync.RWMutex
|
||||
token string
|
||||
expiresAt time.Time
|
||||
}{}
|
||||
|
||||
type SDKAuthInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ImToken string `json:"imToken"`
|
||||
Token string `json:"token,omitempty"`
|
||||
UserID uint64 `json:"userId"`
|
||||
ImUserID int64 `json:"imUserId"`
|
||||
Registered bool `json:"registered"`
|
||||
NewlyRegistered bool `json:"newlyRegistered,omitempty"`
|
||||
DynamicConfigDomain string `json:"dynamicConfigDomain"`
|
||||
SocketURL string `json:"socketURL"`
|
||||
SysType string `json:"sysType,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
OSType string `json:"osType,omitempty"`
|
||||
}
|
||||
|
||||
type SDKPublicConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
SocketURL string `json:"socketUrl"`
|
||||
MerchantCode string `json:"merchantCode"`
|
||||
TenantCode string `json:"tenantCode"`
|
||||
}
|
||||
|
||||
func SDKEnabled() bool {
|
||||
return newSDKClient().Enabled()
|
||||
}
|
||||
|
||||
func GetSDKPublicConfig() SDKPublicConfig {
|
||||
cfg := sdkConfig()
|
||||
return SDKPublicConfig{
|
||||
Enable: newSDKClient().Enabled(),
|
||||
BaseURL: cfg.BaseURL,
|
||||
SocketURL: appg.Conf.ImV2.SocketURL,
|
||||
MerchantCode: cfg.MerchantCode,
|
||||
TenantCode: cfg.TenantCode,
|
||||
}
|
||||
}
|
||||
|
||||
func GetSDKAuth(uid uint64) (*SDKAuthInfo, error) {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
reason := c.DisabledReason()
|
||||
log.Error("GetSDKAuth im sdk disabled", log.Any("uid", uid), log.Any("reason", reason))
|
||||
return nil, fmt.Errorf("im sdk is disabled: %s", reason)
|
||||
}
|
||||
// 确保 IM 用户存在(内部以 imUser 表为准 + 并发锁),并得知本次是否新注册
|
||||
imUserID, newlyRegistered, err := ensureSDKUser(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := withAppToken(c, func(token string) error {
|
||||
return c.SetOnlineStatus(imclient.SetOnlineStatusRequest{
|
||||
UserID: imUserID,
|
||||
ShowOnlineStatus: true,
|
||||
}, token)
|
||||
}); err != nil {
|
||||
log.Warn("set im online status visible failed", log.Any("uid", uid), log.Any("imUserId", imUserID), log.E(err))
|
||||
}
|
||||
token, err := cachedUserToken(c, imUserID)
|
||||
if err != nil {
|
||||
log.Error("get user im token fail", log.Any("uid", uid), log.Any("imUserId", imUserID), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
return &SDKAuthInfo{
|
||||
Enabled: true,
|
||||
ImToken: token,
|
||||
Token: token,
|
||||
UserID: uid,
|
||||
ImUserID: imUserID,
|
||||
Registered: imUserID > 0,
|
||||
NewlyRegistered: newlyRegistered,
|
||||
DynamicConfigDomain: dynamicConfigDomain(),
|
||||
SocketURL: socketURL(),
|
||||
OS: "web",
|
||||
OSType: "web",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func SendPrivateMessage(sendUID, takeUID uint64, content string, imgURLs []string) error {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
senderID, err := EnsureSDKUser(sendUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
receiverID, err := EnsureSDKUser(takeUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if content != "" {
|
||||
if err = withAppToken(c, func(token string) error {
|
||||
return c.SendMessage(imclient.SendMessageRequest{
|
||||
SenderID: senderID,
|
||||
ReceiverID: receiverID,
|
||||
Content: content,
|
||||
MessageType: imclient.MessageTypeText,
|
||||
}, token)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, imgURL := range imgURLs {
|
||||
if imgURL == "" {
|
||||
continue
|
||||
}
|
||||
if err = withAppToken(c, func(token string) error {
|
||||
return c.SendMessage(imclient.SendMessageRequest{
|
||||
SenderID: senderID,
|
||||
ReceiverID: receiverID,
|
||||
Content: "",
|
||||
MessageType: imclient.MessageTypeImage,
|
||||
Attachment: &imclient.Attachment{
|
||||
URL: imgURL,
|
||||
},
|
||||
}, token)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncSDKBaseInfo(uid uint64, name, portrait, signature *string) error {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
imUserID, err := EnsureSDKUser(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := imclient.UpdateBaseInfoRequest{
|
||||
UserID: imUserID,
|
||||
}
|
||||
if name != nil {
|
||||
req.Name = *name
|
||||
}
|
||||
if portrait != nil {
|
||||
req.ImgURL = common.BindUrl(appg.Conf.URL.OriginUrl, *portrait)
|
||||
}
|
||||
if signature != nil {
|
||||
req.Signature = *signature
|
||||
}
|
||||
if req.Name == "" && req.ImgURL == "" && req.Signature == "" {
|
||||
return nil
|
||||
}
|
||||
return withAppToken(c, func(token string) error {
|
||||
return c.UpdateBaseInfo(req, token)
|
||||
})
|
||||
}
|
||||
|
||||
// EnsureSDKUser 确保 IM 用户存在并返回 imUserID。
|
||||
func EnsureSDKUser(uid uint64) (int64, error) {
|
||||
imUserID, _, err := ensureSDKUser(uid)
|
||||
return imUserID, err
|
||||
}
|
||||
|
||||
// ensureSDKUser 确保 IM 用户存在并返回 imUserID;newlyRegistered 表示本次是否真正发起了 SDK 注册
|
||||
// (命中 imUser 表或回填历史用户均为 false)。以 imUser 表为准判断是否已注册。
|
||||
func ensureSDKUser(uid uint64) (int64, bool, error) {
|
||||
if uid == 0 {
|
||||
log.Warn("EnsureSDKUser uid is zero")
|
||||
return 0, false, fmt.Errorf("uid is empty")
|
||||
}
|
||||
// fast path:以 imUser 表为准,已注册直接返回
|
||||
if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, false, nil
|
||||
}
|
||||
// 未注册 → 抢锁串行化,防止并发重复注册。只抢一次、不自旋。
|
||||
mu := redis.BuildLock(appg.Redis, redisconst.IMSDKEnsureLockKey(uid),
|
||||
redsync.WithExpiry(10*time.Second),
|
||||
redsync.WithTries(1),
|
||||
)
|
||||
if err := mu.Lock(); err != nil {
|
||||
// 没抢到锁 → 已有别的请求在注册:每 200ms 轮询一次 imUser 表,最多等 2s
|
||||
for i := 0; i < imSDKEnsureWaitTries; i++ {
|
||||
time.Sleep(imSDKEnsureWaitInterval)
|
||||
if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, false, nil
|
||||
}
|
||||
}
|
||||
return 0, false, fmt.Errorf("ensure sdk user busy: wait imUser timeout, uid=%d", uid)
|
||||
}
|
||||
defer func() { _, _ = mu.Unlock() }()
|
||||
// 双重检查:等锁期间可能已被其他请求注册完,避免重复注册
|
||||
if mapping, _ := imusermod.FindByUIDQuiet(uid); mapping.IMUserID > 0 {
|
||||
return mapping.IMUserID, false, nil
|
||||
}
|
||||
u, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser usermod.FindUserByUID fail uid=%d", uid), log.E(err))
|
||||
return 0, false, err
|
||||
}
|
||||
if u == nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser user is nil uid=%d", uid))
|
||||
return 0, false, fmt.Errorf("user %d not found", uid)
|
||||
}
|
||||
// 一切以 imUser 表为准:表里无映射即视为未注册,直接走 SDK 注册(不信任 user.ImUserID,不回填)
|
||||
c := newSDKClient()
|
||||
thirdPartyID := strconv.FormatUint(uid, 10)
|
||||
var imUserID int64
|
||||
err = withAppToken(c, func(token string) error {
|
||||
var registerErr error
|
||||
imUserID, registerErr = c.Register(imclient.RegisterRequest{
|
||||
ThirdPartyID: thirdPartyID,
|
||||
Password: sdkPassword(uid),
|
||||
Nickname: u.Name,
|
||||
Avatar: u.Portrait,
|
||||
}, token)
|
||||
return registerErr
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser c.Register fail uid=%d", uid), log.E(err))
|
||||
return 0, false, err
|
||||
}
|
||||
if imUserID <= 0 {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser im register returned empty user id uid=%d", uid), log.E(err))
|
||||
return 0, false, fmt.Errorf("im register returned empty user id")
|
||||
}
|
||||
if err = imusermod.UpsertByUID(uid, imUserID, thirdPartyID); err != nil {
|
||||
log.Warn(fmt.Sprintf("EnsureSDKUser upsert fail uid=%d", uid), log.E(err))
|
||||
return 0, false, err
|
||||
}
|
||||
return imUserID, true, nil
|
||||
}
|
||||
|
||||
// InitAppToken 启动时异步取一次 appToken(不阻断启动),并每小时刷新一次,写入内存+redis。
|
||||
func InitAppToken() {
|
||||
if !SDKEnabled() {
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
if err := refreshAppToken(); err != nil {
|
||||
log.Warn("init app token failed", log.E(err))
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
ticker := time.NewTicker(time.Hour)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if err := refreshAppToken(); err != nil {
|
||||
log.Warn("refresh app token failed", log.E(err))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// refreshAppToken 从 SDK 取最新 appToken,写入内存 + redis。
|
||||
func refreshAppToken() error {
|
||||
c := newSDKClient()
|
||||
if !c.Enabled() {
|
||||
return nil
|
||||
}
|
||||
token, err := c.AppToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == "" {
|
||||
return fmt.Errorf("im app token empty")
|
||||
}
|
||||
ttl := sdkTokenTTL()
|
||||
cacheSeconds := ttl - 60
|
||||
if cacheSeconds < 60 {
|
||||
cacheSeconds = ttl
|
||||
}
|
||||
setToken(token, time.Duration(cacheSeconds)*time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// setToken 写入内存 + redis。
|
||||
func setToken(token string, ttl time.Duration) {
|
||||
appTokenCache.Lock()
|
||||
appTokenCache.token = token
|
||||
appTokenCache.expiresAt = time.Now().Add(ttl)
|
||||
appTokenCache.Unlock()
|
||||
if err := appg.Redis.Set(redisconst.IMSDKAppTokenKey, token, ttl); err != nil {
|
||||
log.Warn("set app token to redis fail", log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
// getToken 取 appToken:优先内存(读锁),取不到再取 redis(命中则回种内存)。
|
||||
func getToken() string {
|
||||
appTokenCache.RLock()
|
||||
token := appTokenCache.token
|
||||
appTokenCache.RUnlock()
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
if v, err := appg.Redis.Get(redisconst.IMSDKAppTokenKey); err == nil && v != nil && *v != "" {
|
||||
appTokenCache.Lock()
|
||||
appTokenCache.token = *v
|
||||
appTokenCache.Unlock()
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func appToken(c *imclient.Client) (string, error) {
|
||||
if token := getToken(); token != "" {
|
||||
return token, nil
|
||||
}
|
||||
// 内存和 redis 都没有(启动初期或刷新失败)→ 兜底同步取一次
|
||||
if err := refreshAppToken(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token := getToken(); token != "" {
|
||||
return token, nil
|
||||
}
|
||||
return "", fmt.Errorf("im app token unavailable")
|
||||
}
|
||||
|
||||
func invalidateAppToken() {
|
||||
appTokenCache.Lock()
|
||||
appTokenCache.token = ""
|
||||
appTokenCache.expiresAt = time.Time{}
|
||||
appTokenCache.Unlock()
|
||||
_, _ = appg.Redis.Del(redisconst.IMSDKAppTokenKey)
|
||||
}
|
||||
|
||||
func withAppToken(c *imclient.Client, fn func(token string) error) error {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
token, err := appToken(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = fn(token)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if attempt == 0 && imclient.IsSessionExpired(err) {
|
||||
invalidateAppToken()
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("im app token retry exhausted")
|
||||
}
|
||||
|
||||
// cachedUserToken 获取 IM 用户 token,缓存 12 小时,避免每次鉴权都请求 SDK。
|
||||
// 缓存按 imUserID 维度,Redis 故障时降级为直接请求,不阻断鉴权。
|
||||
func cachedUserToken(c *imclient.Client, imUserID int64) (string, error) {
|
||||
key := redisconst.IMSDKUserTokenKey(imUserID)
|
||||
if cached, err := appg.Redis.Get(key); err != nil {
|
||||
log.Warn("get im user token cache fail", log.Any("imUserId", imUserID), log.E(err))
|
||||
} else if cached != nil && *cached != "" {
|
||||
return *cached, nil
|
||||
}
|
||||
token, err := c.UserToken(imUserID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token != "" {
|
||||
if err = appg.Redis.Set(key, token, redisconst.IMSDKUserTokenExpire); err != nil {
|
||||
log.Warn("set im user token cache fail", log.Any("imUserId", imUserID), log.E(err))
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func sdkTokenTTL() int64 {
|
||||
ttl := sdkConfig().TokenTTL
|
||||
if ttl <= 0 {
|
||||
ttl = 86400
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
func newSDKClient() *imclient.Client {
|
||||
return imclient.New(sdkConfig())
|
||||
}
|
||||
|
||||
func sdkConfig() imclient.Config {
|
||||
cfg := appg.Conf.ImV2
|
||||
return imclient.Config{
|
||||
Enable: true,
|
||||
BaseURL: cfg.BaseURL,
|
||||
MerchantCode: cfg.MerchantCode,
|
||||
TenantCode: cfg.TenantCode,
|
||||
AppKey: cfg.AppKey,
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
SignKey: imclient.DefaultSignKey,
|
||||
AESKey: imclient.DefaultAESKey,
|
||||
EnableSign: true,
|
||||
EncryptTimestamp: true,
|
||||
TokenTTL: 86400,
|
||||
}
|
||||
}
|
||||
|
||||
func dynamicConfigDomain() string {
|
||||
if domain := strings.TrimRight(appg.Conf.ImV2.DynamicConfigDomain, "/"); domain != "" {
|
||||
return domain
|
||||
}
|
||||
rawBaseURL := strings.TrimSpace(appg.Conf.ImV2.BaseURL)
|
||||
if rawBaseURL != "" {
|
||||
if u, err := url.Parse(rawBaseURL); err == nil && u.Scheme != "" && u.Host != "" {
|
||||
return u.Scheme + "://" + u.Host
|
||||
}
|
||||
}
|
||||
return defaultIMV2DynamicConfigDomain
|
||||
}
|
||||
|
||||
func socketURL() string {
|
||||
if socket := strings.TrimSpace(appg.Conf.ImV2.SocketURL); socket != "" {
|
||||
return socket
|
||||
}
|
||||
return defaultIMV2SocketURL
|
||||
}
|
||||
|
||||
func sdkPassword(uid uint64) string {
|
||||
return fmt.Sprintf("hjll%014d", uid%100000000000000)
|
||||
}
|
||||
|
||||
func LogSDKSendError(sendUID, takeUID uint64, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Warn("im sdk send private message error", log.Any("sendUID", sendUID), log.Any("takeUID", takeUID), log.E(err))
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package infmtser
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/v/noticefmtmod"
|
||||
"91porn-server/models/v/noticerecdmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// ObjectID
|
||||
type ObjectID = primitive.ObjectID
|
||||
|
||||
type Sender = noticefmtmod.Sender
|
||||
|
||||
type NoticeFmt = noticefmtmod.NoticeFmt
|
||||
|
||||
type Notice struct {
|
||||
SendAt time.Time `json:"sendAt"` //消息的发送时间
|
||||
Sender Sender `json:"sender"` //发送者
|
||||
Title string `json:"title"` //标题
|
||||
Content string `json:"content"` //内容
|
||||
}
|
||||
|
||||
// 通知预览
|
||||
type NoticPreview struct {
|
||||
NewsCount int64 `json:"newsCount"`
|
||||
Notice
|
||||
}
|
||||
|
||||
type FMTEnableMatch = noticefmtmod.EnableMatch
|
||||
|
||||
func UpdateNoticePreviewList(uid uint64, curTime time.Time) ([]NoticPreview, bool, error) {
|
||||
enable := true
|
||||
enableMatch := FMTEnableMatch{Enable: &enable}
|
||||
noticeFmts, err := noticefmtmod.GetNoticeFmtListByUID(uid, enableMatch.New())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
noticeFmtsLen := len(noticeFmts)
|
||||
var (
|
||||
noticeCodeList = make([]string, noticeFmtsLen)
|
||||
noticeFmtMap = make(map[string]NoticeFmt)
|
||||
systemNoticeCodeList = make([]string, 0, noticeFmtsLen)
|
||||
actAsstNoticeCodeList = make([]string, 0, noticeFmtsLen)
|
||||
)
|
||||
for i, n := range noticeFmts {
|
||||
sender := n.Sender
|
||||
if sender == noticefmtmod.System {
|
||||
systemNoticeCodeList = append(systemNoticeCodeList, n.NoticeCode)
|
||||
} else if sender == noticefmtmod.ActAsst {
|
||||
actAsstNoticeCodeList = append(actAsstNoticeCodeList, n.NoticeCode)
|
||||
}
|
||||
noticeFmtMap[n.NoticeCode] = n
|
||||
noticeCodeList[i] = n.NoticeCode
|
||||
}
|
||||
lastSendAtMap, err := noticerecdmod.GetLastSendAtMap(uid, noticeCodeList)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var nextSendAtMap = make(map[string]time.Time) //新通知
|
||||
for _, fmt := range noticeFmts {
|
||||
nextSendTimeList := noticefmtmod.TimeSlice{}
|
||||
switch fmt.NoticeType {
|
||||
case noticefmtmod.MailNoticeType:
|
||||
nextSendTimeList = fmt.MailNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode])
|
||||
case noticefmtmod.RegularNoticeType:
|
||||
nextSendTimeList = fmt.RegularNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode])
|
||||
case noticefmtmod.PlanNoticeType:
|
||||
nextSendTimeList = fmt.PlanNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode])
|
||||
case noticefmtmod.SpecifyNoticeType:
|
||||
nextSendTimeList = fmt.SpecifyNotice.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode])
|
||||
case noticefmtmod.AfterRegistNoticeType:
|
||||
user, _ := usermod.FindUserByUID(uid)
|
||||
if user != nil {
|
||||
afterRegistNoticeEx := noticefmtmod.AfterRegistNoticeEx{AfterRegistNotice: fmt.AfterRegistNotice, CreatedAt: user.CreatedAt}
|
||||
nextSendTimeList = afterRegistNoticeEx.NextSendTimeList(curTime, lastSendAtMap[fmt.NoticeCode])
|
||||
}
|
||||
}
|
||||
if nextSendTimeList.Len() != 0 {
|
||||
nextSendAtMap[fmt.NoticeCode] = nextSendTimeList[0]
|
||||
}
|
||||
}
|
||||
if err = noticerecdmod.UpsertLastSendAt(uid, nextSendAtMap); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return getNoticePreview(uid, noticeFmtMap, systemNoticeCodeList, actAsstNoticeCodeList)
|
||||
}
|
||||
|
||||
func getNoticePreview(uid uint64, noticeFmtMap map[string]NoticeFmt, systemNoticeCodeList []string, actAsstNoticeCodeList []string) ([]NoticPreview, bool, error) {
|
||||
sysRecordSlice, err := noticerecdmod.GetRecordSlice(uid, systemNoticeCodeList)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
astRecordSlice, err := noticerecdmod.GetRecordSlice(uid, actAsstNoticeCodeList)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
var noticTalkList []NoticPreview
|
||||
totalHasNew := false
|
||||
fill := func(recordSlice noticerecdmod.RecordSlice) {
|
||||
if recordSlice.Len() == 0 {
|
||||
return
|
||||
}
|
||||
sort.Sort(sort.Reverse(recordSlice)) //发送时间降序
|
||||
top := recordSlice[0]
|
||||
noticeFmt, ok := noticeFmtMap[top.NoticeCode]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
notice := Notice{
|
||||
SendAt: top.LastSendAt,
|
||||
Sender: noticeFmt.Sender,
|
||||
Title: noticeFmt.Title,
|
||||
Content: noticeFmt.Content,
|
||||
}
|
||||
newsCount := int64(0)
|
||||
for _, v := range recordSlice {
|
||||
if v.LastSendAt.After(v.LastReadAt) {
|
||||
newsCount++
|
||||
}
|
||||
}
|
||||
totalHasNew = totalHasNew || (newsCount > 0)
|
||||
noticTalkList = append(noticTalkList, NoticPreview{newsCount, notice})
|
||||
}
|
||||
fill(sysRecordSlice)
|
||||
fill(astRecordSlice)
|
||||
return noticTalkList, totalHasNew, nil
|
||||
}
|
||||
|
||||
type NoticePage struct {
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []Notice `json:"list"`
|
||||
}
|
||||
|
||||
type FMTSenderMatch = noticefmtmod.SenderMatch
|
||||
|
||||
type RecdNoticeCodeInMatch = noticerecdmod.NoticeCodeInMatch
|
||||
|
||||
type RecdUIDMatch = noticerecdmod.UIDMatch
|
||||
|
||||
func NoticePages(sender Sender, uid uint64, skip int64, limit int64) (NoticePage, error) {
|
||||
enable := true
|
||||
enableMatch := FMTEnableMatch{Enable: &enable}
|
||||
senderMatch := FMTSenderMatch{Sender: &sender}
|
||||
noticeFmts, err := noticefmtmod.GetNoticeFmtListByUID(uid, senderMatch.New(), enableMatch.New())
|
||||
if err != nil {
|
||||
return NoticePage{}, err
|
||||
}
|
||||
noticeCodeList := make([]string, len(noticeFmts))
|
||||
fmtMap := make(map[string]NoticeFmt)
|
||||
for i, fmt := range noticeFmts {
|
||||
noticeCodeList[i] = fmt.NoticeCode
|
||||
fmtMap[fmt.NoticeCode] = fmt
|
||||
}
|
||||
sort := bson.D{{Key: "lastSendAt", Value: -1}}
|
||||
recdNoticeCodeInMatch := RecdNoticeCodeInMatch{NoticeCodes: noticeCodeList}
|
||||
recdUIDMatch := RecdUIDMatch{UID: &uid}
|
||||
limitEx := limit + 1
|
||||
list, err := noticerecdmod.List(sort, skip, limitEx, recdUIDMatch.New(), recdNoticeCodeInMatch.New())
|
||||
if err != nil {
|
||||
return NoticePage{}, err
|
||||
}
|
||||
hasNext := false
|
||||
if len(list) > int(limit) {
|
||||
hasNext = true
|
||||
list = list[:limit]
|
||||
}
|
||||
noticeList := make([]Notice, 0, list.Len())
|
||||
for _, v := range list {
|
||||
noticeCode := v.NoticeCode
|
||||
fmt, ok := fmtMap[noticeCode]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
notice := Notice{
|
||||
SendAt: v.LastSendAt,
|
||||
Sender: fmt.Sender,
|
||||
Title: fmt.Title,
|
||||
Content: fmt.Content,
|
||||
}
|
||||
noticeList = append(noticeList, notice)
|
||||
}
|
||||
return NoticePage{
|
||||
hasNext,
|
||||
noticeList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func UpdateNoticeReadTime(sender Sender, uid uint64, readAt time.Time) error {
|
||||
senderMatch := FMTSenderMatch{Sender: &sender}
|
||||
noticeFmts, err := noticefmtmod.GetNoticeFmtListByUID(uid, senderMatch.New())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
noticeCodeList := make([]string, len(noticeFmts))
|
||||
for i, fmt := range noticeFmts {
|
||||
noticeCodeList[i] = fmt.NoticeCode
|
||||
}
|
||||
return noticerecdmod.UpdateReadTime(readAt, uid, noticeCodeList)
|
||||
}
|
||||
|
||||
const (
|
||||
Fans = noticerecdmod.Fans //粉丝
|
||||
Like = noticerecdmod.Like //点赞
|
||||
Cmet = noticerecdmod.Cmet //评论
|
||||
Itte = noticerecdmod.Itte //互动
|
||||
)
|
||||
|
||||
// 动态预览
|
||||
type TrendPreview struct {
|
||||
Count int64 `json:"count"` //数量
|
||||
}
|
||||
|
||||
// 动态预览映射
|
||||
func TrendPreviewMap(uid uint64) (map[noticerecdmod.TrendType]TrendPreview, bool) {
|
||||
var (
|
||||
fensPreviewCount int64 //粉丝预览数
|
||||
likePreviewCount int64 //点赞预览数
|
||||
cmetPreviewCount int64 //评论预览数
|
||||
ittePreviewCount int64 //互动预览数 暂不做
|
||||
)
|
||||
hasNews := (fensPreviewCount + likePreviewCount + cmetPreviewCount + ittePreviewCount) > 0
|
||||
return map[noticerecdmod.TrendType]TrendPreview{
|
||||
Fans: {fensPreviewCount},
|
||||
Like: {likePreviewCount},
|
||||
Cmet: {cmetPreviewCount},
|
||||
Itte: {ittePreviewCount},
|
||||
}, hasNews
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
package integral_config_ser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/backpackmod"
|
||||
"91porn-server/models/v/coupon_record_mod"
|
||||
"91porn-server/models/v/integralconfigmod"
|
||||
"91porn-server/models/v/integralexcangemod"
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/videocoupon"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func GetAllConfig() []integralconfigmod.APPIntegralConfig {
|
||||
var data []integralconfigmod.APPIntegralConfig
|
||||
op := options.Find().SetSort(bson.M{"duration": 1})
|
||||
filter := bson.M{"status": true}
|
||||
list, err := integralconfigmod.QueryAllList(filter, op)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
if len(list) <= 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
for _, i := range list {
|
||||
appdata := integralconfigmod.APPIntegralConfig{
|
||||
ID: i.ID,
|
||||
Name: i.Name,
|
||||
Type: i.Type,
|
||||
Desc: i.Desc,
|
||||
Price: i.Price,
|
||||
Img: i.Img,
|
||||
Duration: i.Duration,
|
||||
}
|
||||
data = append(data, appdata)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func ExchangeIntegral(uid uint64, in *integralconfigmod.ExchangeIntegralReq) stderr.Code {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if user == nil || user.ID.IsZero() {
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
if user.HasBanned || user.HasLocked {
|
||||
return stderr.ErrAccountHasBinded
|
||||
}
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if wallet.ID.IsZero() || wallet.Integral <= 0 {
|
||||
return stderr.InsufficientIntegral
|
||||
}
|
||||
id, err := primitive.ObjectIDFromHex(in.ID)
|
||||
if err != nil {
|
||||
return stderr.Failure
|
||||
}
|
||||
// 获取积分配置
|
||||
config, err := integralconfigmod.FindOneById(id)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if config.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
|
||||
if config.Price > wallet.Integral {
|
||||
return stderr.InsufficientIntegral
|
||||
}
|
||||
var code stderr.Code
|
||||
switch config.Type {
|
||||
case integralconfigmod.VIPDate:
|
||||
// 1 VIP兑换
|
||||
code = HandeExchangeVip(config, user)
|
||||
case integralconfigmod.AICount:
|
||||
// 2 AI黑科技卷
|
||||
code = HandeExchangeAIUndressCount(config, user)
|
||||
//code = HandeExchangeAICount(config, user)
|
||||
case integralconfigmod.GoldCoinBonus:
|
||||
// 3 金币加赠券
|
||||
code = HandeExchangeGoldCoinBonus(config, user)
|
||||
case integralconfigmod.GoldWatch:
|
||||
// 4 金币观影券
|
||||
code = HandeExchangeGoldWatch(config, user)
|
||||
case integralconfigmod.InKind:
|
||||
// 5 实物奖励
|
||||
code = HandeExchangeInKind(config, user, in)
|
||||
case integralconfigmod.FreeCount:
|
||||
// 6 幸运抽奖次数
|
||||
code = HandeExchangeFreeCount(config, user)
|
||||
default:
|
||||
code = stderr.Success
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// HandeExchangeAIUndressCount 兑换ai脱衣次数
|
||||
func HandeExchangeAIUndressCount(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) {
|
||||
uid := user.UID
|
||||
integral := -config.Price
|
||||
var creditPlan = walletmod.CreditPlan{Integral: &integral}
|
||||
|
||||
undressTimes := config.EquivalentPrice
|
||||
creditPlan.AiUndressFreeTimes = &undressTimes
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.Credit(t, creditPlan, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeAICount.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeAICount),
|
||||
Desc: "积分兑换AI黑科技券-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeAIUndressCount Transaction Log err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeAIUndressCount Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// HandeExchangeAICount 积分兑换AI黑科技劵
|
||||
func HandeExchangeAICount(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) {
|
||||
var (
|
||||
goodsList []backpackmod.Backpack
|
||||
now = time.Now()
|
||||
uid = user.UID
|
||||
)
|
||||
|
||||
goodsList = append(goodsList, backpackmod.Backpack{
|
||||
UID: user.UID,
|
||||
GoodsName: config.Name,
|
||||
GoodsType: backpackmod.AiChangeFaceDiscount,
|
||||
GoodsValue: config.EquivalentPrice,
|
||||
GoodsOrigin: "积分兑换AI黑科技劵",
|
||||
GoodsDesc: config.Desc,
|
||||
Status: backpackmod.Unused,
|
||||
ExpiredTime: now.AddDate(0, 0, int(config.Duration)),
|
||||
CreateTime: now,
|
||||
})
|
||||
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.DebitIntegral(t, config.Price, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
if err := backpackmod.AddGoodsMany(t, uid, goodsList); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeAICount.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeAICount),
|
||||
Desc: "积分兑换AI黑科技券-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeAICount Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeAICount Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// HandeExchangeGoldCoinBonus 积分兑换金币加赠券
|
||||
func HandeExchangeGoldCoinBonus(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) {
|
||||
uid := user.UID
|
||||
// 保存用户优惠券
|
||||
now := time.Now()
|
||||
cId := HandleCId(config.ID)
|
||||
// 获取观影券是否存在
|
||||
c, err := coupon_record_mod.FindOneByCId(cId)
|
||||
if err != nil {
|
||||
return stderr.Failure
|
||||
}
|
||||
if !c.ID.IsZero() {
|
||||
return stderr.Failure
|
||||
}
|
||||
newCo := coupon_record_mod.CouponRecord{
|
||||
CID: cId,
|
||||
UID: user.UID,
|
||||
UserName: user.Name,
|
||||
Name: config.Name,
|
||||
Count: 1,
|
||||
Price: config.EquivalentPrice,
|
||||
Type: coupon_record_mod.PrizeTypeGoldCoinBonus,
|
||||
Used: false,
|
||||
IsDelete: false,
|
||||
ExpireTime: now.Add(time.Hour * 24 * time.Duration(config.Duration)),
|
||||
}
|
||||
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.DebitIntegral(t, config.Price, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeGoldCoinBonus.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeGoldCoinBonus),
|
||||
Desc: "积分兑换金币加赠券-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
err = coupon_record_mod.InsertOne(newCo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeGoldCoinBonus Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeGoldCoinBonus Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// HandeExchangeGoldWatch 积分兑换金币观影券
|
||||
func HandeExchangeGoldWatch(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) {
|
||||
uid := user.UID
|
||||
var coupons []videocoupon.UserGoldVideoCoupon
|
||||
coupon := videocoupon.UserGoldVideoCoupon{
|
||||
UID: uid,
|
||||
Num: int(config.EquivalentPrice),
|
||||
Used: false,
|
||||
Source: "积分兑换金币观影券",
|
||||
}
|
||||
coupons = append(coupons, coupon)
|
||||
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.DebitIntegral(t, config.Price, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeGoldWatch.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeGoldWatch),
|
||||
Desc: "积分兑换金币观影券-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
// 赠送观影券
|
||||
if len(coupons) > 0 {
|
||||
if err := videocoupon.InsertMany(coupons); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeGoldWatch Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeGoldWatch Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// HandeExchangeFreeCount 积分兑换幸运抽奖次数
|
||||
func HandeExchangeFreeCount(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) {
|
||||
uid := user.UID
|
||||
integral := -config.Price
|
||||
var creditPlan = walletmod.CreditPlan{Integral: &integral}
|
||||
|
||||
lotteryTimes := config.EquivalentPrice
|
||||
creditPlan.LotteryTimes = &lotteryTimes
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.Credit(t, creditPlan, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeFreeCount.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeFreeCount),
|
||||
Desc: "积分兑换幸运抽奖次数-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeFreeCount Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeFreeCount Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// HandeExchangeVip 积分兑换VIP
|
||||
func HandeExchangeVip(config integralconfigmod.IntegralConfig, user *usermod.User) (code stderr.Code) {
|
||||
var vipExpire time.Time
|
||||
uid := user.UID
|
||||
now := time.Now()
|
||||
d := time.Hour * 24 * time.Duration(config.EquivalentPrice)
|
||||
if user.VipExpireDate.After(now) { //renew
|
||||
vipExpire = user.VipExpireDate.Add(d)
|
||||
} else {
|
||||
vipExpire = now.Add(d)
|
||||
}
|
||||
vipLevel := usermod.RenewVIPLevel(user.VipExpireDate, now, user.VipLevel, 1)
|
||||
sel := usermod.UserSelector{VipExpireDate: &vipExpire, VipLevel: &vipLevel}
|
||||
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.DebitIntegral(t, config.Price, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
history := prdcthsomod.ProductHistory{ //插入商品购买记录
|
||||
ID: primitive.NewObjectID(),
|
||||
UID: uid,
|
||||
ProductID: config.ID,
|
||||
Name: config.Name,
|
||||
Amount: config.Price,
|
||||
Income: 0,
|
||||
ProductType: prdcthsomod.VIP,
|
||||
SysType: user.SysType,
|
||||
}
|
||||
if err = prdcthsomod.InsertProductHistory(t, &history); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改会员
|
||||
if err = usermod.UpdateVIP(t, uid, user.VipExpireDate, sel); err != nil {
|
||||
return err
|
||||
}
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeVip.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeVip),
|
||||
TransNo: history.ID,
|
||||
Desc: "积分兑换VIP-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeVip Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeVip Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// HandeExchangeInKind 积分兑换实体
|
||||
func HandeExchangeInKind(config integralconfigmod.IntegralConfig, user *usermod.User, in *integralconfigmod.ExchangeIntegralReq) (code stderr.Code) {
|
||||
//if in.Name == "" || in.Tel == "" || in.Address == "" {
|
||||
// return stderr.ErrParamError
|
||||
//}
|
||||
uid := user.UID
|
||||
now := time.Now()
|
||||
|
||||
ie := integralexcangemod.IntegralExchange{
|
||||
EID: config.ID,
|
||||
UID: user.UID,
|
||||
Portrait: user.Portrait,
|
||||
Name: in.Name,
|
||||
Tel: in.Tel,
|
||||
Address: in.Address,
|
||||
Status: integralexcangemod.PENDING,
|
||||
CreateTime: now,
|
||||
}
|
||||
|
||||
if err := appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
var err error
|
||||
wallet, err := walletmod.DebitIntegral(t, config.Price, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
err = integralexcangemod.InsertOne(t, ie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txnLogs := []txnmod.TransactionLog{
|
||||
{
|
||||
UID: uid,
|
||||
Integral: -config.Price,
|
||||
ActualIntegral: float64(-config.Price),
|
||||
TranType: txnmod.IntegralExchangeInKind.Key(),
|
||||
TranTypeInt: int64(txnmod.IntegralExchangeInKind),
|
||||
Desc: "积分兑换实物-" + config.Name,
|
||||
SysType: user.SysType,
|
||||
RealIntegral: wallet.RealIntegral(),
|
||||
},
|
||||
}
|
||||
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeInKind Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("HandeExchangeInKind Transaction err %s", err.Error()))
|
||||
return stderr.BuyFailed //通知消息
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func HandleCId(id primitive.ObjectID) string {
|
||||
return id.Hex() + common.UUID()
|
||||
}
|
||||
|
||||
func GetRecordConfig(uid uint64) []integralconfigmod.AppIntegralRecord {
|
||||
var data []integralconfigmod.AppIntegralRecord
|
||||
filter := bson.M{"tranTypeInt": bson.M{"$in": []txnmod.TransType{
|
||||
txnmod.IntegralExchangeVip,
|
||||
txnmod.IntegralExchangeAICount,
|
||||
txnmod.IntegralExchangeGoldCoinBonus,
|
||||
txnmod.IntegralExchangeGoldWatch,
|
||||
txnmod.IntegralExchangeInKind,
|
||||
txnmod.IntegralExchangeFreeCount,
|
||||
}}, "uid": uid}
|
||||
op := options.Find().SetSort(bson.D{{"createdAt", -1}})
|
||||
all, err := txnmod.QueryAll(filter, op)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
for _, i := range all {
|
||||
appdata := integralconfigmod.AppIntegralRecord{
|
||||
ID: i.ID,
|
||||
Integral: i.Integral,
|
||||
Name: i.TranType,
|
||||
Desc: i.Desc,
|
||||
Type: int(i.TranTypeInt),
|
||||
CreatedAt: i.CreatedAt,
|
||||
}
|
||||
data = append(data, appdata)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package likeser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/messageser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
topser "91porn-server/common/top"
|
||||
topasist "91porn-server/common/top/asistant"
|
||||
"91porn-server/models/v/aiplazamod"
|
||||
"91porn-server/models/v/cmtmod"
|
||||
"91porn-server/models/v/likemod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/messagemod"
|
||||
"91porn-server/models/v/tagmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// ThumbsUp 视频/评论点赞
|
||||
func ThumbsUp(ctx *gin.Context, uid uint64, objType string, objID primitive.ObjectID, tagID primitive.ObjectID) (code stderr.Code, err error) {
|
||||
var publisherID uint64
|
||||
var vMod vidmod.VideoModel
|
||||
var cMod cmtmod.Comment
|
||||
var media mediamod.Media
|
||||
|
||||
switch objType {
|
||||
case constant.LikeTypeSP, constant.LikeTypeShort, constant.LikeTypeCover, constant.LikeTypePic, constant.LikeTypeSEED_LINK:
|
||||
publisherID, vMod, err = handleVideo(objID)
|
||||
if err != nil {
|
||||
return stderr.Failure, err
|
||||
}
|
||||
case constant.LikeTypeText, constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeDrama:
|
||||
media, err = handleMedia(objID)
|
||||
if err != nil {
|
||||
return stderr.Failure, err
|
||||
}
|
||||
if objType == constant.LikeTypeDrama && (media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete) {
|
||||
return stderr.LikeErrVideoIsInvalid, errors.New("drama is inactive")
|
||||
}
|
||||
|
||||
case constant.AiPlaza:
|
||||
// ai广场
|
||||
|
||||
case constant.LikeTypeComment:
|
||||
publisherID, cMod, vMod, err = handleComment(objID)
|
||||
if err != nil {
|
||||
return stderr.Failure, err
|
||||
}
|
||||
publisherID = cMod.UserID
|
||||
}
|
||||
|
||||
lMod, err := likemod.FindOneByUIDAndObjID(uid, objID)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrGetDataFailed
|
||||
log.Error("app service like ThumbsUp FindOneByUIDAndObjID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID))
|
||||
return
|
||||
}
|
||||
if !lMod.ID.IsZero() {
|
||||
code = stderr.LikeHasAlreadyThumbs
|
||||
return
|
||||
}
|
||||
likeMod := likemod.Like{
|
||||
Type: objType,
|
||||
ObjID: objID,
|
||||
UserID: uid,
|
||||
LikedUserID: publisherID,
|
||||
}
|
||||
|
||||
if !tagID.IsZero() && tagmod.IsExists(tagID) {
|
||||
likeMod.TagID = tagID
|
||||
_ = tagmod.IncreaseTagLikeCount(tagID)
|
||||
}
|
||||
|
||||
err = likemod.InsertOne(&likeMod)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrThumbsFailed
|
||||
log.Error("app service like ThumbsUp InsertOne err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID))
|
||||
return
|
||||
}
|
||||
common.Go(func() {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 增加视频/评论 点赞次数
|
||||
switch objType {
|
||||
case constant.AiPlaza:
|
||||
aiplazamod.IncLikeCount(objID)
|
||||
case constant.CollectTypeSP, constant.CollectTypeShort, constant.CollectTypeCover, constant.CollectTypePIC:
|
||||
topasist.Incr(topser.TypeVideo(objType), objID.Hex(), 1)
|
||||
fallthrough
|
||||
case constant.LikeTypeSEED_LINK:
|
||||
if err := vidmod.IncrementRecommendInteraction(objID, vidmod.RecommendInteractionLike); err != nil {
|
||||
log.Error("increment recommend like failed", log.E(err), log.Any("objID", objID))
|
||||
}
|
||||
err = vidmod.IncLikeCount(objID)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrIncCountFailed
|
||||
log.Error("app service like ThumbsUp IncLikeCount err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID))
|
||||
return
|
||||
}
|
||||
// 发送动态消息
|
||||
_ = messageser.DynamicMsgUserGroup(messagemod.PublicAddMsg{
|
||||
SendUid: uid,
|
||||
TakeUid: vMod.PublisherID,
|
||||
MsgType: messagemod.LikeMsg,
|
||||
ObjId: vMod.ID,
|
||||
ObjName: vMod.Title,
|
||||
ObjCover: vMod.Cover,
|
||||
ObjType: vMod.NewsType,
|
||||
})
|
||||
|
||||
break
|
||||
case constant.LikeTypeComment:
|
||||
err = cmtmod.CmtIncreaseLikeCountByID(1, objID)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrIncCountFailed
|
||||
log.Error("app service like ThumbsUp CmtIncreaseLikeCountByID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID))
|
||||
return
|
||||
}
|
||||
dynamicContent := fmt.Sprintf("%s 于 %s 点赞了你的评论", user.Name, time.Now().Format("15:04:05"))
|
||||
if !vMod.ID.IsZero() && vMod.PublisherID > 0 {
|
||||
// 增加点赞消息通知动态
|
||||
messageser.DynamicMsgUserGroup(messagemod.PublicAddMsg{
|
||||
SendUid: uid,
|
||||
TakeUid: publisherID,
|
||||
MsgType: messagemod.LikeCommentMsg,
|
||||
ObjId: vMod.ID,
|
||||
ObjName: vMod.Title,
|
||||
ObjCover: vMod.Cover,
|
||||
ObjType: vMod.NewsType,
|
||||
Content: dynamicContent,
|
||||
})
|
||||
}
|
||||
|
||||
case constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeText, constant.LikeTypeDrama:
|
||||
err = mediamod.IncreaseCountLikeByIDs(1, objID)
|
||||
if err != nil {
|
||||
log.Error("app service media like ThumbsUp IncreaseLikeCountByID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objID", objID))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("ReportVideoPurchase fail", log.E(err))
|
||||
code = stderr.LikeErrThumbsFailed
|
||||
}
|
||||
}
|
||||
})
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
|
||||
func handleMedia(id primitive.ObjectID) (media mediamod.Media, err error) {
|
||||
// 获取播单列表
|
||||
media, err = mediamod.GetInfo(id)
|
||||
if err != nil {
|
||||
return media, err
|
||||
}
|
||||
|
||||
if media.ID.IsZero() {
|
||||
return media, errors.New("media is null")
|
||||
}
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func handleVideo(id primitive.ObjectID) (uid uint64, vMod vidmod.VideoModel, err error) {
|
||||
var publishId uint64
|
||||
vMod, err = vidmod.GetVideoInfo(id.Hex())
|
||||
if err != nil {
|
||||
log.Error("app service like ThumbsUp GetVideoInfo err", log.E(err), log.Any("uid", uid), log.Any("objID", id))
|
||||
return publishId, vMod, err
|
||||
}
|
||||
if vMod.ID.IsZero() {
|
||||
log.Error("app service like ThumbsUp video mod is nil err", log.Any("uid", uid), log.Any("objID", id))
|
||||
return publishId, vMod, errors.New("video is null")
|
||||
}
|
||||
// 视频未通过审核/未审核
|
||||
if vMod.Status != 1 && vMod.Status != 3 {
|
||||
return publishId, vMod, errors.New("video is status is err")
|
||||
}
|
||||
publishId = vMod.PublisherID
|
||||
// 增加作者点赞数
|
||||
common.Go(func() {
|
||||
usermod.IncLikeCount(publishId)
|
||||
})
|
||||
return publishId, vMod, nil
|
||||
}
|
||||
|
||||
func handleComment(id primitive.ObjectID) (publishId uint64, cMod cmtmod.Comment, vMod vidmod.VideoModel, err error) {
|
||||
|
||||
cMod, err = cmtmod.CmtFindOneByID(id)
|
||||
if err != nil {
|
||||
err = stderr.ErrDbQueryError
|
||||
return
|
||||
}
|
||||
if cMod.ID.IsZero() {
|
||||
err = stderr.ErrDbQueryError
|
||||
}
|
||||
if cMod.ObjType == "video" {
|
||||
// 获取视频
|
||||
vMod, err = vidmod.GetVideoInfo(cMod.ObjID.Hex())
|
||||
if err != nil {
|
||||
err = stderr.ErrDbQueryError
|
||||
log.Error("app service like ThumbsUp GetVideoInfo err", log.E(err), log.Any("objID", cMod.ObjID.Hex()))
|
||||
return
|
||||
}
|
||||
if vMod.ID.IsZero() {
|
||||
err = stderr.ErrDbQueryError
|
||||
log.Error("app service like ThumbsUp video mod is nil err", log.Any("objID", cMod.ObjID.Hex()))
|
||||
return
|
||||
}
|
||||
// 视频未通过审核/未审核
|
||||
if vMod.Status != 1 && vMod.Status != 3 {
|
||||
err = stderr.LikeErrVideoIsInvalid
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return cMod.UserID, cMod, vMod, err
|
||||
}
|
||||
|
||||
// ThumbsDown 取消点赞
|
||||
func ThumbsDown(ctx *gin.Context, uid uint64, objType string, objIDs ...primitive.ObjectID) (code stderr.Code, err error) {
|
||||
if len(objIDs) == 0 {
|
||||
return stderr.Success, nil
|
||||
}
|
||||
lMods, err := likemod.FindLikesByObjIDS(objType, uid, objIDs)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrGetDataFailed
|
||||
log.Error("app service like ThumbsDown FindOneByUIDAndObjID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs))
|
||||
return
|
||||
}
|
||||
if len(lMods) == 0 {
|
||||
code = stderr.LikeHasNotThumbs
|
||||
return
|
||||
}
|
||||
err = likemod.DeleteMany(uid, objType, objIDs)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrThumbsFailed
|
||||
log.Error("app service like ThumbsDown DeleteOne err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs))
|
||||
return
|
||||
}
|
||||
// 减少视频/评论 点赞数
|
||||
switch objType {
|
||||
case constant.AiPlaza:
|
||||
aiplazamod.DecLikeCount(objIDs)
|
||||
case constant.LikeTypeSP, constant.LikeTypeShort, constant.LikeTypeCover, constant.LikeTypePic, constant.LikeTypeSEED_LINK:
|
||||
err = vidmod.DecLikeCount(objIDs...)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrDecCountFailed
|
||||
log.Error("app service like ThumbsDown DecLikeCount err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs))
|
||||
return
|
||||
}
|
||||
|
||||
case constant.LikeTypeCartoon, constant.LikeTypeComics, constant.LikeTypeText, constant.LikeTypeDrama:
|
||||
err = mediamod.IncreaseCountLikeByIDs(-1, objIDs...)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrGetDataFailed
|
||||
return
|
||||
}
|
||||
|
||||
case constant.LikeTypeComment:
|
||||
err = cmtmod.CmtIncreaseLikeCountByID(-1, objIDs...)
|
||||
if err != nil {
|
||||
code = stderr.LikeErrDecCountFailed
|
||||
log.Error("app service like ThumbsDown CmtIncreaseLikeCountByID err", log.E(err), log.Any("uid", uid), log.Any("objType", objType), log.Any("objIDs", objIDs))
|
||||
return
|
||||
}
|
||||
}
|
||||
code = stderr.Success
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package likeser
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/service/searcher"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/likemod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type VideoRes = searcher.VideoRes
|
||||
|
||||
type UserBInfo = usermod.BaseInfo
|
||||
|
||||
type ObjectID = primitive.ObjectID
|
||||
|
||||
type Like = likemod.Like
|
||||
|
||||
type Record struct {
|
||||
//点赞者UID
|
||||
LUID uint64 `json:"lUID"`
|
||||
//点赞者名字
|
||||
LName string `json:"lName"`
|
||||
//点赞者头像
|
||||
LPortrait string `json:"lPortrait"`
|
||||
//点赞类型 video:对视频点赞 comment:对评论点赞
|
||||
LType string `json:"lType"`
|
||||
//点赞时间
|
||||
LTime time.Time `json:"lTime"`
|
||||
//视屏信息
|
||||
Video VideoRes `json:"video"`
|
||||
}
|
||||
|
||||
type RecordPage struct {
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []Record `json:"list"`
|
||||
}
|
||||
|
||||
func RecordPages(uid uint64, skip, limit int64) (RecordPage, error) {
|
||||
limitEx := limit + 1
|
||||
likeList, err := likemod.ListByLikedUserID(uid, skip, limitEx)
|
||||
if err != nil {
|
||||
return RecordPage{}, err
|
||||
}
|
||||
hasNext := false
|
||||
if len(likeList) > int(limit) {
|
||||
hasNext = true
|
||||
likeList = likeList[:limit]
|
||||
}
|
||||
vidMap := make(map[ObjectID]ObjectID, len(likeList)) //object->vid map
|
||||
vids := make([]ObjectID, 0, len(likeList))
|
||||
cmtIDs := make([]ObjectID, 0, len(likeList))
|
||||
uids := make([]uint64, 0, len(likeList))
|
||||
|
||||
for _, v := range likeList {
|
||||
if v.Type == "video" {
|
||||
vids = append(vids, v.ObjID) //视屏ID
|
||||
vidMap[v.ObjID] = v.ObjID
|
||||
} else if v.Type == "comment" {
|
||||
cmtIDs = append(cmtIDs, v.ObjID) //评论ID
|
||||
}
|
||||
uids = append(uids, v.UserID)
|
||||
}
|
||||
|
||||
var (
|
||||
wg = sync.WaitGroup{}
|
||||
videoResList []VideoRes
|
||||
userInfos []*UserBInfo
|
||||
)
|
||||
wg.Add(2)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
list, err := searcher.GetVideoResList(uid, vids)
|
||||
if err != nil {
|
||||
log.Error("commentser GetVideoResList faild", log.E(err))
|
||||
}
|
||||
videoResList = list
|
||||
})
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
list, err := usermod.GetUsersBaseInfo(uids)
|
||||
if err != nil {
|
||||
log.Error("commentser GetUsersBaseInfo faild", log.E(err))
|
||||
}
|
||||
userInfos = list
|
||||
})
|
||||
wg.Wait()
|
||||
videoResMap := make(map[ObjectID]VideoRes, len(videoResList))
|
||||
for _, v := range videoResList {
|
||||
vidp := v.VideoInfo.ID
|
||||
if vidp != nil {
|
||||
videoResMap[*vidp] = v
|
||||
}
|
||||
}
|
||||
userInfoMap := make(map[uint64]*usermod.BaseInfo, len(userInfos))
|
||||
for _, v := range userInfos {
|
||||
userInfoMap[v.UID] = v
|
||||
}
|
||||
recordList := tRecordList(likeList, vidMap, videoResMap, userInfoMap)
|
||||
return RecordPage{
|
||||
hasNext,
|
||||
recordList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tRecordList(likeList []Like,
|
||||
vidMap map[ObjectID]ObjectID,
|
||||
videoResMap map[ObjectID]VideoRes,
|
||||
userInfoMap map[uint64]*usermod.BaseInfo,
|
||||
) []Record {
|
||||
recordList := make([]Record, 0, len(likeList))
|
||||
for _, v := range likeList {
|
||||
userInfo, ok := userInfoMap[v.UserID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
vid, ok := vidMap[v.ObjID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
videoRes, ok := videoResMap[vid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
record := Record{
|
||||
LUID: userInfo.UID,
|
||||
LName: userInfo.Name,
|
||||
LPortrait: userInfo.Portrait,
|
||||
LType: v.Type,
|
||||
LTime: v.CreatedAt,
|
||||
Video: videoRes,
|
||||
}
|
||||
recordList = append(recordList, record)
|
||||
}
|
||||
return recordList
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package luckydrawser
|
||||
|
||||
import "time"
|
||||
|
||||
type PrizeType int32
|
||||
|
||||
const (
|
||||
// 实物
|
||||
InKindPrize PrizeType = iota + 1
|
||||
// VIP
|
||||
VipPrize
|
||||
// 金币
|
||||
GoldPrize
|
||||
)
|
||||
|
||||
type VipType int32
|
||||
|
||||
const (
|
||||
// 默认值
|
||||
Default VipType = iota
|
||||
// 月卡
|
||||
MonthCard
|
||||
// 季卡
|
||||
QuarterlyCard
|
||||
// 年卡
|
||||
YearCard
|
||||
// 终身会员卡
|
||||
LifelongCard
|
||||
)
|
||||
|
||||
type CenterBaseInfo struct {
|
||||
FreeCount int32 `json:"freeCount"` // 免费次数
|
||||
RefreshTime time.Time `json:"refreshTime"` // 免费次数恢复时间
|
||||
PrizeList []*PrizeInfo `json:"prizeList"` // 奖品列表
|
||||
DrawPrize int64 `json:"drawPrize"` // 单次抽奖价格
|
||||
ActivityTitle string `json:"activityTitle"` // 活动标题
|
||||
ActivityDesc string `json:"activityDesc"` // 活动注意事项
|
||||
ActivityDate string `json:"activityDate"` // 活动时间
|
||||
}
|
||||
|
||||
// 奖品信息
|
||||
type PrizeInfo struct {
|
||||
PrizeId int64 `json:"prizeId"`
|
||||
PrizeName string `json:"prizeName"`
|
||||
PrizeImgCover string `json:"prizeImgCover"`
|
||||
PrizePrice int64 `json:"prizePrice"`
|
||||
PrizeCount int32 `json:"prizeCount"`
|
||||
PrizeType int32 `json:"prizeType"`
|
||||
VipCard int32 `json:"vipCard"`
|
||||
}
|
||||
|
||||
type CenterDrawReq struct {
|
||||
AppId int32 `json:"appId"` // 应用id
|
||||
ActivityId int64 `json:"activityId"` // 活动id
|
||||
UserId int64 `json:"userId"` // 用户id
|
||||
UserName string `json:"userName"`
|
||||
UserAvatar string `json:"userAvatar"`
|
||||
Count int `json:"count"` // 抽奖次数
|
||||
}
|
||||
|
||||
// 抽奖返回
|
||||
type CenterDrawResp struct {
|
||||
List []*PrizeInfo `json:"list"`
|
||||
}
|
||||
|
||||
// 抽奖记录
|
||||
type CenterRecordReq struct {
|
||||
AppId int32 `json:"appId"` // 应用id
|
||||
ActivityId int64 `json:"activityId"` // 活动id
|
||||
UserId int64 `json:"userId"` // 用户id
|
||||
}
|
||||
|
||||
// 抽奖记录返回
|
||||
type CenterRecordResp struct {
|
||||
List []*RecordInfo `json:"list"`
|
||||
}
|
||||
|
||||
// 跑马灯
|
||||
type CenterMarqueeReq struct {
|
||||
AppId int32 `json:"appId"` // 应用id
|
||||
ActivityId int64 `json:"activityId"` // 活动id
|
||||
}
|
||||
|
||||
// 跑马灯返回
|
||||
type CenterMarqueeResp struct {
|
||||
List []*RecordInfo `json:"list"`
|
||||
}
|
||||
|
||||
// 幸运狼友
|
||||
type CenterLuckUserReq struct {
|
||||
AppId int32 `json:"appId"` // 应用id
|
||||
ActivityId int64 `json:"activityId"` // 活动id
|
||||
}
|
||||
|
||||
type CenterLuckUserResp struct {
|
||||
List []*RecordInfo `json:"list"`
|
||||
}
|
||||
|
||||
type RecordInfo struct {
|
||||
PrizeName string `json:"prizeName"` // 奖品名称
|
||||
CreateTime time.Time `json:"createTime"` // 创建时间
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package luckydrawser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/timeutil"
|
||||
"91porn-server/models/v/activitymod"
|
||||
"91porn-server/models/v/prizemod"
|
||||
"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"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func BaseInfo(ctx context.Context, in *BaseInfoReq) (out *BaseInfoResp, err error) {
|
||||
now := time.Now()
|
||||
wal, err := walletmod.GetWallet(in.UserId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id, err := primitive.ObjectIDFromHex(in.ActivityId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取活动
|
||||
activityInfo, err := activitymod.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if activityInfo == nil {
|
||||
return nil, errors.New("活动不存在")
|
||||
}
|
||||
if !activityInfo.Status {
|
||||
return nil, errors.New("活动未开启")
|
||||
}
|
||||
// 活动时间判定
|
||||
if !now.After(activityInfo.StartTime) || now.After(activityInfo.EndTime) {
|
||||
return nil, errors.New("活动已结束")
|
||||
}
|
||||
// 获取奖池配置
|
||||
prizePoolInfo, err := activitymod.GetPrizePool(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prizePoolInfo == nil {
|
||||
return nil, errors.New("奖池配置为空")
|
||||
}
|
||||
var freeCount int32
|
||||
if prizePoolInfo.FreeCount > 0 {
|
||||
// 获取用户免费记录
|
||||
userFree, err := activitymod.GetUserFree(id, in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userFree == nil {
|
||||
if err = activitymod.AddUserFree(id, in.UserId, prizePoolInfo.FreeCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
freeCount = prizePoolInfo.FreeCount
|
||||
} else {
|
||||
// 今日已使用
|
||||
if userFree.UpdateTime.After(timeutil.BeginningOfDay(time.Now())) {
|
||||
freeCount = userFree.FreeCount
|
||||
} else {
|
||||
// 刷新免费次数
|
||||
if err = activitymod.UpdateOneSet(id, in.UserId, prizePoolInfo.FreeCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
freeCount = prizePoolInfo.FreeCount
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取奖品列表
|
||||
prizeList, err := prizemod.GetPrizeListByActivityID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(prizeList) == 0 {
|
||||
return nil, errors.New("奖品配置为空")
|
||||
}
|
||||
// 获取用户当日抽奖消费金额
|
||||
consumerAmount, _, err := txnmod.GetIncomeByType(in.UserId, txnmod.JiuGongGeDraw, timeutil.BeginningOfDay(now), timeutil.BeginningOfDay(now.AddDate(0, 0, 1)))
|
||||
out = &BaseInfoResp{
|
||||
Balance: wal.Amount,
|
||||
FreeCount: freeCount,
|
||||
Price: prizePoolInfo.Price,
|
||||
Prizes: prizeList,
|
||||
ConsumerAmount: -int64(consumerAmount),
|
||||
ActivityTitle: activityInfo.Title,
|
||||
ActivityDesc: activityInfo.Desc,
|
||||
ActivityDate: activityInfo.StartTime.Format("2006-01-02") + "到" + activityInfo.EndTime.Format("2006-01-02"),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Draw(ctx context.Context, in *DrawReq) ([]*prizemod.Prize, error) {
|
||||
id, err := primitive.ObjectIDFromHex(in.ActivityId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 获取活动
|
||||
activityInfo, err := activitymod.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if activityInfo == nil {
|
||||
return nil, errors.New("活动不存在")
|
||||
} else {
|
||||
if !activityInfo.Status {
|
||||
return nil, errors.New("活动未开启")
|
||||
}
|
||||
// 活动时间判定
|
||||
now := time.Now()
|
||||
if !now.After(activityInfo.StartTime) || now.After(activityInfo.EndTime) {
|
||||
return nil, errors.New("活动已结束")
|
||||
}
|
||||
}
|
||||
// 获取奖品列表
|
||||
prizeList, err := prizemod.GetPrizeListByActivityID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(prizeList) == 0 {
|
||||
return nil, errors.New("奖品配置为空")
|
||||
}
|
||||
// 获取奖池配置
|
||||
prizePoolInfo, err := activitymod.GetPrizePool(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prizePoolInfo == nil {
|
||||
return nil, errors.New("奖池配置为空")
|
||||
}
|
||||
// 奖池模式处理
|
||||
switch prizePoolInfo.Model {
|
||||
case activitymod.Game:
|
||||
case activitymod.Free:
|
||||
return freeModelHanlder(in, id, prizeList, prizePoolInfo)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 个人抽奖记录
|
||||
func Record(ctx context.Context, activityId string, uid uint64) (interface{}, error) {
|
||||
id, err := primitive.ObjectIDFromHex(activityId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := activitymod.QueryAllRecord(bson.M{"activityId": id, "uid": uid}, options.Find().SetLimit(100).SetSort(bson.M{"createTime": -1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make([]map[string]interface{}, len(list))
|
||||
for i, v := range list {
|
||||
data[i] = map[string]interface{}{
|
||||
"name": v.Name + "*" + decimal.NewFromInt32(v.Count).String(),
|
||||
"createTime": v.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func Marquee(activityId string) ([]string, error) {
|
||||
id, err := primitive.ObjectIDFromHex(activityId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := activitymod.GetMarquee(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listLen := len(list)
|
||||
uids := make([]uint64, listLen)
|
||||
for i, v := range list {
|
||||
uids[i] = v.UID
|
||||
}
|
||||
userMap := make(map[uint64]string, 10)
|
||||
if len(uids) > 0 {
|
||||
userList, err := usermod.FindUsersByUID(uids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range userList {
|
||||
userMap[v.UID] = v.Name
|
||||
}
|
||||
for _, v := range userList {
|
||||
userMap[v.UID] = v.Name
|
||||
}
|
||||
}
|
||||
strs := make([]string, listLen)
|
||||
for i, v := range list {
|
||||
if v.UserName != "" {
|
||||
strs[i] = fmt.Sprintf("恭喜%s 抽中%s", v.UserName, v.Name)
|
||||
continue
|
||||
}
|
||||
strs[i] = fmt.Sprintf("恭喜%s 抽中%s", userMap[v.UID], v.Name)
|
||||
}
|
||||
return strs, nil
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package luckydrawser
|
||||
|
||||
import (
|
||||
"91porn-server/models/v/taskrecordmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/activitymod"
|
||||
"91porn-server/models/v/prizemod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/smallnest/weighted"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// 免费模式处理
|
||||
func freeModelHanlder(in *DrawReq, activityId primitive.ObjectID, prizeList []*prizemod.Prize, prizePool *activitymod.PrizePool) ([]*prizemod.Prize, error) {
|
||||
var (
|
||||
wg *weighted.RandW = weighted.NewRandW()
|
||||
drawPrice int64 // 本次抽奖总支出
|
||||
prizes []*prizemod.Prize // 本次抽奖结果
|
||||
reservoir int64 // 蓄水池金额
|
||||
systemIncome int64 // 系统收益金额
|
||||
systemRake int64 // 系统抽成金额
|
||||
now time.Time = time.Now()
|
||||
)
|
||||
// 奖品分类
|
||||
prizeListLen := len(prizeList)
|
||||
lowPrizes := make([]*prizemod.Prize, 0, prizeListLen) // 低级奖品列表
|
||||
middlePrizes := make([]*prizemod.Prize, 0, prizeListLen) // 中级奖品列表
|
||||
for _, v := range prizeList {
|
||||
if v.Level == prizemod.Low {
|
||||
lowPrizes = append(lowPrizes, v)
|
||||
}
|
||||
if v.Level == prizemod.Middle {
|
||||
middlePrizes = append(middlePrizes, v)
|
||||
}
|
||||
wg.Add(v, int(v.Weights.Mul(decimal.NewFromInt(100)).IntPart()))
|
||||
}
|
||||
// 获取用户签到信息
|
||||
signInfo, err := taskrecordmod.GetByUID(in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if signInfo == nil {
|
||||
if _, err = taskrecordmod.AddTaskRecord(in.UserId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// 获取用户免费次数
|
||||
userFree, err := activitymod.GetUserFree(activityId, in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 使用金币抽奖
|
||||
if userFree != nil && userFree.FreeCount < int32(in.Count) {
|
||||
drawPrice = prizePool.Price * in.Count
|
||||
wal, err := walletmod.GetWallet(in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 余额不足
|
||||
if wal.Amount < drawPrice {
|
||||
return nil, errors.New("余额不足")
|
||||
}
|
||||
}
|
||||
log.Info(fmt.Sprintf("本次抽奖所需金币为[%d]", drawPrice))
|
||||
// 本次抽奖总抽成收益
|
||||
systemRake += decimal.NewFromInt(drawPrice).Mul(decimal.NewFromFloat(0.1)).IntPart()
|
||||
// 本次抽奖蓄水池抽成收益
|
||||
reservoir += decimal.NewFromInt(drawPrice).Mul(decimal.NewFromFloat(0.1)).IntPart()
|
||||
// 本次抽奖系统收益
|
||||
systemIncome += drawPrice - systemRake - reservoir
|
||||
drawRecord := make([]activitymod.UserRecord, in.Count) // 抽奖记录
|
||||
for i := 0; i < int(in.Count); i++ {
|
||||
p, ok := wg.Next().(*prizemod.Prize)
|
||||
if !ok || p == nil {
|
||||
return nil, errors.New("异常错误")
|
||||
}
|
||||
log.Info(fmt.Sprintf("金币抽奖-正常获取奖品[%s]", p.Name))
|
||||
// 玩家使用免费抽奖
|
||||
if drawPrice == 0 {
|
||||
// 替换为低级奖品
|
||||
if p.Level != prizemod.Low {
|
||||
p = lowPrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(lowPrizes))]
|
||||
}
|
||||
}
|
||||
// 金币抽奖 杀分判定 固定70%几率杀分 亏损达到预期2倍100%杀分
|
||||
if prizePool.SystemIncome < -(prizePool.EstimatedIncome/2) && drawPrice > 0 && p.Level != prizemod.Low {
|
||||
if rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100) > 30 || prizePool.SystemIncome < -(prizePool.EstimatedIncome*2) {
|
||||
log.Info(fmt.Sprintf("金币抽奖-杀分判定成功[当前奖品:%s]", p.Name))
|
||||
// 替换为低级奖品
|
||||
p = lowPrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(lowPrizes))]
|
||||
log.Info(fmt.Sprintf("金币抽奖-杀分判定成功[替换奖品:%s]", p.Name))
|
||||
}
|
||||
}
|
||||
// 放分判定 固定30%几率放分
|
||||
if prizePool.SystemIncome > prizePool.EstimatedIncome && rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(100) < 30 && drawPrice > 0 && p.Level != prizemod.Middle {
|
||||
log.Info(fmt.Sprintf("金币抽奖-放分判定成功[当前奖品:%s]", p.Name))
|
||||
// 替换为中级奖品
|
||||
p = middlePrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(middlePrizes))]
|
||||
log.Info(fmt.Sprintf("金币抽奖-放分判定成功[替换奖品:%s]", p.Name))
|
||||
}
|
||||
// 高级奖品放出判定
|
||||
if p.Level == prizemod.High {
|
||||
if prizePool.Reservoir < p.GetPrice() {
|
||||
// 替换为中级奖品
|
||||
p = middlePrizes[rand.New(rand.NewSource(time.Now().UnixNano())).Intn(len(middlePrizes))]
|
||||
} else {
|
||||
// 扣除蓄水池
|
||||
prizePool.Reservoir -= p.GetPrice()
|
||||
reservoir -= p.GetPrice()
|
||||
}
|
||||
}
|
||||
// 增加系统收益 不包含本次抽奖收益
|
||||
prizePool.SystemIncome -= p.GetPrice()
|
||||
systemIncome -= p.GetPrice()
|
||||
prizes = append(prizes, p)
|
||||
// 增加抽奖记录
|
||||
drawRecord[i] = activitymod.UserRecord{
|
||||
ActivityID: activityId,
|
||||
VIPCardID: p.VIPCardID,
|
||||
UID: in.UserId,
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Price: p.Price,
|
||||
Count: p.Count,
|
||||
Value: p.Value,
|
||||
Level: p.Level,
|
||||
DrawPrice: prizePool.Price,
|
||||
CreateTime: now,
|
||||
}
|
||||
}
|
||||
hanlders := make([]prizemod.PrizeHanlder, 0, len(prizes))
|
||||
var addGoldHanlder prizemod.PrizeHanlder
|
||||
var transactionLogs []txnmod.TransactionLog // 资金流水记录
|
||||
for _, v := range prizes {
|
||||
hanlder, err := prizemod.Run(in.UserId, *v, txnmod.JiuGongGeDraw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch v.Type {
|
||||
case prizemod.Gold:
|
||||
addGoldHanlder = hanlder
|
||||
hanlders = append(hanlders, hanlder)
|
||||
default:
|
||||
if hanlder != nil {
|
||||
hanlders = append(hanlders, hanlder)
|
||||
transactionLogs = append(transactionLogs, hanlder.GetTransactionLog()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
if reservoir != 0 || systemIncome != 0 || systemRake != 0 {
|
||||
// 更新奖池
|
||||
if err := activitymod.UpdatePrizePoolInc(t, activityId, reservoir, systemIncome, systemRake); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 更新用户免费次数
|
||||
if drawPrice == 0 {
|
||||
if err := activitymod.UpdateOneUserFreeInc(t, activityId, in.UserId, -int32(in.Count)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if drawPrice > 0 {
|
||||
wal, err := walletmod.DebitAmount(t, drawPrice, in.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transactionLogs = append(transactionLogs, txnmod.TransactionLog{
|
||||
TransNo: primitive.NewObjectID(),
|
||||
UID: in.UserId,
|
||||
Amount: -drawPrice,
|
||||
ActualAmount: -float64(drawPrice),
|
||||
TranType: txnmod.JiuGongGeDraw.Key(),
|
||||
TranTypeInt: int64(txnmod.JiuGongGeDraw),
|
||||
Desc: fmt.Sprintf("%s消费-%d个金币", "", drawPrice),
|
||||
RealAmount: wal.RealAmount(),
|
||||
})
|
||||
}
|
||||
// 更新抽奖记录
|
||||
if len(drawRecord) > 0 {
|
||||
if err = activitymod.AddMany(drawRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, v := range hanlders {
|
||||
if err = v.Run(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if addGoldHanlder != nil {
|
||||
transactionLogs = append(transactionLogs, addGoldHanlder.GetTransactionLog()...)
|
||||
}
|
||||
if len(transactionLogs) > 0 {
|
||||
if err = txnmod.InsertManyTransactionLog(t, transactionLogs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prizes, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package luckydrawser
|
||||
|
||||
import "91porn-server/models/v/prizemod"
|
||||
|
||||
type BaseInfoReq struct {
|
||||
ActivityId string `json:"activityId" form:"activityId"` // 活动id
|
||||
AppId uint64 `json:"appId" form:"appId"` // 活动id
|
||||
UserId uint64
|
||||
}
|
||||
|
||||
type BaseInfoResp struct {
|
||||
Balance int64 `json:"balance"` // 用户余额(金币)
|
||||
FreeCount int32 `json:"freeCount"` // 免费次数
|
||||
Price int64 `json:"price"` // 单次抽奖价格
|
||||
Prizes []*prizemod.Prize `json:"prizes"` // 奖品列表
|
||||
ConsumerAmount int64 `json:"consumerAmount"` // 当日消费金额
|
||||
ActivityTitle string `json:"activityTitle"` // 活动标题
|
||||
ActivityDesc string `json:"activityDesc"` // 活动注意事项
|
||||
ActivityDate string `json:"activityDate"` // 活动时间
|
||||
}
|
||||
|
||||
type DrawReq struct {
|
||||
ActivityId string `json:"activityId" form:"activityId"` // 活动id
|
||||
Count int64 `json:"count" form:"count"` // 抽奖次数
|
||||
UserId uint64
|
||||
}
|
||||
|
||||
type DrawResp []*PrizeInfo
|
||||
|
||||
type RecordReq struct {
|
||||
ActivityId string `json:"activityId" form:"activityId"` // 活动id
|
||||
}
|
||||
|
||||
type RecordResp []*RecordInfo
|
||||
|
||||
type MarqueeReq struct {
|
||||
ActivityId string `json:"activityId" form:"activityId"` // 活动id
|
||||
}
|
||||
|
||||
type MarqueeResp []string
|
||||
|
||||
type LuckUserReq struct {
|
||||
ActivityId uint64 `json:"activityId" form:"activityId"` // 活动id
|
||||
AppId uint64 `json:"appId" form:"appId"` // 活动id
|
||||
}
|
||||
|
||||
type LuckUserResp []*RecordInfo
|
||||
@@ -0,0 +1,159 @@
|
||||
package m3u8ticket
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"91porn-server/common"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// maxSignDepth 反射兜底遍历响应体的最大深度,防止异常对象图导致过深递归(视频响应本身是浅树)。
|
||||
const maxSignDepth = 8
|
||||
|
||||
// ticketSigner 实现 vidmod.M3u8Signer:持有本次请求的签票上下文(用户、客户端 IP/UA),
|
||||
// 由各响应体的 SignM3u8 回调,对交出的每个 m3u8 地址字段就地签票。
|
||||
type ticketSigner struct {
|
||||
userID uint64
|
||||
ip string
|
||||
ua string
|
||||
}
|
||||
|
||||
// SignM3u8URL 对单个 m3u8 地址字段就地签票;空串或签发失败(绝对地址、非 m3u8 等)时保持原值不变。
|
||||
func (s *ticketSigner) SignM3u8URL(field *string, playable, preview bool) {
|
||||
if field == nil || *field == "" {
|
||||
return
|
||||
}
|
||||
if signed := BuildSignedURL(s.userID, *field, s.ip, s.ua, playable, preview); signed != "" {
|
||||
*field = signed
|
||||
}
|
||||
}
|
||||
|
||||
func requestIP(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
return common.GetIP(c)
|
||||
}
|
||||
|
||||
func requestUA(c *gin.Context) string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
u, err := common.GetUA(c)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.UserAgent
|
||||
}
|
||||
|
||||
// Sign 是 SignResponse 的便捷封装:自动从上下文取登录用户ID,控制器只需在返回前加一行
|
||||
// m3u8ticket.Sign(ctx, data) 即可对响应体里的视频 m3u8 地址按需签票。
|
||||
func Sign(c *gin.Context, data interface{}) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
SignResponse(c, common.TryGetUID(c), data)
|
||||
}
|
||||
|
||||
// SignResponse 是各视频接口的统一入口:对响应体 data 里的视频 m3u8 地址就地签票(不再限 H5)。
|
||||
//
|
||||
// data 只要实现 vidmod.M3u8Signable(所有携带视频地址的响应体都已实现)即可零反射签票;另外兼容控制器直接
|
||||
// 下发的裸切片(recommser.GetAd 等)与 gin.H(RecommendList 等)。少数尚未实现接口的历史/边缘类型走反射兜底,
|
||||
// 保证不漏签。data 为 nil / userID 为 0 / 未配置密钥(BuildSignedURL 返回空)时零副作用,兼容灰度未配置密钥。
|
||||
func SignResponse(c *gin.Context, userID uint64, data interface{}) {
|
||||
if data == nil || userID == 0 {
|
||||
return
|
||||
}
|
||||
// data 可能是服务出错时返回的类型化 nil 指针((*Resp)(nil)):接口本身非 nil,但对值接收者方法会解引用
|
||||
// nil 而 panic,先挡掉(等价于旧反射对 nil 指针的 IsNil 保护)。
|
||||
if rv := reflect.ValueOf(data); rv.Kind() == reflect.Ptr && rv.IsNil() {
|
||||
return
|
||||
}
|
||||
signData(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}, data)
|
||||
}
|
||||
|
||||
// signData 把 data 分派给对应签票实现:优先零反射的接口/裸切片/map,其余历史类型走反射兜底。
|
||||
func signData(s vidmod.M3u8Signer, data interface{}) {
|
||||
switch d := data.(type) {
|
||||
case nil:
|
||||
return
|
||||
case vidmod.M3u8Signable:
|
||||
d.SignM3u8(s)
|
||||
case []*vidmod.VideoInfo:
|
||||
vidmod.SignM3u8Infos(s, d)
|
||||
case []*vidmod.VideoInfoResp:
|
||||
vidmod.SignM3u8Resps(s, d)
|
||||
case gin.H:
|
||||
for _, v := range d {
|
||||
signData(s, v)
|
||||
}
|
||||
default:
|
||||
signByReflect(s, reflect.ValueOf(data), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// signByReflect 是反射兜底:仅用于尚未实现 vidmod.M3u8Signable 的历史/边缘响应类型,语义与旧实现一致——
|
||||
// 遍历对象图,命中 *VideoInfo/*VideoInfoResp 即签票。正常高频接口都走上面的接口分支,不会到这里。
|
||||
// 待剩余类型补齐 SignM3u8 后,可整体删除本函数及 reflect 依赖。
|
||||
func signByReflect(s vidmod.M3u8Signer, rv reflect.Value, depth int) {
|
||||
if depth > maxSignDepth || !rv.IsValid() {
|
||||
return
|
||||
}
|
||||
switch rv.Kind() {
|
||||
case reflect.Ptr:
|
||||
if rv.IsNil() {
|
||||
return
|
||||
}
|
||||
switch v := rv.Interface().(type) {
|
||||
case *vidmod.VideoInfo:
|
||||
v.SignM3u8(s)
|
||||
return
|
||||
case *vidmod.VideoInfoResp:
|
||||
v.SignM3u8(s)
|
||||
return
|
||||
}
|
||||
signByReflect(s, rv.Elem(), depth+1)
|
||||
case reflect.Interface:
|
||||
if rv.IsNil() {
|
||||
return
|
||||
}
|
||||
signByReflect(s, rv.Elem(), depth+1)
|
||||
case reflect.Struct:
|
||||
if rv.CanAddr() {
|
||||
switch v := rv.Addr().Interface().(type) {
|
||||
case *vidmod.VideoInfo:
|
||||
v.SignM3u8(s)
|
||||
return
|
||||
case *vidmod.VideoInfoResp:
|
||||
v.SignM3u8(s)
|
||||
return
|
||||
}
|
||||
}
|
||||
t := rv.Type()
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
if t.Field(i).PkgPath != "" { // 跳过未导出字段,reflect 无法安全访问
|
||||
continue
|
||||
}
|
||||
signByReflect(s, rv.Field(i), depth+1)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
signByReflect(s, rv.Index(i), depth+1)
|
||||
}
|
||||
case reflect.Map:
|
||||
for _, k := range rv.MapKeys() {
|
||||
signByReflect(s, rv.MapIndex(k), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SignURL 供只有单个 m3u8 地址字段的响应(官网视频、媒体内容等)复用。
|
||||
// 不再限制 H5:已配置密钥时改写,未配置(BuildSignedURL 返回空)则原样返回,兼容灰度未配置密钥的情况。
|
||||
func SignURL(c *gin.Context, userID uint64, field *string, playable, preview bool) {
|
||||
if field == nil || *field == "" || userID == 0 {
|
||||
return
|
||||
}
|
||||
(&ticketSigner{userID: userID, ip: requestIP(c), ua: requestUA(c)}).SignM3u8URL(field, playable, preview)
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
// Package m3u8ticket 实现 H5 m3u8 播放防盗链票据。
|
||||
//
|
||||
// 背景:H5 播放地址 /api/app/vid/h5/m3u8/{path} 原先只靠一个可被任意复制的 path 就能换取真实
|
||||
// m3u8,导致地址一旦泄露即可被第三方站点盗链直接播放。
|
||||
//
|
||||
// 方案:服务端在返回视频地址时,把「用户ID + m3u8 相对 path + 过期时间 + 客户端 IP 摘要 + 播放/试看
|
||||
// 语义」用二进制紧凑打包,用服务端私钥做 AES 加密(crypt.CoreAesEncryptEx),再 URL-safe base64,得到
|
||||
// 一个不可读、看似随机的单段 token,拼在一个版本前缀之后作为整条播放地址:
|
||||
//
|
||||
// {version}/{token}.m3u8
|
||||
// 例:tk1/AbC-d....m3u8 (真实 path 已加密进 token,地址上不再出现;结尾 .m3u8 仅供前端/CDN 按后缀识别)
|
||||
//
|
||||
// 真实 m3u8 相对路径整段收进密文,第三方从地址上看不到任何真实资源路径或 bucket 结构;token 任何一个
|
||||
// 字节被篡改都会导致解密/校验失败。DownloadM3u8H5 在放行前剥出 token 解密,校验过期时间/IP 摘要/用户,
|
||||
// 得到密文里的真实 path 再取 m3u8;校验失败则改下发广告 fallback,从而阻断盗链。
|
||||
//
|
||||
// 放进 path 段而不是 query:前端本就把 sourceURL 当作相对路径直接拼在 /api/app/vid/h5/m3u8/ 后面,
|
||||
// 放进 path 段对前端透明,也不与前端自带的 ?token= 冲突。m3u8 内的 TS/密钥地址均为绝对(CDN)或根绝对
|
||||
// 地址,不受该前缀影响。
|
||||
//
|
||||
// 仅 H5 端启用;原生 App 仍走原始地址与既有鉴权。未配置 h5M3u8TicketKey 时整套逻辑关闭(不签不验),
|
||||
// 便于先发代码再由运维配置私钥开启严格模式。
|
||||
package m3u8ticket
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common/crypt"
|
||||
"91porn-server/models/commod"
|
||||
)
|
||||
|
||||
const (
|
||||
// ticketTTL 票据有效期,签发后 6 小时内有效(覆盖列表签发到实际起播的较长间隔)。
|
||||
ticketTTL = 6 * time.Hour
|
||||
// ticketVer 载荷版本(占 1 字节),轮换 h5M3u8TicketKey 或调整载荷格式时用于兼容判断。
|
||||
ticketVer = byte(1)
|
||||
// aesNonceLen 与 crypt.CoreAesEncryptEx 约定的随机 nonce 长度。
|
||||
aesNonceLen = 12
|
||||
// digestLen IP/UA 摘要保留的字节数(HMAC 截断),8 字节足够防复用又显著缩短 token。
|
||||
digestLen = 8
|
||||
|
||||
// pathScheme 是票据的版本标识段(首段),用于把带票地址和裸地址区分开。
|
||||
// 取一个真实 m3u8 路径不会用到的前缀(真实路径首段为 v1/v2/v3/bktuserup/bktadminup/sp/pms/laosiji 等)。
|
||||
pathScheme = "tk1"
|
||||
|
||||
// bindIP 是否把客户端 IP 摘要绑进票据,开启后可降低票据被异地/第三方站点复用的风险。
|
||||
bindIP = true
|
||||
// bindUA 是否把 UA 摘要绑进票据,默认关闭(H5 内嵌播放器与外层 UA 未必一致),预留更严格策略。
|
||||
bindUA = false
|
||||
|
||||
// FallbackPath 校验失败时下发的广告/提示 m3u8(与 limitHandler 限流兜底同一资源)。
|
||||
FallbackPath = "/bktadminup/sp/zp/hb/7a/0q/860513c4947143b29083160ec41588b5.m3u8"
|
||||
)
|
||||
|
||||
// 载荷标志位(1 字节)。
|
||||
const (
|
||||
flagPlayable byte = 1 << iota // 是否真实可播放(正式播放权限)
|
||||
flagPreview // 是否试看/预览票
|
||||
flagHasIP // 载荷内是否含 IP 摘要
|
||||
flagHasUA // 载荷内是否含 UA 摘要
|
||||
)
|
||||
|
||||
var errBadTicket = errors.New("m3u8 ticket malformed")
|
||||
|
||||
// Ticket 是解密后的票据载荷(内存态)。序列化用二进制紧凑格式,不导出摘要字段。
|
||||
type Ticket struct {
|
||||
UserID uint64 // 播放用户ID,绑定 /vid/info 等鉴权通过的用户
|
||||
Path string // 允许播放的 m3u8 归一化相对路径(收进密文,不出现在地址上)
|
||||
ExpireAt int64 // 过期时间戳(秒)
|
||||
Playable bool // 是否真实可播放
|
||||
Preview bool // 是否试看/预览票
|
||||
ipDigest []byte // 客户端 IP 的 HMAC 截断摘要,仅校验用
|
||||
uaDigest []byte // 客户端 UA 的 HMAC 截断摘要,仅校验用
|
||||
}
|
||||
|
||||
// Enabled 报告防盗链票据是否开启(即是否已配置服务端私钥)。
|
||||
func Enabled() bool {
|
||||
return key() != ""
|
||||
}
|
||||
|
||||
func key() string {
|
||||
if appg.Conf == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(appg.Conf.Base.H5M3u8TicketKey)
|
||||
}
|
||||
|
||||
// appPrefix 返回带票地址最前面的应用标识前缀 JHA-{appid}/(appid = commod.KFK_APPID,补零至少 3 位,
|
||||
// 与数据上报口径一致)。例:appid=204 -> "JHA-204/",appid=1 -> "JHA-001/",appid=2001 -> "JHA-2001/"。
|
||||
func appPrefix() string {
|
||||
return fmt.Sprintf("JHA-%03d/", commod.KFK_APPID)
|
||||
}
|
||||
|
||||
// stripAppPrefix 剥掉带票地址最前面可能的 JHA-{appid}/ 前缀,兼容带/不带前缀两种形态(前缀仅供 CDN/前端识别)。
|
||||
func stripAppPrefix(s string) string {
|
||||
if strings.HasPrefix(s, "JHA-") {
|
||||
if i := strings.IndexByte(s, '/'); i >= 0 {
|
||||
return s[i+1:]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// BuildSignedURL 为一条 m3u8 相对 path 签发票据,返回 {version}/{token}.m3u8 形式的相对地址。
|
||||
//
|
||||
// 真实 path 整段加密进 token,返回地址上不再出现真实路径。返回值保留调用方传入 rawPath 的前导斜杠形态
|
||||
// (在版本段之前),这样 H5 端沿用旧逻辑自行拼接 /api/app/vid/h5/m3u8/ 前缀即可。
|
||||
// 返回空串表示无法签发(未开启、userID 非法、path 非合法 m3u8),调用方应保持原值不变。
|
||||
func BuildSignedURL(userID uint64, rawPath, ip, ua string, playable, preview bool) string {
|
||||
k := key()
|
||||
if k == "" || userID == 0 {
|
||||
return ""
|
||||
}
|
||||
normalizedPath, ok := normalizePath(rawPath)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := &Ticket{
|
||||
UserID: userID,
|
||||
Path: normalizedPath,
|
||||
ExpireAt: time.Now().Add(ticketTTL).Unix(),
|
||||
Playable: playable,
|
||||
Preview: preview,
|
||||
}
|
||||
token, err := encodeTicket(t, ip, ua, k)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 最前面加 JHA-{appid}/ 应用标识前缀(供 CDN/前端识别);末尾补 .m3u8 后缀让 HLS 播放器/CDN 按后缀识别。
|
||||
// 前缀与后缀仅用于展示,VerifyPath/StripTicket 会先剥掉再解 token。
|
||||
signed := appPrefix() + pathScheme + "/" + token + ".m3u8"
|
||||
if strings.HasPrefix(strings.TrimSpace(rawPath), "/") {
|
||||
signed = "/" + signed
|
||||
}
|
||||
return signed
|
||||
}
|
||||
|
||||
// VerifyPath 校验并剥离带票 m3u8 请求路径。
|
||||
// 入参 source 为 DownloadM3u8H5 拿到的原始 c.Param("source")(形如 /tk1/{token})。
|
||||
// 返回密文里解出的真实 source(带前导斜杠,供 GetAPPM3u8 使用)、票据载荷、是否校验通过。
|
||||
// 未带票据前缀或任一校验失败时返回 ok=false;调用方应改下发广告兜底 m3u8。
|
||||
func VerifyPath(source, ip, ua string) (realSource string, t *Ticket, ok bool) {
|
||||
k := key()
|
||||
if k == "" {
|
||||
return source, nil, false
|
||||
}
|
||||
s := strings.TrimLeft(strings.TrimSpace(source), "/")
|
||||
s = stripAppPrefix(s) // 先剥掉 JHA-{appid}/ 应用前缀
|
||||
if !strings.HasPrefix(s, pathScheme+"/") {
|
||||
return source, nil, false // 非带票地址(裸 path)
|
||||
}
|
||||
token := s[len(pathScheme)+1:]
|
||||
if i := strings.IndexByte(token, '/'); i >= 0 {
|
||||
token = token[:i] // 防御:token 为单段,只取票据段
|
||||
}
|
||||
token = strings.TrimSuffix(token, ".m3u8") // 剥掉展示用后缀,还原纯票据
|
||||
if token == "" {
|
||||
return source, nil, false
|
||||
}
|
||||
|
||||
data, err := decodeTicket(token, k)
|
||||
if err != nil || data.UserID == 0 {
|
||||
return source, nil, false
|
||||
}
|
||||
if time.Now().Unix() > data.ExpireAt {
|
||||
return source, nil, false
|
||||
}
|
||||
//if bindIP && !hmac.Equal(data.ipDigest, digest(k, ip)) {
|
||||
// return source, nil, false
|
||||
//}
|
||||
//if bindUA && !hmac.Equal(data.uaDigest, digest(k, ua)) {
|
||||
// return source, nil, false
|
||||
//}
|
||||
// path 来自密文,天然可信;再归一化一次做纵深防御(理论上签发时已归一化)。
|
||||
normalizedPath, nok := normalizePath(data.Path)
|
||||
if !nok {
|
||||
return source, nil, false
|
||||
}
|
||||
return "/" + normalizedPath, data, true
|
||||
}
|
||||
|
||||
// StripTicket 供不做严格校验的播放接口(如 /vid/m3u8)使用:若 source 是带票地址,仅解密取出真实 path
|
||||
// 返回,不校验过期/IP/用户;否则(裸 path 或无法解密)原样返回。用于兼容老明文链接与新带票链接都能播放。
|
||||
func StripTicket(source string) string {
|
||||
k := key()
|
||||
if k == "" {
|
||||
return source
|
||||
}
|
||||
s := strings.TrimLeft(strings.TrimSpace(source), "/")
|
||||
s = stripAppPrefix(s) // 先剥掉 JHA-{appid}/ 应用前缀
|
||||
if !strings.HasPrefix(s, pathScheme+"/") {
|
||||
return source // 裸 path,原样返回
|
||||
}
|
||||
token := s[len(pathScheme)+1:]
|
||||
if i := strings.IndexByte(token, '/'); i >= 0 {
|
||||
token = token[:i]
|
||||
}
|
||||
token = strings.TrimSuffix(token, ".m3u8")
|
||||
data, err := decodeTicket(token, k)
|
||||
if err != nil || data.UserID == 0 {
|
||||
return source // 解不出就原样,尽量不影响播放
|
||||
}
|
||||
normalizedPath, ok := normalizePath(data.Path)
|
||||
if !ok {
|
||||
return source
|
||||
}
|
||||
return "/" + normalizedPath
|
||||
}
|
||||
|
||||
// NormalizePath 对外暴露的 path 归一化(便于测试与调用方复用)。
|
||||
func NormalizePath(path string) string {
|
||||
normalized, _ := normalizePath(path)
|
||||
return normalized
|
||||
}
|
||||
|
||||
// normalizePath 归一化 m3u8 相对 path:剔除完整 URL、反斜杠、../、%2e、非 .m3u8,统一去掉前导斜杠。
|
||||
func normalizePath(path string) (string, bool) {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", false
|
||||
}
|
||||
lower := strings.ToLower(path)
|
||||
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
|
||||
return "", false
|
||||
}
|
||||
if idx := strings.IndexByte(path, '?'); idx >= 0 {
|
||||
path = path[:idx]
|
||||
}
|
||||
path = strings.TrimLeft(path, "/")
|
||||
lower = strings.ToLower(path)
|
||||
if path == "" || strings.Contains(path, `\`) || strings.Contains(path, "..") || strings.Contains(lower, "%2e") {
|
||||
return "", false
|
||||
}
|
||||
if !strings.HasSuffix(lower, ".m3u8") {
|
||||
return "", false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
// encodeTicket 把载荷二进制紧凑打包 -> AES 加密 -> URL-safe base64,得到单段 token。
|
||||
// 二进制布局:ver(1) flags(1) userID(8,BE) expireAt(4,BE) [ipDigest(8)] [uaDigest(8)] pathLen(2,BE) path(N)。
|
||||
func encodeTicket(t *Ticket, ip, ua, k string) (string, error) {
|
||||
pathBytes := []byte(t.Path)
|
||||
if len(pathBytes) > 0xffff {
|
||||
return "", errors.New("m3u8 ticket path too long")
|
||||
}
|
||||
var flags byte
|
||||
if t.Playable {
|
||||
flags |= flagPlayable
|
||||
}
|
||||
if t.Preview {
|
||||
flags |= flagPreview
|
||||
}
|
||||
if bindIP {
|
||||
flags |= flagHasIP
|
||||
}
|
||||
if bindUA {
|
||||
flags |= flagHasUA
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, 16+digestLen*2+len(pathBytes))
|
||||
buf = append(buf, ticketVer, flags)
|
||||
buf = binary.BigEndian.AppendUint64(buf, t.UserID)
|
||||
buf = binary.BigEndian.AppendUint32(buf, uint32(t.ExpireAt))
|
||||
if flags&flagHasIP != 0 {
|
||||
buf = append(buf, digest(k, ip)...)
|
||||
}
|
||||
if flags&flagHasUA != 0 {
|
||||
buf = append(buf, digest(k, ua)...)
|
||||
}
|
||||
buf = binary.BigEndian.AppendUint16(buf, uint16(len(pathBytes)))
|
||||
buf = append(buf, pathBytes...)
|
||||
|
||||
encrypted, err := crypt.CoreAesEncryptEx(buf, aesNonceLen, k)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// RawURLEncoding:字符集 [A-Za-z0-9_-],无 '/' 无 '=' 填充,可安全作为单个 path 段。
|
||||
return base64.RawURLEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// decodeTicket 解 base64 -> AES 解密 -> 按二进制布局解析。对不可信输入做严格边界检查,非法返回错误。
|
||||
func decodeTicket(token, k string) (*Ticket, error) {
|
||||
token = strings.TrimSpace(token)
|
||||
raw, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
if raw, err = base64.URLEncoding.DecodeString(token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
b, err := safeDecrypt(raw, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 最小:ver(1)+flags(1)+userID(8)+expireAt(4) = 14
|
||||
if len(b) < 14 {
|
||||
return nil, errBadTicket
|
||||
}
|
||||
off := 0
|
||||
ver := b[off]
|
||||
off++
|
||||
if ver != ticketVer {
|
||||
return nil, errBadTicket
|
||||
}
|
||||
flags := b[off]
|
||||
off++
|
||||
userID := binary.BigEndian.Uint64(b[off : off+8])
|
||||
off += 8
|
||||
expireAt := int64(binary.BigEndian.Uint32(b[off : off+4]))
|
||||
off += 4
|
||||
|
||||
t := &Ticket{
|
||||
UserID: userID,
|
||||
ExpireAt: expireAt,
|
||||
Playable: flags&flagPlayable != 0,
|
||||
Preview: flags&flagPreview != 0,
|
||||
}
|
||||
if flags&flagHasIP != 0 {
|
||||
if len(b) < off+digestLen {
|
||||
return nil, errBadTicket
|
||||
}
|
||||
t.ipDigest = b[off : off+digestLen]
|
||||
off += digestLen
|
||||
}
|
||||
if flags&flagHasUA != 0 {
|
||||
if len(b) < off+digestLen {
|
||||
return nil, errBadTicket
|
||||
}
|
||||
t.uaDigest = b[off : off+digestLen]
|
||||
off += digestLen
|
||||
}
|
||||
if len(b) < off+2 {
|
||||
return nil, errBadTicket
|
||||
}
|
||||
pathLen := int(binary.BigEndian.Uint16(b[off : off+2]))
|
||||
off += 2
|
||||
if pathLen == 0 || len(b) < off+pathLen {
|
||||
return nil, errBadTicket
|
||||
}
|
||||
t.Path = string(b[off : off+pathLen])
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// safeDecrypt 包住底层解密,避免个别非法票据触发 panic 影响请求进程。
|
||||
func safeDecrypt(encrypted []byte, k string) (decrypted []byte, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("m3u8 ticket decrypt panic: %v", r)
|
||||
}
|
||||
}()
|
||||
decrypted, err = crypt.CoreAesDecryptEx(encrypted, aesNonceLen, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(decrypted) == 0 {
|
||||
return nil, errors.New("m3u8 ticket empty payload")
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// digest 用服务端私钥对 value 做 HMAC-SHA256 并截断到 digestLen 字节。
|
||||
func digest(k, value string) []byte {
|
||||
h := hmac.New(sha256.New, []byte(k))
|
||||
h.Write([]byte(strings.TrimSpace(value)))
|
||||
return h.Sum(nil)[:digestLen]
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package m3u8ticket
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/models/v/vidmod"
|
||||
)
|
||||
|
||||
const testKey = "unit-test-h5-m3u8-ticket-key-32byteslong!!"
|
||||
|
||||
func setKey(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
conf := &appg.GlobalConfig{}
|
||||
conf.Base.H5M3u8TicketKey = key
|
||||
appg.Conf = conf
|
||||
}
|
||||
|
||||
// signedWith 直接用给定票据构造带票地址(单段 token),供构造过期等异常场景。
|
||||
func signedWith(t *testing.T, tk *Ticket, ip, ua string) string {
|
||||
t.Helper()
|
||||
token, err := encodeTicket(tk, ip, ua, testKey)
|
||||
if err != nil {
|
||||
t.Fatalf("encodeTicket: %v", err)
|
||||
}
|
||||
return "/" + pathScheme + "/" + token
|
||||
}
|
||||
|
||||
// tamperToken 翻转带票地址里 token 的末位字符,用于构造被篡改的密文。
|
||||
func tamperToken(signed string) string {
|
||||
// 篡改 token 主体(跳过展示用 .m3u8 后缀)的末位字符,模拟密文被改。
|
||||
s := strings.TrimSuffix(signed, ".m3u8")
|
||||
if len(s) == 0 {
|
||||
return signed
|
||||
}
|
||||
b := []byte(s)
|
||||
if b[len(b)-1] == 'A' {
|
||||
b[len(b)-1] = 'B'
|
||||
} else {
|
||||
b[len(b)-1] = 'A'
|
||||
}
|
||||
return string(b) + ".m3u8"
|
||||
}
|
||||
|
||||
func TestSignVerifyRoundTrip(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
const (
|
||||
uid = uint64(10086)
|
||||
path = "bktuserup/sp/9o/06/g7/fn/894552ffed374e63b1a4d4f0d86e7e8c.m3u8"
|
||||
ip = "1.2.3.4"
|
||||
)
|
||||
signed := BuildSignedURL(uid, path, ip, "ua", true, false)
|
||||
if signed == "" {
|
||||
t.Fatal("expected non-empty signed url")
|
||||
}
|
||||
if !strings.HasPrefix(signed, "JHA-") || !strings.Contains(signed, "/"+pathScheme+"/") {
|
||||
t.Fatalf("signed url should be JHA-<appid>/tk1/..., got %q", signed)
|
||||
}
|
||||
if !strings.HasSuffix(signed, ".m3u8") {
|
||||
t.Fatalf("signed url should end with .m3u8, got %q", signed)
|
||||
}
|
||||
real, tk, ok := VerifyPath(signed, ip, "ua")
|
||||
if !ok {
|
||||
t.Fatal("valid ticket should pass VerifyPath")
|
||||
}
|
||||
if real != "/"+path {
|
||||
t.Fatalf("realSource mismatch: got %q want %q", real, "/"+path)
|
||||
}
|
||||
if tk.UserID != uid || !tk.Playable || tk.Preview {
|
||||
t.Fatalf("ticket payload wrong: %+v", tk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRejectsTamperedPath(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
signed := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false)
|
||||
// 真实 path 已加密进 token,无法替换 path;篡改 token 任一字节都必须导致解密/校验失败。
|
||||
if _, _, ok := VerifyPath(tamperToken(signed), "1.1.1.1", ""); ok {
|
||||
t.Fatal("tampered ticket must not validate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRejectsWrongIP(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
signed := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false)
|
||||
if _, _, ok := VerifyPath(signed, "9.9.9.9", ""); ok {
|
||||
t.Fatal("ticket must not validate from a different IP")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRejectsExpired(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
tk := &Ticket{
|
||||
UserID: 1,
|
||||
Path: "v3/av/a.m3u8",
|
||||
ExpireAt: time.Now().Add(-time.Minute).Unix(), // 已过期
|
||||
Playable: true,
|
||||
}
|
||||
if _, _, ok := VerifyPath(signedWith(t, tk, "1.1.1.1", ""), "1.1.1.1", ""); ok {
|
||||
t.Fatal("expired ticket must not validate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRejectsWrongKey(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
signed := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false)
|
||||
setKey(t, "another-key-totally-different-000000000000")
|
||||
if _, _, ok := VerifyPath(signed, "1.1.1.1", ""); ok {
|
||||
t.Fatal("ticket signed with a different key must not validate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyRejectsGarbageAndBarePath(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
cases := []string{
|
||||
"",
|
||||
"/bktuserup/sp/a.m3u8", // 裸 path,无票据前缀
|
||||
"/tk1//bktuserup/sp/a.m3u8", // 空票据段
|
||||
"/tk1/not-base64!!/v3/av/a.m3u8", // 非法 base64
|
||||
"/tk1/YWJj/v3/av/a.m3u8", // 可解码但非合法密文
|
||||
}
|
||||
for _, in := range cases {
|
||||
if _, _, ok := VerifyPath(in, "1.1.1.1", ""); ok {
|
||||
t.Fatalf("input %q must not validate", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisabledWhenNoKey(t *testing.T) {
|
||||
setKey(t, "")
|
||||
if Enabled() {
|
||||
t.Fatal("must be disabled without key")
|
||||
}
|
||||
if got := BuildSignedURL(1, "v3/av/a.m3u8", "1.1.1.1", "", true, false); got != "" {
|
||||
t.Fatalf("must not sign without key, got %q", got)
|
||||
}
|
||||
if _, _, ok := VerifyPath("/tk1/x/v3/av/a.m3u8", "1.1.1.1", ""); ok {
|
||||
t.Fatal("must not validate without key")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStripTicket 验证非严格接口的还原:带票地址(含过期)解出真实 path、明文原样、未配密钥原样。
|
||||
func TestStripTicket(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
const realPath = "bktuserup/sp/a.m3u8"
|
||||
// 带票地址 -> 解出真实 path
|
||||
signed := BuildSignedURL(1, realPath, "1.1.1.1", "", true, false)
|
||||
if got := StripTicket(signed); got != "/"+realPath {
|
||||
t.Fatalf("StripTicket(signed) = %q, want /%s", got, realPath)
|
||||
}
|
||||
// 过期票也应还原(StripTicket 不校验过期/IP)
|
||||
expired := signedWith(t, &Ticket{UserID: 1, Path: realPath, ExpireAt: time.Now().Add(-time.Hour).Unix(), Playable: true}, "9.9.9.9", "")
|
||||
if got := StripTicket(expired); got != "/"+realPath {
|
||||
t.Fatalf("StripTicket(expired) = %q, want /%s (no expiry check)", got, realPath)
|
||||
}
|
||||
// 裸明文 path 原样返回
|
||||
if got := StripTicket("/" + realPath); got != "/"+realPath {
|
||||
t.Fatalf("StripTicket(plain) = %q, want unchanged", got)
|
||||
}
|
||||
// 未配置密钥时原样返回
|
||||
setKey(t, "")
|
||||
if got := StripTicket(signed); got != signed {
|
||||
t.Fatalf("StripTicket without key should be unchanged, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignRejectsNonM3u8AndZeroUser(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
if got := BuildSignedURL(1, "v3/av/a.mp4", "1.1.1.1", "", true, false); got != "" {
|
||||
t.Fatalf("non-m3u8 must not be signed, got %q", got)
|
||||
}
|
||||
if got := BuildSignedURL(0, "v3/av/a.m3u8", "1.1.1.1", "", true, false); got != "" {
|
||||
t.Fatalf("zero user must not be signed, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePath(t *testing.T) {
|
||||
a, okA := normalizePath("/v3/av/a.m3u8")
|
||||
b, okB := normalizePath("v3/av/a.m3u8")
|
||||
if !okA || !okB || a != b {
|
||||
t.Fatalf("leading slash should normalize equally: %q ok=%v vs %q ok=%v", a, okA, b, okB)
|
||||
}
|
||||
bad := []string{"", "http://x/a.m3u8", "https://x/a.m3u8", "../a.m3u8", `v3\a.m3u8`, "v3/%2e%2e/a.m3u8", "v3/av/a.mp4"}
|
||||
for _, in := range bad {
|
||||
if _, ok := normalizePath(in); ok {
|
||||
t.Fatalf("path %q should be rejected", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedURLPreservesLeadingSlash(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
// 无前导斜杠输入 -> 无前导斜杠输出(与示例 sourceURL 形态一致)。
|
||||
if got := BuildSignedURL(1, "bktuserup/sp/a.m3u8", "1.1.1.1", "", true, false); strings.HasPrefix(got, "/") {
|
||||
t.Fatalf("should not add leading slash, got %q", got)
|
||||
}
|
||||
// 有前导斜杠输入 -> 保留前导斜杠(应用前缀在斜杠之后)。
|
||||
if got := BuildSignedURL(1, "/bktuserup/sp/a.m3u8", "1.1.1.1", "", true, false); !strings.HasPrefix(got, "/"+appPrefix()) {
|
||||
t.Fatalf("should preserve leading slash, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignedURLHasAppPrefix 验证带票地址最前面带 JHA-{appid} 前缀,且验票/StripTicket 能剥前缀还原。
|
||||
func TestSignedURLHasAppPrefix(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
const realPath = "bktuserup/sp/a.m3u8"
|
||||
signed := BuildSignedURL(1, realPath, "1.1.1.1", "", true, false)
|
||||
if !strings.HasPrefix(signed, appPrefix()) {
|
||||
t.Fatalf("signed url should start with %q, got %q", appPrefix(), signed)
|
||||
}
|
||||
if real, _, ok := VerifyPath(signed, "1.1.1.1", ""); !ok || real != "/"+realPath {
|
||||
t.Fatalf("VerifyPath with app prefix: ok=%v real=%q", ok, real)
|
||||
}
|
||||
if got := StripTicket(signed); got != "/"+realPath {
|
||||
t.Fatalf("StripTicket with app prefix = %q, want /%s", got, realPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFallbackSignsNestedVideoInfo 验证反射兜底能穿过未实现 M3u8Signable 的外层 wrapper -> 切片 -> 指针,
|
||||
// 就地改写 VideoInfo,且改写后的地址可被 VerifyPath 还原并校验通过。
|
||||
func TestFallbackSignsNestedVideoInfo(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
v1 := &vidmod.VideoInfo{}
|
||||
v1.SourceURL = "bktuserup/sp/a.m3u8"
|
||||
v1.H265Url = "bktuserup/sp/a265.m3u8"
|
||||
v1.PreviewURL = "bktuserup/sp/p.m3u8"
|
||||
// 匿名 wrapper 不实现 M3u8Signable,signData 会落到反射兜底 signByReflect。
|
||||
wrapper := struct{ List []*vidmod.VideoInfo }{List: []*vidmod.VideoInfo{v1}}
|
||||
|
||||
signData(&ticketSigner{userID: 1, ip: "1.1.1.1", ua: "ua"}, &wrapper)
|
||||
|
||||
for _, raw := range []string{v1.SourceURL, v1.H265Url, v1.PreviewURL} {
|
||||
if !strings.HasPrefix(raw, "JHA-") {
|
||||
t.Fatalf("field not signed: %q", raw)
|
||||
}
|
||||
if _, _, ok := VerifyPath("/"+raw, "1.1.1.1", "ua"); !ok {
|
||||
t.Fatalf("signed field should verify: %q", raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFallbackSignsSliceOfValues 验证 []VideoInfoResp 值切片元素(可寻址)也能被兜底改写。
|
||||
func TestFallbackSignsSliceOfValues(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
list := []vidmod.VideoInfoResp{{SourceURL: "bktuserup/sp/a.m3u8"}}
|
||||
signByReflect(&ticketSigner{userID: 2, ip: "2.2.2.2", ua: "ua"}, reflect.ValueOf(&list), 0)
|
||||
if !strings.HasPrefix(list[0].SourceURL, "JHA-") {
|
||||
t.Fatal("value-slice element SourceURL should have been signed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignDataUsesInterface 验证实现了 M3u8Signable 的响应体走零反射的接口分支即可就地签票
|
||||
// (含 vidmod.VideoInfo 的三个地址字段与 previewURL 的试看语义)。
|
||||
func TestSignDataUsesInterface(t *testing.T) {
|
||||
setKey(t, testKey)
|
||||
v := &vidmod.VideoInfo{}
|
||||
v.SourceURL = "bktuserup/sp/a.m3u8"
|
||||
v.H265Url = "bktuserup/sp/a265.m3u8"
|
||||
v.PreviewURL = "bktuserup/sp/p.m3u8"
|
||||
// *vidmod.VideoInfo 实现 M3u8Signable:应命中接口分支而非反射。
|
||||
if _, ok := interface{}(v).(vidmod.M3u8Signable); !ok {
|
||||
t.Fatal("*vidmod.VideoInfo should implement vidmod.M3u8Signable")
|
||||
}
|
||||
signData(&ticketSigner{userID: 3, ip: "3.3.3.3", ua: "ua"}, v)
|
||||
|
||||
for _, raw := range []string{v.SourceURL, v.H265Url, v.PreviewURL} {
|
||||
if !strings.HasPrefix(raw, "JHA-") {
|
||||
t.Fatalf("field not signed via interface: %q", raw)
|
||||
}
|
||||
}
|
||||
// previewURL 必须是试看票(Preview=true, Playable=false)。
|
||||
_, tk, ok := VerifyPath("/"+v.PreviewURL, "3.3.3.3", "ua")
|
||||
if !ok || !tk.Preview || tk.Playable {
|
||||
t.Fatalf("previewURL should be a preview ticket: ok=%v tk=%+v", ok, tk)
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
package mediabookshelfser
|
||||
|
||||
import (
|
||||
"91porn-server/app/service/mediaser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/middleware/ua"
|
||||
"91porn-server/models/cache/mediabookshelfdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/mediabookshelfmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AppQueryListReq struct {
|
||||
Type string `json:"type" form:"type" binding:"required,oneof=video image text drama"` // 类型 "video":动漫,"image":漫画,"text":小说,"drama":短剧
|
||||
SortType int `json:"sortType" form:"sortType"` // 排序 0、最新收藏 1.最近更新
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total" bson:"total"` // 总数
|
||||
HasNext bool `json:"hasNext" bson:"hasNext"` // 是否有下一页
|
||||
List []mediabookshelfdata.AppSimpleData `json:"list" bson:"list"` // 列表数据
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList(uid uint64) (any, error) {
|
||||
if p.Type == mediamod.MediaTypeDrama {
|
||||
return p.getDramaList(uid)
|
||||
}
|
||||
if p.SortType == 0 {
|
||||
return p.getListByCreateAt(uid)
|
||||
}
|
||||
// 按照更新时间获取
|
||||
// 获取列表
|
||||
filter := bson.M{"shelfType": p.Type, "uid": uid}
|
||||
data, _, _, err := mediabookshelfdata.GetListFromCache(uid, filter, 0, 100, bson.D{})
|
||||
if err != nil {
|
||||
log.Error("获取媒体书架列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return nil, err
|
||||
}
|
||||
ids := []primitive.ObjectID{}
|
||||
for _, v := range data {
|
||||
ids = append(ids, v.MID)
|
||||
}
|
||||
// 按照子集更新时间来排
|
||||
res := AppListRes{}
|
||||
res.List = mediabookshelfdata.FormatAppSimpleDataList(data, int64(p.Skip()), int64(p.Limit()), bson.D{{Key: "contentUpdateTime", Value: -1}})
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *AppQueryListReq) getDramaList(uid uint64) (res struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []*mediamod.AppMediaBase `json:"list"`
|
||||
}, err error) {
|
||||
ids, total, hasNext, err := mediabookshelfmod.GetDramaMediaIDs(
|
||||
uid, int64(p.Skip()), int64(p.Limit()), p.SortType == 1,
|
||||
)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
_, mediaMap, err := mediamod.GetListByIds(ids)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
ordered := make([]*mediamod.Media, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
media, ok := mediaMap[id]
|
||||
if !ok || media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete {
|
||||
continue
|
||||
}
|
||||
copy := media
|
||||
ordered = append(ordered, ©)
|
||||
}
|
||||
res.Total = total
|
||||
res.HasNext = hasNext
|
||||
res.List = mediaser.FillMedias(ordered, uid, true)
|
||||
if res.List == nil {
|
||||
res.List = make([]*mediamod.AppMediaBase, 0)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *AppQueryListReq) getListByCreateAt(uid uint64) (res AppListRes, err error) {
|
||||
|
||||
filter := bson.M{"shelfType": p.Type, "uid": uid}
|
||||
sort := bson.D{{"readAt", -1}, {"_id", -1}}
|
||||
// 获取列表
|
||||
var data []mediabookshelfmod.MediaBookshelf
|
||||
data, res.Total, res.HasNext, err = mediabookshelfdata.GetListFromCache(uid, filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("获取媒体书架列表列表数据错误", log.Any("Params", *p), log.E(err))
|
||||
return
|
||||
}
|
||||
res.List = mediabookshelfdata.FormatAppSimpleDataList(data, 0, int64(len(data)), bson.D{})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type AppAddBookshelfReq struct {
|
||||
Id string `json:"id" form:"id"` // 媒体ID
|
||||
}
|
||||
|
||||
// Add 添加入书架
|
||||
func (p *AppAddBookshelfReq) Add(uid uint64, ua ua.UA, ip string) (err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取媒体
|
||||
mediaInfo, err := mediamod.GetInfo(oid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mediaInfo.ID.IsZero() {
|
||||
return errors.New("media is not exist")
|
||||
}
|
||||
if mediaInfo.MediaType == mediamod.MediaTypeDrama && (mediaInfo.Status != 1 || mediaInfo.IsDelete) {
|
||||
return errors.New("drama is inactive")
|
||||
}
|
||||
//// 获取当前用户书架里的书的数量
|
||||
//total, err := mediabookshelfmod.GetCount(uid)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//user, err := usermod.FindUserByUID(uid)
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//if (user.VipLevel > 0 && total >= 100) || (user.VipLevel == 0 && total >= 30) {
|
||||
// log.Info("加入书架失败,超过数量限制", log.Any("uid", uid), log.Any("vipLvel", user.VipLevel), log.Any("total", total))
|
||||
// return errors.New("加入失败")
|
||||
//}
|
||||
// 获取是否添加书架
|
||||
filter := bson.M{"uid": uid, "mid": mediaInfo.ID}
|
||||
info, err := mediabookshelfmod.GetInfoByCond(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.ID.IsZero() {
|
||||
return errors.New("book already added")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
// 添加数据记录
|
||||
data := mediabookshelfmod.MediaBookshelf{
|
||||
ShelfType: mediaInfo.MediaType,
|
||||
MID: mediaInfo.ID,
|
||||
UID: uid,
|
||||
IsDelete: false,
|
||||
ReadAt: now,
|
||||
UpdateTime: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
// 创建数据
|
||||
if _, err := mediabookshelfdata.InsertData(nil, uid, data); err != nil {
|
||||
return err
|
||||
}
|
||||
common.Go(func() {
|
||||
mediamod.IncCollectCount(mediaInfo.ID, 1)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
type AppDelBookshelfReq struct {
|
||||
Id string `json:"id" bson:"id"` // 媒体id
|
||||
}
|
||||
|
||||
// Del 删除书架中书籍
|
||||
func (p *AppDelBookshelfReq) Del(uid uint64, ua ua.UA, ip string) (err error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
return errors.New("user is not exist")
|
||||
}
|
||||
|
||||
oid, _ := primitive.ObjectIDFromHex(p.Id)
|
||||
// 获取媒体
|
||||
mediaInfo, err := mediamod.GetInfo(oid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filter := bson.M{"mid": oid, "uid": uid}
|
||||
err = mediabookshelfdata.DeleteDataByCond(nil, uid, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
common.Go(func() {
|
||||
|
||||
mediamod.IncCollectCount(mediaInfo.ID, -1)
|
||||
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AppDelBatchBookshelfReq struct {
|
||||
IDS []primitive.ObjectID `json:"ids" bson:"ids"` // acg的id
|
||||
}
|
||||
|
||||
// Del 删除书架中书籍
|
||||
func (p *AppDelBatchBookshelfReq) Del(uid uint64) (err error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil || user.ID.IsZero() {
|
||||
return errors.New("user is not exist")
|
||||
}
|
||||
|
||||
err = mediabookshelfdata.DeleteDataByIds(nil, uid, p.IDS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for _, id := range p.IDS {
|
||||
mediamod.IncCollectCount(id, -1)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package mediacontentser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
)
|
||||
|
||||
func TestDramaResponsesDoNotExposeServerProgress(t *testing.T) {
|
||||
tests := map[string]any{
|
||||
"media": mediamod.AppMediaBase{},
|
||||
"episode list": AppMediaContent{},
|
||||
"episode info": MediaContentInfo{},
|
||||
}
|
||||
for name, response := range tests {
|
||||
data, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal %s response: %v", name, err)
|
||||
}
|
||||
if bytes.Contains(data, []byte(`"resume"`)) || bytes.Contains(data, []byte(`"progressSeconds"`)) {
|
||||
t.Fatalf("%s response still exposes server progress: %s", name, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDramaAccess(t *testing.T) {
|
||||
media := mediamod.Media{FreeEpisode: 1}
|
||||
tests := []struct {
|
||||
name string
|
||||
content mediacontentmod.MediaContent
|
||||
hasBuy bool
|
||||
hasCard bool
|
||||
wantCan bool
|
||||
wantAccess string
|
||||
}{
|
||||
{name: "free episode", content: mediacontentmod.MediaContent{EpisodeNumber: 1, Price: 30, ListenPermission: 1}, wantCan: true, wantAccess: dramaAccessFree},
|
||||
{name: "bought episode", content: mediacontentmod.MediaContent{EpisodeNumber: 2, Price: 30, ListenPermission: 1}, hasBuy: true, wantCan: true, wantAccess: dramaAccessBought},
|
||||
{name: "card episode", content: mediacontentmod.MediaContent{EpisodeNumber: 2, Price: 30, ListenPermission: 1}, hasCard: true, wantCan: true, wantAccess: dramaAccessCard},
|
||||
{name: "locked episode", content: mediacontentmod.MediaContent{EpisodeNumber: 2, Price: 30, ListenPermission: 1}, wantCan: false, wantAccess: dramaAccessCoin},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, canPlay, accessType := ResolveDramaAccess(tt.content, media, tt.hasBuy, tt.hasCard)
|
||||
if canPlay != tt.wantCan || accessType != tt.wantAccess {
|
||||
t.Fatalf("ResolveDramaAccess() = (%v, %q), want (%v, %q)", canPlay, accessType, tt.wantCan, tt.wantAccess)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Executable
+412
@@ -0,0 +1,412 @@
|
||||
package mediacontentser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
topser "91porn-server/common/top"
|
||||
topasist "91porn-server/common/top/asistant"
|
||||
"91porn-server/models/v/media_buy_record_mod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/cache/mediacontentdata"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// MediaContentInfo 移动端返回内容
|
||||
type MediaContentInfo struct {
|
||||
ID primitive.ObjectID `json:"id"` // 文档id
|
||||
MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID
|
||||
MediaType string `json:"mediaType"` // 媒体类型
|
||||
EpisodeNumber int `json:"episodeNumber"` // 第几集
|
||||
ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费
|
||||
Price int64 `json:"price"` // 购买价格
|
||||
Name string `json:"name"` // 章节名
|
||||
Cover string `json:"cover"` // 封面
|
||||
Text string `json:"text"` // 内容(小说专用)
|
||||
Md5 string `json:"md5"` // 媒体md5
|
||||
VideoUrl string `json:"videoUrl"` // 视频地址
|
||||
H265Url string `json:"h265Url"` // H.265 视频地址
|
||||
AudioUrl string `json:"audioUrl"` // 音频地址
|
||||
UrlSet []string `json:"urlSet"` // 地址集(多个资源地址用 例如漫画)
|
||||
Height int `json:"height"` // 高
|
||||
Weight int `json:"weight"` // 宽
|
||||
MediaSize int64 `json:"mediaSize"` // 资源大小
|
||||
PlayTime uint `json:"playTime"` // 影片长度
|
||||
IsActive bool `json:"isActive"` // 是否激活
|
||||
Ratio float64 `json:"ratio"` // 宽高比
|
||||
CreatedAt time.Time `json:"createdAt"` // 文档创建时间
|
||||
UpdateTime time.Time `json:"updateTime"` // 文档更新时间
|
||||
HasBuy bool `json:"hasBuy"` // 是否已购买
|
||||
IsFree bool `json:"isFree"`
|
||||
HasDramaCard bool `json:"hasDramaCard"`
|
||||
CanPlay bool `json:"canPlay"`
|
||||
AccessType string `json:"accessType"`
|
||||
PreviewEnabled bool `json:"previewEnabled"`
|
||||
PreviewStart int `json:"previewStart"`
|
||||
PreviewSeconds int `json:"previewSeconds"`
|
||||
PreviewVideoUrl string `json:"previewVideoUrl"`
|
||||
PreviewH265Url string `json:"previewH265Url"`
|
||||
Prev *OtherContent `json:"prev"` // 上一集
|
||||
Next *OtherContent `json:"next"` // 下一集
|
||||
Paywall *PaywallInfo `json:"paywall"`
|
||||
}
|
||||
|
||||
type OtherContent struct {
|
||||
Id primitive.ObjectID `json:"id"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Name string `json:"name"`
|
||||
Cover string `json:"cover"`
|
||||
}
|
||||
|
||||
type PaywallInfo struct {
|
||||
CheckoutContextID string `json:"checkoutContextId"`
|
||||
Title string `json:"title"`
|
||||
CoinBalance int64 `json:"coinBalance"`
|
||||
UnlockCoin int64 `json:"unlockCoin"`
|
||||
CoinButtonText string `json:"coinButtonText"`
|
||||
CardButtonText string `json:"cardButtonText"`
|
||||
}
|
||||
|
||||
type AppQueryListReq struct {
|
||||
PageNumber uint64 `form:"pageNumber" json:"pageNumber" binding:"required,min=1"`
|
||||
PageSize uint64 `form:"pageSize" json:"pageSize" binding:"required,min=1,max=200"`
|
||||
MediaId string `json:"mediaId" form:"mediaId" binding:"required"`
|
||||
SortType int `json:"sortType" form:"sortType" binding:"omitempty,oneof=0 1"` // 排序类型 0-正序 1-倒序
|
||||
}
|
||||
|
||||
func (p *AppQueryListReq) Skip() uint64 { return (p.PageNumber - 1) * p.PageSize }
|
||||
func (p *AppQueryListReq) Limit() uint64 { return p.PageSize }
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []AppMediaContent `json:"list"`
|
||||
}
|
||||
|
||||
type AppMediaContent struct {
|
||||
ID primitive.ObjectID `json:"id"` // 文档id
|
||||
MediaID primitive.ObjectID `json:"mediaId"` // 媒体资源ID
|
||||
MediaType string `json:"mediaType"` // 媒体类型
|
||||
EpisodeNumber int `json:"episodeNumber"` // 第几集
|
||||
ListenPermission int `json:"listenPermission"` // 收听权限 0:会员 1:金币购买 2:免费
|
||||
VideoUrl string `json:"videoUrl"` // 视频地址
|
||||
H265Url string `json:"h265Url"` // H.265 视频地址
|
||||
AudioUrl string `json:"audioUrl"` // 音频地址
|
||||
PlayTime uint `json:"playTime"` // 播放时长
|
||||
Price int64 `json:"price"` // 购买价格
|
||||
Name string `json:"name"` // 章节名
|
||||
Cover string `json:"cover"` // 封面
|
||||
HasBuy bool `json:"hasBuy"` // 是否已购买
|
||||
IsFree bool `json:"isFree"`
|
||||
HasDramaCard bool `json:"hasDramaCard"`
|
||||
CanPlay bool `json:"canPlay"`
|
||||
AccessType string `json:"accessType"`
|
||||
PreviewEnabled bool `json:"previewEnabled"`
|
||||
PreviewStart int `json:"previewStart"`
|
||||
PreviewSeconds int `json:"previewSeconds"`
|
||||
PreviewVideoUrl string `json:"previewVideoUrl"`
|
||||
PreviewH265Url string `json:"previewH265Url"`
|
||||
Ratio float64 `json:"ratio"`
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
UpdateTime time.Time `json:"updateTime"`
|
||||
}
|
||||
|
||||
const (
|
||||
dramaAccessFree = "free"
|
||||
dramaAccessBought = "bought"
|
||||
dramaAccessCard = "card"
|
||||
dramaAccessCoin = "coin"
|
||||
)
|
||||
|
||||
func ActiveDramaCard(uid uint64) bool {
|
||||
ok, err := ActiveDramaCardStatus(uid)
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
func ActiveDramaCardStatus(uid uint64) (bool, error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return user.DramaExpire.After(time.Now()), nil
|
||||
}
|
||||
|
||||
func ResolveDramaAccess(
|
||||
item mediacontentmod.MediaContent,
|
||||
media mediamod.Media,
|
||||
hasBuy, hasCard bool,
|
||||
) (isFree, canPlay bool, accessType string) {
|
||||
isFree = item.ListenPermission == 2 || item.Price <= 0 ||
|
||||
(media.FreeEpisode > 0 && item.EpisodeNumber <= media.FreeEpisode)
|
||||
switch {
|
||||
case isFree:
|
||||
return true, true, dramaAccessFree
|
||||
case hasBuy:
|
||||
return false, true, dramaAccessBought
|
||||
case hasCard:
|
||||
return false, true, dramaAccessCard
|
||||
default:
|
||||
return false, false, dramaAccessCoin
|
||||
}
|
||||
}
|
||||
|
||||
func newPaywall(uid uint64, price int64) *PaywallInfo {
|
||||
balance := int64(0)
|
||||
if wallet, err := walletmod.GetWallet(uid); err == nil && wallet != nil {
|
||||
balance = wallet.Amount + wallet.Income
|
||||
}
|
||||
return &PaywallInfo{
|
||||
CheckoutContextID: "drama-checkout-" + primitive.NewObjectID().Hex(),
|
||||
Title: "更多精彩解锁即享",
|
||||
CoinBalance: balance,
|
||||
UnlockCoin: price,
|
||||
CoinButtonText: fmt.Sprintf("%d金币解锁", price),
|
||||
CardButtonText: "开通短剧卡免费看",
|
||||
}
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList(uid uint64) (res AppListRes, err error) {
|
||||
mediaId, err := primitive.ObjectIDFromHex(p.MediaId)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
media, err := mediamod.GetInfo(mediaId)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
filter := bson.M{"mediaId": mediaId, "isActive": true, "isDelete": false}
|
||||
if media.MediaType == mediamod.MediaTypeDrama {
|
||||
filter["mediaType"] = mediamod.MediaTypeDrama
|
||||
if media.Status != 1 || media.IsDelete {
|
||||
return res, fmt.Errorf("drama is inactive")
|
||||
}
|
||||
}
|
||||
|
||||
sort := bson.D{{Key: "episodeNumber", Value: 1}}
|
||||
if p.SortType == 1 {
|
||||
sort = bson.D{{Key: "episodeNumber", Value: -1}}
|
||||
}
|
||||
// 获取列表
|
||||
var data []mediacontentmod.MediaContent
|
||||
data, res.Total, res.HasNext, err = mediacontentdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
log.Error("AppQueryListReq.GetList mediacontentdata.GetListFromCache fail", log.Any("req", p), log.E(err))
|
||||
return res, err
|
||||
}
|
||||
contentIds := []primitive.ObjectID{}
|
||||
// 获取是否已经购买
|
||||
for _, item := range data {
|
||||
contentIds = append(contentIds, item.ID)
|
||||
}
|
||||
buyMap, isWholeBuy, err := media_buy_record_mod.IsBuyBatch(uid, mediaId, contentIds)
|
||||
if err != nil {
|
||||
log.Error("AppQueryListReq.GetList media_buy_record_mod.IsBuyBatch fail", log.Any("req", p), log.E(err))
|
||||
return res, err
|
||||
}
|
||||
hasCard := false
|
||||
if media.MediaType == mediamod.MediaTypeDrama {
|
||||
hasCard, err = ActiveDramaCardStatus(uid)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
// 组装返回数据
|
||||
for _, item := range data {
|
||||
tmp := AppMediaContent{
|
||||
ID: item.ID,
|
||||
MediaID: item.MediaID,
|
||||
MediaType: item.MediaType,
|
||||
EpisodeNumber: item.EpisodeNumber,
|
||||
ListenPermission: item.ListenPermission,
|
||||
Price: item.Price,
|
||||
Name: item.Name,
|
||||
PlayTime: item.PlayTime,
|
||||
Cover: item.Cover,
|
||||
AudioUrl: item.AudioUrl,
|
||||
VideoUrl: item.VideoUrl,
|
||||
H265Url: appg.H265URLForApp(item.H265Url),
|
||||
HasBuy: buyMap[item.ID],
|
||||
Ratio: item.Ratio,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdateTime: item.UpdateTime,
|
||||
}
|
||||
if strings.TrimSpace(tmp.Name) == "" {
|
||||
tmp.Name = fmt.Sprintf("第%v集", item.EpisodeNumber)
|
||||
}
|
||||
// 是否整本购买
|
||||
if isWholeBuy {
|
||||
tmp.HasBuy = true
|
||||
}
|
||||
if item.MediaType == mediamod.MediaTypeDrama {
|
||||
tmp.HasDramaCard = hasCard
|
||||
tmp.IsFree, tmp.CanPlay, tmp.AccessType = ResolveDramaAccess(item, media, tmp.HasBuy, hasCard)
|
||||
tmp.PreviewEnabled = item.PreviewEnabled
|
||||
tmp.PreviewStart = item.PreviewStart
|
||||
tmp.PreviewSeconds = item.PreviewSeconds
|
||||
if !tmp.CanPlay {
|
||||
if tmp.PreviewEnabled {
|
||||
tmp.PreviewVideoUrl = tmp.VideoUrl
|
||||
tmp.PreviewH265Url = tmp.H265Url
|
||||
}
|
||||
tmp.AudioUrl = ""
|
||||
tmp.VideoUrl = ""
|
||||
tmp.H265Url = ""
|
||||
}
|
||||
}
|
||||
if tmp.Cover == "" && len(item.UrlSet) > 0 {
|
||||
tmp.Cover = item.UrlSet[0]
|
||||
}
|
||||
res.List = append(res.List, tmp)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type AppQueryInfoReq struct {
|
||||
ID string `json:"id" form:"id"` // id
|
||||
}
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *AppQueryInfoReq) GetInfo(uid uint64) (res MediaContentInfo, err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var item mediacontentmod.MediaContent
|
||||
item, err = mediacontentdata.GetInfoFromCache(oid)
|
||||
if err != nil {
|
||||
log.Error("AppQueryInfoReq.GetInfo mediacontentdata.GetInfoFromCache fail", log.Any("mediaContentId", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
var media mediamod.Media
|
||||
if item.MediaType == mediamod.MediaTypeDrama {
|
||||
media, err = mediamod.GetInfo(item.MediaID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
if media.MediaType != mediamod.MediaTypeDrama || media.Status != 1 || media.IsDelete || !item.IsActive || item.IsDelete {
|
||||
return res, fmt.Errorf("drama episode is inactive")
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否已经购买
|
||||
hasBuy, err := media_buy_record_mod.IsBuy(uid, item.MediaID, item.ID)
|
||||
if err != nil {
|
||||
log.Error("AppQueryInfoReq.GetInfo media_buy_record_mod.IsBuy fail", log.Any("mediaContentId", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
res = MediaContentInfo{
|
||||
ID: item.ID,
|
||||
MediaID: item.MediaID,
|
||||
MediaType: item.MediaType,
|
||||
EpisodeNumber: item.EpisodeNumber,
|
||||
ListenPermission: item.ListenPermission,
|
||||
Price: item.Price,
|
||||
Name: item.Name,
|
||||
Cover: item.Cover,
|
||||
Text: item.Text,
|
||||
Md5: item.Md5,
|
||||
VideoUrl: item.VideoUrl,
|
||||
H265Url: appg.H265URLForApp(item.H265Url),
|
||||
AudioUrl: item.AudioUrl,
|
||||
UrlSet: item.UrlSet,
|
||||
Height: item.Height,
|
||||
Weight: item.Weight,
|
||||
MediaSize: item.MediaSize,
|
||||
PlayTime: item.PlayTime,
|
||||
IsActive: item.IsActive,
|
||||
//Ratio: item.Ratio,
|
||||
Ratio: 1,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdateTime: item.UpdateTime,
|
||||
HasBuy: hasBuy,
|
||||
PreviewEnabled: item.PreviewEnabled,
|
||||
PreviewStart: item.PreviewStart,
|
||||
PreviewSeconds: item.PreviewSeconds,
|
||||
}
|
||||
if item.MediaType == mediamod.MediaTypeDrama {
|
||||
res.HasDramaCard, err = ActiveDramaCardStatus(uid)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.IsFree, res.CanPlay, res.AccessType = ResolveDramaAccess(item, media, hasBuy, res.HasDramaCard)
|
||||
res.Ratio = item.Ratio
|
||||
if !res.CanPlay {
|
||||
if res.PreviewEnabled {
|
||||
res.PreviewVideoUrl = res.VideoUrl
|
||||
res.PreviewH265Url = res.H265Url
|
||||
}
|
||||
res.VideoUrl = ""
|
||||
res.H265Url = ""
|
||||
res.AudioUrl = ""
|
||||
res.UrlSet = []string{}
|
||||
res.Text = ""
|
||||
res.Md5 = ""
|
||||
res.Paywall = newPaywall(uid, item.Price)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(res.Name) == "" {
|
||||
res.Name = fmt.Sprintf("第%v集", item.EpisodeNumber)
|
||||
}
|
||||
if res.Cover == "" && len(item.UrlSet) > 0 {
|
||||
res.Cover = item.UrlSet[0]
|
||||
}
|
||||
EpisodeNumbers := []int{}
|
||||
if item.EpisodeNumber-1 > 0 {
|
||||
EpisodeNumbers = append(EpisodeNumbers, item.EpisodeNumber-1)
|
||||
}
|
||||
|
||||
EpisodeNumbers = append(EpisodeNumbers, item.EpisodeNumber+1)
|
||||
|
||||
// 获取上一集下一集
|
||||
otherFilter := bson.M{"mediaId": item.MediaID, "episodeNumber": bson.M{"$in": EpisodeNumbers}, "isActive": true, "isDelete": false}
|
||||
if item.MediaType == mediamod.MediaTypeDrama {
|
||||
otherFilter["mediaType"] = mediamod.MediaTypeDrama
|
||||
}
|
||||
otherContents, _, _, err := mediacontentdata.GetListFromCache(otherFilter, 0, 2, bson.D{})
|
||||
if err != nil {
|
||||
log.Error("AppQueryInfoReq.GetInfo mediacontentdata.GetListData fail", log.Any("mediaContentId", p.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
for _, v := range otherContents {
|
||||
if v.Cover == "" && len(v.UrlSet) > 0 {
|
||||
v.Cover = v.UrlSet[0]
|
||||
}
|
||||
if v.EpisodeNumber == item.EpisodeNumber-1 {
|
||||
// 上一集
|
||||
res.Prev = &OtherContent{
|
||||
Id: v.ID,
|
||||
EpisodeNumber: v.EpisodeNumber,
|
||||
Name: v.Name,
|
||||
Cover: v.Cover,
|
||||
}
|
||||
}
|
||||
if v.EpisodeNumber == item.EpisodeNumber+1 {
|
||||
// 下一集
|
||||
res.Next = &OtherContent{
|
||||
Id: v.ID,
|
||||
EpisodeNumber: v.EpisodeNumber,
|
||||
Name: v.Name,
|
||||
Cover: v.Cover,
|
||||
}
|
||||
}
|
||||
}
|
||||
// 累加浏览数
|
||||
common.Go(func() {
|
||||
// 增加父级的浏览数量
|
||||
mediamod.IncBrowseCount(item.MediaID, 1)
|
||||
topasist.Incr(topser.TypeMedia(item.MediaType), item.ID.Hex(), 1)
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package mediacontentser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMediaContentInfoMarshalH265URL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
h265URL string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "resource exists",
|
||||
h265URL: "https://cdn.example.com/h265/index.m3u8",
|
||||
want: "https://cdn.example.com/h265/index.m3u8",
|
||||
},
|
||||
{
|
||||
name: "resource missing",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := json.Marshal(MediaContentInfo{
|
||||
VideoUrl: "https://cdn.example.com/h264/index.m3u8",
|
||||
H265Url: tt.h265URL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal media content info: %v", err)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
if response["videoUrl"] != "https://cdn.example.com/h264/index.m3u8" {
|
||||
t.Fatalf("unexpected H.264 URL: %v", response["videoUrl"])
|
||||
}
|
||||
if response["h265Url"] != tt.want {
|
||||
t.Fatalf("unexpected H.265 URL: %v", response["h265Url"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppMediaContentMarshalH265URL(t *testing.T) {
|
||||
data, err := json.Marshal(AppMediaContent{
|
||||
VideoUrl: "https://cdn.example.com/h264/index.m3u8",
|
||||
H265Url: "https://cdn.example.com/h265/index.m3u8",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal app media content: %v", err)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(data, &response); err != nil {
|
||||
t.Fatalf("unmarshal app media content: %v", err)
|
||||
}
|
||||
if response["videoUrl"] != "https://cdn.example.com/h264/index.m3u8" {
|
||||
t.Fatalf("unexpected H.264 URL: %v", response["videoUrl"])
|
||||
}
|
||||
if response["h265Url"] != "https://cdn.example.com/h265/index.m3u8" {
|
||||
t.Fatalf("unexpected H.265 URL: %v", response["h265Url"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package mediaser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/service/mediatagser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/timeutil/timerange"
|
||||
"91porn-server/models/s/statrecordmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/mediatagmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func buildDedupTagsAndDetails(tagIDs []primitive.ObjectID, tagMap map[primitive.ObjectID]*mediatagmod.MediaTag) ([]primitive.ObjectID, []mediamod.TagDetail) {
|
||||
if len(tagIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(tagIDs))
|
||||
uniqueTagIDs := make([]primitive.ObjectID, 0, len(tagIDs))
|
||||
tagDetails := make([]mediamod.TagDetail, 0, len(tagIDs))
|
||||
for _, id := range tagIDs {
|
||||
tag, ok := tagMap[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(tag.Name)
|
||||
if _, exists := seen[name]; exists {
|
||||
continue
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
uniqueTagIDs = append(uniqueTagIDs, tag.ID)
|
||||
tagDetails = append(tagDetails, mediamod.TagDetail{
|
||||
Id: tag.ID,
|
||||
Name: tag.Name,
|
||||
})
|
||||
}
|
||||
return uniqueTagIDs, tagDetails
|
||||
}
|
||||
|
||||
func FillMedias(medias []*mediamod.Media, uid uint64, needDefaultContent bool, dramaCard ...bool) []*mediamod.AppMediaBase {
|
||||
if len(medias) == 0 {
|
||||
return nil
|
||||
}
|
||||
tagIDs := make([]primitive.ObjectID, 0)
|
||||
var (
|
||||
mIds []primitive.ObjectID
|
||||
mStatus map[primitive.ObjectID]mediamod.MediaStatus
|
||||
hasDrama bool
|
||||
hasDramaCard bool
|
||||
)
|
||||
|
||||
for i := range medias {
|
||||
if medias[i] == nil {
|
||||
continue
|
||||
}
|
||||
mIds = append(mIds, medias[i].ID)
|
||||
tagIDs = append(tagIDs, medias[i].Tags...)
|
||||
if medias[i].MediaType == mediamod.MediaTypeDrama {
|
||||
hasDrama = true
|
||||
}
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
mStatus = GetUserStatus(uid, mIds)
|
||||
})
|
||||
if hasDrama {
|
||||
if len(dramaCard) > 0 {
|
||||
hasDramaCard = dramaCard[0]
|
||||
} else {
|
||||
wg.Add(1)
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
hasDramaCard = err == nil && user != nil && user.DramaExpire.After(time.Now())
|
||||
})
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
var infos []*mediamod.AppMediaBase
|
||||
tagMap := mediatagser.GetTagsByIDs2Map(tagIDs)
|
||||
for i := range medias {
|
||||
if medias[i] == nil {
|
||||
continue
|
||||
}
|
||||
mid := ""
|
||||
if !medias[i].MID.IsZero() {
|
||||
mid = medias[i].MID.Hex()
|
||||
}
|
||||
temp := mediamod.AppMediaBase{
|
||||
ID: medias[i].ID,
|
||||
SortCode: medias[i].SortCode,
|
||||
Status: medias[i].Status,
|
||||
Title: medias[i].Title,
|
||||
HorizontalCover: medias[i].HorizontalCover,
|
||||
VerticalCover: medias[i].VerticalCover,
|
||||
Tags: make([]primitive.ObjectID, 0),
|
||||
TagDetails: make([]mediamod.TagDetail, 0),
|
||||
Summary: medias[i].Summary,
|
||||
TotalEpisode: medias[i].TotalEpisode,
|
||||
CurrentEpisode: medias[i].CurrentEpisode,
|
||||
FreeEpisode: medias[i].FreeEpisode,
|
||||
MediaType: medias[i].MediaType,
|
||||
MediaSubType: medias[i].MediaSubType,
|
||||
Kind: medias[i].Kind,
|
||||
Price: medias[i].Price,
|
||||
ContentsPrice: medias[i].ContentsPrice,
|
||||
Direction: medias[i].Direction,
|
||||
UpdateStatus: medias[i].UpdateStatus,
|
||||
Permission: medias[i].Permission,
|
||||
PermissionIconHide: medias[i].PermissionIconHide,
|
||||
Style: medias[i].Style,
|
||||
FreeTime: medias[i].FreeTime,
|
||||
CountPurchases: medias[i].CountPurchases,
|
||||
CountBrowse: medias[i].CountBrowse,
|
||||
CountCollect: medias[i].CountCollect,
|
||||
CountLike: medias[i].CountLike,
|
||||
CountDisLike: medias[i].CountDisLike,
|
||||
CountView: medias[i].CountView,
|
||||
CountShare: medias[i].CountShare,
|
||||
HasFollow: medias[i].HasFollow,
|
||||
MediaStatus: mStatus[medias[i].ID],
|
||||
CountComment: medias[i].CountComment,
|
||||
Number: medias[i].Number,
|
||||
CreatedAt: medias[i].CreatedAt,
|
||||
UpdateTime: medias[i].UpdateTime,
|
||||
LatestPublishedAt: medias[i].LatestPublishedAt,
|
||||
MID: mid,
|
||||
ModuleName: medias[i].ModuleName,
|
||||
SectionName: medias[i].SectionName,
|
||||
SID: medias[i].SID,
|
||||
SectionSort: medias[i].SectionSort,
|
||||
LsjId: medias[i].LsjId,
|
||||
}
|
||||
if medias[i].MediaType == mediamod.MediaTypeDrama {
|
||||
temp.HasDramaCard = hasDramaCard
|
||||
}
|
||||
if temp.MediaStatus.HasCollected {
|
||||
temp.CountCollect = temp.CountCollect + 1 // 避免缓存详情的情况,收藏完成之后数字没有任何变化
|
||||
}
|
||||
if needDefaultContent {
|
||||
temp.DefaultContent = medias[i].DefaultContent
|
||||
}
|
||||
if temp.DefaultContent.Cover == "" && len(temp.DefaultContent.UrlSet) > 0 {
|
||||
temp.DefaultContent.Cover = temp.DefaultContent.UrlSet[0]
|
||||
}
|
||||
if strings.TrimSpace(temp.DefaultContent.Name) == "" {
|
||||
temp.DefaultContent.Name = "第一集"
|
||||
}
|
||||
temp.Tags, temp.TagDetails = buildDedupTagsAndDetails(medias[i].Tags, tagMap)
|
||||
|
||||
infos = append(infos, &temp)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
func FillNotStatusMedias(medias []*mediamod.Media, needDefaultContent bool) []*mediamod.AppMediaBase {
|
||||
if len(medias) == 0 {
|
||||
return []*mediamod.AppMediaBase{}
|
||||
}
|
||||
tagIDs := make([]primitive.ObjectID, 0)
|
||||
var (
|
||||
mIds []primitive.ObjectID
|
||||
)
|
||||
|
||||
for i := range medias {
|
||||
mIds = append(mIds, medias[i].ID)
|
||||
if medias[i] != nil {
|
||||
tagIDs = append(tagIDs, medias[i].Tags...)
|
||||
}
|
||||
}
|
||||
|
||||
infos := []*mediamod.AppMediaBase{}
|
||||
tagMap := mediatagser.GetTagsByIDs2Map(tagIDs)
|
||||
for i := range medias {
|
||||
if medias[i] == nil {
|
||||
continue
|
||||
}
|
||||
temp := mediamod.AppMediaBase{
|
||||
ID: medias[i].ID,
|
||||
Title: medias[i].Title,
|
||||
HorizontalCover: medias[i].HorizontalCover,
|
||||
VerticalCover: medias[i].VerticalCover,
|
||||
Tags: make([]primitive.ObjectID, 0),
|
||||
TagDetails: make([]mediamod.TagDetail, 0),
|
||||
Summary: medias[i].Summary,
|
||||
TotalEpisode: medias[i].TotalEpisode,
|
||||
CurrentEpisode: medias[i].CurrentEpisode,
|
||||
FreeEpisode: medias[i].FreeEpisode,
|
||||
MediaType: medias[i].MediaType,
|
||||
MediaSubType: medias[i].MediaSubType,
|
||||
Kind: medias[i].Kind,
|
||||
Price: medias[i].Price,
|
||||
ContentsPrice: medias[i].ContentsPrice,
|
||||
Direction: medias[i].Direction,
|
||||
UpdateStatus: medias[i].UpdateStatus,
|
||||
Permission: medias[i].Permission,
|
||||
CountPurchases: medias[i].CountPurchases,
|
||||
CountBrowse: medias[i].CountBrowse,
|
||||
CountCollect: medias[i].CountCollect,
|
||||
CountLike: medias[i].CountLike,
|
||||
HasFollow: medias[i].HasFollow,
|
||||
CountComment: medias[i].CountComment,
|
||||
Number: medias[i].Number,
|
||||
CreatedAt: medias[i].CreatedAt,
|
||||
UpdateTime: medias[i].UpdateTime,
|
||||
LatestPublishedAt: medias[i].LatestPublishedAt,
|
||||
}
|
||||
if temp.MediaStatus.HasCollected {
|
||||
temp.CountCollect = temp.CountCollect + 1 // 避免缓存详情的情况,收藏完成之后数字没有任何变化
|
||||
}
|
||||
if needDefaultContent {
|
||||
temp.DefaultContent = medias[i].DefaultContent
|
||||
}
|
||||
if temp.DefaultContent.Cover == "" && len(temp.DefaultContent.UrlSet) > 0 {
|
||||
temp.DefaultContent.Cover = temp.DefaultContent.UrlSet[0]
|
||||
}
|
||||
if strings.TrimSpace(temp.DefaultContent.Name) == "" {
|
||||
temp.DefaultContent.Name = "第一集"
|
||||
}
|
||||
temp.Tags, temp.TagDetails = buildDedupTagsAndDetails(medias[i].Tags, tagMap)
|
||||
|
||||
infos = append(infos, &temp)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// IncrCountBrowse 增加播放量
|
||||
func IncrCountBrowse(mediaId primitive.ObjectID) {
|
||||
now := time.Now()
|
||||
recentMinute := timerange.RecentMinute(now, statrecordmod.FiveMinuteScale)
|
||||
key := redisconst.AcgBrowseCountListKey(recentMinute.Unix())
|
||||
if err := appg.Redis.RPush(key, mediaId.Hex()); err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = appg.Redis.ExpireKey(key, time.Minute*20)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package mediaser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"91porn-server/models/v/mediamod"
|
||||
"91porn-server/models/v/mediatagmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestBuildDedupTagsAndDetails(t *testing.T) {
|
||||
tagID1 := primitive.NewObjectID()
|
||||
tagID2 := primitive.NewObjectID()
|
||||
duplicateNameTagID := primitive.NewObjectID()
|
||||
missingTagID := primitive.NewObjectID()
|
||||
tagIDs := []primitive.ObjectID{tagID1, tagID2, duplicateNameTagID, missingTagID}
|
||||
tagMap := map[primitive.ObjectID]*mediatagmod.MediaTag{
|
||||
tagID1: {ID: tagID1, Name: "剧情"},
|
||||
tagID2: {ID: tagID2, Name: "恋爱"},
|
||||
duplicateNameTagID: {ID: duplicateNameTagID, Name: "剧情"},
|
||||
}
|
||||
|
||||
gotTagIDs, gotDetails := buildDedupTagsAndDetails(tagIDs, tagMap)
|
||||
wantTagIDs := []primitive.ObjectID{tagID1, tagID2}
|
||||
wantDetails := []mediamod.TagDetail{
|
||||
{Id: tagID1, Name: "剧情"},
|
||||
{Id: tagID2, Name: "恋爱"},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotTagIDs, wantTagIDs) {
|
||||
t.Fatalf("tag IDs = %#v, want %#v", gotTagIDs, wantTagIDs)
|
||||
}
|
||||
if !reflect.DeepEqual(gotDetails, wantDetails) {
|
||||
t.Fatalf("tag details = %#v, want %#v", gotDetails, wantDetails)
|
||||
}
|
||||
}
|
||||
Executable
+1302
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
package mediaser
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"91porn-server/common/elastic"
|
||||
"91porn-server/models/v/mediamod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestHandleShortSortMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sortType int
|
||||
field string
|
||||
}{
|
||||
{name: "默认最新", sortType: 0, field: "updateTime"},
|
||||
{name: "精选", sortType: 1, field: "choiceSort"},
|
||||
{name: "最新上架", sortType: 2, field: "updateTime"},
|
||||
{name: "最热", sortType: 3, field: "countLike"},
|
||||
{name: "最多收藏", sortType: 4, field: "countCollect"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
wantElastic := elastic.A{{tt.field: elastic.M{"order": "desc"}}}
|
||||
if got := HandleShort(tt.sortType); !reflect.DeepEqual(got, wantElastic) {
|
||||
t.Fatalf("HandleShort(%d) = %#v, want %#v", tt.sortType, got, wantElastic)
|
||||
}
|
||||
|
||||
wantMongo := bson.D{{Key: tt.field, Value: -1}}
|
||||
request := mediamod.AppLibraryReq{SortType: tt.sortType}
|
||||
if got := request.Options(); !reflect.DeepEqual(got, wantMongo) {
|
||||
t.Fatalf("AppLibraryReq.Options(%d) = %#v, want %#v", tt.sortType, got, wantMongo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderMediaByIDsPreservesElasticSearchOrder(t *testing.T) {
|
||||
firstID := primitive.NewObjectID()
|
||||
secondID := primitive.NewObjectID()
|
||||
thirdID := primitive.NewObjectID()
|
||||
|
||||
mongoResult := []*mediamod.Media{
|
||||
{ID: secondID},
|
||||
{ID: thirdID},
|
||||
{ID: firstID},
|
||||
}
|
||||
elasticSearchOrder := []primitive.ObjectID{firstID, secondID, thirdID}
|
||||
|
||||
got := orderMediaByIDs(mongoResult, elasticSearchOrder)
|
||||
if len(got) != len(elasticSearchOrder) {
|
||||
t.Fatalf("orderMediaByIDs() length = %d, want %d", len(got), len(elasticSearchOrder))
|
||||
}
|
||||
for i, wantID := range elasticSearchOrder {
|
||||
if got[i].ID != wantID {
|
||||
t.Fatalf("orderMediaByIDs()[%d].ID = %s, want %s", i, got[i].ID.Hex(), wantID.Hex())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package mediaser
|
||||
|
||||
type libraryType string
|
||||
|
||||
const (
|
||||
Video = "video" // 动漫
|
||||
Image = "image" // 漫画
|
||||
AudioBook = "audiobook" // 有声小说
|
||||
)
|
||||
Executable
+204
@@ -0,0 +1,204 @@
|
||||
package mediatagser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/cache/mediatagdata"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/mediatagmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// MediaTagInfo 移动端返回内容
|
||||
type MediaTagInfo struct {
|
||||
ID primitive.ObjectID `json:"id"` // 文档id
|
||||
Kind int `json:"kind"` // 种类(动漫 漫画)
|
||||
Dimension string `json:"dimension"` // 维度
|
||||
Name string `json:"name"` // 标签名称
|
||||
Active bool `json:"active"` // 是都激活
|
||||
Sort int `json:"sort"` // 排序码
|
||||
IsDelete bool `json:"isDelete"` // 是否删除
|
||||
UpdatedAct string `json:"updatedAct"` // 操作用户账号
|
||||
CreatedAt time.Time `json:"createdAt"` // 文档创建时间
|
||||
UpdateTime time.Time `json:"updateTime"` // 文档更新时间
|
||||
}
|
||||
|
||||
type AppQueryListReq struct {
|
||||
commod.Page
|
||||
}
|
||||
|
||||
type AppListRes struct {
|
||||
Total int64 `json:"total"`
|
||||
HasNext bool `json:"hasNext"`
|
||||
List []MediaTagInfo `json:"list"`
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (p *AppQueryListReq) GetList() AppListRes {
|
||||
var res AppListRes
|
||||
var err error
|
||||
filter := bson.M{}
|
||||
|
||||
sort := bson.D{{"_id", -1}}
|
||||
// 获取列表
|
||||
var data []mediatagmod.MediaTag
|
||||
data, res.Total, res.HasNext, err = mediatagdata.GetListFromCache(filter, int64(p.Skip()), int64(p.Limit()), sort)
|
||||
if err != nil {
|
||||
return res
|
||||
}
|
||||
// 组装返回数据
|
||||
for _, item := range data {
|
||||
tmp := MediaTagInfo{
|
||||
ID: item.ID,
|
||||
Kind: item.Kind,
|
||||
Dimension: item.Dimension,
|
||||
Name: item.Name,
|
||||
Active: item.Active,
|
||||
Sort: item.Sort,
|
||||
IsDelete: item.IsDelete,
|
||||
UpdatedAct: item.UpdatedAct,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdateTime: item.UpdateTime,
|
||||
}
|
||||
res.List = append(res.List, tmp)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type AppQueryInfoReq struct {
|
||||
ID string `json:"id" form:"id"` // id
|
||||
}
|
||||
|
||||
// GetInfo 获取详情
|
||||
func (p *AppQueryInfoReq) GetInfo() (res MediaTagInfo, err error) {
|
||||
oid, err := primitive.ObjectIDFromHex(p.ID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var item mediatagmod.MediaTag
|
||||
item, err = mediatagdata.GetInfoFromCache(oid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = MediaTagInfo{
|
||||
ID: item.ID,
|
||||
Kind: item.Kind,
|
||||
Dimension: item.Dimension,
|
||||
Name: item.Name,
|
||||
Active: item.Active,
|
||||
Sort: item.Sort,
|
||||
IsDelete: item.IsDelete,
|
||||
UpdatedAct: item.UpdatedAct,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdateTime: item.UpdateTime,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagsByIDs2Map 通过tagid列表获取tag详情列表
|
||||
func GetTagsByIDs2Map(ids []primitive.ObjectID) map[primitive.ObjectID]*mediatagmod.MediaTag {
|
||||
m := make(map[primitive.ObjectID]*mediatagmod.MediaTag)
|
||||
if len(ids) == 0 {
|
||||
return m
|
||||
}
|
||||
tags, ok := GetTagsByIDs(ids)
|
||||
if !ok {
|
||||
return m
|
||||
}
|
||||
for i := range tags {
|
||||
m[tags[i].ID] = tags[i]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// GetTagsByIDs 通过tagid列表获取tag详情列表
|
||||
func GetTagsByIDs(ids []primitive.ObjectID) ([]*mediatagmod.MediaTag, bool) {
|
||||
tags, unExistsTid, err := getTagsByIDsFromRedis(ids)
|
||||
if err != nil || len(unExistsTid) != 0 {
|
||||
unExistsTInfo, err := mediatagmod.FindTagsByIDs(unExistsTid)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
common.Go(func() {
|
||||
_ = setTagsByIDs2Redis(unExistsTInfo)
|
||||
})
|
||||
tags = append(tags, unExistsTInfo...)
|
||||
}
|
||||
return tags, true
|
||||
}
|
||||
|
||||
func getTagsByIDsFromRedis(ids []primitive.ObjectID) ([]*mediatagmod.MediaTag, []primitive.ObjectID, error) {
|
||||
sInfo := getTidKey(ids)
|
||||
uInfo, err := appg.Redis.MGet(sInfo...)
|
||||
if err != nil {
|
||||
log.Error("getTagsByIDsFromRedis error", log.Any("ids", ids), log.Any("sInfo", sInfo), log.Any("err", err))
|
||||
return nil, ids, err
|
||||
}
|
||||
tagInfo := parseTagInfo(uInfo)
|
||||
unExist := getUnExistTID(ids, tagInfo)
|
||||
return tagInfo, unExist, nil
|
||||
}
|
||||
|
||||
func setTagsByIDs2Redis(tags []*mediatagmod.MediaTag) error {
|
||||
expire := redisconst.MediaTagInfoExpire()
|
||||
for _, t := range tags {
|
||||
d, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s := redisconst.MediaTagInfoKey(t.ID.Hex())
|
||||
_ = appg.Redis.Set(s, string(d), expire)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUnExistTID(ids []primitive.ObjectID, info []*mediatagmod.MediaTag) []primitive.ObjectID {
|
||||
m := make(map[primitive.ObjectID]bool)
|
||||
var unExist []primitive.ObjectID
|
||||
for _, i := range info {
|
||||
m[i.ID] = true
|
||||
}
|
||||
for _, i := range ids {
|
||||
if !m[i] {
|
||||
unExist = append(unExist, i)
|
||||
}
|
||||
}
|
||||
return unExist
|
||||
}
|
||||
|
||||
func getTidKey(tids []primitive.ObjectID) []string {
|
||||
var keys []string
|
||||
for _, t := range tids {
|
||||
s := redisconst.MediaTagInfoKey(t.Hex())
|
||||
keys = append(keys, s)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func parseTagInfo(data []interface{}) []*mediatagmod.MediaTag {
|
||||
var infos []*mediatagmod.MediaTag
|
||||
for _, d := range data {
|
||||
str, ok := d.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var t mediatagmod.MediaTag
|
||||
err := json.Unmarshal([]byte(str), &t)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
infos = append(infos, &t)
|
||||
}
|
||||
return infos
|
||||
}
|
||||
@@ -0,0 +1,861 @@
|
||||
package messageser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/middleware/authuser"
|
||||
"91porn-server/app/service/imser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/db"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/services/message"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/cache/sysconfdata"
|
||||
"91porn-server/models/v/imusermod"
|
||||
"91porn-server/models/v/messagemod"
|
||||
"91porn-server/models/v/sessionmod"
|
||||
"91porn-server/models/v/sysconfmod"
|
||||
"91porn-server/models/v/txnmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// QueryDynamics 查询动态列表
|
||||
func QueryDynamics(uid uint64, req *messagemod.QueryCond) ([]messagemod.MsgDynamicsApp, bool, stderr.Code) {
|
||||
//now := time.Now()
|
||||
// 处理用户消息提示状态
|
||||
_ = message.CancelTip(uid)
|
||||
//动态类型消息
|
||||
var msgType []string
|
||||
if req.MsgType == 1 {
|
||||
msgType = []string{"like_msg", "like_comment_msg"}
|
||||
} else {
|
||||
msgType = []string{"comment_msg", "reply_comment_msg"}
|
||||
}
|
||||
cond := bson.M{"msgType": bson.M{"$in": msgType}, "takeUid": uid}
|
||||
mData, hasNext, err := messagemod.QueryDynamics(cond, int(req.PageSize), int(req.PageNumber))
|
||||
if err != nil {
|
||||
return nil, false, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(mData) > 0 {
|
||||
//用户查看自己的动态列表, 则消息接受者是查看用户
|
||||
uids := []uint64{}
|
||||
vid := []primitive.ObjectID{}
|
||||
for _, m := range mData {
|
||||
uids = append(uids, m.SendUid)
|
||||
vid = append(vid, m.ObjId)
|
||||
}
|
||||
//查询帖子信息
|
||||
vidMap, _ := vidmod.VideoMap(vid)
|
||||
if len(uids) > 0 {
|
||||
//查询用户信息
|
||||
uData, err := usermod.FindUsersMapByUID(uids)
|
||||
if err != nil {
|
||||
return mData, false, stderr.ErrDbQueryError
|
||||
}
|
||||
//循环获取组装用户信息
|
||||
for i, m := range mData {
|
||||
if uData[m.SendUid] != nil {
|
||||
mData[i].SendName = uData[m.SendUid].Name
|
||||
mData[i].SendAvatar = uData[m.SendUid].Portrait
|
||||
mData[i].SendGender = uData[m.SendUid].Gender
|
||||
//mData[i].SuperUser = uData[m.SendUid].GetSuperUser(now)
|
||||
mData[i].SuperUser = 0
|
||||
mData[i].Awards = uData[m.SendUid].Awards
|
||||
mData[i].VipExpireDate = uData[m.SendUid].VipExpireDate
|
||||
mData[i].VipLevel = uData[m.SendUid].VipLevel
|
||||
}
|
||||
if m.MsgType == messagemod.LikeCommentMsg {
|
||||
mData[i].LikeCount = m.UserCount
|
||||
} else if m.MsgType == messagemod.LikeMsg {
|
||||
if video, ok := vidMap[m.ObjId]; ok {
|
||||
mData[i].LikeCount = video.LikeCount
|
||||
mData[i].ImgUrl = video.Cover
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//获取成功后, 将当前动态全部变为已读
|
||||
common.Go(func() {
|
||||
//更新状态
|
||||
messagemod.UpdDynamicsIsReadByUid(nil, uid)
|
||||
//清除redis
|
||||
appg.Redis.Del(messagemod.NoRedDynamicNumRedisKey + strconv.FormatUint(uid, 10))
|
||||
})
|
||||
}
|
||||
return mData, hasNext, stderr.Success
|
||||
}
|
||||
|
||||
// 查询未读动态数
|
||||
func QueryNoRedDynamicNum(uid uint64) (messagemod.NoReadMsgDynamicsCountApp, stderr.Code) {
|
||||
var ret = messagemod.NoReadMsgDynamicsCountApp{
|
||||
DtCount: 0,
|
||||
MsgCount: 0,
|
||||
}
|
||||
//redis限制, 一分钟一次
|
||||
key := messagemod.NoRedDynamicNumRedisKey + strconv.FormatUint(uid, 10)
|
||||
result, err := appg.Redis.Get(key)
|
||||
if err != nil || result == nil {
|
||||
mt := messagemod.GetDynamics()
|
||||
//包含私信
|
||||
mt = append(mt, messagemod.PrivateLetterMsg)
|
||||
//动态类型消息
|
||||
cond := bson.M{"msgType": bson.M{"$in": mt}, "takeUid": uid, "isRead": false}
|
||||
data, err := messagemod.FindByFilter(cond)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser QueryNoRedDynamicNum messagemod.FindByFilter error:%+v:", err), log.Any("uid", uid))
|
||||
return ret, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(data) > 0 {
|
||||
for _, d := range data {
|
||||
if d.MsgType == messagemod.PrivateLetterMsg {
|
||||
ret.MsgCount += 1
|
||||
} else {
|
||||
ret.DtCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(*result), &ret)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser QueryNoRedDynamicNum json.Unmarshal error:%+v:", err), log.Any("uid", uid))
|
||||
return ret, stderr.Failure
|
||||
}
|
||||
return ret, stderr.Success
|
||||
}
|
||||
//将对象转为字符串
|
||||
jsonstr, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser QueryNoRedDynamicNum json.Marshal error:%+v:", err), log.Any("uid", uid))
|
||||
return ret, stderr.Success
|
||||
}
|
||||
//设置redis缓存
|
||||
_ = appg.Redis.Set(key, string(jsonstr), time.Minute*1)
|
||||
return ret, stderr.Success
|
||||
}
|
||||
|
||||
// QueryPrivateLetterSession 查询私信会话列表
|
||||
func QueryPrivateLetterSession(uid uint64, req *sessionmod.QueryCond) ([]sessionmod.SessionApp, bool, stderr.Code) {
|
||||
// 处理用户消息提示状态
|
||||
_ = message.CancelTip(uid)
|
||||
//查询当前用户的会话
|
||||
cond := bson.M{"$or": []bson.M{{"sendUid": uid}, {"takeUid": uid}}}
|
||||
sData, hasNext, err := sessionmod.Query(cond, req)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add sessionmod.Query error:%+v:", err), log.Any("uid", uid))
|
||||
return nil, false, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(sData) > 0 {
|
||||
var uids []uint64
|
||||
for i, m := range sData {
|
||||
//查询未读条数
|
||||
count, err := messagemod.Count(bson.M{"sessionId": m.SessionId, "takeUid": uid, "isRead": false})
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("messageser Add messagemod.Count error:%+v:", err), log.Any("sessionId", m.ID.Hex()), log.Any("uid", uid))
|
||||
continue
|
||||
}
|
||||
sData[i].NoReadNum = count
|
||||
//sendId是当前用户,则会话是有当前用户主动发起的,反之,对方用户主动发起
|
||||
//注意, 会话列表用来展示对方用户头像姓名, 所有这里取对方用户信息
|
||||
if uid == m.SendUid {
|
||||
uids = append(uids, m.TakeUid)
|
||||
continue
|
||||
}
|
||||
if uid == m.TakeUid {
|
||||
uids = append(uids, m.SendUid)
|
||||
}
|
||||
}
|
||||
if len(uids) > 0 {
|
||||
//查询用户信息
|
||||
uData, err := usermod.FindUsersMapByUID(uids)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add usermod.FindUsersMapByUID error:%+v:", err), log.Any("uids", uids), log.Any("uid", uid))
|
||||
return sData, false, stderr.ErrDbQueryError
|
||||
}
|
||||
imByUID, _ := imusermod.IMUserIDMapByUIDs(uids) // uid → imUserId(imusermod 为准)
|
||||
//循环获取组装用户信息
|
||||
for i, m := range sData {
|
||||
if uid == m.SendUid && uData[m.TakeUid] != nil {
|
||||
peer := uData[m.TakeUid]
|
||||
sData[i].UserId = peer.UID
|
||||
sData[i].PeerImUserID = imByUID[peer.UID]
|
||||
sData[i].UserName = peer.Name
|
||||
sData[i].UserAvatar = peer.Portrait
|
||||
continue
|
||||
}
|
||||
if uid == m.TakeUid && uData[m.SendUid] != nil {
|
||||
peer := uData[m.SendUid]
|
||||
sData[i].UserId = peer.UID
|
||||
sData[i].PeerImUserID = imByUID[peer.UID]
|
||||
sData[i].UserName = peer.Name
|
||||
sData[i].UserAvatar = peer.Portrait
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sData, hasNext, stderr.Success
|
||||
}
|
||||
|
||||
// DelSession 删除私信会话
|
||||
func DelSession(uid uint64, sessionId string) stderr.Code {
|
||||
//查询会话是否存在
|
||||
session, err := sessionmod.FindOneByFilter(bson.M{"sessionId": sessionId})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser DelSession sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid), log.Any("sessionId", sessionId))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if session.ID.IsZero() {
|
||||
log.Warn("messageser DelSession session is null", log.Any("uid", uid), log.Any("sessionId", sessionId))
|
||||
return stderr.Failure
|
||||
}
|
||||
if session.SendUid != uid && session.TakeUid != uid {
|
||||
log.Warn("messageser DelSession session.SendUid != uid && session.TakeUid != uid", log.Any("uid", uid), log.Any("sessionId", sessionId))
|
||||
return stderr.Failure
|
||||
}
|
||||
//删除
|
||||
err = sessionmod.Delete(bson.M{"sessionId": sessionId})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser DelSession sessionmod.Delete error:%+v:", err), log.Any("uid", uid), log.Any("sessionId", sessionId))
|
||||
return stderr.ErrDbDeleteError
|
||||
}
|
||||
//删除会话对应的消息
|
||||
_, err = messagemod.DeleteMsgBySessionId(sessionId)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser DelSession messagemod.DeleteMsgBySessionId error:%+v:", err), log.Any("uid", uid), log.Any("sessionId", sessionId))
|
||||
return stderr.ErrDbDeleteError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// GetSessionId 获取sessionId
|
||||
func GetSessionId(uid, takeUid uint64) string {
|
||||
//会话id
|
||||
sessionId := ""
|
||||
if uid == takeUid {
|
||||
log.Warn(fmt.Sprintf("messageser GetSessionId uid == takeUid"), log.Any("uid", uid), log.Any("takeUid", takeUid))
|
||||
return sessionId
|
||||
}
|
||||
//先查询是否有会话信息
|
||||
cond := bson.M{"$or": []bson.M{bson.M{"sendUid": uid, "takeUid": takeUid}, bson.M{"sendUid": takeUid, "takeUid": uid}}}
|
||||
session, err := sessionmod.FindOneByFilter(cond)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser GetSessionId sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid))
|
||||
return sessionId
|
||||
}
|
||||
if session.ID.IsZero() {
|
||||
sessionId = common.UUID()
|
||||
//新增session
|
||||
s := &sessionmod.Session{
|
||||
SendUid: uid,
|
||||
TakeUid: takeUid,
|
||||
SessionId: sessionId,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
err := sessionmod.InsertOne(nil, s)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser GetSessionId sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("uid", uid), log.Any("takeUid", takeUid))
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
sessionId = session.SessionId
|
||||
}
|
||||
return sessionId
|
||||
}
|
||||
|
||||
// QueryPrivateLetterMsg 查询私信聊天列表
|
||||
func QueryPrivateLetterMsg(uid uint64, req *messagemod.QueryMsgCond) ([]messagemod.MessageApp, bool, stderr.Code) {
|
||||
//私信类型消息
|
||||
msgTypes := [...]string{string(messagemod.PrivateLetterMsg), string(messagemod.OfficialPrivateLetterMsg)}
|
||||
cond := bson.M{"msgType": bson.M{"$in": msgTypes}, "sessionId": req.SessionId}
|
||||
mData, hasNext, err := messagemod.Query(cond, int(req.PageSize), int(req.PageNumber))
|
||||
if err != nil {
|
||||
return nil, false, stderr.ErrDbQueryError
|
||||
}
|
||||
if len(mData) > 0 {
|
||||
uids := []uint64{}
|
||||
for _, m := range mData {
|
||||
uids = append(uids, m.SendUid)
|
||||
uids = append(uids, m.TakeUid)
|
||||
}
|
||||
if len(uids) > 0 {
|
||||
//查询用户信息
|
||||
uData, err := usermod.FindUsersMapByUID(uids)
|
||||
if err != nil {
|
||||
return mData, false, stderr.ErrDbQueryError
|
||||
}
|
||||
//循环获取组装用户信息
|
||||
for i, m := range mData {
|
||||
if sendUser := uData[m.SendUid]; sendUser != nil {
|
||||
mData[i].SendName = sendUser.Name
|
||||
mData[i].SendAvatar = sendUser.Portrait
|
||||
}
|
||||
if takeUser := uData[m.TakeUid]; takeUser != nil {
|
||||
mData[i].TakeName = takeUser.Name
|
||||
mData[i].TakeAvatar = takeUser.Portrait
|
||||
}
|
||||
peerUID := m.SendUid
|
||||
if uid == m.SendUid {
|
||||
peerUID = m.TakeUid
|
||||
}
|
||||
if peer := uData[peerUID]; peer != nil {
|
||||
mData[i].PeerImUserID = imusermod.IMUserIDByUID(peer.UID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//请求列表时,将当前用户当前会话所有未读消息更新为已读
|
||||
common.Go(func() {
|
||||
messagemod.UpdIsReadBySessionIdAndUid(nil, uid, req.SessionId)
|
||||
})
|
||||
return mData, hasNext, stderr.Success
|
||||
}
|
||||
|
||||
// AddPrivateLetter 新增私信消息
|
||||
func AddPrivateLetter(uid uint64, req messagemod.AddMsgReqInfo) stderr.Code {
|
||||
data, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || data == nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add send user is null"), log.Any("uid", uid), log.Any("takeUid", req.TakeUid))
|
||||
return stderr.CommentUserInfoErr
|
||||
}
|
||||
if data.HasBanned {
|
||||
return stderr.CommentUserHasBanned
|
||||
}
|
||||
if uid == req.TakeUid {
|
||||
log.Warn(fmt.Sprintf("messageser Add user cant self add msg"), log.Any("uid", uid), log.Any("takeUid", req.TakeUid))
|
||||
return stderr.NoSelfAddMsg
|
||||
}
|
||||
//校验私信用户是否vip
|
||||
//if !data.IsVIP(time.Now()) {
|
||||
// return stderr.Success
|
||||
//}
|
||||
////校验vip是否是充值过的,没有充值则不允许私信
|
||||
//if uid > 150000 && !walletmod.IsRechargeUser(nil, uid) {
|
||||
// log.Warn(fmt.Sprintf("messageser Add user no recharge"), log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("content", req.Content))
|
||||
// return stderr.Success
|
||||
//}
|
||||
price := CheckChatPrice(data)
|
||||
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrParamError
|
||||
}
|
||||
|
||||
if price > 0 && (wallet == nil || wallet.ID.IsZero()) {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
|
||||
checkUserBalance, deAmt, deIncome := CheckUserBalance(wallet, price)
|
||||
if !checkUserBalance {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
//if wallet.Amount+wallet.Income<=0 || wallet.Amount+wallet.Income
|
||||
|
||||
fengjin := false
|
||||
//查询被私信的用户是否存在
|
||||
takeUser, err := usermod.FindUserByUID(req.TakeUid)
|
||||
if err != nil || takeUser == nil {
|
||||
fengjin = true
|
||||
}
|
||||
//校验用户请求频率,5秒内超过5次,则封禁
|
||||
//key := "add-private-letter-" + strconv.FormatUint(uid, 10)
|
||||
//ret, err := appg.Redis.Get(key)
|
||||
//if !fengjin && err == nil {
|
||||
// var i int = 0
|
||||
// if ret != nil {
|
||||
// i, _ = strconv.Atoi(*ret)
|
||||
// }
|
||||
// if i > 5 {
|
||||
// //封禁
|
||||
// fengjin = true
|
||||
// } else {
|
||||
// i += 1
|
||||
// appg.Redis.Set(key, strconv.Itoa(i), time.Second*5)
|
||||
// }
|
||||
//}
|
||||
//检测私信内容是否包含网址
|
||||
//if truthutil.CheckStrIsUrl(req.Content) {
|
||||
// fengjin = true
|
||||
// //这里直接返回成功,让广告用户误以为成功了,免得一直来麻烦
|
||||
// log.Warn("messageser Add checkStrIsUrl true", log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("content", req.Content))
|
||||
// return stderr.Success
|
||||
//}
|
||||
if fengjin {
|
||||
common.Go(func() {
|
||||
log.Warn("messageser Add takeUser == nil", log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.Any("content", req.Content))
|
||||
//如果用户不存在, 则证明是脚本在刷私信, 封禁该用户
|
||||
hasLocked := true
|
||||
lockReason := "脚本打广告的用户"
|
||||
doc := usermod.UserSelector{
|
||||
HasLocked: &hasLocked,
|
||||
LockReason: &lockReason,
|
||||
}
|
||||
usermod.Update(uid, doc)
|
||||
//删除token
|
||||
authuser.RevokeTokenCache(uid)
|
||||
})
|
||||
return stderr.CommentUserHasBanned
|
||||
}
|
||||
//会话id
|
||||
sessionId := ""
|
||||
//新增私聊信息之前, 先查询是否有会话信息
|
||||
cond := bson.M{"$or": []bson.M{bson.M{"sendUid": uid, "takeUid": req.TakeUid}, bson.M{"sendUid": req.TakeUid, "takeUid": uid}}}
|
||||
session, err := sessionmod.FindOneByFilter(cond)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
t := time.Now()
|
||||
if session.ID.IsZero() {
|
||||
sessionId = common.UUID()
|
||||
//新增session
|
||||
s := &sessionmod.Session{
|
||||
SendUid: uid,
|
||||
TakeUid: req.TakeUid,
|
||||
SessionId: sessionId,
|
||||
CreatedAt: t,
|
||||
UpdatedAt: t,
|
||||
}
|
||||
err := sessionmod.InsertOne(nil, s)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("uid", uid), log.Any("takeUid", req.TakeUid))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
} else {
|
||||
sessionId = session.SessionId
|
||||
}
|
||||
|
||||
if deAmt > 0 || deIncome > 0 {
|
||||
// 扣除用户余额
|
||||
plan := &walletmod.DebitPlan{
|
||||
Amount: deAmt,
|
||||
Income: deIncome,
|
||||
}
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
wallet, err := walletmod.Debit(t, plan, uid)
|
||||
if err != nil { //扣钱
|
||||
return err
|
||||
}
|
||||
|
||||
var txnLogs []txnmod.TransactionLog
|
||||
if deAmt > 0 {
|
||||
txnLogs = append(txnLogs, txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: -deAmt,
|
||||
ActualAmount: float64(-deAmt),
|
||||
TranType: txnmod.SendMsgDebitAmountGold.Key(),
|
||||
TranTypeInt: int64(txnmod.SendMsgDebitAmountGoldReturn),
|
||||
Desc: fmt.Sprintf("用户私信扣除%v个金币", deAmt),
|
||||
SysType: data.SysType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
})
|
||||
}
|
||||
if deIncome > 0 {
|
||||
if err = txnmod.InsertTransactionLog(t, &txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: -deIncome,
|
||||
ActualAmount: float64(-deIncome),
|
||||
TranType: txnmod.SendMsgDebitIncomeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.SendMsgDebitIncomeGold),
|
||||
Desc: fmt.Sprintf("用户私信扣除收益%v个金币", deIncome),
|
||||
SysType: data.SysType,
|
||||
RealAmount: walletmod.GetRealAmount(wallet),
|
||||
}); err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
}
|
||||
|
||||
if len(txnLogs) > 0 {
|
||||
if err = txnmod.InsertManyTransactionLog(t, txnLogs); err != nil {
|
||||
log.Warn(fmt.Sprintf("productser BuyVIP Transaction err %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(fmt.Sprintf("send meaasge Transaction err %s", err.Error()))
|
||||
return stderr.ErrDbUpdateError //通知消息
|
||||
}
|
||||
}
|
||||
|
||||
if err = imser.SendPrivateMessage(uid, req.TakeUid, req.Content, req.ImgUrl); err != nil {
|
||||
imser.LogSDKSendError(uid, req.TakeUid, err)
|
||||
return stderr.Failure
|
||||
}
|
||||
|
||||
code := PublicAddMsg(messagemod.PublicAddMsg{
|
||||
SendUid: uid,
|
||||
TakeUid: req.TakeUid,
|
||||
SessionId: sessionId,
|
||||
Content: req.Content,
|
||||
ImgUrl: req.ImgUrl,
|
||||
MsgType: messagemod.PrivateLetterMsg,
|
||||
})
|
||||
if code != stderr.Success {
|
||||
return code
|
||||
}
|
||||
common.Go(func() {
|
||||
//更新会话预览消息
|
||||
content := req.Content
|
||||
if len(req.ImgUrl) > 0 && content == "" {
|
||||
content = "发送了一张图片"
|
||||
}
|
||||
err := sessionmod.UpdOneByFilter(nil, bson.M{"sessionId": sessionId}, bson.M{"$set": bson.M{"preContent": content, "updatedAt": t}})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add messagemod.UpdOneByFilter error:%+v:", err), log.Any("sessionId", sessionId), log.Any("preContent", req.Content), log.Any("uid", uid))
|
||||
}
|
||||
})
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// SendIMPrivateLetter 走第三方 IM 通道发送私信:
|
||||
// 校验发送/接收方 → 校验余额 → 扣费(含 txn log)→ 调用 imser.SendPrivateMessage 走 IM SDK 投递。
|
||||
func SendIMPrivateLetter(uid uint64, req messagemod.AddMsgReqInfo) stderr.Code {
|
||||
sender, err := usermod.FindUserByUID(uid)
|
||||
if err != nil || sender == nil {
|
||||
log.Warn("messageser SendIMPrivateLetter sender not found",
|
||||
log.Any("uid", uid), log.Any("takeUid", req.TakeUid))
|
||||
return stderr.CommentUserInfoErr
|
||||
}
|
||||
if sender.HasBanned {
|
||||
return stderr.CommentUserHasBanned
|
||||
}
|
||||
if uid == req.TakeUid {
|
||||
return stderr.NoSelfAddMsg
|
||||
}
|
||||
takeUser, err := usermod.FindUserByUID(req.TakeUid)
|
||||
if err != nil || takeUser == nil {
|
||||
log.Warn("messageser SendIMPrivateLetter take user not found",
|
||||
log.Any("uid", uid), log.Any("takeUid", req.TakeUid))
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
|
||||
price := CheckChatPrice(sender)
|
||||
if uid != 100001 && price > 0 {
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrParamError
|
||||
}
|
||||
if wallet == nil || wallet.ID.IsZero() {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
ok, deAmt, deIncome := CheckUserBalance(wallet, price)
|
||||
if !ok {
|
||||
return stderr.InsufficientBalance
|
||||
}
|
||||
if err = appg.VideoDB.Trans(func(t *db.MongoTool) error {
|
||||
w, err := walletmod.Debit(t, &walletmod.DebitPlan{Amount: deAmt, Income: deIncome}, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var logs []txnmod.TransactionLog
|
||||
if deAmt > 0 {
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: -deAmt,
|
||||
ActualAmount: float64(-deAmt),
|
||||
TranType: txnmod.SendMsgDebitAmountGold.Key(),
|
||||
TranTypeInt: int64(txnmod.SendMsgDebitAmountGoldReturn),
|
||||
Desc: fmt.Sprintf("用户私信(IM)扣除%v个金币", deAmt),
|
||||
SysType: sender.SysType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
}
|
||||
if deIncome > 0 {
|
||||
logs = append(logs, txnmod.TransactionLog{
|
||||
UID: uid,
|
||||
Amount: -deIncome,
|
||||
ActualAmount: float64(-deIncome),
|
||||
TranType: txnmod.SendMsgDebitIncomeGold.Key(),
|
||||
TranTypeInt: int64(txnmod.SendMsgDebitIncomeGold),
|
||||
Desc: fmt.Sprintf("用户私信(IM)扣除收益%v个金币", deIncome),
|
||||
SysType: sender.SysType,
|
||||
RealAmount: w.RealAmount(),
|
||||
})
|
||||
}
|
||||
if len(logs) > 0 {
|
||||
if err = txnmod.InsertManyTransactionLog(t, logs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn("messageser SendIMPrivateLetter charge tx fail",
|
||||
log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.E(err))
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
}
|
||||
|
||||
if err := imser.SendPrivateMessage(uid, req.TakeUid, req.Content, req.ImgUrl); err != nil {
|
||||
log.Error("messageser SendIMPrivateLetter imser.SendPrivateMessage fail",
|
||||
log.Any("uid", uid), log.Any("takeUid", req.TakeUid), log.E(err))
|
||||
return stderr.Failure
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func CheckChatPrice(u *usermod.User) int64 {
|
||||
configure, _ := sysconfdata.GetAllFromCache()
|
||||
if u == nil {
|
||||
return configure.GetInt(sysconfmod.SendMsgPrice)
|
||||
}
|
||||
var price int64
|
||||
// 校验私信用户是否vip
|
||||
isVip := u.IsPaidVIP()
|
||||
if isVip && u.ChatPrice > 0 {
|
||||
price = u.ChatPrice
|
||||
} else if isVip && u.ChatPrice < 0 {
|
||||
// vip私信价格设置为负数的时候,则说明不需要钱(因为旧的用户数据这个字段都是0,所以都得读默认配置)
|
||||
price = 0
|
||||
} else {
|
||||
// 非vip用户或者是旧的vip用户的u.ChatPrice字段为0的时候,都是默认读配置
|
||||
price = configure.GetInt(sysconfmod.SendMsgPrice)
|
||||
}
|
||||
return price
|
||||
}
|
||||
|
||||
func CheckUserBalance(w *walletmod.Wallet, price int64) (checkStatus bool, deAmt, deIncome int64) {
|
||||
totalAmt := w.Amount + w.Income
|
||||
var dAmt, dIncome int64
|
||||
if totalAmt < price {
|
||||
return false, dAmt, dIncome
|
||||
}
|
||||
|
||||
if totalAmt >= price && w.Amount >= price {
|
||||
dAmt = price
|
||||
}
|
||||
|
||||
if totalAmt >= price && w.Amount < price {
|
||||
dAmt = w.Amount
|
||||
dIncome = price - w.Amount
|
||||
}
|
||||
|
||||
return true, dAmt, dIncome
|
||||
|
||||
}
|
||||
|
||||
// ReadMsg 阅读消息
|
||||
func ReadMsg(uid uint64, req messagemod.ReadMsgReqInfo) stderr.Code {
|
||||
if len(req.MsgIds) < 0 {
|
||||
log.Error(fmt.Sprintf("messageser readMsg req.MsgIds <= 0"), log.Any("uid", uid))
|
||||
return stderr.Success
|
||||
}
|
||||
var ids []primitive.ObjectID
|
||||
for _, v := range req.MsgIds {
|
||||
id, _ := primitive.ObjectIDFromHex(v)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) < 0 {
|
||||
log.Error(fmt.Sprintf("messageser readMsg ids <= 0"), log.Any("uid", uid))
|
||||
return stderr.Success
|
||||
}
|
||||
//更新消息已读状态
|
||||
err := messagemod.UpdIsRead(nil, uid, ids)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("messageser messagemod.UpdIsRead error:%+v:", err), log.Any("ids", ids), log.Any("uid", uid))
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// DynamicMsgUserGroup 同一obj下的相同操作归类相同用户
|
||||
func DynamicMsgUserGroup(params messagemod.PublicAddMsg) stderr.Code {
|
||||
//查询用户信息
|
||||
user, err := usermod.FindUserByUID(params.SendUid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if user == nil || user.HasLocked {
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
|
||||
filter := bson.M{"takeUid": params.TakeUid, "msgType": params.MsgType, "objId": params.ObjId}
|
||||
msg, err := messagemod.FindOneByFilter(filter)
|
||||
if err != nil || msg.ID.IsZero() {
|
||||
params.UserCount = 1
|
||||
params.UserGroup = []messagemod.UserInfo{
|
||||
{
|
||||
Uid: user.UID,
|
||||
Name: user.Name,
|
||||
Avatar: user.Portrait,
|
||||
},
|
||||
}
|
||||
// 发布消息并返回
|
||||
return PublicAddMsg(params)
|
||||
}
|
||||
|
||||
ug := msg.UserGroup
|
||||
for _, v := range ug {
|
||||
// 已经有记录直接返回
|
||||
if v.Uid == params.SendUid {
|
||||
return stderr.Success
|
||||
}
|
||||
}
|
||||
// 取前9个用户 + 当前用户
|
||||
if len(ug) >= 10 {
|
||||
ug = ug[:9]
|
||||
}
|
||||
ug = append([]messagemod.UserInfo{{
|
||||
Uid: user.UID,
|
||||
Name: user.Name,
|
||||
Avatar: user.Portrait,
|
||||
}}, ug...)
|
||||
|
||||
set := bson.M{"userCount": msg.UserCount + 1, "updatedAt": time.Now(), "userGroup": ug}
|
||||
err = messagemod.UpdOneByFilter(nil, bson.M{"_id": msg.ID}, bson.M{"$set": set})
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("PublicMsgUserGroup error:%+v:", err), log.Any("set", set), log.Any("msgID", msg.ID))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// PublicAddMsg 添加消息公共方法
|
||||
func PublicAddMsg(prams messagemod.PublicAddMsg) stderr.Code {
|
||||
//查询用户信息
|
||||
user, err := usermod.FindUserByUID(prams.SendUid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if user == nil || user.HasLocked {
|
||||
return stderr.UserIsNotExists
|
||||
}
|
||||
now := time.Now()
|
||||
//新增消息
|
||||
msg := &messagemod.Message{
|
||||
SendUid: prams.SendUid,
|
||||
TakeUid: prams.TakeUid,
|
||||
MsgType: prams.MsgType,
|
||||
Content: prams.Content,
|
||||
ImgUrl: prams.ImgUrl,
|
||||
UserCount: prams.UserCount,
|
||||
UserGroup: prams.UserGroup,
|
||||
IsRead: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
// 是否加入未读提醒
|
||||
needTip := false
|
||||
switch prams.MsgType {
|
||||
case messagemod.PrivateLetterMsg:
|
||||
//私信
|
||||
msg.SessionId = prams.SessionId
|
||||
needTip = true
|
||||
case messagemod.OfficialPrivateLetterMsg:
|
||||
//官方私信
|
||||
msg.SessionId = prams.SessionId
|
||||
msg.ContentType = prams.ContentType
|
||||
needTip = true
|
||||
case messagemod.FollowMsg:
|
||||
//关注
|
||||
msg.Content = "关注了你"
|
||||
//needTip = true
|
||||
case messagemod.LikeMsg:
|
||||
//点赞
|
||||
msg.Content = "点赞了你的帖子"
|
||||
msg.ObjId = prams.ObjId
|
||||
msg.ObjName = prams.ObjName
|
||||
msg.ObjType = prams.ObjType
|
||||
needTip = true
|
||||
case messagemod.ForwardMsg: //转发
|
||||
msg.Content = "转发了你的帖子"
|
||||
msg.ObjId = prams.ObjId
|
||||
msg.ObjName = prams.ObjName
|
||||
case messagemod.CommentMsg: //评论
|
||||
//msg.Content = "评论了你的帖子"
|
||||
msg.ObjId = prams.ObjId
|
||||
msg.ObjName = prams.ObjName
|
||||
msg.ObjCover = prams.ObjCover
|
||||
msg.ObjType = prams.ObjType
|
||||
needTip = true
|
||||
case messagemod.DesireMsg:
|
||||
msg.Content = "回复了你的工单"
|
||||
msg.ObjId = prams.ObjId
|
||||
msg.ObjName = prams.ObjName
|
||||
//needTip = true
|
||||
case messagemod.LikeCommentMsg: // 点赞评论
|
||||
msg.Content = "点赞了你的评论"
|
||||
msg.ObjId = prams.ObjId
|
||||
msg.ObjName = prams.ObjName
|
||||
msg.ObjType = prams.ObjType
|
||||
needTip = true
|
||||
case messagemod.ReplyCommentMsg: // 回复评论
|
||||
msg.Content = "回复了你的评论"
|
||||
msg.ObjId = prams.ObjId
|
||||
msg.ObjName = prams.ObjName
|
||||
msg.ObjType = prams.ObjType
|
||||
//needTip = true
|
||||
}
|
||||
_, err = messagemod.InsertOne(nil, msg)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add messagemod.InsertOne error:%+v:", err), log.Any("message", msg))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
go func() {
|
||||
err = usermod.IncDynamic(prams.TakeUid)
|
||||
}()
|
||||
// 更新提示状态
|
||||
if needTip {
|
||||
_ = message.SetTip(prams.TakeUid)
|
||||
}
|
||||
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func HandleOfficialMsg(receiveUId uint64, content string, contentType int) stderr.Code {
|
||||
var (
|
||||
uid uint64 = 100000 // 官方ID
|
||||
sessionId = "" // 会话ID
|
||||
)
|
||||
//新增私聊信息之前, 先查询是否有会话信息
|
||||
cond := bson.M{"$or": []bson.M{bson.M{"sendUid": uid, "takeUid": receiveUId}, bson.M{"sendUid": receiveUId, "takeUid": uid}}}
|
||||
session, err := sessionmod.FindOneByFilter(cond)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add sessionmod.FindOneByFilter error:%+v:", err), log.Any("uid", uid))
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
t := time.Now()
|
||||
if session.ID.IsZero() {
|
||||
sessionId = common.UUID()
|
||||
//新增session
|
||||
s := &sessionmod.Session{
|
||||
SendUid: uid,
|
||||
TakeUid: receiveUId,
|
||||
SessionId: sessionId,
|
||||
CreatedAt: t,
|
||||
UpdatedAt: t,
|
||||
}
|
||||
err := sessionmod.InsertOne(nil, s)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add sessionmod.InsertOne error:%+v:", err), log.Any("session", s), log.Any("uid", uid), log.Any("takeUid", receiveUId))
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
} else {
|
||||
sessionId = session.SessionId
|
||||
}
|
||||
code := PublicAddMsg(messagemod.PublicAddMsg{
|
||||
SendUid: uid,
|
||||
TakeUid: receiveUId,
|
||||
SessionId: sessionId,
|
||||
Content: content,
|
||||
ContentType: contentType,
|
||||
MsgType: messagemod.OfficialPrivateLetterMsg,
|
||||
})
|
||||
if code != stderr.Success {
|
||||
return code
|
||||
}
|
||||
common.Go(func() {
|
||||
//更新会话预览消息
|
||||
err := sessionmod.UpdOneByFilter(nil, bson.M{"sessionId": sessionId}, bson.M{"$set": bson.M{"preContent": content, "updatedAt": t}})
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("messageser Add messagemod.UpdOneByFilter error:%+v:", err), log.Any("sessionId", sessionId), log.Any("preContent", content), log.Any("uid", uid))
|
||||
}
|
||||
})
|
||||
return stderr.Success
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package mineser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/app/middleware/authuser"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/followmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/models/v/walletmod"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Follow 关注或者取消关注
|
||||
func Follow(uid uint64, followUID uint64, isFollow bool, isShort bool) (code stderr.Code, data interface{}) {
|
||||
if isFollow {
|
||||
//判断被关注用户是否存在或者被封
|
||||
user, err := usermod.FindUserByUID(followUID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
if user == nil || user.HasLocked {
|
||||
return stderr.UserIsNotExists, ""
|
||||
}
|
||||
//关注人数限制
|
||||
if user.Follows >= followmod.MaxFollowUsers {
|
||||
return stderr.Success, nil
|
||||
}
|
||||
//判断关注用户是否存在或者被封
|
||||
followUser, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
if followUser == nil || followUser.HasLocked {
|
||||
return stderr.UserIsNotExists, ""
|
||||
}
|
||||
if _, err := followmod.AddFollow(uid, followUID, isShort); err != nil {
|
||||
return stderr.ErrDbInsertError, ""
|
||||
} else { //关注成功
|
||||
common.Go(func() {
|
||||
//校验该用户一天关注上限,超过50人则封禁
|
||||
key := "user-follow-" + strconv.FormatUint(uid, 10)
|
||||
ret, _ := appg.Redis.Get(key)
|
||||
//当前用户24小时内关注次数
|
||||
i := 0
|
||||
if ret != nil {
|
||||
i, _ = strconv.Atoi(*ret)
|
||||
//24小时内关注超过上限,封禁
|
||||
if i > followmod.ToDayFollowLimit {
|
||||
log.Warn("user follow upper limit", log.Any("uid", uid), log.Any("followUID", followUID), log.Any("followCount", i))
|
||||
//如果用户不存在, 则证明是脚本在刷私信, 封禁该用户
|
||||
hasLocked := true
|
||||
lockReason := "脚本刷关注用户"
|
||||
summary := "未知"
|
||||
portrait := "bf211222/image/121/si/1d8/2nw/d9af01c3203a5d0615319a7c7d8faf72.jpeg"
|
||||
name := "游客" + strconv.FormatUint(uid, 10)
|
||||
doc := usermod.UserSelector{
|
||||
HasLocked: &hasLocked,
|
||||
LockReason: &lockReason,
|
||||
Summary: &summary,
|
||||
Portrait: &portrait,
|
||||
Name: &name,
|
||||
}
|
||||
usermod.Update(uid, doc)
|
||||
//删除token
|
||||
authuser.RevokeTokenCache(uid)
|
||||
//删除redis
|
||||
appg.Redis.Del(key)
|
||||
}
|
||||
}
|
||||
if i <= followmod.ToDayFollowLimit {
|
||||
i += 1
|
||||
appg.Redis.Set(key, strconv.Itoa(i), time.Hour*24)
|
||||
}
|
||||
})
|
||||
common.Go(func() {
|
||||
// 排行榜
|
||||
//fans.Incr(followUID)
|
||||
_ = usermod.IncFans(followUID)
|
||||
_ = usermod.IncFollow(uid)
|
||||
//r := int64(1)
|
||||
//_ = leaderboardmod.Insert(nil, followUID, leaderboardmod.Inc{FollowCountWeek: &r})
|
||||
})
|
||||
////存动态
|
||||
//common.Go(func() {
|
||||
// _ = messageser.PublicAddMsg(messagemod.PublicAddMsg{
|
||||
// SendUid: uid,
|
||||
// TakeUid: followUID,
|
||||
// MsgType: messagemod.FollowMsg,
|
||||
// })
|
||||
//})
|
||||
}
|
||||
|
||||
return stderr.Success, nil
|
||||
}
|
||||
if count, err := followmod.CloseFollow(uid, followUID); err != nil {
|
||||
return stderr.ErrDbDeleteError, ""
|
||||
} else if count > 0 {
|
||||
common.Go(func() {
|
||||
// // 排行榜
|
||||
// fans.Decr(followUID)
|
||||
_ = usermod.DecFans(followUID)
|
||||
_ = usermod.DecFollow(uid)
|
||||
// r := int64(-1)
|
||||
// _ = leaderboardmod.Insert(nil, followUID, leaderboardmod.Inc{FollowCountWeek: &r})
|
||||
})
|
||||
}
|
||||
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
func UseDownloadCount(uid uint64) (code stderr.Code, err error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
if user == nil || user.HasLocked {
|
||||
return stderr.UserIsNotExists, nil
|
||||
}
|
||||
|
||||
wallet, err := walletmod.GetWallet(uid)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError, nil
|
||||
}
|
||||
|
||||
if wallet == nil || wallet.ID.IsZero() {
|
||||
return stderr.CodeEmptyData, nil
|
||||
}
|
||||
if wallet.DownloadCount <= 0 {
|
||||
return stderr.DownloadCountIsNotEnough, errors.New("DownloadCount is not enough")
|
||||
}
|
||||
|
||||
err = walletmod.CreditDownloadCount(nil, 1, uid)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("UseDownloadCount walletmod.CreditDownloadCount err %s", err.Error()))
|
||||
return stderr.ErrDbUpdateError, nil
|
||||
}
|
||||
|
||||
return stderr.Success, nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package moduleser
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/marqueemod"
|
||||
"91porn-server/models/v/moduleconfmod"
|
||||
"91porn-server/models/v/modulesectionmod"
|
||||
"91porn-server/models/v/tagmod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AnnouncementResp struct {
|
||||
//跑马灯
|
||||
Announcement []string `json:"announcement"`
|
||||
}
|
||||
|
||||
const (
|
||||
repeat = 5
|
||||
usualAnnouncements = `首充任意金额即送7天视频VIP,限时充值返利,最高返利10%,累计充值送大礼,最高赠送1688元,添加游戏专属土豆福利群@pfqipai,不定时发放惊喜大礼`
|
||||
activityAnnouncements = `限时抢购虎虎生威永久卡,金币视频五折折扣,免费赠送50张观影券 `
|
||||
)
|
||||
|
||||
// GetModuleAnnouncements 获取模块下的跑马灯
|
||||
func GetModuleAnnouncements() (AnnouncementResp, error) {
|
||||
marq, err := marqueemod.Get()
|
||||
if err != nil {
|
||||
return AnnouncementResp{}, err
|
||||
}
|
||||
res := AnnouncementResp{Announcement: make([]string, repeat)}
|
||||
for i := 0; i < repeat; i++ {
|
||||
res.Announcement[i] = marq.Content
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func GetPublishTag(uid uint64) (data *tagmod.TagInfoRes, err error) {
|
||||
user, err := usermod.FindUserByUID(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user == nil || user.ID.IsZero() || user.HasLocked || user.HasBanned {
|
||||
return nil, errors.New("user is null")
|
||||
}
|
||||
|
||||
str, err := appg.Redis.Get(redisconst.PublishTagInfoCache)
|
||||
if err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;缓存获取广告列表信息异常:%v", uid, err))
|
||||
}
|
||||
|
||||
data = &tagmod.TagInfoRes{}
|
||||
if str != nil {
|
||||
if err = json.Unmarshal([]byte(*str), &data); err == nil {
|
||||
return
|
||||
}
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;解析缓存数据异常:%v", uid, err))
|
||||
}
|
||||
|
||||
// 获取模块
|
||||
moduleConfs, err := moduleconfmod.GetModuleConfByType(moduleconfmod.Community)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sId []primitive.ObjectID
|
||||
var tagIds []primitive.ObjectID
|
||||
for _, i := range moduleConfs {
|
||||
sId = append(sId, i.ID)
|
||||
}
|
||||
|
||||
sections, err := modulesectionmod.GetBySectionBySids(sId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(sections) <= 0 {
|
||||
return
|
||||
}
|
||||
for _, s := range sections {
|
||||
if s.TagIds != nil && len(*s.TagIds) > 0 {
|
||||
tagIds = append(tagIds, *s.TagIds...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tagIds) <= 0 {
|
||||
return
|
||||
}
|
||||
tags, err := tagmod.FindOneTagByIds(tagIds)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
info := tagmod.TagInfo{
|
||||
ID: t.ID,
|
||||
Description: t.Description,
|
||||
FollowCount: t.CollCount,
|
||||
Name: t.TagName,
|
||||
VidCount: t.VideoCount,
|
||||
PlayCount: t.TPlayCount,
|
||||
CoverImg: t.CoverImg,
|
||||
}
|
||||
data.List = append(data.List, info)
|
||||
}
|
||||
|
||||
common.Go(func() {
|
||||
if data != nil {
|
||||
d, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = appg.Redis.Set(redisconst.PublishTagInfoCache, d, 15*time.Minute); err != nil {
|
||||
log.Warn(fmt.Sprintf("用户ID:%d;保存缓存数据异常:%v", uid, err))
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package modulevideoser
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/modulesectionmod"
|
||||
"91porn-server/models/v/modulevidmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
var (
|
||||
getModuleVideoByID = func(id primitive.ObjectID) (modulevidmod.SectionVideo, error) {
|
||||
videos, err := modulevidmod.Find(bson.M{"_id": id})
|
||||
if err != nil || len(videos) == 0 {
|
||||
return modulevidmod.SectionVideo{}, err
|
||||
}
|
||||
return videos[0], nil
|
||||
}
|
||||
getModuleSectionByID = modulesectionmod.GetBySectionByID
|
||||
updateModuleVideo = modulevidmod.UpdateOne
|
||||
syncVideoModuleID = func(videoID, moduleID primitive.ObjectID) error {
|
||||
_, err := vidmod.UpdateOneByID(videoID, bson.M{
|
||||
"mId": moduleID.Hex(),
|
||||
"updatedAt": time.Now(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
)
|
||||
|
||||
func AddVideo(in *modulevidmod.SectionVideo) stderr.Code {
|
||||
section, err := modulesectionmod.GetBySectionByID(in.SectionID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if section.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
mId := section.SubModuleID
|
||||
|
||||
videoInfo, err := vidmod.GetVideoInfo(in.VideoID.Hex())
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
|
||||
if videoInfo.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
|
||||
update := bson.M{}
|
||||
update["mId"] = mId.Hex()
|
||||
update["updatedAt"] = time.Now()
|
||||
in.NewsType = videoInfo.NewsType // 冗余一个字段
|
||||
in.VideoReviewedAt = videoInfo.ReviewAt // 冗余审核时间,方便排序
|
||||
_, err = vidmod.UpdateOneByID(in.VideoID, update)
|
||||
if err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
|
||||
if err := modulevidmod.InsertOne(in); err != nil {
|
||||
return stderr.ErrDbInsertError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// UpdateVideo 更新专题视频关系,并在移动专题时同步视频所属亚模块。
|
||||
func UpdateVideo(in *modulevidmod.EditSelector) stderr.Code {
|
||||
if in == nil {
|
||||
return stderr.ErrParamError
|
||||
}
|
||||
|
||||
hasID := in.ID != nil && !in.ID.IsZero()
|
||||
hasSectionAndVideo := in.SectionID != nil && !in.SectionID.IsZero() && in.VideoID != nil && !in.VideoID.IsZero()
|
||||
if !hasID && !hasSectionAndVideo {
|
||||
return stderr.ErrParamError
|
||||
}
|
||||
|
||||
// 只修改排序时不需要更新视频归属。
|
||||
if in.SectionID == nil || in.SectionID.IsZero() {
|
||||
if err := updateModuleVideo(in); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
var videoID primitive.ObjectID
|
||||
if in.VideoID != nil {
|
||||
videoID = *in.VideoID
|
||||
}
|
||||
if videoID.IsZero() {
|
||||
current, err := getModuleVideoByID(*in.ID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if current.ID.IsZero() || current.VideoID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
videoID = current.VideoID
|
||||
}
|
||||
|
||||
section, err := getModuleSectionByID(*in.SectionID)
|
||||
if err != nil {
|
||||
return stderr.ErrDbQueryError
|
||||
}
|
||||
if section.ID.IsZero() {
|
||||
return stderr.CodeEmptyData
|
||||
}
|
||||
|
||||
if err = updateModuleVideo(in); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
if err = syncVideoModuleID(videoID, section.SubModuleID); err != nil {
|
||||
return stderr.ErrDbUpdateError
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user