feat: 实现每日签到功能并优化战斗和道具逻辑
This commit is contained in:
20
modules/config/controller/admin/sign.go
Normal file
20
modules/config/controller/admin/sign.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
"blazing/modules/config/service"
|
||||
)
|
||||
|
||||
type SignController struct {
|
||||
*cool.Controller
|
||||
}
|
||||
|
||||
func init() {
|
||||
cool.RegisterController(&SignController{
|
||||
&cool.Controller{
|
||||
Prefix: "/admin/config/sign",
|
||||
Api: []string{"Add", "Delete", "Update", "Info", "List", "Page"},
|
||||
Service: service.NewSignInService(),
|
||||
},
|
||||
})
|
||||
}
|
||||
31
modules/config/model/sign.go
Normal file
31
modules/config/model/sign.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
)
|
||||
|
||||
const TableNameSignIn = "config_sign_in"
|
||||
|
||||
// SignIn 签到活动配置表。
|
||||
type SignIn struct {
|
||||
*cool.Model
|
||||
SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'签到活动ID'" json:"sign_in_id"`
|
||||
Status uint32 `gorm:"not null;default:0;comment:'签到状态(0-未启用 1-启用)'" json:"status"`
|
||||
RewardScript string `gorm:"type:varchar(2048);default:'';comment:'签到奖励配置(JSON)'" json:"reward_script"`
|
||||
}
|
||||
|
||||
func (*SignIn) TableName() string {
|
||||
return TableNameSignIn
|
||||
}
|
||||
|
||||
func (*SignIn) GroupName() string {
|
||||
return "default"
|
||||
}
|
||||
|
||||
func NewSignIn() *SignIn {
|
||||
return &SignIn{Model: cool.NewModel()}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cool.CreateTable(&SignIn{})
|
||||
}
|
||||
37
modules/config/service/sign.go
Normal file
37
modules/config/service/sign.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
"blazing/modules/config/model"
|
||||
)
|
||||
|
||||
type SignInService struct {
|
||||
*cool.Service
|
||||
}
|
||||
|
||||
func (s *SignInService) GetActive(signInID uint32) *model.SignIn {
|
||||
m := cool.DBM(s.Model)
|
||||
if signInID != 0 {
|
||||
m.Where("sign_in_id", signInID)
|
||||
}
|
||||
m.Where("status", 1)
|
||||
m.Order("sign_in_id", "asc")
|
||||
|
||||
var out *model.SignIn
|
||||
m.Scan(&out)
|
||||
return out
|
||||
}
|
||||
|
||||
func NewSignInService() *SignInService {
|
||||
return &SignInService{
|
||||
&cool.Service{
|
||||
Model: model.NewSignIn(),
|
||||
ListQueryOp: &cool.QueryOp{
|
||||
FieldEQ: []string{"sign_in_id", "status"},
|
||||
},
|
||||
PageQueryOp: &cool.QueryOp{
|
||||
FieldEQ: []string{"sign_in_id", "status"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
20
modules/player/controller/admin/sign.go
Normal file
20
modules/player/controller/admin/sign.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
"blazing/modules/player/service"
|
||||
)
|
||||
|
||||
type SignRecordController struct {
|
||||
*cool.Controller
|
||||
}
|
||||
|
||||
func init() {
|
||||
cool.RegisterController(&SignRecordController{
|
||||
&cool.Controller{
|
||||
Prefix: "/admin/game/signrecord",
|
||||
Api: []string{"Delete", "Update", "Info", "List", "Page"},
|
||||
Service: service.NewSignService(0),
|
||||
},
|
||||
})
|
||||
}
|
||||
106
modules/player/controller/app/sign.go
Normal file
106
modules/player/controller/app/sign.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
configservice "blazing/modules/config/service"
|
||||
playerservice "blazing/modules/player/service"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/deatil/go-cryptobin/cryptobin/crypto"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type SignController struct {
|
||||
*cool.Controller
|
||||
}
|
||||
|
||||
func init() {
|
||||
controller := &SignController{
|
||||
&cool.Controller{
|
||||
Prefix: "/seer/game/sign",
|
||||
Api: []string{},
|
||||
Service: configservice.NewSignInService(),
|
||||
},
|
||||
}
|
||||
cool.RegisterController(controller)
|
||||
}
|
||||
|
||||
type SignStateReq struct {
|
||||
g.Meta `path:"/state" method:"GET"`
|
||||
UserID uint32 `json:"user_id" v:"required|min:1#用户ID不能为空|用户ID非法"`
|
||||
Session string `json:"session" v:"required#session不能为空"`
|
||||
SignInID uint32 `json:"sign_in_id" d:"1" v:"min:1#签到活动ID非法"`
|
||||
}
|
||||
|
||||
type SignClaimReq struct {
|
||||
g.Meta `path:"/claim" method:"POST"`
|
||||
UserID uint32 `json:"user_id" v:"required|min:1#用户ID不能为空|用户ID非法"`
|
||||
Session string `json:"session" v:"required#session不能为空"`
|
||||
SignInID uint32 `json:"sign_in_id" d:"1" v:"min:1#签到活动ID非法"`
|
||||
}
|
||||
|
||||
func (c *SignController) State(ctx context.Context, req *SignStateReq) (res *cool.BaseRes, err error) {
|
||||
if err = g.Validator().Data(req).Run(ctx); err != nil {
|
||||
return cool.Fail(err.Error()), nil
|
||||
}
|
||||
if err = validateGameSession(req.UserID, req.Session); err != nil {
|
||||
return cool.Fail(err.Error()), nil
|
||||
}
|
||||
|
||||
state, err := playerservice.NewSignService(req.UserID).GetState(req.SignInID)
|
||||
if err != nil {
|
||||
return cool.Fail(err.Error()), nil
|
||||
}
|
||||
return cool.Ok(state), nil
|
||||
}
|
||||
|
||||
func (c *SignController) Claim(ctx context.Context, req *SignClaimReq) (res *cool.BaseRes, err error) {
|
||||
if err = g.Validator().Data(req).Run(ctx); err != nil {
|
||||
return cool.Fail(err.Error()), nil
|
||||
}
|
||||
if err = validateGameSession(req.UserID, req.Session); err != nil {
|
||||
return cool.Fail(err.Error()), nil
|
||||
}
|
||||
|
||||
result, err := playerservice.NewSignService(req.UserID).Claim(req.SignInID)
|
||||
if err != nil {
|
||||
return cool.Fail(err.Error()), nil
|
||||
}
|
||||
return cool.Ok(result), nil
|
||||
}
|
||||
|
||||
func validateGameSession(userID uint32, session string) error {
|
||||
if userID == 0 {
|
||||
return fmt.Errorf("user_id不能为空")
|
||||
}
|
||||
session = strings.TrimSpace(session)
|
||||
if session == "" {
|
||||
return fmt.Errorf("session不能为空")
|
||||
}
|
||||
|
||||
cached, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", userID))
|
||||
if err != nil || cached.IsEmpty() {
|
||||
return fmt.Errorf("session已过期,请重新登录")
|
||||
}
|
||||
|
||||
rawSession := session
|
||||
decrypted := crypto.
|
||||
FromBase64String(session).
|
||||
SetKey("gfertf12dfertf12").
|
||||
SetIv("gfertf12dfertf12").
|
||||
Aes().
|
||||
CBC().
|
||||
PKCS7Padding().
|
||||
Decrypt().
|
||||
ToString()
|
||||
if decrypted != "" {
|
||||
rawSession = decrypted
|
||||
}
|
||||
|
||||
if rawSession != cached.String() {
|
||||
return fmt.Errorf("session无效,请重新登录")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func (pet *PetInfo) RnadEffect() {
|
||||
func (pet *PetInfo) GetEffect(ptype int) (int, *PetEffectInfo, bool) {
|
||||
|
||||
return utils.FindWithIndex(pet.EffectInfo, func(item PetEffectInfo) bool {
|
||||
return item.Status == 1
|
||||
return int(item.Status) == ptype
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@ type SignInRecord struct {
|
||||
PlayerID uint32 `gorm:"not null;index:idx_player_id;comment:'玩家ID'" json:"player_id"`
|
||||
SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'关联的签到活动ID(对应player_sign_in表的SignInID)'" json:"sign_in_id"`
|
||||
|
||||
IsCompleted bool `gorm:"not null;default:false;comment:'签到是否完成(0-未完成 1-已完成)'" json:"is_completed"`
|
||||
//通过bitset来实现签到的进度记录
|
||||
SignInProgress []uint32 `gorm:"type:jsonb;not null;comment:'签到进度(状压实现,存储每日签到状态)'" json:"sign_in_progress"`
|
||||
IsCompleted bool `gorm:"not null;default:false;comment:'签到是否完成(0-未完成 1-已完成)'" json:"is_completed"`
|
||||
ContinuousDays uint32 `gorm:"not null;default:0;comment:'连续签到天数'" json:"continuous_days"`
|
||||
TotalDays uint32 `gorm:"not null;default:0;comment:'累计签到天数'" json:"total_days"`
|
||||
LastSignDate string `gorm:"type:varchar(10);not null;default:'';comment:'最近一次签到日期(YYYY-MM-DD)'" json:"last_sign_date"`
|
||||
// 通过 bitset 记录每日签到状态,位索引从 0 开始,对应签到第 1 天。
|
||||
SignInProgress []uint32 `gorm:"type:jsonb;not null;default:'[]';comment:'签到进度(状压实现,存储每日签到状态)'" json:"sign_in_progress"`
|
||||
}
|
||||
|
||||
// TableName 指定表名(遵循现有规范)
|
||||
|
||||
@@ -1,40 +1,14 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"blazing/cool"
|
||||
)
|
||||
import configmodel "blazing/modules/config/model"
|
||||
|
||||
// 表名常量(遵循现有命名规范:小写+下划线)
|
||||
const TableNameSignIn = "config_sign_in"
|
||||
// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。
|
||||
const TableNameSignIn = configmodel.TableNameSignIn
|
||||
|
||||
// SignIn 签到记录表
|
||||
// 核心字段:签到完成状态、状压签到进度、签到奖励脚本
|
||||
type SignIn struct {
|
||||
*cool.Model // 嵌入基础Model(包含主键、创建/更新时间等通用字段)
|
||||
SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'签到活动ID'" json:"sign_in_id"`
|
||||
Status uint32 `gorm:"not null;default:0;comment:'签到状态(0-未完成 1-已完成)'" json:"status"`
|
||||
//传入用户名,签到天数,给予奖励,这个搭配里程碑表实现
|
||||
RewardScript string `gorm:"type:varchar(512);default:'';comment:'签到奖励脚本(执行奖励发放的脚本内容)'" json:"reward_script"`
|
||||
}
|
||||
|
||||
// TableName 指定表名(遵循现有规范)
|
||||
func (*SignIn) TableName() string {
|
||||
return TableNameSignIn
|
||||
}
|
||||
|
||||
// GroupName 指定表分组(默认分组,与现有Item表/精灵特效表一致)
|
||||
func (*SignIn) GroupName() string {
|
||||
return "default"
|
||||
}
|
||||
|
||||
// NewSignIn 创建签到记录表实例(初始化基础Model)
|
||||
func NewSignIn() *SignIn {
|
||||
return &SignIn{
|
||||
Model: cool.NewModel(),
|
||||
}
|
||||
}
|
||||
|
||||
// init 程序启动时自动创建表(与现有PlayerPetSpecialEffect表的初始化逻辑一致)
|
||||
func init() {
|
||||
cool.CreateTable(&SignIn{})
|
||||
// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。
|
||||
type SignIn = configmodel.SignIn
|
||||
|
||||
// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。
|
||||
func NewSignIn() *configmodel.SignIn {
|
||||
return configmodel.NewSignIn()
|
||||
}
|
||||
|
||||
554
modules/player/service/sign.go
Normal file
554
modules/player/service/sign.go
Normal file
@@ -0,0 +1,554 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"blazing/common/data"
|
||||
"blazing/cool"
|
||||
baseservice "blazing/modules/base/service"
|
||||
configservice "blazing/modules/config/service"
|
||||
"blazing/modules/player/model"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pointernil/bitset32"
|
||||
)
|
||||
|
||||
// SignRewardItem 定义单日签到奖励中的物品条目。
|
||||
type SignRewardItem struct {
|
||||
ItemID uint32 `json:"item_id"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// SignRewardDay 定义单日签到奖励。
|
||||
type SignRewardDay struct {
|
||||
Day uint32 `json:"day"`
|
||||
GiftItemIDs []uint32 `json:"gift_item_ids,omitempty"`
|
||||
Items []SignRewardItem `json:"items,omitempty"`
|
||||
PetRewardIDs []uint32 `json:"pet_reward_ids,omitempty"`
|
||||
TitleRewardIDs []uint32 `json:"title_reward_ids,omitempty"`
|
||||
Coins int64 `json:"coins,omitempty"`
|
||||
Gold int64 `json:"gold,omitempty"`
|
||||
FreeGold int64 `json:"free_gold,omitempty"`
|
||||
ExpPool int64 `json:"exp_pool,omitempty"`
|
||||
EVPool int64 `json:"ev_pool,omitempty"`
|
||||
}
|
||||
|
||||
// SignRewardPreview 用于返回签到面板里的奖励和领取状态。
|
||||
type SignRewardPreview struct {
|
||||
SignRewardDay
|
||||
Signed bool `json:"signed"`
|
||||
}
|
||||
|
||||
// SignState 表示玩家当前活动的签到状态。
|
||||
type SignState struct {
|
||||
SignInID uint32 `json:"sign_in_id"`
|
||||
Status uint32 `json:"status"`
|
||||
TotalDays uint32 `json:"total_days"`
|
||||
ContinuousDays uint32 `json:"continuous_days"`
|
||||
LastSignDate string `json:"last_sign_date"`
|
||||
TodaySigned bool `json:"today_signed"`
|
||||
Completed bool `json:"completed"`
|
||||
CanSignToday bool `json:"can_sign_today"`
|
||||
NextSignDay uint32 `json:"next_sign_day"`
|
||||
SignedDays []uint32 `json:"signed_days"`
|
||||
Rewards []SignRewardPreview `json:"rewards"`
|
||||
}
|
||||
|
||||
// SignGrantResult 表示本次签到实际发放的奖励。
|
||||
type SignGrantResult struct {
|
||||
Items []data.ItemInfo `json:"items,omitempty"`
|
||||
PetIDs []uint32 `json:"pet_ids,omitempty"`
|
||||
TitleIDs []uint32 `json:"title_ids,omitempty"`
|
||||
Coins int64 `json:"coins,omitempty"`
|
||||
Gold int64 `json:"gold,omitempty"`
|
||||
FreeGold int64 `json:"free_gold,omitempty"`
|
||||
ExpPool int64 `json:"exp_pool,omitempty"`
|
||||
EVPool int64 `json:"ev_pool,omitempty"`
|
||||
RewardDay uint32 `json:"reward_day"`
|
||||
}
|
||||
|
||||
// SignClaimResult 表示签到后的完整结果。
|
||||
type SignClaimResult struct {
|
||||
State *SignState `json:"state"`
|
||||
Reward *SignGrantResult `json:"reward"`
|
||||
}
|
||||
|
||||
type signRewardPayload struct {
|
||||
Days []SignRewardDay `json:"days"`
|
||||
}
|
||||
|
||||
// SignService 管理玩家签到进度。
|
||||
type SignService struct {
|
||||
BaseService
|
||||
}
|
||||
|
||||
func NewSignService(id uint32) *SignService {
|
||||
return &SignService{
|
||||
BaseService: BaseService{
|
||||
userid: id,
|
||||
Service: &cool.Service{
|
||||
Model: model.NewSignInRecord(),
|
||||
ListQueryOp: &cool.QueryOp{
|
||||
FieldEQ: []string{"player_id", "sign_in_id", "is_completed"},
|
||||
},
|
||||
PageQueryOp: &cool.QueryOp{
|
||||
FieldEQ: []string{"player_id", "sign_in_id", "is_completed"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignService) GetState(signInID uint32) (*SignState, error) {
|
||||
cfg := configservice.NewSignInService().GetActive(signInID)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("签到活动不存在或未启用")
|
||||
}
|
||||
|
||||
rewards, err := parseRewardDays(cfg.RewardScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rewards) == 0 {
|
||||
return nil, fmt.Errorf("签到活动未配置奖励")
|
||||
}
|
||||
|
||||
record, err := s.getRecord(cfg.SignInID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buildSignState(cfg.SignInID, cfg.Status, rewards, record), nil
|
||||
}
|
||||
|
||||
func (s *SignService) Claim(signInID uint32) (*SignClaimResult, error) {
|
||||
cfg := configservice.NewSignInService().GetActive(signInID)
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("签到活动不存在或未启用")
|
||||
}
|
||||
|
||||
rewards, err := parseRewardDays(cfg.RewardScript)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rewards) == 0 {
|
||||
return nil, fmt.Errorf("签到活动未配置奖励")
|
||||
}
|
||||
|
||||
record, isNew, err := s.getOrInitRecord(cfg.SignInID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
today := currentDateString()
|
||||
if record.LastSignDate == today {
|
||||
return nil, fmt.Errorf("今天已经签到过了")
|
||||
}
|
||||
|
||||
nextDay, reward := nextRewardDay(rewards, record)
|
||||
if reward == nil || nextDay == 0 {
|
||||
return nil, fmt.Errorf("当前签到活动已全部完成")
|
||||
}
|
||||
|
||||
prevDate := record.LastSignDate
|
||||
progress := progressBitset(record.SignInProgress)
|
||||
progress.Set(uint(nextDay - 1))
|
||||
record.SignInProgress = progress.Bytes()
|
||||
record.TotalDays = uint32(progress.Count())
|
||||
record.LastSignDate = today
|
||||
if isYesterday(prevDate, today) {
|
||||
record.ContinuousDays++
|
||||
} else {
|
||||
record.ContinuousDays = 1
|
||||
}
|
||||
_, pendingReward := nextRewardDay(rewards, record)
|
||||
record.IsCompleted = pendingReward == nil
|
||||
|
||||
if err := s.saveRecord(record, isNew); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grant, err := s.applyReward(*reward)
|
||||
if err != nil {
|
||||
cool.Logger.Error(context.TODO(), "sign reward apply failed", s.userid, cfg.SignInID, nextDay, err)
|
||||
return nil, err
|
||||
}
|
||||
grant.RewardDay = nextDay
|
||||
|
||||
state := buildSignState(cfg.SignInID, cfg.Status, rewards, record)
|
||||
return &SignClaimResult{State: state, Reward: grant}, nil
|
||||
}
|
||||
|
||||
func (s *SignService) getRecord(signInID uint32) (*model.SignInRecord, error) {
|
||||
var out *model.SignInRecord
|
||||
if err := s.dbm(s.Model).Where("sign_in_id", signInID).Scan(&out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out != nil && out.SignInProgress == nil {
|
||||
out.SignInProgress = []uint32{}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *SignService) getOrInitRecord(signInID uint32) (*model.SignInRecord, bool, error) {
|
||||
record, err := s.getRecord(signInID)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if record != nil {
|
||||
return record, false, nil
|
||||
}
|
||||
|
||||
return &model.SignInRecord{
|
||||
Base: model.Base{
|
||||
Model: cool.NewModel(),
|
||||
IsVip: cool.Config.ServerInfo.IsVip,
|
||||
},
|
||||
PlayerID: s.userid,
|
||||
SignInID: signInID,
|
||||
IsCompleted: false,
|
||||
ContinuousDays: 0,
|
||||
TotalDays: 0,
|
||||
LastSignDate: "",
|
||||
SignInProgress: []uint32{},
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (s *SignService) saveRecord(record *model.SignInRecord, isNew bool) error {
|
||||
if record == nil {
|
||||
return errors.New("签到记录为空")
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"player_id": record.PlayerID,
|
||||
"sign_in_id": record.SignInID,
|
||||
"is_completed": record.IsCompleted,
|
||||
"continuous_days": record.ContinuousDays,
|
||||
"total_days": record.TotalDays,
|
||||
"last_sign_date": record.LastSignDate,
|
||||
"sign_in_progress": record.SignInProgress,
|
||||
"is_vip": cool.Config.ServerInfo.IsVip,
|
||||
}
|
||||
|
||||
if isNew {
|
||||
_, err := cool.DBM(s.Model).Data(data).Insert()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := s.dbm(s.Model).Where("sign_in_id", record.SignInID).Data(data).Update()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SignService) applyReward(reward SignRewardDay) (*SignGrantResult, error) {
|
||||
result := &SignGrantResult{}
|
||||
infoService := NewInfoService(s.userid)
|
||||
playerInfo := infoService.GetLogin()
|
||||
if playerInfo == nil {
|
||||
return nil, fmt.Errorf("玩家角色不存在")
|
||||
}
|
||||
|
||||
var (
|
||||
needSaveInfo bool
|
||||
bagItems []data.ItemInfo
|
||||
)
|
||||
|
||||
appendRewardItem := func(itemID uint32, count int64) {
|
||||
if itemID == 0 || count <= 0 {
|
||||
return
|
||||
}
|
||||
switch itemID {
|
||||
case 1:
|
||||
result.Coins += count
|
||||
case 3:
|
||||
result.ExpPool += count
|
||||
case 5:
|
||||
result.Gold += count
|
||||
case 9:
|
||||
result.EVPool += count
|
||||
default:
|
||||
bagItems = append(bagItems, data.ItemInfo{ItemId: int64(itemID), ItemCnt: count})
|
||||
}
|
||||
}
|
||||
|
||||
for _, giftID := range reward.GiftItemIDs {
|
||||
gift := configservice.NewItemService().GetItemCount(giftID)
|
||||
appendRewardItem(uint32(gift.ItemId), gift.ItemCnt)
|
||||
}
|
||||
for _, item := range reward.Items {
|
||||
appendRewardItem(item.ItemID, item.Count)
|
||||
}
|
||||
|
||||
result.Coins += reward.Coins
|
||||
result.Gold += reward.Gold
|
||||
result.FreeGold += reward.FreeGold
|
||||
result.ExpPool += reward.ExpPool
|
||||
result.EVPool += reward.EVPool
|
||||
|
||||
if result.Coins != 0 {
|
||||
playerInfo.Coins += result.Coins
|
||||
needSaveInfo = true
|
||||
}
|
||||
if result.ExpPool != 0 {
|
||||
playerInfo.ExpPool += result.ExpPool
|
||||
needSaveInfo = true
|
||||
}
|
||||
if result.EVPool != 0 {
|
||||
playerInfo.EVPool += result.EVPool
|
||||
needSaveInfo = true
|
||||
}
|
||||
if needSaveInfo {
|
||||
infoService.Save(*playerInfo)
|
||||
}
|
||||
|
||||
if result.Gold != 0 {
|
||||
baseservice.NewBaseSysUserService().UpdateGold(s.userid, result.Gold*100)
|
||||
}
|
||||
if result.FreeGold != 0 {
|
||||
baseservice.NewBaseSysUserService().UpdateFreeGold(s.userid, result.FreeGold*100)
|
||||
}
|
||||
|
||||
if len(bagItems) > 0 {
|
||||
items, err := NewItemService(s.userid).AddItems(bagItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Items = items
|
||||
}
|
||||
|
||||
for _, petRewardID := range reward.PetRewardIDs {
|
||||
cfg := configservice.NewPetRewardService().Get(petRewardID)
|
||||
if cfg == nil || cfg.MonID == 0 {
|
||||
continue
|
||||
}
|
||||
petInfo := model.GenPetInfo(int(cfg.MonID), int(cfg.DV), int(cfg.Nature), int(cfg.Effect), int(cfg.Lv), nil, 0)
|
||||
if _, err := NewPetService(s.userid).PetAdd(petInfo, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.PetIDs = append(result.PetIDs, uint32(cfg.MonID))
|
||||
}
|
||||
|
||||
for _, titleID := range reward.TitleRewardIDs {
|
||||
if titleID == 0 {
|
||||
continue
|
||||
}
|
||||
NewTitleService(s.userid).Give(titleID)
|
||||
result.TitleIDs = append(result.TitleIDs, titleID)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildSignState(signInID, status uint32, rewards []SignRewardDay, record *model.SignInRecord) *SignState {
|
||||
today := currentDateString()
|
||||
progress := progressBitset(nil)
|
||||
state := &SignState{
|
||||
SignInID: signInID,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if record != nil {
|
||||
progress = progressBitset(record.SignInProgress)
|
||||
state.ContinuousDays = record.ContinuousDays
|
||||
state.LastSignDate = record.LastSignDate
|
||||
state.TodaySigned = record.LastSignDate == today
|
||||
state.Completed = record.IsCompleted
|
||||
}
|
||||
|
||||
state.SignedDays = signedDays(progress)
|
||||
state.TotalDays = uint32(len(state.SignedDays))
|
||||
if record != nil && record.TotalDays > state.TotalDays {
|
||||
state.TotalDays = record.TotalDays
|
||||
}
|
||||
|
||||
state.Rewards = make([]SignRewardPreview, 0, len(rewards))
|
||||
for _, reward := range rewards {
|
||||
state.Rewards = append(state.Rewards, SignRewardPreview{
|
||||
SignRewardDay: reward,
|
||||
Signed: progress.Test(uint(reward.Day - 1)),
|
||||
})
|
||||
}
|
||||
|
||||
nextDay, _ := nextRewardDay(rewards, record)
|
||||
state.NextSignDay = nextDay
|
||||
if len(rewards) > 0 && nextDay == 0 {
|
||||
state.Completed = true
|
||||
}
|
||||
state.CanSignToday = !state.TodaySigned && !state.Completed && nextDay != 0
|
||||
return state
|
||||
}
|
||||
|
||||
func nextRewardDay(rewards []SignRewardDay, record *model.SignInRecord) (uint32, *SignRewardDay) {
|
||||
progress := progressBitset(nil)
|
||||
if record != nil {
|
||||
progress = progressBitset(record.SignInProgress)
|
||||
}
|
||||
for i := range rewards {
|
||||
if rewards[i].Day == 0 {
|
||||
continue
|
||||
}
|
||||
if !progress.Test(uint(rewards[i].Day - 1)) {
|
||||
return rewards[i].Day, &rewards[i]
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func parseRewardDays(raw string) ([]SignRewardDay, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var payload signRewardPayload
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err == nil && len(payload.Days) > 0 {
|
||||
return normalizeRewardDays(payload.Days), nil
|
||||
}
|
||||
|
||||
var list []SignRewardDay
|
||||
if err := json.Unmarshal([]byte(raw), &list); err == nil && len(list) > 0 {
|
||||
return normalizeRewardDays(list), nil
|
||||
}
|
||||
|
||||
var dayMap map[string]SignRewardDay
|
||||
if err := json.Unmarshal([]byte(raw), &dayMap); err == nil && len(dayMap) > 0 {
|
||||
list = make([]SignRewardDay, 0, len(dayMap))
|
||||
for key, reward := range dayMap {
|
||||
if reward.Day == 0 {
|
||||
day, convErr := strconv.ParseUint(key, 10, 32)
|
||||
if convErr != nil {
|
||||
return nil, fmt.Errorf("签到奖励配置中的 day 非法: %s", key)
|
||||
}
|
||||
reward.Day = uint32(day)
|
||||
}
|
||||
list = append(list, reward)
|
||||
}
|
||||
return normalizeRewardDays(list), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("签到奖励配置格式不支持")
|
||||
}
|
||||
|
||||
func normalizeRewardDays(input []SignRewardDay) []SignRewardDay {
|
||||
merged := make(map[uint32]*SignRewardDay)
|
||||
for _, reward := range input {
|
||||
if reward.Day == 0 {
|
||||
continue
|
||||
}
|
||||
current, ok := merged[reward.Day]
|
||||
if !ok {
|
||||
copyReward := reward
|
||||
copyReward.GiftItemIDs = append([]uint32{}, reward.GiftItemIDs...)
|
||||
copyReward.Items = append([]SignRewardItem{}, reward.Items...)
|
||||
copyReward.PetRewardIDs = append([]uint32{}, reward.PetRewardIDs...)
|
||||
copyReward.TitleRewardIDs = append([]uint32{}, reward.TitleRewardIDs...)
|
||||
merged[reward.Day] = ©Reward
|
||||
continue
|
||||
}
|
||||
current.GiftItemIDs = append(current.GiftItemIDs, reward.GiftItemIDs...)
|
||||
current.Items = append(current.Items, reward.Items...)
|
||||
current.PetRewardIDs = append(current.PetRewardIDs, reward.PetRewardIDs...)
|
||||
current.TitleRewardIDs = append(current.TitleRewardIDs, reward.TitleRewardIDs...)
|
||||
current.Coins += reward.Coins
|
||||
current.Gold += reward.Gold
|
||||
current.FreeGold += reward.FreeGold
|
||||
current.ExpPool += reward.ExpPool
|
||||
current.EVPool += reward.EVPool
|
||||
}
|
||||
|
||||
days := make([]uint32, 0, len(merged))
|
||||
for day := range merged {
|
||||
days = append(days, day)
|
||||
}
|
||||
sort.Slice(days, func(i, j int) bool { return days[i] < days[j] })
|
||||
|
||||
result := make([]SignRewardDay, 0, len(days))
|
||||
for _, day := range days {
|
||||
reward := merged[day]
|
||||
reward.GiftItemIDs = uniqueUint32(reward.GiftItemIDs)
|
||||
reward.PetRewardIDs = uniqueUint32(reward.PetRewardIDs)
|
||||
reward.TitleRewardIDs = uniqueUint32(reward.TitleRewardIDs)
|
||||
reward.Items = normalizeRewardItems(reward.Items)
|
||||
result = append(result, *reward)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeRewardItems(items []SignRewardItem) []SignRewardItem {
|
||||
merged := make(map[uint32]int64)
|
||||
for _, item := range items {
|
||||
if item.ItemID == 0 || item.Count <= 0 {
|
||||
continue
|
||||
}
|
||||
merged[item.ItemID] += item.Count
|
||||
}
|
||||
ids := make([]uint32, 0, len(merged))
|
||||
for itemID := range merged {
|
||||
ids = append(ids, itemID)
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
|
||||
result := make([]SignRewardItem, 0, len(ids))
|
||||
for _, itemID := range ids {
|
||||
result = append(result, SignRewardItem{ItemID: itemID, Count: merged[itemID]})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func uniqueUint32(values []uint32) []uint32 {
|
||||
seen := make(map[uint32]struct{}, len(values))
|
||||
result := make([]uint32, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
result = append(result, value)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||
return result
|
||||
}
|
||||
|
||||
func progressBitset(progress []uint32) *bitset32.BitSet32 {
|
||||
if len(progress) == 0 {
|
||||
return bitset32.New(0)
|
||||
}
|
||||
return bitset32.From(progress)
|
||||
}
|
||||
|
||||
func signedDays(progress *bitset32.BitSet32) []uint32 {
|
||||
if progress == nil {
|
||||
return []uint32{}
|
||||
}
|
||||
result := make([]uint32, 0, progress.Count())
|
||||
for idx, ok := progress.NextSet(0); ok; idx, ok = progress.NextSet(idx + 1) {
|
||||
result = append(result, uint32(idx+1))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func currentDateString() string {
|
||||
return time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
func isYesterday(previousDate, currentDate string) bool {
|
||||
if previousDate == "" || currentDate == "" {
|
||||
return false
|
||||
}
|
||||
prev, err := time.ParseInLocation("2006-01-02", previousDate, time.Local)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
curr, err := time.ParseInLocation("2006-01-02", currentDate, time.Local)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return prev.Add(24 * time.Hour).Equal(curr)
|
||||
}
|
||||
60
modules/player/service/sign_test.go
Normal file
60
modules/player/service/sign_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"blazing/modules/player/model"
|
||||
"testing"
|
||||
|
||||
"github.com/pointernil/bitset32"
|
||||
)
|
||||
|
||||
func TestParseRewardDays(t *testing.T) {
|
||||
rewards, err := parseRewardDays(`{"days":[{"day":2,"coins":10,"items":[{"item_id":1001,"count":1}],"gift_item_ids":[8]},{"day":1,"gold":2},{"day":2,"coins":5,"items":[{"item_id":1001,"count":2}]}]}`)
|
||||
if err != nil {
|
||||
t.Fatalf("parseRewardDays returned error: %v", err)
|
||||
}
|
||||
if len(rewards) != 2 {
|
||||
t.Fatalf("expected 2 reward days, got %d", len(rewards))
|
||||
}
|
||||
if rewards[0].Day != 1 || rewards[0].Gold != 2 {
|
||||
t.Fatalf("unexpected first reward: %+v", rewards[0])
|
||||
}
|
||||
if rewards[1].Day != 2 {
|
||||
t.Fatalf("unexpected second reward day: %+v", rewards[1])
|
||||
}
|
||||
if rewards[1].Coins != 15 {
|
||||
t.Fatalf("expected merged coins to be 15, got %d", rewards[1].Coins)
|
||||
}
|
||||
if len(rewards[1].Items) != 1 || rewards[1].Items[0].Count != 3 {
|
||||
t.Fatalf("expected merged item count to be 3, got %+v", rewards[1].Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRewardDaysMapFormat(t *testing.T) {
|
||||
rewards, err := parseRewardDays(`{"1":{"coins":1},"3":{"ev_pool":9}}`)
|
||||
if err != nil {
|
||||
t.Fatalf("parseRewardDays returned error: %v", err)
|
||||
}
|
||||
if len(rewards) != 2 {
|
||||
t.Fatalf("expected 2 reward days, got %d", len(rewards))
|
||||
}
|
||||
if rewards[0].Day != 1 || rewards[0].Coins != 1 {
|
||||
t.Fatalf("unexpected day 1 reward: %+v", rewards[0])
|
||||
}
|
||||
if rewards[1].Day != 3 || rewards[1].EVPool != 9 {
|
||||
t.Fatalf("unexpected day 3 reward: %+v", rewards[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextRewardDay(t *testing.T) {
|
||||
rewards := []SignRewardDay{{Day: 1}, {Day: 2}, {Day: 3}}
|
||||
progress := bitset32.New(0).Set(0).Set(1)
|
||||
record := &model.SignInRecord{SignInProgress: progress.Bytes()}
|
||||
|
||||
day, reward := nextRewardDay(rewards, record)
|
||||
if reward == nil {
|
||||
t.Fatal("expected next reward, got nil")
|
||||
}
|
||||
if day != 3 || reward.Day != 3 {
|
||||
t.Fatalf("expected next day to be 3, got day=%d reward=%+v", day, reward)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user