This commit is contained in:
昔念
2026-04-07 17:26:52 +08:00
91 changed files with 3417 additions and 811 deletions

View 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(),
},
})
}

View File

@@ -2,6 +2,10 @@ package model
import (
"blazing/cool"
"fmt"
"strings"
"github.com/dop251/goja"
)
const (
@@ -24,25 +28,143 @@ type BossConfig struct {
Rule []uint32 `gorm:"type:jsonb; ;comment:'战胜规则'" json:"rule"`
}
// TableName 指定BossConfig对应的数据库表名
func (*BossConfig) TableName() string {
return TableNameBossConfig
// BossHookSkillContext 为脚本暴露当前精灵技能可用信息。
type BossHookSkillContext struct {
SkillID uint32 `json:"skill_id"`
PP uint32 `json:"pp"`
CanUse bool `json:"can_use"`
}
// GroupName 指定表所属的分组(保持和怪物刷新表一致)
func (*BossConfig) GroupName() string {
return "default"
// BossHookPetContext 为脚本暴露战斗中双方精灵简要信息。
type BossHookPetContext struct {
PetID uint32 `json:"pet_id"`
CatchTime uint32 `json:"catch_time"`
Hp uint32 `json:"hp"`
MaxHp uint32 `json:"max_hp"`
}
// NewBossConfig 创建一个新的BossConfig实例初始化通用Model字段+所有默认值)
func NewBossConfig() *BossConfig {
return &BossConfig{
Model: cool.NewModel(),
}
// BossHookAttackContext 参考 AttackValue为脚本暴露关键战斗面板/结果字段。
type BossHookAttackContext struct {
SkillID uint32 `json:"skill_id"`
AttackTime uint32 `json:"attack_time"`
IsCritical uint32 `json:"is_critical"`
LostHp uint32 `json:"lost_hp"`
GainHp int32 `json:"gain_hp"`
RemainHp int32 `json:"remain_hp"`
MaxHp uint32 `json:"max_hp"`
State uint32 `json:"state"`
Offensive float32 `json:"offensive"`
Status []int8 `json:"status"`
Prop []int8 `json:"prop"`
}
// init 程序启动时自动创建/同步boss_config表结构
// BossHookActionContext 为 boss 脚本提供可读写的出手上下文。
type BossHookActionContext struct {
HookAction bool `json:"hookaction"` // effect 链原始 HookAction 判定
Round uint32 `json:"round"` // 当前回合数
IsFirst bool `json:"is_first"` // 是否先手
Our *BossHookPetContext `json:"our"` // 我方当前精灵
Opp *BossHookPetContext `json:"opp"` // 对方当前精灵
Skills []BossHookSkillContext `json:"skills"` // 我方技能
OurAttack *BossHookAttackContext `json:"our_attack"` // 我方AttackValue快照
OppAttack *BossHookAttackContext `json:"opp_attack"` // 对方AttackValue快照
Action string `json:"action"` // auto/skill/switch
SkillID uint32 `json:"skill_id"` // action=skill
CatchTime uint32 `json:"catch_time"` // action=switch
UseSkillFn func(skillID uint32) `json:"-"`
SwitchPetFn func(catchTime uint32) `json:"-"`
}
func (*BossConfig) TableName() string { return TableNameBossConfig }
func (*BossConfig) GroupName() string { return "default" }
func NewBossConfig() *BossConfig { return &BossConfig{Model: cool.NewModel()} }
func init() {
cool.CreateTable(&BossConfig{})
}
// RunHookActionScript 执行 BOSS 脚本 hookAction。
func (b *BossConfig) RunHookActionScript(hookAction any) (bool, error) {
if b == nil || strings.TrimSpace(b.Script) == "" {
return true, nil
}
program, err := goja.Compile("boss_hook_action.js", b.Script, false)
if err != nil {
return false, fmt.Errorf("compile boss script: %w", err)
}
vm := goja.New()
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
bindBossScriptFunctions(vm, hookAction)
if _, err = vm.RunProgram(program); err != nil {
return false, fmt.Errorf("run boss script: %w", err)
}
var (
callable goja.Callable
ok bool
)
for _, fnName := range []string{"hookAction", "HookAction", "hookaction"} {
callable, ok = goja.AssertFunction(vm.Get(fnName))
if ok {
break
}
}
if !ok {
return false, fmt.Errorf("boss script function not found: hookAction")
}
result, err := callable(goja.Undefined(), vm.ToValue(hookAction))
if err != nil {
return false, fmt.Errorf("execute boss hookAction: %w", err)
}
if goja.IsUndefined(result) || goja.IsNull(result) {
return defaultHookActionResult(hookAction), nil
}
return result.ToBoolean(), nil
}
func bindBossScriptFunctions(vm *goja.Runtime, hookAction any) {
ctx, ok := hookAction.(*BossHookActionContext)
if !ok || ctx == nil {
return
}
_ = vm.Set("useSkill", func(call goja.FunctionCall) goja.Value {
if ctx.UseSkillFn == nil || len(call.Arguments) == 0 {
return goja.Undefined()
}
skillID := call.Arguments[0].ToInteger()
if skillID < 0 {
return goja.Undefined()
}
ctx.UseSkillFn(uint32(skillID))
return goja.Undefined()
})
_ = vm.Set("switchPet", func(call goja.FunctionCall) goja.Value {
if ctx.SwitchPetFn == nil || len(call.Arguments) == 0 {
return goja.Undefined()
}
catchTime := call.Arguments[0].ToInteger()
if catchTime < 0 {
return goja.Undefined()
}
ctx.SwitchPetFn(uint32(catchTime))
return goja.Undefined()
})
}
func defaultHookActionResult(hookAction any) bool {
if ctx, ok := hookAction.(*BossHookActionContext); ok {
return ctx.HookAction
}
if val, ok := hookAction.(bool); ok {
return val
}
return true
}

View File

@@ -0,0 +1,87 @@
package model
import "testing"
func TestBossConfigRunHookActionScript(t *testing.T) {
boss := &BossConfig{
Script: `
function hookAction(hookaction) {
return hookaction.hookaction === true;
}
`,
}
ctx := &BossHookActionContext{HookAction: true}
ok, err := boss.RunHookActionScript(ctx)
if err != nil {
t.Fatalf("RunHookActionScript returned error: %v", err)
}
if !ok {
t.Fatalf("RunHookActionScript = false, want true")
}
}
func TestBossConfigRunHookActionScriptCallUseSkillFn(t *testing.T) {
boss := &BossConfig{
Script: `
function hookAction(hookaction) {
if (hookaction.round >= 2) {
useSkill(5001);
}
return true;
}
`,
}
ctx := &BossHookActionContext{
HookAction: true,
Round: 2,
Action: "auto",
}
ctx.UseSkillFn = func(skillID uint32) {
ctx.Action = "skill"
ctx.SkillID = skillID
}
ok, err := boss.RunHookActionScript(ctx)
if err != nil {
t.Fatalf("RunHookActionScript returned error: %v", err)
}
if !ok {
t.Fatalf("RunHookActionScript = false, want true")
}
if ctx.Action != "skill" || ctx.SkillID != 5001 {
t.Fatalf("useSkill not applied, got action=%q skill_id=%d", ctx.Action, ctx.SkillID)
}
}
func TestBossConfigRunHookActionScriptCallSwitchPetFn(t *testing.T) {
boss := &BossConfig{
Script: `
function hookAction(hookaction) {
switchPet(3);
return true;
}
`,
}
ctx := &BossHookActionContext{
HookAction: true,
Action: "auto",
}
ctx.SwitchPetFn = func(catchTime uint32) {
ctx.Action = "switch"
ctx.CatchTime = catchTime
}
ok, err := boss.RunHookActionScript(ctx)
if err != nil {
t.Fatalf("RunHookActionScript returned error: %v", err)
}
if !ok {
t.Fatalf("RunHookActionScript = false, want true")
}
if ctx.Action != "switch" || ctx.CatchTime != 3 {
t.Fatalf("switchPet not applied, got action=%q catch_time=%d", ctx.Action, ctx.CatchTime)
}
}

View File

@@ -0,0 +1,34 @@
package model
import "blazing/cool"
const TableNameSignIn = "config_sign_in"
const (
SignTypeTotal uint32 = 1
SignTypeContinuous uint32 = 2
)
// SignIn 签到阶段配置表。
type SignIn struct {
*BaseConfig
SignType uint32 `gorm:"not null;default:1;uniqueIndex:idx_sign_type_stage;comment:'签到类别1-累计 2-连续)'" json:"sign_type"`
StageDays uint32 `gorm:"not null;default:1;uniqueIndex:idx_sign_type_stage;comment:'签到阶段天数0/1/3/7/14/30'" json:"stage_days"`
CdkID uint32 `gorm:"not null;uniqueIndex;comment:'绑定的CDK配置ID'" json:"cdk_id"`
}
func (*SignIn) TableName() string {
return TableNameSignIn
}
func (*SignIn) GroupName() string {
return "default"
}
func NewSignIn() *SignIn {
return &SignIn{BaseConfig: NewBaseConfig()}
}
func init() {
cool.CreateTable(&SignIn{})
}

View File

@@ -8,14 +8,13 @@ import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/grand"
"github.com/google/uuid"
) // 1. 扩展字符集:数字+大小写字母+安全符号避开URL/输入易冲突的符号,如/、?、&
)
const charsetWithSymbol = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"
func Generate16CharSecure() string {
result := make([]byte, 16)
for i := 0; i < 16; i++ {
result[i] = charsetWithSymbol[grand.N(0, len(charsetWithSymbol)-1)]
}
return string(result)
@@ -38,22 +37,30 @@ func NewCdkService() *CdkService {
},
}
}
func (s *CdkService) Get(id string) *model.CDKConfig {
var item *model.CDKConfig
dbm_notenable(s.Model).Where("cdk_code", id).WhereNot("exchange_remain_count", 0).Scan(&item)
return item
}
func (s *CdkService) GetByID(id uint32) *model.CDKConfig {
if id == 0 {
return nil
}
var item *model.CDKConfig
dbm_notenable(s.Model).Where("id", id).Scan(&item)
return item
}
func (s *CdkService) All() []model.CDKConfig {
var item []model.CDKConfig
dbm_notenable(s.Model).WhereLT("exchange_remain_count", 0).Scan(&item)
return item
}
func (s *CdkService) Set(id string) bool {
func (s *CdkService) Set(id string) bool {
res, err := cool.DBM(s.Model).Where("cdk_code", id).WhereNot("exchange_remain_count", 0).Decrement("exchange_remain_count", 1)
if err != nil {
return false
@@ -62,7 +69,5 @@ func (s *CdkService) Set(id string) bool {
if rows == 0 {
return false
}
return true
}

View File

@@ -0,0 +1,80 @@
package service
import (
"blazing/cool"
"blazing/modules/config/model"
"context"
"fmt"
"sort"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
var signStageDays = map[uint32]struct{}{
0: {},
1: {},
3: {},
7: {},
14: {},
30: {},
}
type SignInService struct {
*cool.Service
}
func NewSignInService() *SignInService {
return &SignInService{
&cool.Service{
Model: model.NewSignIn(),
PageQueryOp: &cool.QueryOp{
FieldEQ: []string{"sign_type", "stage_days", "cdk_id", "is_enable"},
KeyWordField: []string{"remark"},
},
ListQueryOp: &cool.QueryOp{
FieldEQ: []string{"sign_type", "stage_days", "cdk_id", "is_enable"},
},
},
}
}
func (s *SignInService) ModifyBefore(ctx context.Context, method string, param g.MapStrAny) (err error) {
if method == "Delete" {
return nil
}
signType := gconv.Uint32(param["sign_type"])
if signType != model.SignTypeTotal && signType != model.SignTypeContinuous {
return fmt.Errorf("签到类别非法只支持1(累计)或2(连续)")
}
stageDays := gconv.Uint32(param["stage_days"])
if _, ok := signStageDays[stageDays]; !ok {
return fmt.Errorf("签到阶段仅支持0、1、3、7、14、30天")
}
cdkID := gconv.Uint32(param["cdk_id"])
if cdkID == 0 {
return fmt.Errorf("cdk_id不能为空")
}
if NewCdkService().GetByID(cdkID) == nil {
return fmt.Errorf("绑定的CDK不存在")
}
return nil
}
func (s *SignInService) GetEnabled() []model.SignIn {
var items []model.SignIn
dbm_enable(s.Model).Scan(&items)
sort.Slice(items, func(i, j int) bool {
if items[i].SignType != items[j].SignType {
return items[i].SignType < items[j].SignType
}
if items[i].StageDays != items[j].StageDays {
return items[i].StageDays < items[j].StageDays
}
return items[i].CdkID < items[j].CdkID
})
return items
}

View File

@@ -0,0 +1,36 @@
package admin
import (
"blazing/cool"
"blazing/modules/player/service"
"context"
"github.com/gogf/gf/v2/frame/g"
)
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),
},
})
}
type ResetAllReq struct {
g.Meta `path:"/resetAll" method:"POST"`
Authorization string `json:"Authorization" in:"header"`
}
func (c *SignRecordController) ResetAll(ctx context.Context, req *ResetAllReq) (res *cool.BaseRes, err error) {
result, err := service.NewSignService(0).ResetAll()
if err != nil {
return cool.Fail(err.Error()), nil
}
return cool.Ok(result), nil
}

