feat(pet): 添加宠物收集功能和称号系统

- 实现了宠物收集任务状态查询功能
- 新增Collect方法处理宠物收集逻辑,包括类型验证和ID合法性检查
- 创建validTypeIDMap映射表统一管理合法的类型ID集合
- 重构任务状态判断逻辑,基于model.Completion状态进行判断

refactor(map): 统一玩家信息结构体

- 将OutInfo重命名为SimpleInfo并添加Title字段
- 更新EnterMap方法的返回类型为SimpleInfo
- 修改space包中的UserInfo映射类型为SimpleInfo

feat(task): 集成称号奖励到任务系统

- 在PlayerInfo结构体中添加Title字段
- 扩展TaskConfig模型支持称号奖励配置
- 更新用户信息服务处理用户名大小写转换

refactor(space): 优化空间服务数据结构

- 更新GetInfo方法返回SimpleInfo切片
- 调整UserInfo CsMap泛型类型参数
- 修改ListMapPlayerOutboundInfo中Player数组类型

style(login): 规范化用户名输入处理

- 登录时将用户名转换为小写进行比较
- 使用strings.EqualFold进行大小
This commit is contained in:
昔念
2026-01-17 00:47:41 +08:00
parent b6754df9a0
commit 08ebf849eb
17 changed files with 168 additions and 61 deletions

View File

