439 lines
14 KiB
Go
439 lines
14 KiB
Go
package taskcenter
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// GetConfig 返回后台任务配置。
|
|
func (s *Service) GetConfig(ctx context.Context, sysOrigin string, taskCategory string) (*ConfigResponse, error) {
|
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
|
category := normalizeCategory(taskCategory)
|
|
if err := validateCategory(category); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var configRow model.TaskCenterConfig
|
|
err := s.db.WithContext(ctx).
|
|
Where("sys_origin = ?", sysOrigin).
|
|
First(&configRow).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &ConfigResponse{
|
|
Configured: false,
|
|
SysOrigin: sysOrigin,
|
|
Enabled: false,
|
|
Timezone: s.defaultTimezone(),
|
|
TaskCategory: category,
|
|
Tasks: []TaskConfigPayload{},
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tasks, err := s.loadTaskConfigs(ctx, configRow.ID, category, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ConfigResponse{
|
|
Configured: true,
|
|
ID: configRow.ID,
|
|
SysOrigin: configRow.SysOrigin,
|
|
Enabled: configRow.Enabled,
|
|
Timezone: normalizeTimezone(configRow.Timezone, s.defaultTimezone()),
|
|
TaskCategory: category,
|
|
UpdateTime: formatDateTime(configRow.UpdateTime),
|
|
Tasks: buildTaskConfigPayloads(tasks),
|
|
}, nil
|
|
}
|
|
|
|
// SaveConfig 保存后台任务配置。后台每日任务页支持 DAILY 和一次性的 NEWBIE 专属任务。
|
|
func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
|
|
requestID := req.ID.Int64()
|
|
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
|
category := normalizeCategory(req.TaskCategory)
|
|
if err := validateCategory(category); err != nil {
|
|
return nil, err
|
|
}
|
|
timezone := normalizeTimezone(req.Timezone, s.defaultTimezone())
|
|
if _, err := time.LoadLocation(timezone); err != nil {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
|
|
}
|
|
|
|
tasks, err := s.normalizeTaskInputs(req.Tasks, category)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Enabled && !hasEnabledTask(tasks) {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_tasks", "enabled config must contain at least one enabled task")
|
|
}
|
|
|
|
var savedConfigID int64
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
now := time.Now()
|
|
var configRow model.TaskCenterConfig
|
|
if requestID > 0 {
|
|
err := tx.Where("id = ?", requestID).First(&configRow).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return NewAppError(http.StatusNotFound, "task_center_config_not_found", "config not found")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(req.SysOrigin) == "" {
|
|
sysOrigin = configRow.SysOrigin
|
|
}
|
|
} else {
|
|
err := tx.Where("sys_origin = ?", sysOrigin).First(&configRow).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
configID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return idErr
|
|
}
|
|
configRow = model.TaskCenterConfig{
|
|
ID: configID,
|
|
SysOrigin: sysOrigin,
|
|
CreateTime: now,
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
configRow.SysOrigin = sysOrigin
|
|
configRow.Enabled = req.Enabled
|
|
configRow.Timezone = timezone
|
|
configRow.UpdateTime = now
|
|
if configRow.CreateTime.IsZero() {
|
|
configRow.CreateTime = now
|
|
}
|
|
if err := tx.Save(&configRow).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
keepIDs := make([]int64, 0, len(tasks))
|
|
for index := range tasks {
|
|
if tasks[index].ID > 0 {
|
|
var existing model.TaskCenterTaskConfig
|
|
err := tx.Where("id = ? AND config_id = ? AND task_category = ?", tasks[index].ID, configRow.ID, category).
|
|
First(&existing).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return NewAppError(http.StatusNotFound, "task_config_not_found", "task config not found")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tasks[index].CreateTime = existing.CreateTime
|
|
} else {
|
|
taskID, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return idErr
|
|
}
|
|
tasks[index].ID = taskID
|
|
tasks[index].CreateTime = now
|
|
}
|
|
tasks[index].ConfigID = configRow.ID
|
|
tasks[index].SysOrigin = sysOrigin
|
|
tasks[index].TaskCategory = category
|
|
tasks[index].UpdateTime = now
|
|
if tasks[index].CreateTime.IsZero() {
|
|
tasks[index].CreateTime = now
|
|
}
|
|
if err := tx.Save(&tasks[index]).Error; err != nil {
|
|
return err
|
|
}
|
|
keepIDs = append(keepIDs, tasks[index].ID)
|
|
}
|
|
|
|
deleteQuery := tx.Where("config_id = ? AND task_category = ?", configRow.ID, category)
|
|
if len(keepIDs) > 0 {
|
|
deleteQuery = deleteQuery.Where("id NOT IN ?", keepIDs)
|
|
}
|
|
if err := deleteQuery.Delete(&model.TaskCenterTaskConfig{}).Error; err != nil {
|
|
return err
|
|
}
|
|
savedConfigID = configRow.ID
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.GetConfig(ctx, taskCenterSysOriginBySavedID(ctx, s.db, savedConfigID, sysOrigin), category)
|
|
}
|
|
|
|
// PageClaimRecords 分页查询领取记录。
|
|
func (s *Service) PageClaimRecords(ctx context.Context, sysOrigin string, taskCategory string, userID int64, taskCode string, status string, cursor int, limit int) (*PageResponse[ClaimRecordView], error) {
|
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
|
category := normalizeCategory(taskCategory)
|
|
if err := validateCategory(category); err != nil {
|
|
return nil, err
|
|
}
|
|
cursor, limit = NormalizePage(cursor, limit)
|
|
|
|
query := s.db.WithContext(ctx).Model(&model.TaskCenterClaimRecord{}).
|
|
Where("sys_origin = ? AND task_category = ?", sysOrigin, category)
|
|
if userID > 0 {
|
|
query = query.Where("user_id = ?", userID)
|
|
}
|
|
if code := strings.ToUpper(strings.TrimSpace(taskCode)); code != "" {
|
|
query = query.Where("task_code = ?", code)
|
|
}
|
|
if normalizedStatus := strings.ToUpper(strings.TrimSpace(status)); normalizedStatus != "" {
|
|
query = query.Where("status = ?", normalizedStatus)
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
var rows []model.TaskCenterClaimRecord
|
|
if err := query.Order("create_time desc, id desc").
|
|
Offset((cursor - 1) * limit).
|
|
Limit(limit).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
records := make([]ClaimRecordView, 0, len(rows))
|
|
for _, row := range rows {
|
|
records = append(records, claimRecordView(row))
|
|
}
|
|
return &PageResponse[ClaimRecordView]{Records: records, Total: total, Current: cursor, Size: limit}, nil
|
|
}
|
|
|
|
// PageEvents 分页查询事件流水。
|
|
func (s *Service) PageEvents(ctx context.Context, sysOrigin string, taskCategory string, userID int64, taskCode string, eventType string, cursor int, limit int) (*PageResponse[EventView], error) {
|
|
sysOrigin = s.normalizeSysOrigin(sysOrigin)
|
|
category := normalizeCategory(taskCategory)
|
|
if err := validateCategory(category); err != nil {
|
|
return nil, err
|
|
}
|
|
cursor, limit = NormalizePage(cursor, limit)
|
|
|
|
query := s.db.WithContext(ctx).Model(&model.TaskCenterEvent{}).
|
|
Where("sys_origin = ? AND task_category = ?", sysOrigin, category)
|
|
if userID > 0 {
|
|
query = query.Where("user_id = ?", userID)
|
|
}
|
|
if code := strings.ToUpper(strings.TrimSpace(taskCode)); code != "" {
|
|
query = query.Where("task_code = ?", code)
|
|
}
|
|
if normalizedEventType := strings.ToUpper(strings.TrimSpace(eventType)); normalizedEventType != "" {
|
|
query = query.Where("event_type = ?", normalizedEventType)
|
|
}
|
|
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
var rows []model.TaskCenterEvent
|
|
if err := query.Order("create_time desc, id desc").
|
|
Offset((cursor - 1) * limit).
|
|
Limit(limit).
|
|
Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
records := make([]EventView, 0, len(rows))
|
|
for _, row := range rows {
|
|
records = append(records, eventView(row))
|
|
}
|
|
return &PageResponse[EventView]{Records: records, Total: total, Current: cursor, Size: limit}, nil
|
|
}
|
|
|
|
func (s *Service) normalizeTaskInputs(inputs []SaveTaskConfigInput, category string) ([]model.TaskCenterTaskConfig, error) {
|
|
if len(inputs) == 0 {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_tasks", "tasks is required")
|
|
}
|
|
|
|
seenCodes := make(map[string]struct{}, len(inputs))
|
|
rows := make([]model.TaskCenterTaskConfig, 0, len(inputs))
|
|
for _, item := range inputs {
|
|
taskCategory := strings.ToUpper(strings.TrimSpace(item.TaskCategory))
|
|
if taskCategory == "" {
|
|
taskCategory = category
|
|
} else {
|
|
taskCategory = normalizeCategory(taskCategory)
|
|
}
|
|
if taskCategory != category {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_task_category", "taskCategory does not match request category")
|
|
}
|
|
taskCode := strings.ToUpper(strings.TrimSpace(item.TaskCode))
|
|
if taskCode == "" {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_task_code", "taskCode is required")
|
|
}
|
|
if _, exists := seenCodes[taskCode]; exists {
|
|
return nil, NewAppError(http.StatusBadRequest, "duplicate_task_code", "taskCode must not duplicate")
|
|
}
|
|
seenCodes[taskCode] = struct{}{}
|
|
|
|
taskName := strings.TrimSpace(item.TaskName)
|
|
if taskName == "" {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_task_name", "taskName is required")
|
|
}
|
|
conditionType := strings.ToUpper(strings.TrimSpace(item.ConditionType))
|
|
if conditionType == "" {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_condition_type", "conditionType is required")
|
|
}
|
|
if !isAllowedConditionType(conditionType) {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_condition_type", "conditionType is not supported")
|
|
}
|
|
if item.TargetValue <= 0 {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_target_value", "targetValue must be greater than 0")
|
|
}
|
|
if item.RewardGold <= 0 {
|
|
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_gold", "rewardGold must be greater than 0")
|
|
}
|
|
rows = append(rows, model.TaskCenterTaskConfig{
|
|
ID: item.ID.Int64(),
|
|
TaskCode: taskCode,
|
|
TaskCategory: category,
|
|
Enabled: item.Enabled,
|
|
TaskName: taskName,
|
|
TaskDesc: strings.TrimSpace(item.TaskDesc),
|
|
ConditionType: conditionType,
|
|
TargetValue: item.TargetValue,
|
|
TargetUnit: strings.TrimSpace(item.TargetUnit),
|
|
RewardGold: item.RewardGold,
|
|
JumpType: normalizeJumpType(item.JumpType),
|
|
JumpPage: strings.TrimSpace(item.JumpPage),
|
|
TaskIcon: strings.TrimSpace(item.TaskIcon),
|
|
Cover: strings.TrimSpace(item.Cover),
|
|
SortOrder: item.SortOrder,
|
|
})
|
|
}
|
|
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
if rows[i].SortOrder == rows[j].SortOrder {
|
|
return rows[i].TaskCode < rows[j].TaskCode
|
|
}
|
|
return rows[i].SortOrder < rows[j].SortOrder
|
|
})
|
|
return rows, nil
|
|
}
|
|
|
|
func (s *Service) loadTaskConfigs(ctx context.Context, configID int64, category string, enabledOnly bool) ([]model.TaskCenterTaskConfig, error) {
|
|
if configID <= 0 {
|
|
return []model.TaskCenterTaskConfig{}, nil
|
|
}
|
|
query := s.db.WithContext(ctx).
|
|
Where("config_id = ? AND task_category = ?", configID, normalizeCategory(category))
|
|
if enabledOnly {
|
|
query = query.Where("enabled = ?", true)
|
|
}
|
|
var rows []model.TaskCenterTaskConfig
|
|
if err := query.Order("sort_order asc, id asc").Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func buildTaskConfigPayloads(rows []model.TaskCenterTaskConfig) []TaskConfigPayload {
|
|
result := make([]TaskConfigPayload, 0, len(rows))
|
|
for _, row := range rows {
|
|
result = append(result, TaskConfigPayload{
|
|
ID: row.ID,
|
|
TaskCode: row.TaskCode,
|
|
TaskCategory: row.TaskCategory,
|
|
Enabled: row.Enabled,
|
|
TaskName: row.TaskName,
|
|
TaskDesc: row.TaskDesc,
|
|
ConditionType: row.ConditionType,
|
|
TargetValue: row.TargetValue,
|
|
TargetUnit: row.TargetUnit,
|
|
RewardGold: row.RewardGold,
|
|
JumpType: row.JumpType,
|
|
JumpPage: row.JumpPage,
|
|
TaskIcon: row.TaskIcon,
|
|
Cover: row.Cover,
|
|
SortOrder: row.SortOrder,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func claimRecordView(row model.TaskCenterClaimRecord) ClaimRecordView {
|
|
return ClaimRecordView{
|
|
ID: row.ID,
|
|
SysOrigin: row.SysOrigin,
|
|
UserID: row.UserID,
|
|
TaskID: row.TaskID,
|
|
TaskCode: row.TaskCode,
|
|
TaskName: row.TaskName,
|
|
TaskCategory: row.TaskCategory,
|
|
CycleKey: row.CycleKey,
|
|
RewardGold: row.RewardGold,
|
|
WalletEventID: row.WalletEventID,
|
|
Status: row.Status,
|
|
FailureReason: row.FailureReason,
|
|
ClaimedAt: formatDateTimePtr(row.ClaimedAt),
|
|
CreateTime: formatDateTime(row.CreateTime),
|
|
UpdateTime: formatDateTime(row.UpdateTime),
|
|
}
|
|
}
|
|
|
|
func eventView(row model.TaskCenterEvent) EventView {
|
|
return EventView{
|
|
ID: row.ID,
|
|
SysOrigin: row.SysOrigin,
|
|
EventID: row.EventID,
|
|
EventType: row.EventType,
|
|
UserID: row.UserID,
|
|
TaskID: row.TaskID,
|
|
TaskCode: row.TaskCode,
|
|
TaskCategory: row.TaskCategory,
|
|
CycleKey: row.CycleKey,
|
|
DeltaValue: row.DeltaValue,
|
|
CompletedValue: row.CompletedValue,
|
|
OccurredAt: formatDateTime(row.OccurredAt),
|
|
CreateTime: formatDateTime(row.CreateTime),
|
|
UpdateTime: formatDateTime(row.UpdateTime),
|
|
}
|
|
}
|
|
|
|
func taskCenterSysOriginBySavedID(ctx context.Context, db taskCenterDB, configID int64, fallback string) string {
|
|
if configID <= 0 {
|
|
return fallback
|
|
}
|
|
var row model.TaskCenterConfig
|
|
if err := db.WithContext(ctx).Where("id = ?", configID).First(&row).Error; err == nil && strings.TrimSpace(row.SysOrigin) != "" {
|
|
return row.SysOrigin
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func hasEnabledTask(tasks []model.TaskCenterTaskConfig) bool {
|
|
for _, task := range tasks {
|
|
if task.Enabled {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func normalizeTimezone(value string, fallback string) string {
|
|
timezone := strings.TrimSpace(value)
|
|
if timezone == "" {
|
|
timezone = strings.TrimSpace(fallback)
|
|
}
|
|
if timezone == "" {
|
|
timezone = defaultTaskCenterTimezone
|
|
}
|
|
return timezone
|
|
}
|
|
|
|
func normalizeJumpType(value string) string {
|
|
jumpType := strings.ToUpper(strings.TrimSpace(value))
|
|
if jumpType == "" {
|
|
jumpType = "NONE"
|
|
}
|
|
return jumpType
|
|
}
|