Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+414
View File
@@ -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
}
+15
View File
@@ -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"` // 有效期(单位: 天)
}
+7
View File
@@ -0,0 +1,7 @@
package activityclient
// Start 启动活动服客户端的所有定时同步任务(域名 + 红包场次等)
func Start() {
startDomainSync()
startHongbaoSync()
}
+120
View File
@@ -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))
})
}
+161
View File
@@ -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)))
}
+100
View File
@@ -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
}
+37
View File
@@ -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
}
+69
View File
@@ -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))
}
+573
View File
@@ -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
}
+156
View File
@@ -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
}
+378
View File
@@ -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: &times}
_, 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,
})
}
+51
View File
@@ -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
}
+11
View File
@@ -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"`
}
+83
View File
@@ -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))
}
+154
View File
@@ -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
}
+262
View File
@@ -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
}
+222
View File
@@ -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
}
+91
View File
@@ -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
}
+111
View File
@@ -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
View File
@@ -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
}
+383
View File
@@ -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
}
+164
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+330
View File
@@ -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
}
+329
View File
@@ -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)
}
+325
View File
@@ -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
}
+244
View File
@@ -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,
})
})
}
+219
View File
@@ -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
View File
@@ -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
}
+63
View File
@@ -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
}
+56
View File
@@ -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
}
+55
View File
@@ -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"`
}
+15
View File
@@ -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
}
+644
View File
@@ -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
}
+240
View File
@@ -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
}
+71
View File
@@ -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)
}
+328
View File
@@ -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
}
+32
View File
@@ -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
}
+251
View File
@@ -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
}
+276
View File
@@ -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
}
+355
View File
@@ -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)
}
+118
View File
@@ -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
}
+534
View File
@@ -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 = &currency.GiveGold
}
// 赠送果币
if currency.GiveFruitCoin > 0 {
creditPlan.FruitCoin = &currency.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)
}
}
}
+459
View File
@@ -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
}
+51
View File
@@ -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
}
+284
View File
@@ -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
}
+174
View File
@@ -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[:])
}
+109
View File
@@ -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)
}
+341
View File
@@ -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
}
+115
View File
@@ -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, &params); 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)
}
}
+274
View File
@@ -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
}
+84
View File
@@ -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)
}
}
+241
View File
@@ -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
}
+85
View File
@@ -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"`
}
+54
View File
@@ -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
}
+203
View File
@@ -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
}
+39
View File
@@ -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
}
+27
View File
@@ -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
}
+360
View File
@@ -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
}
+546
View File
@@ -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
}
+54
View File
@@ -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" // 服务状态关闭
)
+133
View File
@@ -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
View File
@@ -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
}
+63
View File
@@ -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
}
+80
View File
@@ -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
}
+312
View File
@@ -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
}
+89
View File
@@ -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"
}
+458
View File
@@ -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))
}
+234
View File
@@ -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
}
+279
View File
@@ -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
}
+140
View File
@@ -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
}
+102
View File
@@ -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"` // 创建时间
}
+206
View File
@@ -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
}
+212
View File
@@ -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
}
+47
View File
@@ -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
+159
View File
@@ -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)
}
+377
View File
@@ -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]
}
+279
View File
@@ -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
View File
@@ -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, &copy)
}
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)
}
})
}
}
+412
View File
@@ -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"])
}
}
+244
View File
@@ -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)
}
+38
View File
@@ -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)
}
}
+1302
View File
File diff suppressed because it is too large Load Diff
+64
View File
@@ -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())
}
}
}
+9
View File
@@ -0,0 +1,9 @@
package mediaser
type libraryType string
const (
Video = "video" // 动漫
Image = "image" // 漫画
AudioBook = "audiobook" // 有声小说
)
+204
View File
@@ -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
}
+861
View File
@@ -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
}
+144
View File
@@ -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
}
+127
View File
@@ -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
}
+120
View File
@@ -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