@@ -15,7 +15,7 @@ import (
"github.com/jinzhu/copier"
)
func (h Controller) EnterMap(data *space.InInfo, c *player.Player) (result *info.OutInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
func (h Controller) EnterMap(data *space.InInfo, c *player.Player) (result *info.SimpleInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
c.Info.MapID = data.MapId //登录地图
c.GetSpace().User.Store(c.Info.UserID, c) //添加玩家

View File

@@ -0,0 +1,27 @@
package controller
import (
"blazing/common/socket/errorcode"
"blazing/logic/service/pet"
"blazing/logic/service/player"
)
// GetPetBargeList 精灵图鉴
func (h Controller) GetPetBargeList(data *pet.PetBargeListInboundInfo, player *player.Player) (result *pet.PetBargeListOutboundInfo, err errorcode.ErrorCode) {
ret := &pet.PetBargeListOutboundInfo{
PetBargeList: make([]pet.PetBargeListInfo, 0),
}
r := player.Service.Barge.Get(data.StartPetId, data.EndPetId)
for _, v := range r {
ret.PetBargeList = append(ret.PetBargeList, pet.PetBargeListInfo{
PetId: v.PetId,
EnCntCnt: 1,
IsCatched: v.CatchedCount,
IsKilled: v.KilledCount,
})
}
return ret, 0
}

View File

@@ -4,19 +4,58 @@ import (
"blazing/common/socket/errorcode"
"blazing/logic/service/pet"
"blazing/logic/service/player"
"blazing/modules/blazing/model"
"github.com/samber/lo"
)
func (h Controller) IsCollect(
data *pet.C2S_IS_COLLECT, c *player.Player) (result *pet.S2C_IS_COLLECT, err errorcode.ErrorCode) { //这个时候player应该是空的
result = &pet.S2C_IS_COLLECT{
ID: data.Type,
IsCom: 0,
}
// r := c.Service.Barge.Get()
// for _, v := range r {
// }
res := c.Info.GetTask(1335 + int(data.Type)) //第一期
if res == model.Completed {
result.IsCom = 1
}
return result, 0
}
// 定义 Type 与合法 ID 集合的映射表,集中管理所有规则
var validTypeIDMap = map[int][]uint32{
1: {1, 4, 7}, // Type1合法ID为1、4、7
2: {71}, // Type2合法ID为71
3: {275}, // Type3合法ID为275
4: {669}, // Type4合法ID为669你之前提到的是670确认是否笔误
301: {1, 4, 7}, //精灵王计划
}
func (h Controller) Collect(
data *pet.C2S_PET_COLLECT, c *player.Player) (result *pet.S2C_PET_COLLECT, err errorcode.ErrorCode) { //这个时候player应该是空的
result = &pet.S2C_PET_COLLECT{ID: data.ID}
res := c.Info.GetTask(1335 + int(data.Type)) //第一期
if res != model.Unaccepted {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
validIDs, ok := validTypeIDMap[int(data.Type)]
if !ok {
// Type不在映射表中返回系统错误
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
// 2. 判断ID是否在合法集合中
if !lo.Contains(validIDs, data.ID) {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
c.Info.SetTask(1335+int(data.Type), model.Completed)
r := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil)
c.Service.Pet.PetAdd(r)
result.CatchTime = r.CatchTime
return result, 0

View File

@@ -0,0 +1,17 @@
package controller
import (
"blazing/common/socket/errorcode"
"blazing/logic/service/pet"
"blazing/logic/service/player"
)
// Exelist 对应C#的List<Exeing 实现获取精灵训练数据
func (h Controller) PetExt(
data *pet.C2S_NONO_EXE_LIST, player *player.Player) (result *pet.S2C_NONO_EXE_LIST, err errorcode.ErrorCode) { //这个时候player应该是空的
result = &pet.S2C_NONO_EXE_LIST{}
return
}

View File

@@ -222,23 +222,3 @@ func (h Controller) SetPetExp(data *pet.PetSetExpInboundInfo, player *player.Pla
Exp: player.Info.ExpPool,
}, errorcode.ErrorCodes.ErrSystemError
}
// GetPetBargeList 精灵图鉴
func (h Controller) GetPetBargeList(data *pet.PetBargeListInboundInfo, player *player.Player) (result *pet.PetBargeListOutboundInfo, err errorcode.ErrorCode) {
ret := &pet.PetBargeListOutboundInfo{
PetBargeList: make([]pet.PetBargeListInfo, 0),
}
r := player.Service.Barge.Get(data.StartPetId, data.EndPetId)
for _, v := range r {
ret.PetBargeList = append(ret.PetBargeList, pet.PetBargeListInfo{
PetId: v.PetId,
EnCntCnt: 1,
IsCatched: v.CatchedCount,
IsKilled: v.KilledCount,
})
}
return ret, 0
}

View File

@@ -0,0 +1,32 @@
package pet
import "blazing/logic/service/common"
// C2S_PET_COLLECT 客户端(Client)到服务端(Server)的精灵收集请求结构体
type C2S_PET_COLLECT struct {
Head common.TomeeHeader `cmd:"2311|2317" struc:"skip"`
Type uint32 `json:"type"` // 收集任务类型 1 = 普通精灵收集计划 2 = 稀有精灵收集计划
ID uint32 `json:"id"` // 领取的精灵id
}
// S2C_PET_COLLECT 服务端(Server)到客户端(Client)的精灵收集响应结构体
type S2C_PET_COLLECT struct {
ID uint32 `json:"id"` // 前端发来的 领取的精灵id
CatchTime uint32 `json:"catchTime"` // 领取精灵的捕捉时间
}
// C2S_IS_COLLECT 前端(Client)→后端(Server):精灵收集任务状态查询请求包
type C2S_IS_COLLECT struct {
Head common.TomeeHeader `cmd:"2313" struc:"skip"`
Type uint32 `json:"type" msgpack:"type"` // 收集类型301=太空站10胜1=精灵收集计划一期第一种2=精灵收集计划一期第二种
}
// S2C_IS_COLLECT 后端(Server)→前端(Client):精灵收集任务状态查询回包
// 补充说明:
// 1. 此包同时适用于「精灵收集计划」和「太空站10胜送主宠」场景
// 2. isCom=0未领取才可调用 PRIZE_OF_PETKING(2317) 协议领取精灵
// 3. isCom=1已完成/已领取)时,代表奖励已领取,无需再调用领取协议
type S2C_IS_COLLECT struct {
ID uint32 `json:"id" msgpack:"id"` // 收集类型301=太空站10胜1=精灵收集计划一期第一种2=精灵收集计划一期第二种
IsCom uint32 `json:"isCom" msgpack:"isCom"` // 是否完成/是否已领取0=未完成/未领取1=已完成/已领取
}

View File

@@ -82,18 +82,22 @@ type PetDefaultOutboundInfo struct {
IsDefault uint32 `json:"isDefault" fieldDescription:"0: 首发设置失败1: 首发设置成功" uint:"true" autoCodec:"true" outboundMessageType:"Pet_Default"`
}
// C2S_IS_COLLECT 前端(Client)→后端(Server):精灵收集任务状态查询请求包
type C2S_IS_COLLECT struct {
Head common.TomeeHeader `cmd:"2313" struc:"skip"`
Type uint32 `json:"type" msgpack:"type"` // 收集类型301=太空站10胜1=精灵收集计划一期第一种2=精灵收集计划一期第二种
type C2S_NONO_EXE_LIST struct {
Head common.TomeeHeader `cmd:"9015" struc:"skip"`
}
// S2C_IS_COLLECT 后端(Server)→前端(Client):精灵收集任务状态查询回包
// 补充说明:
// 1. 此包同时适用于「精灵收集计划」和「太空站10胜送主宠」场景
// 2. isCom=0未领取才可调用 PRIZE_OF_PETKING(2317) 协议领取精灵
// 3. isCom=1已完成/已领取)时,代表奖励已领取,无需再调用领取协议
type S2C_IS_COLLECT struct {
ID uint32 `json:"id" msgpack:"id"` // 收集类型301=太空站10胜1=精灵收集计划一期第一种2=精灵收集计划一期第二种
IsCom uint32 `json:"isCom" msgpack:"isCom"` // 是否完成/是否已领取0=未完成/未领取1=已完成/已领取
// S2C_NONO_EXE_LIST 对应C#的同名结构体
type S2C_NONO_EXE_LIST struct {
// Exelist 对应C#的List<ExeingPetInfo>Go中用切片([])替代列表
ExelistLen uint32 `json:"exelistLen" struc:"sizeof=Exelist"`
Exelist []ExeingPetInfo `json:"exelist"` // 若需JSON序列化保留原字段名
}
// ExeingPetInfo 对应C#的同名结构体,存储精灵训练相关信息
type ExeingPetInfo struct {
Flag uint32 `json:"_flag"` // 应该是精灵是否在训练对应原_flag
CapTm uint32 `json:"_capTm"` // 精灵的捕捉时间对应原_capTm
PetId uint32 `json:"_petId"` // 训练精灵的id对应原_petId
RemainDay uint32 `json:"_remainDay"` // 停留天数前端用这个值除以3600疑似时间戳对应原_remainDay
Course uint32 `json:"_course"` // 课程对应原_course
}

View File

@@ -69,7 +69,7 @@ func (s *Space) EnterMap(c common.PlayerI) {
}
}
func (s *Space) GetInfo(c common.PlayerI) []info.OutInfo {
func (s *Space) GetInfo(c common.PlayerI) []info.SimpleInfo {
if atomic.LoadUint32(&s.TimeBoss.Flag) == 1 {
defer c.SendPackCmd(2022, &s.TimeBoss)
@@ -81,8 +81,8 @@ func (s *Space) GetInfo(c common.PlayerI) []info.OutInfo {
}
defer c.SendPackCmd(50004, &info.S2C_50004{Id: uint32(s.Weather)}) //获取天气
ret := make([]info.OutInfo, 0)
s.UserInfo.Range(func(k uint32, v info.OutInfo) (stop bool) {
ret := make([]info.SimpleInfo, 0)
s.UserInfo.Range(func(k uint32, v info.SimpleInfo) (stop bool) {
ret = append(ret, v)
return len(ret) > 30
})

View File

@@ -11,7 +11,7 @@ type ListMapPlayerOutboundInfo struct {
PlayersLen uint32 `struc:"sizeof=Player" json:"player_len"`
// 穿戴装备的信息
Player []OutInfo ` json:"player"`
Player []SimpleInfo ` json:"player"`
}
// PeopleWalkOutboundInfo PeopleWalkOutboundInfo类实现OutboundMessage接口
@@ -45,11 +45,12 @@ type MapBossInfo struct {
//var planetmap utils.SyncMap[] //= space.NewSyncMap()
// PeopleInfo PeopleInfo类实现OutboundMessage接口
type OutInfo struct {
type SimpleInfo struct {
UserID uint32 `struc:"uint32" fieldDesc:"米米号" json:"user_id"`
// 16字节昵称
Nick string `struc:"[16]byte" fieldDesc:"16字节昵称" json:"nick"`
Title uint32 `struc:"uint32" json:"title"` // 称号
// 机器人人物颜色 00 rgb
Color uint32 `struc:"uint32" fieldDesc:"机器人人物颜色 00 rgb" json:"color"`
@@ -150,8 +151,8 @@ type OutInfo struct {
Clothes []model.PeopleItemInfo `fieldDesc:"穿戴装备的信息" json:"clothes"`
}
func NewOutInfo() *OutInfo {
l := &OutInfo{}
func NewOutInfo() *SimpleInfo {
l := &SimpleInfo{}
// 自动填充 struct tag 里的 default 值
if err := defaults.Set(l); err != nil {

View File

@@ -27,7 +27,7 @@ var wermap = []uint32{32}
// Space 针对Player的并发安全map键为uint32类型
type Space struct {
User *csmap.CsMap[uint32, common.PlayerI] // 存储玩家数据的map键为玩家ID
UserInfo *csmap.CsMap[uint32, info.OutInfo]
UserInfo *csmap.CsMap[uint32, info.SimpleInfo]
CanRefresh bool //是否能够刷怪
Super uint32
//SuperValue *int32
@@ -57,10 +57,10 @@ func NewSpace() *Space {
// set the total capacity, every shard map has total capacity/shard count capacity. the default value is 0.
// csmap.WithSize[string, int](1000),
),
UserInfo: csmap.New[uint32, info.OutInfo](
UserInfo: csmap.New[uint32, info.SimpleInfo](
// set the number of map shards. the default value is 32.
csmap.WithShardCount[uint32, info.OutInfo](32),
csmap.WithCustomHasher[uint32, info.OutInfo](func(key uint32) uint64 {
csmap.WithShardCount[uint32, info.SimpleInfo](32),
csmap.WithCustomHasher[uint32, info.SimpleInfo](func(key uint32) uint64 {
return uint64(key)
}),

View File

@@ -14,6 +14,7 @@ type SimUserInfoOutboundInfo struct {
UserID uint32 `codec:"true"`
Nick string `struc:"[16]byte" default:"seer" json:"nick"` // 16字节昵称
Title uint32 `struc:"uint32" json:"title"` // 称号
// Color rgb颜色
Color uint32 `codec:"true"`
@@ -80,6 +81,7 @@ type MoreUserInfoOutboundInfo struct {
UserID uint32 `codec:"true"`
Nick string `struc:"[16]byte" default:"seer" json:"nick"` // 16字节昵称
Title uint32 `struc:"uint32" json:"title"` // 称号
RegisterTime uint32 `struc:"uint32" json:"register_time"` // 注册时间(秒时间戳)
// PetAllNum 所有精灵数量
@@ -113,10 +115,6 @@ type MoreUserInfoOutboundInfo struct {
// MaxArenaWins 星际擂台胜场
// 对应 Java 注解: @FieldDescription("星际擂台胜场") @UInt
MaxArenaWins uint32 `codec:"true"`
// CurrentTitleID 当前称号ID
// 对应 Java 注解: @FieldDescription("当前称号ID") @UInt
CurrentTitleID uint32 `codec:"true"`
}
// AimatInboundInfo 对应Java的AimatInboundInfo类

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
@@ -43,7 +44,7 @@ func (s *BaseSysLoginService) Login(ctx context.Context, req *v1.BaseOpenLoginRe
username = req.Username
baseSysUser = model.NewBaseSysUser()
)
username = strings.ToLower(username)
vcode, _ := cool.CacheManager.Get(ctx, "login:"+captchaId)
if vcode.String() != verifyCode {
err = gerror.New("验证码错误")
@@ -66,7 +67,7 @@ func (s *BaseSysLoginService) Login(ctx context.Context, req *v1.BaseOpenLoginRe
md5password, _ := gmd5.Encrypt(password)
var user *model.BaseSysUser
if userInfo.Data.Attributes.Username != username { //说明没查找到用户
if !strings.EqualFold(userInfo.Data.Attributes.Username, username) { //说明没查找到用户
//后端添加的账户
cool.DBM(baseSysUser).Where("username=?", username).Where("password=?", md5password).Where("status=?", 1).Scan(&user)
if user == nil {

View File

@@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"strings"
"blazing/cool"
@@ -118,7 +119,7 @@ func (s *BaseSysUserService) Gen(user UserAttributes) (data interface{}, err err
lastInsertId, err := m.Data(user).Data(
g.Map{
"username": user.Username,
"username": strings.ToLower(user.Username),
"headImg": user.AvatarUrl,
"departmentId": 1,
},
@@ -150,6 +151,9 @@ func (s *BaseSysUserService) ServiceAdd(ctx context.Context, req *cool.AddReq) (
if !r.Get("password").IsNil() {
reqmap["password"] = gmd5.MustEncryptString(r.Get("password").String())
}
if !r.Get("username").IsNil() {
reqmap["username"] = strings.ToLower(r.Get("username").String())
}
if s.UniqueKey != nil {
for k, v := range s.UniqueKey {
if reqmap[k] != nil {
@@ -225,7 +229,9 @@ func (s *BaseSysUserService) ServiceUpdate(ctx context.Context, req *cool.Update
r := g.RequestFromCtx(ctx)
rMap := r.GetMap()
if !r.Get("username").IsNil() {
rMap["username"] = strings.ToLower(r.Get("username").String())
}
// 如果不传入ID代表更新当前用户
userId := r.Get("id", admin.UserId).Uint()
userInfo, err := m.Where("id", userId).One()

View File

@@ -118,6 +118,7 @@ type PlayerInfo struct {
UserID uint32 `struc:"uint32" json:"user_id"` // 米米号 通过sid拿到
RegisterTime uint32 `struc:"uint32" json:"register_time"` // 注册时间(秒时间戳)
Nick string `struc:"[16]byte" default:"seer" json:"nick"` // 16字节昵称
Title uint32 `struc:"uint32" json:"title"` // 称号
Vip uint16 `struc:"uint16" json:"vip"` // 固定0
Viped uint16 `struc:"uint16" default:"15" json:"viped"` // 固定15
DSFlag uint32 `struc:"uint32" json:"ds_flag"` // 固定0

View File

@@ -25,6 +25,8 @@ type TaskConfig struct {
ItemRewardIds []uint32 `gorm:"not null;type:json;default:'[]';comment:'绑定奖励物品ID数组关联item_gift表主键'" json:"item_reward_ids" description:"奖励物品数组"`
ElfRewardIds uint32 `gorm:"not null;default:0;comment:'绑定奖励精灵ID关联elf_gift表主键'" json:"elf_reward_ids" description:"绑定奖励精灵ID"`
//绑定奖励
TitleRewardIds uint32 `gorm:"not null;default:0;comment:'绑定奖励称号'" json:"title_reward_ids" description:"绑定奖励称号"`
// 任务状态和周期
IsEnabled uint32 `gorm:"not null;default:1;comment:'是否启用该任务0-禁用 1-启用)'" json:"is_enabled" description:"是否启用"`

View File

@@ -1 +0,0 @@
E:/newcode/flash/out/dll/30001.xml

Binary file not shown.