2026-05-09 14:12:07 +08:00

698 lines
23 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{},
ConditionJumps: []ConditionJumpConfigPayload{},
}, nil
}
if err != nil {
return nil, err
}
tasks, err := s.loadTaskConfigs(ctx, configRow.ID, category, false)
if err != nil {
return nil, err
}
conditionJumps, err := s.loadConditionJumpConfigs(ctx, configRow.ID, category)
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),
ConditionJumps: buildConditionJumpConfigPayloads(conditionJumps, 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
}
conditionJumps, saveConditionJumps, err := s.normalizeConditionJumpInputs(req.ConditionJumps, category)
if err != nil {
return nil, err
}
replaceConditionJumps := saveConditionJumps
if !saveConditionJumps {
conditionJumps = buildConditionJumpConfigRowsFromTasks(tasks)
saveConditionJumps = len(conditionJumps) > 0
}
if saveConditionJumps {
applyConditionJumpsToTasks(tasks, conditionJumps)
}
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
}
if saveConditionJumps {
if err := s.saveConditionJumpConfigsTx(tx, configRow, category, conditionJumps, now, replaceConditionJumps); 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) normalizeConditionJumpInputs(inputs []SaveConditionJumpConfigInput, category string) ([]model.TaskCenterConditionJumpConfig, bool, error) {
if inputs == nil {
return nil, false, nil
}
seenConditions := make(map[string]struct{}, len(inputs))
rows := make([]model.TaskCenterConditionJumpConfig, 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, true, NewAppError(http.StatusBadRequest, "invalid_task_category", "condition jump taskCategory does not match request category")
}
conditionType := strings.ToUpper(strings.TrimSpace(item.ConditionType))
if conditionType == "" {
return nil, true, NewAppError(http.StatusBadRequest, "invalid_condition_type", "conditionType is required")
}
if !isAllowedConditionType(conditionType) {
return nil, true, NewAppError(http.StatusBadRequest, "invalid_condition_type", "conditionType is not supported")
}
if _, exists := seenConditions[conditionType]; exists {
return nil, true, NewAppError(http.StatusBadRequest, "duplicate_condition_type", "conditionType must not duplicate")
}
seenConditions[conditionType] = struct{}{}
rows = append(rows, model.TaskCenterConditionJumpConfig{
ID: item.ID.Int64(),
TaskCategory: category,
ConditionType: conditionType,
JumpType: normalizeJumpType(item.JumpType),
JumpPage: normalizeConditionJumpPage(item.JumpType, item.JumpPage),
SortOrder: item.SortOrder,
})
}
sort.SliceStable(rows, func(i, j int) bool {
if rows[i].SortOrder == rows[j].SortOrder {
return rows[i].ConditionType < rows[j].ConditionType
}
return rows[i].SortOrder < rows[j].SortOrder
})
return rows, true, nil
}
func applyConditionJumpsToTasks(tasks []model.TaskCenterTaskConfig, jumps []model.TaskCenterConditionJumpConfig) {
jumpByCondition := make(map[string]model.TaskCenterConditionJumpConfig, len(jumps))
for _, jump := range jumps {
jumpByCondition[strings.ToUpper(strings.TrimSpace(jump.ConditionType))] = jump
}
for index := range tasks {
conditionType := strings.ToUpper(strings.TrimSpace(tasks[index].ConditionType))
jump, exists := jumpByCondition[conditionType]
if !exists {
tasks[index].JumpType, tasks[index].JumpPage = defaultConditionJump(conditionType)
continue
}
tasks[index].JumpType = normalizeJumpType(jump.JumpType)
tasks[index].JumpPage = normalizeConditionJumpPage(jump.JumpType, jump.JumpPage)
}
}
func defaultConditionJump(conditionType string) (string, string) {
switch strings.ToUpper(strings.TrimSpace(conditionType)) {
case EventTypeRechargeGold:
return JumpTypeRechargeInApp, ""
case EventTypeMicDurationSeconds, EventTypeGameConsumeGold, EventTypeGiftConsumeGold:
return "ROOM_RANDOM_WITH_MIC", ""
default:
return "NONE", ""
}
}
func (s *Service) saveConditionJumpConfigsTx(tx *gorm.DB, configRow model.TaskCenterConfig, category string, rows []model.TaskCenterConditionJumpConfig, now time.Time, deleteMissing bool) error {
var existingRows []model.TaskCenterConditionJumpConfig
if err := tx.
Where("config_id = ? AND task_category = ?", configRow.ID, category).
Find(&existingRows).Error; err != nil {
return err
}
existingByCondition := make(map[string]model.TaskCenterConditionJumpConfig, len(existingRows))
for _, row := range existingRows {
existingByCondition[strings.ToUpper(strings.TrimSpace(row.ConditionType))] = row
}
keepIDs := make([]int64, 0, len(rows))
for index := range rows {
conditionType := strings.ToUpper(strings.TrimSpace(rows[index].ConditionType))
if rows[index].ID <= 0 {
if existing, exists := existingByCondition[conditionType]; exists {
rows[index].ID = existing.ID
rows[index].CreateTime = existing.CreateTime
}
}
if rows[index].ID <= 0 {
id, idErr := utils.NextID()
if idErr != nil {
return idErr
}
rows[index].ID = id
rows[index].CreateTime = now
}
if rows[index].CreateTime.IsZero() {
rows[index].CreateTime = now
}
rows[index].ConfigID = configRow.ID
rows[index].SysOrigin = configRow.SysOrigin
rows[index].TaskCategory = category
rows[index].ConditionType = conditionType
rows[index].JumpType = normalizeJumpType(rows[index].JumpType)
rows[index].JumpPage = normalizeConditionJumpPage(rows[index].JumpType, rows[index].JumpPage)
rows[index].UpdateTime = now
if err := tx.Save(&rows[index]).Error; err != nil {
return err
}
keepIDs = append(keepIDs, rows[index].ID)
}
if !deleteMissing {
return nil
}
deleteQuery := tx.Where("config_id = ? AND task_category = ?", configRow.ID, category)
if len(keepIDs) > 0 {
deleteQuery = deleteQuery.Where("id NOT IN ?", keepIDs)
}
return deleteQuery.Delete(&model.TaskCenterConditionJumpConfig{}).Error
}
func buildConditionJumpConfigRowsFromTasks(tasks []model.TaskCenterTaskConfig) []model.TaskCenterConditionJumpConfig {
seenConditions := make(map[string]struct{}, len(tasks))
rows := make([]model.TaskCenterConditionJumpConfig, 0, len(tasks))
for _, task := range tasks {
conditionType := strings.ToUpper(strings.TrimSpace(task.ConditionType))
if conditionType == "" {
continue
}
if _, exists := seenConditions[conditionType]; exists {
continue
}
jumpType := normalizeJumpType(task.JumpType)
jumpPage := strings.TrimSpace(task.JumpPage)
if jumpType == "NONE" && jumpPage == "" {
jumpType, jumpPage = defaultConditionJump(conditionType)
}
rows = append(rows, model.TaskCenterConditionJumpConfig{
TaskCategory: task.TaskCategory,
ConditionType: conditionType,
JumpType: jumpType,
JumpPage: normalizeConditionJumpPage(jumpType, jumpPage),
SortOrder: task.SortOrder,
})
seenConditions[conditionType] = struct{}{}
}
sort.SliceStable(rows, func(i, j int) bool {
if rows[i].SortOrder == rows[j].SortOrder {
return rows[i].ConditionType < rows[j].ConditionType
}
return rows[i].SortOrder < rows[j].SortOrder
})
return rows
}
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 (s *Service) loadConditionJumpConfigs(ctx context.Context, configID int64, category string) ([]model.TaskCenterConditionJumpConfig, error) {
if configID <= 0 {
return []model.TaskCenterConditionJumpConfig{}, nil
}
var rows []model.TaskCenterConditionJumpConfig
if err := s.db.WithContext(ctx).
Where("config_id = ? AND task_category = ?", configID, normalizeCategory(category)).
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 buildConditionJumpConfigPayloads(rows []model.TaskCenterConditionJumpConfig, tasks []model.TaskCenterTaskConfig) []ConditionJumpConfigPayload {
result := make([]ConditionJumpConfigPayload, 0, len(rows)+len(tasks))
seenConditions := make(map[string]struct{}, len(rows)+len(tasks))
for _, row := range rows {
conditionType := strings.ToUpper(strings.TrimSpace(row.ConditionType))
if conditionType == "" {
continue
}
seenConditions[conditionType] = struct{}{}
jumpType := normalizeJumpType(row.JumpType)
result = append(result, ConditionJumpConfigPayload{
ID: row.ID,
TaskCategory: row.TaskCategory,
ConditionType: conditionType,
JumpType: jumpType,
JumpPage: normalizeConditionJumpPage(jumpType, row.JumpPage),
SortOrder: row.SortOrder,
})
}
for _, task := range tasks {
conditionType := strings.ToUpper(strings.TrimSpace(task.ConditionType))
if conditionType == "" {
continue
}
if _, exists := seenConditions[conditionType]; exists {
continue
}
jumpType := normalizeJumpType(task.JumpType)
jumpPage := strings.TrimSpace(task.JumpPage)
if jumpType == "NONE" && jumpPage == "" {
jumpType, jumpPage = defaultConditionJump(conditionType)
}
jumpPage = normalizeConditionJumpPage(jumpType, jumpPage)
result = append(result, ConditionJumpConfigPayload{
ID: 0,
TaskCategory: task.TaskCategory,
ConditionType: conditionType,
JumpType: jumpType,
JumpPage: jumpPage,
SortOrder: task.SortOrder,
})
seenConditions[conditionType] = struct{}{}
}
sort.SliceStable(result, func(i, j int) bool {
if result[i].SortOrder == result[j].SortOrder {
return result[i].ConditionType < result[j].ConditionType
}
return result[i].SortOrder < result[j].SortOrder
})
return result
}
func normalizeConditionJumpPage(jumpType string, jumpPage string) string {
return strings.TrimSpace(jumpPage)
}
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
}