Files
huangguo_server/app/service/checkinser/checkin.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

645 lines
18 KiB
Go

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
}