diff --git a/admin/apps/game/domain/comm_resource.go b/admin/apps/game/domain/comm_resource.go index 870117e..7e9ac20 100644 --- a/admin/apps/game/domain/comm_resource.go +++ b/admin/apps/game/domain/comm_resource.go @@ -9,10 +9,15 @@ import ( "admin/apps/game/model/dto" "admin/internal/consts" "admin/internal/errcode" + "admin/internal/event" "admin/lib/xlog" + "database/sql" + "encoding/json" "fmt" "gorm.io/gorm" + "reflect" "strings" + "time" ) type CommonResourceService struct { @@ -39,8 +44,13 @@ func initCommonResourcesRepo(db *gorm.DB) { r(consts.ResourcesName_Role, "角色列表", repo.NewCommonResourceRepo(db, &model.Role{}), ShowMethod_Get) // 角色管理不需要在后台读写数据,都是通过项目api拉 r(consts.ResourcesName_WhiteList, "白名单", repo.NewCommonResourceRepo(db, &model.WhiteList{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Delete) r(consts.ResourcesName_Ban, "封禁管理", repo.NewCommonResourceRepo(db, &model.Ban{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete) - r(consts.ResourcesName_MailRole, "个人邮件", repo.NewCommonResourceRepo(db, &model.RoleMail{}), ShowMethod_Get|ShowMethod_Post) // 个人邮件发放就没法撤回? - r(consts.ResourcesName_MailGlobal, "全服邮件", repo.NewCommonResourceRepo(db, &model.GlobalMail{}), ShowMethod_Get|ShowMethod_Post) // 直接删除,别修改了,玩家收到的更乱 + r(consts.ResourcesName_MailRole, "个人邮件", repo.NewCommonResourceRepo(db, &model.RoleMail{}), ShowMethod_Get|ShowMethod_Post) // 个人邮件发放就没法撤回? + + { + globalMailRepo := r(consts.ResourcesName_MailGlobal, "全服邮件", repo.NewCommonResourceRepo(db, &model.GlobalMail{}), ShowMethod_Get|ShowMethod_Post) + globalMailRepo.HasDelayInvokeCreateHook = true + } // 直接删除,别修改了,玩家收到的更乱 + r(consts.ResourcesName_CDKey, "礼包码", repo.NewCommonResourceRepo(db, &model.CDKey{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete) r(consts.ResourcesName_ItemBag, "道具礼包", repo.NewCommonResourceRepo(db, &model.ItemBag{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete) r(consts.ResourcesName_Notice, "公告(暂无)", repo.NewCommonResourceRepo(db, &model.Notice{}), ShowMethod_Get|ShowMethod_Post|ShowMethod_Put|ShowMethod_Delete) @@ -52,9 +62,48 @@ func NewCommonResourceService(db *gorm.DB) *CommonResourceService { svc := &CommonResourceService{ projectRepo: repo.NewProjectRepo(db), } + svc.startEventSubscriber() + svc.startLoadAllDelayInvokeDbData() return svc } +func (svc *CommonResourceService) startLoadAllDelayInvokeDbData() { + for _, repo := range commResourcesRepo { + if repo.HasDelayInvokeCreateHook { + repo.Repo.ListPagination("`delay_invoke_create_hook` is not NULL and `delay_invoke_create_hook` > ?", []any{time.Now()}, func(po model.IModel) { + var projectId int + var resource string = repo.Resource + var delayAt time.Time + //xlog.Infof("过滤数据:%+v", po) + switch repo.Resource { + case consts.ResourcesName_MailGlobal: + dbData := po.(*model.GlobalMail) + projectId = dbData.ProjectId + if !dbData.DelayInvokeCreateHook.Valid || dbData.DelayInvokeCreateHook.Time.Before(time.Now()) { + return + } + delayAt = dbData.DelayInvokeCreateHook.Time + default: + return + } + + // 延迟执行钩子调用 + xlog.Infof("起服查看全局邮件:%+v延迟到%v发送,添加到全局定时器。。", po, delayAt.Format(time.DateTime)) + objBin, _ := json.Marshal(po) + payload := &event.EventPayload_DelayInvokeCreateHook{ + ProjectId: projectId, + Resource: resource, + DelayAt: delayAt, + Obj: objBin, + } + event.GetMgrInstance().Publish(event.EventTopic_DelayInvokeCreateHook, payload) + return + + }) + } + } +} + func (svc *CommonResourceService) List(projectId int, resource string, listParams *dto.CommonListReq) (int, []*dto.CommonDtoFieldDesc, []dto.CommonDtoValues, error) { _, projectEt, find, err := svc.projectRepo.GetById(projectId) if err != nil { @@ -65,7 +114,7 @@ func (svc *CommonResourceService) List(projectId int, resource string, listParam } rRepo := findCommResourceRepo(resource) - totalCount, fieldsDescInfo, etList, err := rRepo.List(projectEt, listParams) + totalCount, fieldsDescInfo, etList, err := rRepo.Repo.List(projectEt, listParams) if err != nil { return 0, nil, nil, err } @@ -96,7 +145,7 @@ func (svc *CommonResourceService) GetById(projectId int, resource string, id int return nil, nil, nil, false, errcode.New(errcode.ServerError, "not found project %v db data", projectId) } - fieldsDescInfo, et, find, err := findCommResourceRepo(resource).GetById(projectEt, id) + fieldsDescInfo, et, find, err := findCommResourceRepo(resource).Repo.GetById(projectEt, id) if err != nil { return nil, nil, nil, false, err } @@ -114,7 +163,8 @@ func (svc *CommonResourceService) Create(projectId int, resource string, dtoObj } createOne := func(obj dto.CommonDtoValues) (dto.CommonDtoValues, error) { - et, err := findCommResourceRepo(resource).Create(projectEt, resource, obj) + resourceRepo := findCommResourceRepo(resource) + et, err := resourceRepo.Repo.Create(projectEt, resource, obj) if err != nil { return nil, err } @@ -124,6 +174,23 @@ func (svc *CommonResourceService) Create(projectId int, resource string, dtoObj newObj := et.ToCommonDto() // 执行各个项目特定的钩子方法 + if resourceRepo.HasDelayInvokeCreateHook { + field := reflect.ValueOf(et.Po).Elem().FieldByName("DelayInvokeCreateHook") + delayAt := field.Interface().(sql.NullTime) + if delayAt.Valid && delayAt.Time.After(time.Now()) { + // 延迟执行钩子调用 + objBin, _ := json.Marshal(et.Po) + payload := &event.EventPayload_DelayInvokeCreateHook{ + ProjectId: projectId, + Resource: resource, + DelayAt: delayAt.Time, + Obj: objBin, + } + event.GetMgrInstance().Publish(event.EventTopic_DelayInvokeCreateHook, payload) + return newObj, nil + } + } + if hook, ok := projects.GetProjectResourceHook(projectEt, resource).(projects.IPostResourceOpCreateHook); ok { err = hook.Create(projectEt, resource, newObj) if err != nil { @@ -168,7 +235,7 @@ func (svc *CommonResourceService) Edit(projectId int, resource string, dtoObj dt return errcode.New(errcode.ServerError, "not found project %v db data", projectId) } - err = findCommResourceRepo(resource).Edit(projectEt, dtoObj) + err = findCommResourceRepo(resource).Repo.Edit(projectEt, dtoObj) if err != nil { return err } @@ -194,7 +261,7 @@ func (svc *CommonResourceService) Delete(projectId int, resource string, id int) return nil, errcode.New(errcode.ServerError, "not found project %v db data", projectId) } - oldEt, find, err := findCommResourceRepo(resource).Delete(projectEt, id) + oldEt, find, err := findCommResourceRepo(resource).Repo.Delete(projectEt, id) if err != nil { return nil, err } @@ -249,13 +316,14 @@ const ( ) type resourceRepoInfo struct { - Resource string - Desc string - Repo repo.ICommonResourceRepo - ShowMethods []string - showMethods int - GlobalBtns []*api.ResourceBtnInfo - RowBtns []*api.ResourceBtnInfo + Resource string + Desc string + Repo repo.ICommonResourceRepo + ShowMethods []string + showMethods int + GlobalBtns []*api.ResourceBtnInfo + RowBtns []*api.ResourceBtnInfo + HasDelayInvokeCreateHook bool } var commResourcesRepo = make([]*resourceRepoInfo, 0) @@ -286,10 +354,10 @@ func r(resource, desc string, repo repo.ICommonResourceRepo, showMethods int) *r return curRepo } -func findCommResourceRepo(resource string) repo.ICommonResourceRepo { +func findCommResourceRepo(resource string) *resourceRepoInfo { for _, v := range commResourcesRepo { if v.Resource == resource { - return v.Repo + return v } } panic(fmt.Errorf("not found Resource:%v", resource)) diff --git a/admin/apps/game/domain/entity/utils.go b/admin/apps/game/domain/entity/utils.go index a0681b1..3656d6e 100644 --- a/admin/apps/game/domain/entity/utils.go +++ b/admin/apps/game/domain/entity/utils.go @@ -67,6 +67,10 @@ func getFieldTypeDtoDescInfo(project *Project, poValue reflect.Value, fieldType f1.Name = "创建时间" } + if f1.Key == "UpdatedAt" { + f1.Name = "更新时间" + } + if tagType := fieldType.Tag.Get("type"); tagType != "" { f1.Type = tagType } diff --git a/admin/apps/game/domain/event.go b/admin/apps/game/domain/event.go new file mode 100644 index 0000000..e7c530e --- /dev/null +++ b/admin/apps/game/domain/event.go @@ -0,0 +1,71 @@ +package domain + +import ( + "admin/apps/game/domain/entity" + "admin/apps/game/domain/projects" + "admin/apps/game/model" + "admin/internal/consts" + "admin/internal/event" + "admin/internal/global" + "admin/lib/dtimer" + "admin/lib/xlog" + "encoding/json" + "fmt" + "time" +) + +func (svc *CommonResourceService) startEventSubscriber() { + event.GetMgrInstance().Subscribe("resource.create", event.EventTopic_DelayInvokeCreateHook, svc.handleResourceDelayInvokeCreateHook) +} + +func (svc *CommonResourceService) handleResourceDelayInvokeCreateHook(msg *event.Msg) { + payload := &event.EventPayload_DelayInvokeCreateHook{} + err := json.Unmarshal(msg.Payload, payload) + if err != nil { + xlog.Warnf("unmarshal EventPayload_DelayInvokeCreateHook(%+v) error:%v", string(msg.Payload), err) + return + } + + if payload.Resource == consts.ResourcesName_MailGlobal { + mailInfo := &model.GlobalMail{} + err = json.Unmarshal(payload.Obj, mailInfo) + if err != nil { + xlog.Warnf("unmarshal mail(%+v) error:%v", string(payload.Obj), err) + return + } + delayAt := payload.DelayAt + if delayAt.After(time.Now()) { + xlog.Infof("全服邮件[%+v]延迟到%v发送,添加到全局定时器。。。", mailInfo, delayAt.Format(time.DateTime)) + global.GLOB_TIMER.Add(&dtimer.Timer{ + Key: fmt.Sprintf("create.global.mail.%v", mailInfo.ID), + Duration: delayAt.Sub(time.Now()), + Callback: func() { + _, projectEt, _, err := svc.projectRepo.GetById(payload.ProjectId) + if err != nil { + return + } + + obj := new(entity.CommonResource).FromPo(mailInfo).ToCommonDto() + hook, ok := projects.GetProjectResourceHook(projectEt, payload.Resource).(projects.IPostResourceOpCreateHook) + if !ok { + return + } + + err = hook.Create(projectEt, payload.Resource, obj) + if err != nil { + xlog.Errorf("invoke hook create resource:%v obj:%+v error:%v", payload.Resource, obj) + return + } + xlog.Infof("delay send global mail ok, mail info:%+v", mailInfo) + + // 更新数据库延迟时间为NULL + resourceRepo := findCommResourceRepo(payload.Resource) + err = resourceRepo.Repo.UpdateClearDelayInvokeCreateHookFieldN(mailInfo.ID) + if err != nil { + xlog.Errorf("UpdateClearDelayInvokeCreateHookFieldN id:%v error:%v", mailInfo.ID) + } + }, + }) + } + } +} diff --git a/admin/apps/game/domain/project.go b/admin/apps/game/domain/project.go index 7132f07..57ace40 100644 --- a/admin/apps/game/domain/project.go +++ b/admin/apps/game/domain/project.go @@ -4,6 +4,7 @@ import ( "admin/apps/game/domain/entity" "admin/apps/game/domain/projects" "admin/apps/game/domain/repo" + "admin/apps/game/model" "admin/apps/game/model/dto" "admin/internal/errcode" "gorm.io/gorm" @@ -87,3 +88,26 @@ func (svc *ProjectService) GetAllItems(projectId int) ([]*dto.CommonDtoFieldChoi return handler.GetItems(projectEt) } + +func (svc *ProjectService) GetAllItemBag(projectId int) ([]*dto.ItemBagInfo, error) { + dbList, err := new(model.ItemBag).List(projectId) + if err != nil { + return nil, err + } + retList := make([]*dto.ItemBagInfo, 0, len(dbList)) + for _, v := range dbList { + info := &dto.ItemBagInfo{ + Name: v.Name, + } + for _, item := range v.Attach { + info.Items = append(info.Items, &dto.ItemInfo{ + ItemID: int(item.ID), + ItemNum: item.Num, + ItemType: item.ItemType, + Desc: item.Desc, + }) + } + retList = append(retList, info) + } + return retList, nil +} diff --git a/admin/apps/game/domain/projects/smdl/mail_global.go b/admin/apps/game/domain/projects/smdl/mail_global.go index 9ead47f..d7e212a 100644 --- a/admin/apps/game/domain/projects/smdl/mail_global.go +++ b/admin/apps/game/domain/projects/smdl/mail_global.go @@ -11,6 +11,7 @@ import ( "net/url" "strconv" "strings" + "time" ) type MailGlobalHook struct { @@ -68,6 +69,19 @@ func (hook *MailGlobalHook) Create(projectInfo *entity.Project, resource string, params.Add("mailtitle", mailInfo.Title) params.Add("mailcontent", mailInfo.Content) + if mailInfo.ExpireAt.Valid { + params.Add("expiretime", mailInfo.ExpireAt.Time.Format(time.DateTime)) + } + if mailInfo.CreateRoleTimeBefore.Valid { + params.Add("createrole", mailInfo.CreateRoleTimeBefore.Time.Format(time.DateTime)) + } + if mailInfo.TotalPayMoney > 0 { + params.Add("cash", strconv.Itoa(mailInfo.TotalPayMoney)) + } + if mailInfo.RoleLevel > 0 { + params.Add("playerlevel", strconv.Itoa(mailInfo.RoleLevel)) + } + // 通知对应服务器 for _, serverId := range serverIds { serverIdInt, _ := strconv.Atoi(serverId) diff --git a/admin/apps/game/domain/projects/smdl/role.go b/admin/apps/game/domain/projects/smdl/role.go index 783ba82..caa9b12 100644 --- a/admin/apps/game/domain/projects/smdl/role.go +++ b/admin/apps/game/domain/projects/smdl/role.go @@ -9,6 +9,7 @@ import ( "admin/lib/httpclient" "admin/lib/utils" "net/url" + "sort" "strconv" ) @@ -94,5 +95,9 @@ func (hook *RoleHook) List(projectInfo *entity.Project, resource string, params } } + sort.SliceStable(rows, func(i, j int) bool { + return rows[i]["Status"].(string) < rows[j]["Status"].(string) + }) + return totalCount, fields, rows, nil } diff --git a/admin/apps/game/domain/repo/comm_resource.go b/admin/apps/game/domain/repo/comm_resource.go index a19a945..58ad139 100644 --- a/admin/apps/game/domain/repo/comm_resource.go +++ b/admin/apps/game/domain/repo/comm_resource.go @@ -7,6 +7,7 @@ import ( "admin/internal/consts" "admin/internal/errcode" "admin/lib/xlog" + "database/sql" "errors" "fmt" "gorm.io/gorm" @@ -26,6 +27,8 @@ type ICommonResourceRepo interface { Create(projectEt *entity.Project, resource string, et dto.CommonDtoValues) (*entity.CommonResource, error) Edit(projectEt *entity.Project, et dto.CommonDtoValues) error Delete(projectEt *entity.Project, id int) (*entity.CommonResource, bool, error) + ListPagination(whereSql string, whereArgs []any, f func(po model.IModel)) error + UpdateClearDelayInvokeCreateHookFieldN(id int) error } func NewCommonResourceRepo(db *gorm.DB, poTemplate model.IModel) ICommonResourceRepo { @@ -96,11 +99,11 @@ func (repo *commonResourceRepoImpl) List(projectEt *entity.Project, params *dto. } func (repo *commonResourceRepoImpl) GetById(projectEt *entity.Project, id int) ([]*dto.CommonDtoFieldDesc, *entity.CommonResource, bool, error) { - po := repo.newEmptyPo() + po := repo.makeEmptyPo() err := repo.db.Where("id = ?", id).First(po).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return repo.fieldsDescInfoFun(projectEt), (&entity.CommonResource{}).FromPo(repo.newEmptyPo()), false, nil + return repo.fieldsDescInfoFun(projectEt), (&entity.CommonResource{}).FromPo(repo.makeEmptyPo()), false, nil } return nil, nil, false, errcode.New(errcode.DBError, "get resource:%v by id:%v error:%v", repo.poTemplate.TableName(), id, err) } @@ -108,7 +111,7 @@ func (repo *commonResourceRepoImpl) GetById(projectEt *entity.Project, id int) ( } func (repo *commonResourceRepoImpl) Create(projectEt *entity.Project, resource string, dtoObj dto.CommonDtoValues) (*entity.CommonResource, error) { - et := (&entity.CommonResource{}).FromPo(repo.newEmptyPo()).FromDto(dtoObj) + et := (&entity.CommonResource{}).FromPo(repo.makeEmptyPo()).FromDto(dtoObj) if handler, find := createHooks[resource]; find { if err := handler(projectEt, dtoObj); err != nil { return et, err @@ -127,7 +130,7 @@ func (repo *commonResourceRepoImpl) Create(projectEt *entity.Project, resource s } func (repo *commonResourceRepoImpl) Edit(projectEt *entity.Project, dtoObj dto.CommonDtoValues) error { - et := (&entity.CommonResource{}).FromPo(repo.newEmptyPo()).FromDto(dtoObj) + et := (&entity.CommonResource{}).FromPo(repo.makeEmptyPo()).FromDto(dtoObj) err := repo.db.Where("id=?", et.Po.GetId()).Save(et.Po).Error if err != nil { return errcode.New(errcode.DBError, "edit resource:%v obj:%+v error:%v", repo.poTemplate.TableName(), et, err) @@ -151,7 +154,52 @@ func (repo *commonResourceRepoImpl) Delete(projectEt *entity.Project, id int) (* return et, true, nil } -func (repo *commonResourceRepoImpl) newEmptyPo() model.IModel { +func (repo *commonResourceRepoImpl) ListPagination(whereSql string, whereArgs []any, f func(po model.IModel)) error { + pageNo := 0 + pageLen := 100 + for { + limitStart := pageNo * pageLen + limitLen := pageLen + listType := reflect.New(reflect.SliceOf(reflect.TypeOf(repo.poTemplate))) + var txFind *gorm.DB + var err error + if len(whereSql) <= 0 { + txFind = repo.db.Offset(limitStart).Limit(limitLen).Order("created_at desc") + } else { + txFind = repo.db.Where(whereSql, whereArgs...).Offset(limitStart).Limit(limitLen) + } + err = txFind.Find(listType.Interface()).Error + if err != nil { + return err + } + + listType1 := listType.Elem() + listLen := listType1.Len() + + for i := 0; i < listType1.Len(); i++ { + po := listType1.Index(i).Interface().(model.IModel) + f(po) + } + + if listLen < limitLen { + // 遍历完了 + return nil + } + + pageNo++ + } +} + +func (repo *commonResourceRepoImpl) UpdateClearDelayInvokeCreateHookFieldN(id int) error { + repo.makeEmptyPo() + err := repo.db.Model(repo.makeEmptyPo()).Where("id = ?", id).UpdateColumn("delay_invoke_create_hook", sql.NullTime{}).Error + if err != nil { + return err + } + return nil +} + +func (repo *commonResourceRepoImpl) makeEmptyPo() model.IModel { return reflect.New(reflect.TypeOf(repo.poTemplate).Elem()).Interface().(model.IModel) } @@ -168,13 +216,28 @@ func (repo *commonResourceRepoImpl) parseWhereConditions2Sql(conditions []*dto.G } dbFieldName := namer.ColumnName("", field.Name) if field.Type.Name() == "Time" { - cond.Value1, _ = time.ParseInLocation("2006/01/02 15:04:05", cond.Value1.(string), time.Local) - cond.Value2, _ = time.ParseInLocation("2006/01/02 15:04:05", cond.Value2.(string), time.Local) + if cond.Value1 == nil { + cond.Value1 = time.Time{} + } else { + cond.Value1, _ = time.ParseInLocation("2006/01/02 15:04:05", cond.Value1.(string), time.Local) + } + if cond.Value2 == nil { + cond.Value2 = time.Time{} + } else { + cond.Value2, _ = time.ParseInLocation("2006/01/02 15:04:05", cond.Value2.(string), time.Local) + } + } switch field.Tag.Get("where") { case "eq": - whereClause = append(whereClause, fmt.Sprintf("`%v` = ?", dbFieldName)) - whereArgs = append(whereArgs, cond.Value1) + if field.Tag.Get("type") == "[]string" && field.Tag.Get("multi_choice") == "true" { + // eq也要查出来为空的 + whereClause = append(whereClause, fmt.Sprintf("JSON_CONTAINS( `%v`, '\"%v\"', '$' ) or `%v` IS NULL", + dbFieldName, cond.Value1, dbFieldName)) + } else { + whereClause = append(whereClause, fmt.Sprintf("`%v` = ?", dbFieldName)) + whereArgs = append(whereArgs, cond.Value1) + } case "gt": whereClause = append(whereClause, fmt.Sprintf("`%v` > ?", dbFieldName)) whereArgs = append(whereArgs, cond.Value1) @@ -191,10 +254,10 @@ func (repo *commonResourceRepoImpl) parseWhereConditions2Sql(conditions []*dto.G whereClause = append(whereClause, fmt.Sprintf("`%v` like ?", dbFieldName)) whereArgs = append(whereArgs, cond.Value1) case "range": - - if t1, ok1 := cond.Value1.(time.Time); ok1 { - t2, _ := cond.Value2.(time.Time) - if t1.IsZero() && t2.IsZero() { + t1, ok1 := cond.Value1.(time.Time) + t2, ok2 := cond.Value2.(time.Time) + if ok1 || ok2 { + if !t1.IsZero() && !t2.IsZero() { whereClause = append(whereClause, fmt.Sprintf("`%v` >= ? and `%v` <= ?", dbFieldName, dbFieldName)) whereArgs = append(whereArgs, cond.Value1, cond.Value2) } else if !t1.IsZero() { diff --git a/admin/apps/game/model/ban.go b/admin/apps/game/model/ban.go index fccdee1..7fe9e0b 100644 --- a/admin/apps/game/model/ban.go +++ b/admin/apps/game/model/ban.go @@ -23,6 +23,7 @@ type Ban struct { ExpireAt sql.NullTime `name:"封禁到期时间" desc:"封禁到期时间,0表示永久封禁"` CreatedAt time.Time `readonly:"true"` + UpdatedAt time.Time `readonly:"true"` } func (lm *Ban) TableName() string { diff --git a/admin/apps/game/model/cdkey.go b/admin/apps/game/model/cdkey.go index eae4c6f..314f6cc 100644 --- a/admin/apps/game/model/cdkey.go +++ b/admin/apps/game/model/cdkey.go @@ -14,9 +14,9 @@ func init() { type CDKey struct { ID int `gorm:"primarykey" readonly:"true"` - ProjectId int `gorm:"index:idx_project_id"` + ProjectId int `gorm:"uniqueIndex:idx_project_cdkey"` ServerIDs []string `gorm:"type:json;serializer:json" name:"区服" desc:"不选就是全服通用" type:"[]string" choices:"GetChoiceServers" multi_choice:"true" uneditable:"true""` - Name string `gorm:"type:varchar(100);unique" name:"礼包说明" required:"true" uneditable:"true"` + Name string `gorm:"type:varchar(100);uniqueIndex:idx_project_cdkey" name:"礼包说明" required:"true" uneditable:"true"` CodeType int `name:"礼包类型" required:"true" choices:"GetCodeTypeChoices" uneditable:"true"` Code string `gorm:"type:VARCHAR(50);index" name:"礼包码" desc:"一码通用才配置" uneditable:"true"` CodeNum int `name:"礼包数量" desc:"一码一用才配置"` @@ -25,6 +25,7 @@ type CDKey struct { Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"礼包码奖励道具" type:"items" desc:"搜索道具并点击添加"` CreatedAt time.Time `readonly:"true"` + UpdatedAt time.Time `readonly:"true"` } func (lm *CDKey) TableName() string { diff --git a/admin/apps/game/model/cdkey_used.go b/admin/apps/game/model/cdkey_used.go index 7835446..87a2046 100644 --- a/admin/apps/game/model/cdkey_used.go +++ b/admin/apps/game/model/cdkey_used.go @@ -21,6 +21,7 @@ type CDKeyUsed struct { IP string `gorm:"type:varchar(20)"` DeviceId string `gorm:"type:varchar(50)"` CreatedAt time.Time `readonly:"true"` + UpdatedAt time.Time `readonly:"true"` } func (lm *CDKeyUsed) TableName() string { diff --git a/admin/apps/game/model/dto/common.go b/admin/apps/game/model/dto/common.go index dc576b0..298553e 100644 --- a/admin/apps/game/model/dto/common.go +++ b/admin/apps/game/model/dto/common.go @@ -39,7 +39,8 @@ type CommonDtoValues map[string]any type CommonDtoList struct { FieldsDesc []*CommonDtoFieldDesc `json:"fields_desc"` // 数据字段描述信息 TotalCount int `json:"total_count"` - Rows []CommonDtoValues `json:"rows"` // 数据行 + Rows []CommonDtoValues `json:"rows"` // 数据行 + ItemBags []*ItemBagInfo `json:"item_bags"` // 礼包,用来给道具添加功能选择用 } type PathInfo struct { @@ -61,6 +62,11 @@ type ItemInfo struct { Desc string `json:"desc"` } +type ItemBagInfo struct { + Name string `json:"name"` + Items []*ItemInfo `json:"items"` +} + type AccountDetailOrderInfo struct { ServerId string `json:"server_id"` AccountId string `json:"account_id"` diff --git a/admin/apps/game/model/globalmail.go b/admin/apps/game/model/globalmail.go index dd63664..fbcacc2 100644 --- a/admin/apps/game/model/globalmail.go +++ b/admin/apps/game/model/globalmail.go @@ -3,6 +3,7 @@ package model import ( "admin/apps/game/model/dto" "admin/internal/db" + "database/sql" "time" ) @@ -11,14 +12,20 @@ func init() { } type GlobalMail struct { - ID int `gorm:"primarykey" readonly:"true"` - ProjectId int `gorm:"index:idx_project_id"` - ServerIDs []string `gorm:"type:json;serializer:json" desc:"不选就是默认所有区服" name:"区服" type:"[]string" choices:"GetChoiceServers" multi_choice:"true"` - Title string `name:"邮件标题" required:"true"` - Content string `name:"邮件内容" required:"true"` - Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" desc:"搜索道具并点击添加"` + ID int `gorm:"primarykey" readonly:"true"` + ProjectId int `gorm:"index:idx_project_id"` + ServerIDs []string `gorm:"type:json;serializer:json" desc:"不选就是默认所有区服" name:"区服" type:"[]string" choices:"GetChoiceServers" multi_choice:"true"` + Title string `name:"邮件标题" required:"true"` + Content string `name:"邮件内容" required:"true"` + DelayInvokeCreateHook sql.NullTime `name:"邮件定时发送时间" desc:"不填或者无效就立即发送"` + ExpireAt sql.NullTime `name:"邮件到期时间" desc:"不填就是永久有效"` + CreateRoleTimeBefore sql.NullTime `name:"创角时间" desc:"在这时间之前创建的角色才能收到邮件,不填就是都生效"` + TotalPayMoney int `name:"充值金额大于" desc:"充值金额大于的才能收到邮件,不填就是没有要求"` + RoleLevel int `name:"角色等级大于" desc:"角色等级大于的才能收到邮件,不填就是没有要求"` + Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" desc:"搜索道具并点击添加"` CreatedAt time.Time `readonly:"true" where:"range"` + UpdatedAt time.Time `readonly:"true"` } func (lm *GlobalMail) TableName() string { diff --git a/admin/apps/game/model/item_bag.go b/admin/apps/game/model/item_bag.go index 0d9512d..f01ef01 100644 --- a/admin/apps/game/model/item_bag.go +++ b/admin/apps/game/model/item_bag.go @@ -3,6 +3,8 @@ package model import ( "admin/apps/game/model/dto" "admin/internal/db" + "admin/internal/errcode" + "admin/internal/global" "time" ) @@ -13,11 +15,12 @@ func init() { // 道具包,配置一系列道具,供邮件、礼包码填写使用 type ItemBag struct { ID int `gorm:"primarykey" readonly:"true"` - ProjectId int `gorm:"uniqueIndex:idx_whitelist"` - Name string `gorm:"type:varchar(255);uniqu" name:"礼包名称" desc:"请输入礼包名称,全项目唯一" required:"true" where:"eq"` - Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" desc:"搜索道具并点击添加"` + ProjectId int `gorm:"uniqueIndex:idx_bag"` + Name string `gorm:"type:varchar(255);uniqueIndex:idx_bag" name:"礼包名称" desc:"请输入礼包名称,全项目唯一" required:"true" where:"eq"` + Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" desc:"搜索道具并点击添加" required:"true"` CreatedAt time.Time `readonly:"true" where:"range"` + UpdatedAt time.Time `readonly:"true"` } func (lm *ItemBag) TableName() string { @@ -38,3 +41,12 @@ func (m *ItemBag) GetWhitelistTypeChoices(project *Project) []*dto.CommonDtoFiel {Desc: "账号", Value: "account"}, } } + +func (m *ItemBag) List(projectId int) ([]*ItemBag, error) { + list := make([]*ItemBag, 0) + err := global.GLOB_DB.Where("project_id = ?", projectId).Order("created_at desc").Find(&list).Error + if err != nil { + return list, errcode.New(errcode.DBError, "list all item bag error:%v", err) + } + return list, nil +} diff --git a/admin/apps/game/model/notice.go b/admin/apps/game/model/notice.go index 82ec4a5..4b6be23 100644 --- a/admin/apps/game/model/notice.go +++ b/admin/apps/game/model/notice.go @@ -19,6 +19,7 @@ type Notice struct { EndAt time.Time `name:"结束时间" required:"true"` CreatedAt time.Time `readonly:"true"` + UpdatedAt time.Time `readonly:"true"` } func (lm *Notice) TableName() string { diff --git a/admin/apps/game/model/project.go b/admin/apps/game/model/project.go index 3dfb64a..e8ee032 100644 --- a/admin/apps/game/model/project.go +++ b/admin/apps/game/model/project.go @@ -28,6 +28,7 @@ type Project struct { ApiAddr string `name:"游戏api地址" desc:"api服务器地址,例如神魔大陆就是alisrv服务器地址,用于后台调用gm"` SortWeight int `name:"菜单排序" desc:"越大越靠前"` CreatedAt time.Time `readonly:"true"` + UpdatedAt time.Time `readonly:"true"` } func (lm *Project) TableName() string { diff --git a/admin/apps/game/model/rolemail.go b/admin/apps/game/model/rolemail.go index 0d2d5a0..bdb6e93 100644 --- a/admin/apps/game/model/rolemail.go +++ b/admin/apps/game/model/rolemail.go @@ -27,6 +27,7 @@ type RoleMail struct { Attach []*MailAttachItem `gorm:"type:json;serializer:json" name:"邮件附件" type:"items" desc:"搜索道具并点击添加"` CreatedAt time.Time `readonly:"true" where:"range"` + UpdatedAt time.Time `readonly:"true"` } func (lm *RoleMail) TableName() string { diff --git a/admin/apps/game/model/server.go b/admin/apps/game/model/server.go index bf2bf6b..195aae1 100644 --- a/admin/apps/game/model/server.go +++ b/admin/apps/game/model/server.go @@ -29,6 +29,7 @@ type Server struct { //ApiAddr string CreatedAt time.Time `readonly:"true"` + UpdatedAt time.Time `readonly:"true"` } func (lm *Server) TableName() string { diff --git a/admin/apps/game/model/support_account.go b/admin/apps/game/model/support_account.go index e690de1..b7eb997 100644 --- a/admin/apps/game/model/support_account.go +++ b/admin/apps/game/model/support_account.go @@ -17,6 +17,7 @@ type SupportAccount struct { Account string `gorm:"type:varchar(200);uniqueIndex:idx_account;" name:"账号" desc:"用逗号标记多个" required:"true"` CreatedAt time.Time `readonly:"true" where:"range"` + UpdatedAt time.Time `readonly:"true"` } func (lm *SupportAccount) TableName() string { diff --git a/admin/apps/game/model/whitelist.go b/admin/apps/game/model/whitelist.go index ae855c3..1739d86 100644 --- a/admin/apps/game/model/whitelist.go +++ b/admin/apps/game/model/whitelist.go @@ -18,6 +18,7 @@ type WhiteList struct { Value string `gorm:"type:varchar(128);uniqueIndex:idx_whitelist" name:"账号或者ip等值" required:"true"` CreatedAt time.Time `readonly:"true" where:"range"` + UpdatedAt time.Time `readonly:"true"` } func (lm *WhiteList) TableName() string { diff --git a/admin/apps/game/service/service.go b/admin/apps/game/service/service.go index d1ca076..8bff2bf 100644 --- a/admin/apps/game/service/service.go +++ b/admin/apps/game/service/service.go @@ -58,7 +58,11 @@ func (svc *Service) CommonList(ctx context.Context, projectId int, resourceName }}, params.ParsedWhereConditions.Conditions...) } totalCount, fieldsDescInfo, rows, err := svc.resourceSvc.List(projectId, resourceName, params) - return &dto.CommonDtoList{FieldsDesc: fieldsDescInfo, TotalCount: totalCount, Rows: rows}, err + itemBags, err := svc.projectSvc.GetAllItemBag(projectId) + if err != nil { + return nil, err + } + return &dto.CommonDtoList{FieldsDesc: fieldsDescInfo, TotalCount: totalCount, Rows: rows, ItemBags: itemBags}, err } func (svc *Service) CommonPost(ctx context.Context, projectId int, resourceName string, params dto.CommonDtoValues) (dto.CommonDtoValues, error) { diff --git a/admin/internal/event/topic.go b/admin/internal/event/topic.go index 47db473..6f7fb64 100644 --- a/admin/internal/event/topic.go +++ b/admin/internal/event/topic.go @@ -1,7 +1,10 @@ package event +import "time" + const ( - EventTopic_UserExecute = "user.execute" + EventTopic_UserExecute = "user.execute" + EventTopic_DelayInvokeCreateHook = "resource.create.delay" ) type EventPayload_UserExecute struct { @@ -14,3 +17,10 @@ type EventPayload_UserExecute struct { NewData any `json:"new_data"` Any any `json:"any"` } + +type EventPayload_DelayInvokeCreateHook struct { + ProjectId int + Resource string + DelayAt time.Time + Obj []byte +} diff --git a/admin/internal/global/var.go b/admin/internal/global/var.go index 3ab3d19..70ec3e6 100644 --- a/admin/internal/global/var.go +++ b/admin/internal/global/var.go @@ -3,6 +3,7 @@ package global import ( "admin/internal/config" "admin/lib/cache" + "admin/lib/dtimer" "admin/lib/web" "gorm.io/gorm" ) @@ -12,4 +13,5 @@ var ( GLOB_DB *gorm.DB // 数据库 GLOB_API_ENGINE *web.Engine // 全局api服务器 GLOB_LOCAL_CACHER = cache.NewLocalCache() // 全局本地内存缓存器 + GLOB_TIMER = dtimer.NewDTimerMgr() // 全局定时器 ) diff --git a/admin/lib/dtimer/timer.go b/admin/lib/dtimer/timer.go index c121b70..f132002 100644 --- a/admin/lib/dtimer/timer.go +++ b/admin/lib/dtimer/timer.go @@ -1,9 +1,100 @@ package dtimer +import ( + "admin/lib/xlog" + "fmt" + "runtime" + "sync" + "time" +) + /* 分布式定时器,用于解决比如多个进程启动后,只有一个进程触发定时器事件执行, 目标不影响架构复杂性,做个能用的出来。思路是用mysql做分布式锁,多个进程抢占单例执行 */ -type DTimer struct { +type Timer struct { + Key string + Duration time.Duration + IsInterval bool + Callback func() + expireAt time.Time + timerMgr *DTimerMgr +} + +// todo 先实现一个本地数据库单例版本的 +type DTimerMgr struct { + locker *sync.Mutex + timers map[string]*Timer + taskQueue chan *Timer +} + +func NewDTimerMgr() *DTimerMgr { + mgr := new(DTimerMgr) + mgr.locker = new(sync.Mutex) + mgr.timers = make(map[string]*Timer) + mgr.taskQueue = make(chan *Timer, 1024) + go mgr.start() + return mgr +} + +func (mgr *DTimerMgr) Add(task *Timer) (*Timer, bool) { + mgr.locker.Lock() + defer mgr.locker.Unlock() + + old, find := mgr.timers[task.Key] + task.expireAt = time.Now().Add(task.Duration) + mgr.timers[task.Key] = task + return old, find +} + +func (mgr *DTimerMgr) Del(key string) { + mgr.locker.Lock() + defer mgr.locker.Unlock() + delete(mgr.timers, key) +} + +func (mgr *DTimerMgr) start() { + for i := 0; i < runtime.NumCPU(); i++ { + go func() { + for { + select { + case t := <-mgr.taskQueue: + func() { + defer xlog.CatchWithInfo(fmt.Sprintf("handle timer %v panic", t.Key)) + t.Callback() + }() + } + } + }() + } + + tk := time.NewTicker(time.Second) + defer tk.Stop() + for { + select { + case <-tk.C: + mgr.locker.Lock() + mgr.checkExpire() + mgr.locker.Unlock() + } + } +} + +func (mgr *DTimerMgr) checkExpire() { + timeNow := time.Now() + for k, v := range mgr.timers { + if v.expireAt.Before(timeNow) { + if !v.IsInterval { + delete(mgr.timers, k) + } else { + v.expireAt = timeNow.Add(v.Duration) + } + select { + case mgr.taskQueue <- v: + default: + xlog.Warnf("timer channel full!!!") + } + } + } } diff --git a/admin/lib/dtimer/timer_test.go b/admin/lib/dtimer/timer_test.go new file mode 100644 index 0000000..912c5e9 --- /dev/null +++ b/admin/lib/dtimer/timer_test.go @@ -0,0 +1,21 @@ +package dtimer + +import ( + "log" + "testing" + "time" +) + +func TestTimer(t *testing.T) { + mgr := NewDTimerMgr() + log.Printf("start ...") + mgr.Add(&Timer{ + Key: "test1", + Duration: time.Second * 5, + Callback: func() { + log.Printf("trigger timer") + }, + }) + + select {} +} diff --git a/ui/src/components/restful/table.vue b/ui/src/components/restful/table.vue index 9a76bdc..db29c1e 100644 --- a/ui/src/components/restful/table.vue +++ b/ui/src/components/restful/table.vue @@ -40,6 +40,7 @@ const fieldsDescInfo = ref([]) const whereFieldsDescInfo = ref([]) const calcElColSpan = ref(0) const rows = ref([]) +const itemBags = ref([]) const rules = ref({}) const current_page = ref(1) @@ -47,10 +48,10 @@ const page_size = ref(10) const pageSizes = [10, 20, 50, 100] const totalRowCount = ref(0) -const item = ref({ - id: '', - number: 1, -}) +const selectedItem = ref(null) +const selectedItemNum = ref(0) + +const selectedItemBag = ref(null) // console.log("enter table, resource:", cachedResource) @@ -68,7 +69,7 @@ const listData = async () => { conditions: [] } whereFieldsDescInfo.value.forEach((field) => { - if (field.value1) { + if (field.value1 || field.value2) { whereReqConditions.conditions.push({ key: field.key, op: field.where, @@ -85,20 +86,35 @@ const listData = async () => { fieldsDescInfo.value = listRsp.value.data.fields_desc totalRowCount.value = listRsp.value.data.total_count rows.value = listRsp.value.data.rows + itemBags.value = listRsp.value.data.item_bags for (let i = 0; i < fieldsDescInfo.value.length; i++) { var field = fieldsDescInfo.value[i] // dialogObjectForm.value[field.key] = '' - if (field.required == true) { + if (field.required === true) { rules.value[field.key] = [{required: true, message: field.name + "不能为空", trigger: "blur"}] } - if (field.type == "items") { + if (field.type === "items") { dialogObjectForm.value[field.key] = [] for (let j = 0; j < rows.value.length; j++) { rows.value[j].jsonValue = JSON.stringify(rows.value[j][field.key]) } + if (field.required === true) { + rules.value[field.key] = [{ + required: true, + validator: (rule, value, callback) => { + console.log("触发校验道具列表规则:", dialogObjectForm.value) + if (dialogObjectForm.value.Attach === undefined || dialogObjectForm.value.Attach.length === 0) { + callback(new Error("请至少填写一个奖励道具!")) + } else { + callback() + } + }, + trigger: ["blur", "change"], + }] + } } if (field.where !== "") { @@ -313,29 +329,64 @@ const handleDelete = (index, row) => { } function addItem(fieldDescInfo) { - if (item.value.id == null || item.value.id == '' || item.value.id < 0) { - ElMessage('请选择道具!') - return; + let hasValidInput = false; + if (selectedItemBag.value != null && selectedItemBag.value.name !== undefined && selectedItemBag.value.name !== '') { + selectedItemBag.value.items.forEach((bagItem) => { + if (typeof dialogObjectForm.value.Attach === typeof "") { + dialogObjectForm.value.Attach = []; + } + let d = {id: bagItem.item_id, num: bagItem.item_num, desc: bagItem.desc, item_type: bagItem.item_type}; + dialogObjectForm.value.Attach.push(d); + }) + console.log("添加礼包:", selectedItemBag.value) + hasValidInput = true; } - if (item.value.num == null || item.value.num == '' || item.value.num <= 0) { - ElMessage('请输入有效道具数量!') - return; + if (selectedItem.value !== null && selectedItem.value.value !== undefined && selectedItem.value.value !== '') { + if (selectedItemNum.value <= 0) { + ElMessage('请输入有效道具数量!') + return; + } + let d = { + id: selectedItem.value.value, + num: Number(selectedItemNum.value), + desc: selectedItem.value.desc, + item_type: selectedItem.value.type + }; + + console.log("add item:", d) + + if (typeof dialogObjectForm.value.Attach === typeof "") { + dialogObjectForm.value.Attach = []; + } + dialogObjectForm.value.Attach.push(d); + hasValidInput = true; + } + if (!hasValidInput) { + console.log("道具:", selectedItem.value) + console.log("礼包:", selectedItemBag.value) + ElMessage('请选择道具或者礼包!') } - let d = {id: item.value.id, num: Number(item.value.num), desc: item.value.desc, item_type: item.value.item_type}; - - console.log("add item:", d) - - if (typeof dialogObjectForm.value.Attach === typeof "") { - dialogObjectForm.value.Attach = []; + if (dialogAddVisible.value) { + dialogAddFormRef.value.validateField("Attach"); + } else if (dialogEditVisible.value) { + // console.log("删除道具,准备校验表单规则", rules.value) + // console.log("删除道具,准备校验表单规则", dialogEditFormRef.value) + dialogEditFormRef.value.validateField("Attach"); } - dialogObjectForm.value.Attach.push(d); } function deleteItem(row) { // 移除该对象 let number = dialogObjectForm.value.Attach.findIndex(item => item === row); dialogObjectForm.value.Attach.splice(number, 1); + + if (dialogAddVisible.value) { + dialogAddFormRef.value.validateField("Attach"); + } else if (dialogEditVisible.value) { + // console.log("删除道具,准备校验表单规则", rules.value) + dialogEditFormRef.value.validateField("Attach"); + } } const handleCloseDialog = () => { @@ -345,18 +396,14 @@ const handleCloseDialog = () => { dialogObjectForm.value = { Attach: [], } - item.value.desc = '' + selectedItem.value = null + selectedItemNum.value = 0 + selectedItemBag.value = null } const loadingRemoteItems = ref(false) const itemChoices = ref({}) -const handleItemOnSelect = (itemOption) => { - console.log("选中:", itemOption) - item.value.id = itemOption.value - item.value.desc = itemOption.desc -} - const handleQueryItem = (itemQueryStr) => { if (!itemQueryStr) { itemChoices.value = [] @@ -465,13 +512,24 @@ const handlePaginationCurChange = (val) => { - + @@ -547,15 +605,14 @@ const handlePaginationCurChange = (val) => {