331 lines
9.9 KiB
Go
331 lines
9.9 KiB
Go
package taskcenter
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ListTasks 返回 App 任务列表,字段兼容旧 Flutter 模型。
|
|
func (s *Service) ListTasks(ctx context.Context, user AuthUser) ([]AppTaskItem, error) {
|
|
sysOrigin := s.normalizeSysOrigin(user.SysOrigin)
|
|
bundle, err := s.loadEnabledBundle(ctx, sysOrigin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if bundle == nil {
|
|
return []AppTaskItem{}, nil
|
|
}
|
|
|
|
result := make([]AppTaskItem, 0, len(bundle.Tasks))
|
|
for _, task := range bundle.Tasks {
|
|
cycleKey := currentCycleKey(task.TaskCategory, bundle.Location, time.Now())
|
|
progress, exists, err := s.loadProgress(ctx, sysOrigin, user.UserID, task.ID, cycleKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if task.TaskCategory == TaskCategoryNewbie && exists && progress.RewardCollected {
|
|
continue
|
|
}
|
|
|
|
completedValue := int64(0)
|
|
rewardCollected := 0
|
|
if exists {
|
|
completedValue = progress.CompletedValue
|
|
if progress.RewardCollected {
|
|
rewardCollected = 1
|
|
}
|
|
}
|
|
taskStatus := 0
|
|
if completedValue >= task.TargetValue {
|
|
taskStatus = 1
|
|
}
|
|
result = append(result, AppTaskItem{
|
|
TaskType: appTaskType(task.TaskCategory),
|
|
TaskName: task.TaskName,
|
|
TaskDesc: task.TaskDesc,
|
|
JumpPage: resolveJumpPage(task),
|
|
ConditionType: task.ConditionType,
|
|
SortOrder: task.SortOrder,
|
|
TaskID: task.ID,
|
|
TaskStatus: taskStatus,
|
|
Cover: task.Cover,
|
|
TaskIcon: task.TaskIcon,
|
|
ConditionValue: task.ConditionType,
|
|
CompletedValue: completedValue,
|
|
IsRewardCollected: rewardCollected,
|
|
TargetValue: task.TargetValue,
|
|
Quantity: task.RewardGold,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// ClaimableCount 返回可领取任务数量。
|
|
func (s *Service) ClaimableCount(ctx context.Context, user AuthUser) (*ClaimableCountResponse, error) {
|
|
tasks, err := s.ListTasks(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
count := 0
|
|
for _, task := range tasks {
|
|
if task.TaskStatus == 1 && task.IsRewardCollected == 0 {
|
|
count++
|
|
}
|
|
}
|
|
return &ClaimableCountResponse{Count: count}, nil
|
|
}
|
|
|
|
// Claim 领取任务奖励。
|
|
func (s *Service) Claim(ctx context.Context, user AuthUser, taskID int64) (bool, error) {
|
|
if taskID <= 0 {
|
|
return false, NewAppError(http.StatusBadRequest, "invalid_task_id", "taskId is required")
|
|
}
|
|
sysOrigin := s.normalizeSysOrigin(user.SysOrigin)
|
|
bundle, err := s.loadEnabledBundle(ctx, sysOrigin)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if bundle == nil {
|
|
return false, NewAppError(http.StatusNotFound, "task_center_not_configured", "task center is not configured")
|
|
}
|
|
|
|
var task model.TaskCenterTaskConfig
|
|
found := false
|
|
for _, candidate := range bundle.Tasks {
|
|
if candidate.ID == taskID {
|
|
task = candidate
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false, NewAppError(http.StatusNotFound, "task_not_found", "task not found")
|
|
}
|
|
cycleKey := currentCycleKey(task.TaskCategory, bundle.Location, time.Now())
|
|
progress, exists, err := s.loadProgress(ctx, sysOrigin, user.UserID, task.ID, cycleKey)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !exists || progress.CompletedValue < task.TargetValue {
|
|
return false, NewAppError(http.StatusBadRequest, "task_not_completed", "task is not completed")
|
|
}
|
|
if progress.RewardCollected {
|
|
return true, nil
|
|
}
|
|
|
|
walletEventID := buildWalletEventID(sysOrigin, user.UserID, task.TaskCode, cycleKey)
|
|
if err := s.dispatchClaimGold(ctx, sysOrigin, user.UserID, task, walletEventID); err != nil {
|
|
_ = s.saveClaimRecord(ctx, task, progress, walletEventID, ClaimStatusFailed, err.Error(), nil)
|
|
return false, err
|
|
}
|
|
|
|
now := time.Now()
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := s.saveClaimRecordTx(tx, task, progress, walletEventID, ClaimStatusSuccess, "", &now); err != nil {
|
|
return err
|
|
}
|
|
return tx.Model(&model.TaskCenterUserProgress{}).
|
|
Where("id = ?", progress.ID).
|
|
Updates(map[string]any{
|
|
"status": TaskStatusClaimed,
|
|
"reward_collected": true,
|
|
"claimed_at": now,
|
|
"update_time": now,
|
|
}).Error
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *Service) dispatchClaimGold(ctx context.Context, sysOrigin string, userID int64, task model.TaskCenterTaskConfig, walletEventID string) error {
|
|
if task.RewardGold <= 0 {
|
|
return nil
|
|
}
|
|
if s.java == nil {
|
|
return NewAppError(http.StatusBadGateway, "wallet_gateway_missing", "wallet gateway is not configured")
|
|
}
|
|
exists, err := s.java.ExistsGoldEvent(ctx, walletEventID)
|
|
if err != nil {
|
|
return NewAppError(http.StatusBadGateway, "wallet_event_check_failed", err.Error())
|
|
}
|
|
if exists {
|
|
return nil
|
|
}
|
|
if err := s.java.ChangeGoldBalance(ctx, integration.GoldReceiptCommand{
|
|
ReceiptType: "INCOME",
|
|
UserID: userID,
|
|
SysOrigin: sysOrigin,
|
|
EventID: walletEventID,
|
|
Remark: fmt.Sprintf("taskCode=%s cycleKey=%s", task.TaskCode, currentCycleKey(task.TaskCategory, s.location, time.Now())),
|
|
Amount: integration.NewPennyAmountPayloadFromDollar(task.RewardGold),
|
|
CloseDelayAsset: false,
|
|
OpUserType: "APP",
|
|
Origin: "DAILY_TASK",
|
|
}); err != nil {
|
|
return NewAppError(http.StatusBadGateway, "wallet_change_failed", err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) saveClaimRecord(ctx context.Context, task model.TaskCenterTaskConfig, progress model.TaskCenterUserProgress, walletEventID string, status string, failureReason string, claimedAt *time.Time) error {
|
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.saveClaimRecordTx(tx, task, progress, walletEventID, status, failureReason, claimedAt)
|
|
})
|
|
}
|
|
|
|
func (s *Service) saveClaimRecordTx(tx *gorm.DB, task model.TaskCenterTaskConfig, progress model.TaskCenterUserProgress, walletEventID string, status string, failureReason string, claimedAt *time.Time) error {
|
|
now := time.Now()
|
|
var row model.TaskCenterClaimRecord
|
|
err := tx.Where("wallet_event_id = ?", walletEventID).First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
id, idErr := utils.NextID()
|
|
if idErr != nil {
|
|
return idErr
|
|
}
|
|
row = model.TaskCenterClaimRecord{
|
|
ID: id,
|
|
SysOrigin: progress.SysOrigin,
|
|
UserID: progress.UserID,
|
|
TaskID: task.ID,
|
|
TaskCode: task.TaskCode,
|
|
TaskName: task.TaskName,
|
|
TaskCategory: task.TaskCategory,
|
|
CycleKey: progress.CycleKey,
|
|
RewardGold: task.RewardGold,
|
|
WalletEventID: walletEventID,
|
|
CreateTime: now,
|
|
}
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
row.Status = status
|
|
row.FailureReason = strings.TrimSpace(failureReason)
|
|
row.ClaimedAt = claimedAt
|
|
row.UpdateTime = now
|
|
return tx.Save(&row).Error
|
|
}
|
|
|
|
type taskCenterBundle struct {
|
|
Config model.TaskCenterConfig
|
|
Tasks []model.TaskCenterTaskConfig
|
|
Location *time.Location
|
|
}
|
|
|
|
func (s *Service) loadEnabledBundle(ctx context.Context, sysOrigin string) (*taskCenterBundle, error) {
|
|
var configRow model.TaskCenterConfig
|
|
err := s.db.WithContext(ctx).Where("sys_origin = ?", sysOrigin).First(&configRow).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !configRow.Enabled {
|
|
return nil, nil
|
|
}
|
|
location := s.location
|
|
if parsed, parseErr := time.LoadLocation(normalizeTimezone(configRow.Timezone, s.defaultTimezone())); parseErr == nil {
|
|
location = parsed
|
|
}
|
|
|
|
var tasks []model.TaskCenterTaskConfig
|
|
if err := s.db.WithContext(ctx).
|
|
Where("config_id = ? AND enabled = ?", configRow.ID, true).
|
|
Order("sort_order asc, id asc").
|
|
Find(&tasks).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &taskCenterBundle{Config: configRow, Tasks: tasks, Location: location}, nil
|
|
}
|
|
|
|
func (s *Service) loadProgress(ctx context.Context, sysOrigin string, userID int64, taskID int64, cycleKey string) (model.TaskCenterUserProgress, bool, error) {
|
|
var row model.TaskCenterUserProgress
|
|
err := s.db.WithContext(ctx).
|
|
Where("sys_origin = ? AND user_id = ? AND task_id = ? AND cycle_key = ?", sysOrigin, userID, taskID, cycleKey).
|
|
First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return model.TaskCenterUserProgress{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return model.TaskCenterUserProgress{}, false, err
|
|
}
|
|
return row, true, nil
|
|
}
|
|
|
|
func appTaskType(category string) int {
|
|
if normalizeCategory(category) == TaskCategoryDaily {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func resolveJumpPage(task model.TaskCenterTaskConfig) string {
|
|
jumpPage := strings.TrimSpace(task.JumpPage)
|
|
if jumpPage != "" {
|
|
return jumpPage
|
|
}
|
|
if isRechargeTask(task) {
|
|
return defaultRechargeJumpPage
|
|
}
|
|
switch normalizeJumpType(task.JumpType) {
|
|
case JumpTypeRechargeInApp:
|
|
return defaultRechargeJumpPage
|
|
case "ROOM_RANDOM", "ROOM_RANDOM_WITH_MIC":
|
|
return "/room"
|
|
case "NONE":
|
|
return ""
|
|
default:
|
|
return jumpPage
|
|
}
|
|
}
|
|
|
|
func isRechargeTask(task model.TaskCenterTaskConfig) bool {
|
|
return strings.EqualFold(strings.TrimSpace(task.ConditionType), EventTypeRechargeGold)
|
|
}
|
|
|
|
func buildWalletEventID(sysOrigin string, userID int64, taskCode string, cycleKey string) string {
|
|
normalizedSysOrigin := compactWalletEventPart(sysOrigin, 8)
|
|
if normalizedSysOrigin == "" {
|
|
normalizedSysOrigin = "SYS"
|
|
}
|
|
raw := strings.Join([]string{
|
|
strings.ToUpper(strings.TrimSpace(sysOrigin)),
|
|
strconv.FormatInt(userID, 10),
|
|
strings.ToUpper(strings.TrimSpace(taskCode)),
|
|
strings.TrimSpace(cycleKey),
|
|
}, "|")
|
|
sum := sha1.Sum([]byte(raw))
|
|
return fmt.Sprintf("TC:%s:%s:%s", normalizedSysOrigin, strconv.FormatInt(userID, 36), hex.EncodeToString(sum[:])[:16])
|
|
}
|
|
|
|
func compactWalletEventPart(value string, maxLen int) string {
|
|
value = strings.ToUpper(strings.TrimSpace(value))
|
|
if maxLen <= 0 {
|
|
return ""
|
|
}
|
|
var builder strings.Builder
|
|
for _, char := range value {
|
|
if (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') {
|
|
builder.WriteRune(char)
|
|
if builder.Len() >= maxLen {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return builder.String()
|
|
}
|