fix: 引导任务修改bug
This commit is contained in:
parent
dd838fccc4
commit
6866618ff6
@ -78,6 +78,7 @@ message OnboardingStage {
|
||||
string status = 6; // pending/completed/in_progress
|
||||
bool is_current = 7;
|
||||
bool all_tasks_completed = 8; // 该阶段所有任务是否完成
|
||||
bool is_reward_claimed = 9; // 该阶段奖励是否已领取
|
||||
}
|
||||
|
||||
message CompleteGuideRequest {
|
||||
|
||||
@ -57,6 +57,7 @@ type UserOnboardingStatus struct {
|
||||
HasFriendDisplayBonus bool `gorm:"column:has_friend_display_bonus;default:false"`
|
||||
OnboardingCompletedAt *int64 `gorm:"column:onboarding_completed_at"`
|
||||
OnboardingClaimedAt *int64 `gorm:"column:onboarding_claimed_at"`
|
||||
ClaimedStages []int64 `gorm:"column:claimed_stages;type:text;serializer:json"` // 已领取奖励的阶段列表
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
UpdatedAt int64 `gorm:"column:updated_at"`
|
||||
CurrentStage int64 `gorm:"column:current_stage;default:0"`
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/topfans/backend/services/taskService/model"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type OnboardingRepository interface {
|
||||
@ -186,42 +187,22 @@ func (r *onboardingRepository) SaveStageConfigs(configs []*model.OnboardingStage
|
||||
zap.Int64("exp_reward", cfg.ExpReward))
|
||||
|
||||
cfg.UpdatedAt = now
|
||||
|
||||
// First try to update existing record
|
||||
// Use Select to specify fields so GORM properly handles JSON serialization
|
||||
result := r.db.Model(&model.OnboardingStageConfig{}).
|
||||
Where("stage = ?", cfg.Stage).
|
||||
Select("name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at").
|
||||
Updates(cfg)
|
||||
|
||||
if result.Error != nil {
|
||||
logger.Logger.Error("SaveStageConfigs: failed to update config",
|
||||
zap.Int("stage", cfg.Stage),
|
||||
zap.Error(result.Error))
|
||||
return result.Error
|
||||
// Use upsert via ON CONFLICT to properly handle JSON serialization
|
||||
upsert := clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "stage"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"name", "required_task_keys", "crystal_reward", "exp_reward", "is_active", "updated_at",
|
||||
}),
|
||||
}
|
||||
|
||||
logger.Logger.Info("SaveStageConfigs: update result",
|
||||
zap.Int("stage", cfg.Stage),
|
||||
zap.Int64("rows_affected", result.RowsAffected))
|
||||
|
||||
// If no rows affected, create new record
|
||||
if result.RowsAffected == 0 {
|
||||
cfg.CreatedAt = now
|
||||
if err := r.db.Create(cfg).Error; err != nil {
|
||||
logger.Logger.Error("SaveStageConfigs: failed to create config",
|
||||
if err := r.db.Clauses(upsert).Create(cfg).Error; err != nil {
|
||||
logger.Logger.Error("SaveStageConfigs: failed to upsert config",
|
||||
zap.Int("stage", cfg.Stage),
|
||||
zap.Error(err))
|
||||
return err
|
||||
}
|
||||
logger.Logger.Info("SaveStageConfigs: created config",
|
||||
logger.Logger.Info("SaveStageConfigs: upserted config",
|
||||
zap.Int("stage", cfg.Stage),
|
||||
zap.String("name", cfg.Name))
|
||||
} else {
|
||||
logger.Logger.Info("SaveStageConfigs: updated config",
|
||||
zap.Int("stage", cfg.Stage),
|
||||
zap.String("name", cfg.Name))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Logger.Info("SaveStageConfigs: all configs saved successfully")
|
||||
|
||||
@ -51,6 +51,8 @@
|
||||
<script setup>
|
||||
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { completeGuide, advanceStage, getOnboardingStatus } from '@/utils/task-api.js'
|
||||
import { onboardingStages } from '@/utils/guideConfig.js'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
@ -66,10 +68,37 @@ const stepConfig = computed(() => store.getters['guide/currentStepConfig'])
|
||||
const isFirst = computed(() => store.getters['guide/isFirst'])
|
||||
const isLast = computed(() => store.getters['guide/isLast'])
|
||||
|
||||
// 监听引导激活状态
|
||||
watch(() => store.state.guide.isActive, (newVal) => {
|
||||
console.log('[GuideOverlay] isActive changed to:', newVal)
|
||||
}, { immediate: true })
|
||||
// 监听引导激活状态(当 isActive 变为 false 时触发同步)
|
||||
watch(() => isActive.value, async (newVal, oldVal) => {
|
||||
console.log('[GuideOverlay] isActive watch:', oldVal, '->', newVal)
|
||||
if (oldVal === true && newVal === false) {
|
||||
const guideKey = store.state.guide.completedGuideKey
|
||||
if (guideKey) {
|
||||
try {
|
||||
await completeGuide(guideKey, onboardingStages)
|
||||
console.log('[GuideOverlay] guide synced to backend:', guideKey)
|
||||
|
||||
// 检查当前阶段是否所有任务都已完成,如果完成则推进到下一阶段
|
||||
const statusRes = await getOnboardingStatus()
|
||||
const stages = statusRes.data?.stages || []
|
||||
const currentStageData = stages.find(s => s.is_current)
|
||||
if (currentStageData && currentStageData.all_tasks_completed) {
|
||||
const nextStage = (currentStageData.stage || 0) + 1
|
||||
const maxStage = onboardingStages.length - 1
|
||||
if (nextStage <= maxStage) {
|
||||
await advanceStage(nextStage)
|
||||
console.log('[GuideOverlay] advanced to stage:', nextStage)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GuideOverlay] failed to sync guide to backend:', err)
|
||||
} finally {
|
||||
// 清除completedGuideKey,避免重复触发
|
||||
store.state.guide.completedGuideKey = null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 组件挂载时记录当前状态
|
||||
onMounted(() => {
|
||||
|
||||
@ -118,6 +118,8 @@ import {
|
||||
resetGuide,
|
||||
claimGuideReward,
|
||||
markGuideDone,
|
||||
isGuideDone,
|
||||
markGuideRewardClaimed,
|
||||
onboardingStages
|
||||
} from '@/utils/guideConfig.js'
|
||||
|
||||
@ -132,6 +134,7 @@ const emit = defineEmits(['close', 'updated'])
|
||||
|
||||
const loading = ref(true)
|
||||
const guideList = ref([])
|
||||
const backendStages = ref([])
|
||||
|
||||
// 统计数据
|
||||
const totalCount = computed(() => guideList.value.length)
|
||||
@ -261,6 +264,25 @@ function refreshList() {
|
||||
}
|
||||
|
||||
function calculateStatus(key, done, claimed) {
|
||||
// 优先使用后端状态判断
|
||||
const backendStage = backendStages.value.find(s =>
|
||||
s.required_task_keys && s.required_task_keys.includes(key)
|
||||
)
|
||||
if (backendStage) {
|
||||
// 后端状态为 in_progress 表示已领取
|
||||
if (backendStage.status === 'in_progress') {
|
||||
return 'reward_claimed'
|
||||
}
|
||||
// 后端状态为 completed 表示任务完成但未领取(可领取按钮)
|
||||
if (backendStage.status === 'completed' || backendStage.all_tasks_completed) {
|
||||
return 'completed'
|
||||
}
|
||||
// 后端状态为 locked 表示未开始
|
||||
if (backendStage.status === 'locked') {
|
||||
return 'not_started'
|
||||
}
|
||||
}
|
||||
// 回退到本地存储判断
|
||||
if (claimed) return 'reward_claimed'
|
||||
if (done && !claimed) return 'completed'
|
||||
if (hasGuideProgress(key)) return 'in_progress'
|
||||
@ -400,18 +422,29 @@ async function initBackend() {
|
||||
const res = await getOnboardingStatus()
|
||||
const data = res.data || {}
|
||||
|
||||
if (!data.stages || data.stages.length === 0) {
|
||||
await completeGuide('init', onboardingStages)
|
||||
} else {
|
||||
// 保存后端stages数据
|
||||
backendStages.value = data.stages || []
|
||||
|
||||
// 从后端同步已完成状态到本地
|
||||
if (data.stages && data.stages.length > 0) {
|
||||
for (const stage of data.stages) {
|
||||
if (stage.allTasksCompleted && stage.required_task_keys) {
|
||||
// all_tasks_completed 是后端返回的字段名(下划线格式)
|
||||
// 同时检查 status === 'completed' 作为备用判断
|
||||
if ((stage.all_tasks_completed || stage.status === 'completed') && stage.required_task_keys) {
|
||||
for (const taskKey of stage.required_task_keys) {
|
||||
if (!markGuideDone(taskKey)) {
|
||||
// 无论本地是否已标记,都同步后端状态
|
||||
if (!isGuideDone(taskKey)) {
|
||||
console.log('[GuideModal] 从后端同步任务完成状态:', taskKey)
|
||||
markGuideDone(taskKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
// status === 'in_progress' 表示已领取奖励
|
||||
if (stage.status === 'in_progress' && stage.required_task_keys) {
|
||||
for (const taskKey of stage.required_task_keys) {
|
||||
markGuideRewardClaimed(taskKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -712,6 +745,9 @@ const handleClose = () => {
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
min-width: 120rpx;
|
||||
width: 120rpx;
|
||||
height: 50rpx;
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
border: 2rpx solid rgba(150, 150, 150, 0.6);
|
||||
box-shadow: none;
|
||||
|
||||
@ -29,6 +29,8 @@ const state = {
|
||||
pendingAction: null,
|
||||
// 是否处于组件打开模式(遮罩隐藏)
|
||||
componentMode: false,
|
||||
// 刚刚完成的引导 key(用于 GuideOverlay 触发后端同步)
|
||||
completedGuideKey: null,
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
@ -78,6 +80,8 @@ const mutations = {
|
||||
const key = state.currentGuide.key
|
||||
markGuideAsShown(key)
|
||||
markGuideDone(key) // 标记为已完成
|
||||
// 保存刚完成的引导 key(用于 GuideOverlay 触发后端同步)
|
||||
state.completedGuideKey = key
|
||||
// 重置步骤进度
|
||||
setGuideCurrentStep(key, 0)
|
||||
// 清除子步骤完成记录
|
||||
@ -85,8 +89,9 @@ const mutations = {
|
||||
}
|
||||
state.currentGuide = null
|
||||
state.currentStep = 0
|
||||
state.isActive = false
|
||||
state.isNavigating = false
|
||||
// isActive 设为 false 放最后,让 GuideOverlay 的 watch 能读取 completedGuideKey
|
||||
state.isActive = false
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -70,24 +70,45 @@
|
||||
export const onboardingStages = [
|
||||
{
|
||||
stage: 0,
|
||||
name: '初识平台',
|
||||
required_task_keys: ['square_home', 'browse_exhibition'],
|
||||
name: '广场首页',
|
||||
required_task_keys: ['square_home'],
|
||||
crystal_reward: 100,
|
||||
exp_reward: 50
|
||||
},
|
||||
{
|
||||
stage: 1,
|
||||
name: '互动入门',
|
||||
required_task_keys: ['follow_star', 'send_gift'],
|
||||
crystal_reward: 200,
|
||||
exp_reward: 100
|
||||
name: '浏览展厅',
|
||||
required_task_keys: ['browse_exhibition'],
|
||||
crystal_reward: 100,
|
||||
exp_reward: 50
|
||||
},
|
||||
{
|
||||
stage: 2,
|
||||
name: '社交达人',
|
||||
required_task_keys: ['add_friend', 'share_content'],
|
||||
crystal_reward: 300,
|
||||
exp_reward: 150
|
||||
name: '关注明星',
|
||||
required_task_keys: ['follow_star'],
|
||||
crystal_reward: 100,
|
||||
exp_reward: 50
|
||||
},
|
||||
{
|
||||
stage: 3,
|
||||
name: '发送礼物',
|
||||
required_task_keys: ['send_gift'],
|
||||
crystal_reward: 100,
|
||||
exp_reward: 50
|
||||
},
|
||||
{
|
||||
stage: 4,
|
||||
name: '添加好友',
|
||||
required_task_keys: ['add_friend'],
|
||||
crystal_reward: 100,
|
||||
exp_reward: 50
|
||||
},
|
||||
{
|
||||
stage: 5,
|
||||
name: '分享内容',
|
||||
required_task_keys: ['share_content'],
|
||||
crystal_reward: 100,
|
||||
exp_reward: 50
|
||||
}
|
||||
]
|
||||
|
||||
@ -287,6 +308,146 @@ export const guideConfig = {
|
||||
buttons: ['next']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 浏览展厅引导
|
||||
browse_exhibition: {
|
||||
key: 'browse_exhibition',
|
||||
name: '浏览展厅',
|
||||
desc: '了解展馆功能和展品',
|
||||
reward: { exp: 10, diamond: 5 },
|
||||
steps: [
|
||||
{
|
||||
page: '/pages/exhibition/exhibition',
|
||||
mask: true,
|
||||
target: '.exhibition-header',
|
||||
content: '这里是展馆顶部区域,可以查看展馆信息',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
},
|
||||
{
|
||||
page: '/pages/exhibition/exhibition',
|
||||
mask: true,
|
||||
target: '.nft-cards-container',
|
||||
content: '这里展示了你铸造的所有展品',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 关注明星引导
|
||||
follow_star: {
|
||||
key: 'follow_star',
|
||||
name: '关注明星',
|
||||
desc: '关注喜欢的明星获取动态',
|
||||
reward: { exp: 10, diamond: 5 },
|
||||
steps: [
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.star-list',
|
||||
content: '这里可以浏览和关注你喜欢的明星',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
},
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.follow-btn',
|
||||
content: '点击按钮关注明星,关注后能收到他们的最新动态',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 发送礼物引导
|
||||
send_gift: {
|
||||
key: 'send_gift',
|
||||
name: '发送礼物',
|
||||
desc: '为喜欢的明星送礼物支持',
|
||||
reward: { exp: 10, diamond: 5 },
|
||||
steps: [
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.gift-btn',
|
||||
content: '点击这里可以为明星送上礼物',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
},
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.gift-item',
|
||||
content: '选择喜欢的礼物并发送,让明星感受到你的支持',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 添加好友引导
|
||||
add_friend: {
|
||||
key: 'add_friend',
|
||||
name: '添加好友',
|
||||
desc: '与其他粉丝建立联系',
|
||||
reward: { exp: 10, diamond: 5 },
|
||||
steps: [
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.friend-tab',
|
||||
content: '在这里可以查看好友列表',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
},
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.add-friend-btn',
|
||||
content: '点击添加新好友,与志同道合的粉丝交流',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 分享内容引导
|
||||
share_content: {
|
||||
key: 'share_content',
|
||||
name: '分享内容',
|
||||
desc: '将喜欢的内容分享给更多人',
|
||||
reward: { exp: 10, diamond: 5 },
|
||||
steps: [
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.share-btn',
|
||||
content: '点击分享按钮可以将内容分享给好友',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
},
|
||||
{
|
||||
page: '/pages/square/square',
|
||||
mask: true,
|
||||
target: '.share-platform',
|
||||
content: '选择分享平台,让更多人看到这份热爱',
|
||||
center: true,
|
||||
customPosition: { x: 0, y: 0 },
|
||||
buttons: ['next']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,6 +657,18 @@ export function markGuideDone(key) {
|
||||
uni.setStorageSync(makeUserIdKey('guide_done', key), true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记引导奖励已领取
|
||||
* @param {string} key 引导key
|
||||
*/
|
||||
export function markGuideRewardClaimed(key) {
|
||||
const claimed = uni.getStorageSync(makeUserIdKey('guide_rewards_claimed', '')) || []
|
||||
if (!claimed.includes(key)) {
|
||||
claimed.push(key)
|
||||
uni.setStorageSync(makeUserIdKey('guide_rewards_claimed', ''), claimed)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取引导奖励
|
||||
* @param {string} key 引导key
|
||||
|
||||
Loading…
Reference in New Issue
Block a user