View File

@@ -0,0 +1,104 @@
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不能为空"`
}
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不能为空"`
}
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()
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()
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
}

View File

@@ -383,16 +383,119 @@ func (pet *PetInfo) RnadEffect() {
// 7 :繁殖加成
// 8 :体力提升加成
const (
maxHPUpEffectIdx uint16 = 60000
maxHPUpEffectStatus byte = 8
maxHPUpEffectEID uint16 = 26
maxHPUpEffectCap = 20
trainingEffectStatus byte = 5
trainingAttrEffectIdx uint16 = 60001
trainingPowerEffectIdx uint16 = 60002
trainingAttrEffectEID uint16 = 247
trainingPowerEffectEID uint16 = 239
)
// 繁殖加成,体力提升加成 ,这里是防止和其他重复所以定义不同类别,但是实际上,能量珠那些事调用不同id的effect实现
// <!-- Stat: 精灵特效Stat: 0: 无效(默认值), 1: 永久, 2: 有`有效次数'的特效 3: 爆发特效 4: 异能精灵特质5特训6魂印-->
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
})
}
func (pet *PetInfo) getEffectByStatusAndEID(status byte, eid uint16) (int, *PetEffectInfo, bool) {
return utils.FindWithIndex(pet.EffectInfo, func(item PetEffectInfo) bool {
return item.Status == status && item.EID == eid
})
}
func ensureEffectArgsLen(args []int, size int) []int {
if len(args) >= size {
return args
}
next := make([]int, size)
copy(next, args)
return next
}
func (pet *PetInfo) addTrainingEffectDelta(idx uint16, eid uint16, argsLen int, argIndex int, value int) bool {
if pet == nil || value <= 0 || argIndex < 0 || argIndex >= argsLen {
return false
}
if _, eff, ok := pet.getEffectByStatusAndEID(trainingEffectStatus, eid); ok {
if eff.Idx == 0 {
eff.Idx = idx
}
eff.Status = trainingEffectStatus
eff.EID = eid
eff.Args = ensureEffectArgsLen(eff.Args, argsLen)
eff.Args[argIndex] += value
return true
}
args := make([]int, argsLen)
args[argIndex] = value
pet.EffectInfo = append(pet.EffectInfo, PetEffectInfo{
Idx: idx,
Status: trainingEffectStatus,
EID: eid,
Args: args,
})
return true
}
func (pet *PetInfo) AddTrainingAttrBonus(attr int, value int) bool {
return pet.addTrainingEffectDelta(trainingAttrEffectIdx, trainingAttrEffectEID, 6, attr, value)
}
func (pet *PetInfo) AddTrainingPowerBonus(value int) bool {
return pet.addTrainingEffectDelta(trainingPowerEffectIdx, trainingPowerEffectEID, 2, 0, value)
}
func (pet *PetInfo) AddMaxHPUpEffect(itemID uint32, value int) bool {
if pet == nil || value <= 0 {
return false
}
if _, eff, ok := pet.GetEffect(int(maxHPUpEffectStatus)); ok {
current := 0
if len(eff.Args) >= 2 && eff.Args[0] == 0 && eff.Args[1] > 0 {
current = eff.Args[1]
}
if current >= maxHPUpEffectCap {
return false
}
next := current + value
if next > maxHPUpEffectCap {
next = maxHPUpEffectCap
}
eff.ItemID = itemID
eff.Idx = maxHPUpEffectIdx
eff.Status = maxHPUpEffectStatus
eff.EID = maxHPUpEffectEID
eff.Args = []int{0, next}
return next > current
}
if value > maxHPUpEffectCap {
value = maxHPUpEffectCap
}
pet.EffectInfo = append(pet.EffectInfo, PetEffectInfo{
ItemID: itemID,
Idx: maxHPUpEffectIdx,
Status: maxHPUpEffectStatus,
EID: maxHPUpEffectEID,
Args: []int{0, value},
})
return true
}
func (pet *PetInfo) Downgrade(level uint32) {
for pet.Level > uint32(level) {

View File

@@ -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 指定表名(遵循现有规范)

View File

@@ -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()
}

View File

@@ -0,0 +1,123 @@
package service
import (
"blazing/common/data"
baseservice "blazing/modules/base/service"
configservice "blazing/modules/config/service"
"blazing/modules/player/model"
"fmt"
"time"
)
type CdkRewardPet struct {
PetID uint32 `json:"pet_id"`
CatchTime uint32 `json:"catch_time"`
}
type CdkRewardResult struct {
CdkID uint32 `json:"cdk_id"`
Items []data.ItemInfo `json:"items,omitempty"`
Pets []CdkRewardPet `json:"pets,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"`
}
// GrantConfigReward 按 cdk 配置 ID 发放奖励,不处理兑换码次数和领取资格校验。
func (s *CdkService) GrantConfigReward(cdkID uint32) (*CdkRewardResult, error) {
cfg := configservice.NewCdkService().GetByID(cdkID)
if cfg == nil {
return nil, fmt.Errorf("绑定的CDK不存在")
}
if cfg.BindUserId != 0 && cfg.BindUserId != s.userid {
return nil, fmt.Errorf("CDK已绑定其他用户")
}
if !cfg.ValidEndTime.IsZero() && cfg.ValidEndTime.Before(time.Now()) {
return nil, fmt.Errorf("绑定的CDK已过期")
}
result := &CdkRewardResult{CdkID: cdkID}
infoService := NewInfoService(s.userid)
playerInfo := infoService.GetLogin()
if playerInfo == nil {
return nil, fmt.Errorf("玩家角色不存在")
}
var (
infoDirty bool
bagItems []data.ItemInfo
)
appendRewardItem := func(itemID uint32, count int64) {
if itemID == 0 || count <= 0 {
return
}
switch itemID {
case 1:
result.Coins += count
playerInfo.Coins += count
infoDirty = true
case 3:
result.ExpPool += count
playerInfo.ExpPool += count
infoDirty = true
case 5:
result.Gold += count
case 9:
result.EVPool += count
playerInfo.EVPool += count
infoDirty = true
default:
bagItems = append(bagItems, data.ItemInfo{ItemId: int64(itemID), ItemCnt: count})
}
}
for _, rewardID := range cfg.ItemRewardIds {
itemInfo := configservice.NewItemService().GetItemCount(rewardID)
appendRewardItem(uint32(itemInfo.ItemId), itemInfo.ItemCnt)
}
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 _, rewardID := range cfg.ElfRewardIds {
pet := configservice.NewPetRewardService().Get(rewardID)
if pet == nil {
continue
}
petInfo := model.GenPetInfo(int(pet.MonID), int(pet.DV), int(pet.Nature), int(pet.Effect), int(pet.Lv), nil, 0)
catchTime, err := NewPetService(s.userid).PetAdd(petInfo, 0)
if err != nil {
return nil, err
}
result.Pets = append(result.Pets, CdkRewardPet{
PetID: uint32(pet.MonID),
CatchTime: catchTime,
})
}
if cfg.TitleRewardIds != 0 {
NewTitleService(s.userid).Give(cfg.TitleRewardIds)
result.TitleIDs = append(result.TitleIDs, cfg.TitleRewardIds)
}
if infoDirty {
infoService.Save(*playerInfo)
}
return result, nil
}

View File

@@ -29,7 +29,7 @@ func (s *GoldListService) ModifyBefore(ctx context.Context, method string, param
if t > 0 {
return fmt.Errorf("不允许多挂单")
}
if gconv.Float64(param["rate"]) > 1.0576 {
if gconv.Float64(param["rate"]) > 2{
r := g.List{}
for i := 0; i < grand.N(1, 3); i++ {
r = append(r, g.Map{"rate": param["rate"], "exchange_num": param["exchange_num"], "player_id": 10001})

View File

@@ -180,19 +180,51 @@ func (s *InfoService) Gensession() string {
func (s *InfoService) Kick(id uint32) error {
useid1, err := share.ShareManager.GetUserOnline(id)
if err != nil {
return err
if err != nil || useid1 == 0 {
// 请求进入时已经离线,视为成功
return nil
}
cl, ok := cool.GetClientOnly(useid1)
if ok {
err := cl.KickPerson(id) //实现指定服务器踢人
if err != nil {
return err
}
if !ok || cl == nil {
// 目标服务器不在线,清理僵尸在线标记并视为成功
_ = share.ShareManager.DeleteUserOnline(id)
return nil
}
resultCh := make(chan error, 1)
go func() {
resultCh <- cl.KickPerson(id) // 实现指定服务器踢人
}()
select {
case callErr := <-resultCh:
if callErr == nil {
return nil
}
// 调用失败后兜底:若已离线/切服/目标服不在线则视为成功
useid2, err2 := share.ShareManager.GetUserOnline(id)
if err2 != nil || useid2 == 0 || useid2 != useid1 {
return nil
}
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
_ = share.ShareManager.DeleteUserOnline(id)
return nil
}
return callErr
case <-time.After(3 * time.Second):
// 防止异常场景下无限等待;超时不按成功处理
useid2, err2 := share.ShareManager.GetUserOnline(id)
if err2 != nil || useid2 == 0 || useid2 != useid1 {
return nil
}
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
_ = share.ShareManager.DeleteUserOnline(id)
return nil
}
return fmt.Errorf("kick timeout, user still online: uid=%d server=%d", id, useid2)
}
return nil
}
// saveToLocalFile 兜底保存将数据写入本地lose文件夹

View File

@@ -0,0 +1,360 @@
package service
import (
"blazing/common/data"
"blazing/cool"
configmodel "blazing/modules/config/model"
configservice "blazing/modules/config/service"
"blazing/modules/player/model"
"fmt"
"sort"
"time"
)
const signRecordID uint32 = 1
// SignStageState 表示一个签到阶段的当前状态。
type SignStageState struct {
SignType uint32 `json:"sign_type"`
StageDays uint32 `json:"stage_days"`
CdkID uint32 `json:"cdk_id"`
Reached bool `json:"reached"`
Claimed bool `json:"claimed"`
}
// SignState 表示玩家当前签到进度和阶段状态。
type SignState struct {
TotalDays uint32 `json:"total_days"`
ContinuousDays uint32 `json:"continuous_days"`
LastSignDate string `json:"last_sign_date"`
TodaySigned bool `json:"today_signed"`
Stages []SignStageState `json:"stages"`
}
// SignRewardResult 表示一次签到后自动发放的阶段奖励。
type SignRewardResult struct {
SignType uint32 `json:"sign_type"`
StageDays uint32 `json:"stage_days"`
CdkID uint32 `json:"cdk_id"`
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"`
}
// SignClaimResult 表示签到后的完整结果。
type SignClaimResult struct {
State *SignState `json:"state"`
Rewards []SignRewardResult `json:"rewards,omitempty"`
}
// SignResetResult 表示管理端执行的签到重置结果。
type SignResetResult struct {
SignRecordRows int64 `json:"sign_record_rows"`
CdkLogRows int64 `json:"cdk_log_rows"`
ResetCdkIDs []uint32 `json:"reset_cdk_ids"`
}
// 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", "is_completed"},
},
PageQueryOp: &cool.QueryOp{
FieldEQ: []string{"player_id", "is_completed"},
},
},
},
}
}
func (s *SignService) GetState() (*SignState, error) {
record, err := s.getRecord()
if err != nil {
return nil, err
}
return s.buildState(record), nil
}
func (s *SignService) Claim() (*SignClaimResult, error) {
record, isNew, err := s.getOrInitRecord()
if err != nil {
return nil, err
}
today := currentDateString()
if record.LastSignDate == today {
return nil, fmt.Errorf("今天已经签到过了")
}
prevTotalDays := record.TotalDays
prevContinuousDays := record.ContinuousDays
prevDate := record.LastSignDate
record.LastSignDate = today
record.TotalDays++
if isYesterday(prevDate, today) {
record.ContinuousDays++
} else {
record.ContinuousDays = 1
}
rewards, err := s.grantReachedStageRewards(record, prevTotalDays, prevContinuousDays)
if err != nil {
return nil, err
}
if err := s.saveRecord(record, isNew); err != nil {
return nil, err
}
return &SignClaimResult{
State: s.buildState(record),
Rewards: rewards,
}, nil
}
func (s *SignService) ResetAll() (*SignResetResult, error) {
result := &SignResetResult{}
signRes, err := cool.DBM(model.NewSignInRecord()).Delete()
if err != nil {
return nil, err
}
if signRes != nil {
result.SignRecordRows, _ = signRes.RowsAffected()
}
configs := configservice.NewSignInService().GetEnabled()
cdkIDs := make([]uint32, 0, len(configs))
seen := make(map[uint32]struct{}, len(configs))
for _, cfg := range configs {
if cfg.CdkID == 0 {
continue
}
if _, ok := seen[cfg.CdkID]; ok {
continue
}
seen[cfg.CdkID] = struct{}{}
cdkIDs = append(cdkIDs, cfg.CdkID)
}
sort.Slice(cdkIDs, func(i, j int) bool { return cdkIDs[i] < cdkIDs[j] })
result.ResetCdkIDs = cdkIDs
if len(cdkIDs) > 0 {
cdkRes, err := cool.DBM(model.NewCdkLog()).WhereIn("code_id", cdkIDs).Delete()
if err != nil {
return nil, err
}
if cdkRes != nil {
result.CdkLogRows, _ = cdkRes.RowsAffected()
}
}
return result, nil
}
func (s *SignService) grantReachedStageRewards(record *model.SignInRecord, prevTotalDays, prevContinuousDays uint32) ([]SignRewardResult, error) {
configs := configservice.NewSignInService().GetEnabled()
if len(configs) == 0 {
return nil, nil
}
baseRewardBySignType := make(map[uint32]configmodel.SignIn)
for _, cfg := range configs {
if cfg.StageDays == 0 {
baseRewardBySignType[cfg.SignType] = cfg
}
}
cdkLogService := NewCdkService(s.userid)
results := make([]SignRewardResult, 0)
for _, cfg := range configs {
if cfg.StageDays == 0 {
continue
}
if !stageReached(cfg.SignType, cfg.StageDays, record) {
continue
}
if stageReachedByDays(cfg.SignType, cfg.StageDays, prevTotalDays, prevContinuousDays) {
continue
}
rewardCdkID := cfg.CdkID
if !cdkLogService.CanGet(cfg.CdkID) {
baseCfg, ok := baseRewardBySignType[cfg.SignType]
if !ok || !cdkLogService.CanGet(baseCfg.CdkID) {
continue
}
rewardCdkID = baseCfg.CdkID
}
reward, err := cdkLogService.GrantConfigReward(rewardCdkID)
if err != nil {
return nil, err
}
results = append(results, buildSignRewardResult(cfg.SignType, cfg.StageDays, reward))
cdkLogService.Log(rewardCdkID)
}
sort.Slice(results, func(i, j int) bool {
if results[i].SignType != results[j].SignType {
return results[i].SignType < results[j].SignType
}
if results[i].StageDays != results[j].StageDays {
return results[i].StageDays < results[j].StageDays
}
return results[i].CdkID < results[j].CdkID
})
return results, nil
}
func buildSignRewardResult(signType, stageDays uint32, reward *CdkRewardResult) SignRewardResult {
result := SignRewardResult{
SignType: signType,
StageDays: stageDays,
CdkID: reward.CdkID,
Items: reward.Items,
TitleIDs: reward.TitleIDs,
Coins: reward.Coins,
Gold: reward.Gold,
FreeGold: reward.FreeGold,
ExpPool: reward.ExpPool,
EVPool: reward.EVPool,
}
if len(reward.Pets) > 0 {
result.PetIDs = make([]uint32, 0, len(reward.Pets))
for _, pet := range reward.Pets {
result.PetIDs = append(result.PetIDs, pet.PetID)
}
}
return result
}
func (s *SignService) buildState(record *model.SignInRecord) *SignState {
state := &SignState{
Stages: make([]SignStageState, 0),
}
if record != nil {
state.TotalDays = record.TotalDays
state.ContinuousDays = record.ContinuousDays
state.LastSignDate = record.LastSignDate
state.TodaySigned = record.LastSignDate == currentDateString()
}
cdkLogService := NewCdkService(s.userid)
configs := configservice.NewSignInService().GetEnabled()
for _, cfg := range configs {
if cfg.StageDays == 0 {
continue
}
state.Stages = append(state.Stages, SignStageState{
SignType: cfg.SignType,
StageDays: cfg.StageDays,
CdkID: cfg.CdkID,
Reached: stageReached(cfg.SignType, cfg.StageDays, record),
Claimed: !cdkLogService.CanGet(cfg.CdkID),
})
}
return state
}
func stageReached(signType, stageDays uint32, record *model.SignInRecord) bool {
if record == nil {
return false
}
return stageReachedByDays(signType, stageDays, record.TotalDays, record.ContinuousDays)
}
func stageReachedByDays(signType, stageDays, totalDays, continuousDays uint32) bool {
if stageDays == 0 {
return false
}
switch signType {
case configmodel.SignTypeContinuous:
return continuousDays >= stageDays
default:
return totalDays >= stageDays
}
}
func (s *SignService) getRecord() (*model.SignInRecord, error) {
var out *model.SignInRecord
if err := s.dbm(s.Model).Where("sign_in_id", signRecordID).Scan(&out); err != nil {
return nil, err
}
return out, nil
}
func (s *SignService) getOrInitRecord() (*model.SignInRecord, bool, error) {
record, err := s.getRecord()
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: signRecordID,
IsCompleted: false,
ContinuousDays: 0,
TotalDays: 0,
LastSignDate: "",
SignInProgress: []uint32{},
}, true, nil
}
func (s *SignService) saveRecord(record *model.SignInRecord, isNew bool) error {
data := map[string]any{
"player_id": record.PlayerID,
"sign_in_id": record.SignInID,
"is_completed": false,
"continuous_days": record.ContinuousDays,
"total_days": record.TotalDays,
"last_sign_date": record.LastSignDate,
"sign_in_progress": []uint32{},
"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", signRecordID).Data(data).Update()
return err
}
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)
}

View File

@@ -0,0 +1 @@
package service