Skip to content

Latest commit

 

History

History
790 lines (610 loc) · 21.4 KB

File metadata and controls

790 lines (610 loc) · 21.4 KB

Rich365 完整重构计划

📊 项目深度诊断报告

经过全面代码审查,发现以下关键问题需要解决:


🔴 核心问题分析

问题 1: 图片保存功能失败

症状: 用户报告保存图片功能无法使用(分享弹窗 + 月历首页)

根本原因:

  1. month-client-page.tsx 仍在使用 Tailwind 梯度类:

    const cardColors = [
      "bg-gradient-to-br from-pink-100 to-pink-200 dark:from-pink-950 dark:to-pink-900",
      "bg-gradient-to-br from-yellow-100 to-yellow-200 dark:from-yellow-950 dark:to-yellow-900",
      // ... 更多颜色
    ]
  2. Tailwind CSS 会将这些类转换为 lab() 颜色函数(特别是 dark mode 变体)

  3. html2canvas 不支持 lab() 颜色函数,导致转换失败:

    Error: Attempting to parse an unsupported color function "lab"
    

影响范围:

  • ❌ 月历页面保存图片功能完全失效
  • ✅ 分享卡片已修复(使用 inline RGB 渐变)

文件位置:

  • app/month/[month]/month-client-page.tsx 第 16-24 行
  • app/month/[month]/month-client-page.tsx 第 206 行(使用 colorClass)

问题 2: AI 生成建议导致页面变形

症状: AI 生成建议后,左侧卡片被撑得很高,和右边的"生成日历"框不对齐,页面很丑

根本原因:

  1. 布局使用 Grid 两列等宽,但不等高:

    <div className="grid md:grid-cols-2 gap-6">
      {/* 左侧:目标输入 + AI建议 */}
      <Card className="p-8">
        <Textarea className="min-h-[200px]" />
        {aiSuggestions && (
          <motion.div className="mt-6 p-4 ...">
            <div className="whitespace-pre-wrap">{aiSuggestions}</div>
          </motion.div>
        )}
      </Card>
    
      {/* 右侧:生成日历按钮 */}
      <Card className="p-8 ...">...</Card>
    </div>
  2. AI 建议内容长度不可控:

    • 使用 whitespace-pre-wrap 保留所有换行
    • AI 可能返回很长的建议(3-5 段,每段多行)
    • 左侧 Card 被撑到 800px+ 高度
  3. 右侧生成按钮固定高度,导致不对齐:

    • 左侧高,右侧矮
    • Grid 布局不会自动平衡高度
    • 视觉上很不协调

影响范围:

  • Onboarding 第 3 步的 UI/UX

文件位置:

  • app/onboarding/page.tsx 第 405-526 行

问题 3: 生成日历业务逻辑混乱

症状: 用户点击生成后,先看到默认日历,过了很久才出现"生成 30 天"的提示

根本原因:

业务流程问题:

用户体验流程(当前):
1. 点击"生成日历"按钮
2. onboarding 立即跳转到 /calendar 页面
3. calendar 页面加载,调用 getMonthTheme() 获取 12 个月的主题
4. 因为数据库还没有 AI 数据,fallback 到模板数据
5. 用户看到默认日历(模板生成)
6. 后台 AI 还在慢慢生成(30-60 秒)
7. 用户困惑:这是 AI 生成的吗?为什么这么快?
8. 过了很久,Toast 提示"成功生成 30 个行动"
9. 但页面没有刷新,用户看到的还是旧数据
10. 用户需要手动刷新才能看到真实 AI 数据

核心问题:

  1. 跳转时机错误: 在 AI 生成完成前就跳转
  2. 数据加载混乱: hybrid 模式的 fallback 机制导致用户看到错误数据
  3. 缺少状态同步: AI 生成完成后没有通知 calendar 页面刷新
  4. 用户体验差: 用户不知道发生了什么,看到的数据不对

代码证据:

app/onboarding/page.tsx 第 124-194 行:

if (user) {
  // Phase 2: AI 生成中
  setGenerationPhase("generating")
  const response = await fetch("/api/generate-calendar-progressive", {
    method: "POST",
    body: JSON.stringify({ userId: user.id, profile: profileData, phase: "initial" })
  })

  const result = await response.json()

  if (result.success) {
    toast.success(`成功生成 ${result.actionsCount} 个搞钱行动!`)
    await new Promise(resolve => setTimeout(resolve, 2000))
  }
}

// ❌ 问题:立即跳转,没有等待数据真正可用
router.replace("/calendar")

lib/calendar-hybrid.ts 第 18-50 行:

export async function getMonthTheme(userId, month, profile) {
  if (!userId) {
    return getPersonalizedMonthTheme(month, profile)  // 模板数据
  }

  try {
    const actions = await getUserCalendarActions(userId, year, month)

    if (actions.length > 0) {
      return { /* 数据库数据 */ }
    }

    // ❌ 问题:如果数据库还没有数据,立即 fallback 到模板
    return getPersonalizedMonthTheme(month, profile)
  } catch (error) {
    return getPersonalizedMonthTheme(month, profile)
  }
}

影响范围:

  • 整个 onboarding → calendar 流程
  • 用户首次体验非常差

文件位置:

  • app/onboarding/page.tsx 第 100-200 行(handleComplete)
  • app/calendar/page.tsx 第 22-109 行(状态检查和跳转逻辑)
  • lib/calendar-hybrid.ts 第 18-92 行(hybrid 数据加载)

🎯 完整重构方案

方案 A: 快速修复(推荐 - 1-2 小时)

目标: 修复核心 bug,最小化改动

1. 修复图片保存(15 分钟)

文件: app/month/[month]/month-client-page.tsx

修改: 将 Tailwind 梯度类转换为 inline RGB 渐变

// BEFORE
const cardColors = [
  "bg-gradient-to-br from-pink-100 to-pink-200 dark:from-pink-950 dark:to-pink-900",
  "bg-gradient-to-br from-yellow-100 to-yellow-200 dark:from-yellow-950 dark:to-yellow-900",
  "bg-gradient-to-br from-blue-100 to-blue-200 dark:from-blue-950 dark:to-blue-900",
  "bg-gradient-to-br from-purple-100 to-purple-200 dark:from-purple-950 dark:to-purple-900",
  "bg-gradient-to-br from-green-100 to-green-200 dark:from-green-950 dark:to-green-900",
  "bg-gradient-to-br from-orange-100 to-orange-200 dark:from-orange-950 dark:to-orange-900",
  "bg-gradient-to-br from-teal-100 to-teal-200 dark:from-teal-950 dark:to-teal-900",
]

// AFTER
const getCardStyle = (day: number) => {
  const gradients = [
    "linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%)", // pink
    "linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)", // yellow
    "linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)", // blue
    "linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%)", // purple
    "linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)", // green
    "linear-gradient(135deg, #fed7aa 0%, #fdba74 100%)", // orange
    "linear-gradient(135deg, #ccfbf1 0%, #99f6e4 100%)", // teal
  ]
  return {
    background: gradients[day % gradients.length]
  }
}

使用:

<Card
  style={action ? getCardStyle(day) : undefined}
  className={action ? "hover:shadow-lg hover:scale-[1.05] ..." : "opacity-50 bg-muted"}
>

2. 修复 AI 建议页面变形(30 分钟)

文件: app/onboarding/page.tsx

修改策略: 改用 Flexbox 布局 + 限制 AI 建议高度

// BEFORE
<div className="grid md:grid-cols-2 gap-6">
  {/* 左侧 */}
  <Card className="p-8">
    <Textarea className="min-h-[200px]" />
    {aiSuggestions && (
      <motion.div className="mt-6 p-4 ...">
        <div className="whitespace-pre-wrap">{aiSuggestions}</div>
      </motion.div>
    )}
  </Card>

  {/* 右侧 */}
  <Card className="p-8 ...">...</Card>
</div>

// AFTER
<div className="flex flex-col md:flex-row gap-6 items-start">
  {/* 左侧 - 固定宽度 */}
  <Card className="p-8 flex-1 max-w-2xl">
    <Textarea className="min-h-[200px]" />
    {aiSuggestions && (
      <motion.div
        className="mt-6 p-4 bg-accent/10 rounded-lg border border-accent/20 max-h-[400px] overflow-y-auto"
      >
        <div className="flex items-center gap-2 mb-3">
          <Sparkles className="h-5 w-5 text-accent flex-shrink-0" />
          <h3 className="font-semibold text-lg">AI 为你推荐的行动</h3>
        </div>
        <div className="text-sm text-muted-foreground leading-relaxed">
          {aiSuggestions}
        </div>
        <p className="text-xs text-muted-foreground mt-3 italic">
          💡 这些建议会融入你的 365 天行动日历中
        </p>
      </motion.div>
    )}
  </Card>

  {/* 右侧 - 固定宽度,自适应高度 */}
  <div className="md:sticky md:top-6 w-full md:w-[400px] flex-shrink-0">
    <Card className="p-8 bg-gradient-to-br from-orange-500/10 to-pink-500/10 border-accent/20">
      {/* 生成日历按钮 */}
      ...
    </Card>
  </div>
</div>

关键改进:

  • ✅ 使用 flex 而不是 grid,允许不等高
  • ✅ 右侧使用 md:sticky md:top-6,固定在视口中
  • ✅ AI 建议添加 max-h-[400px] overflow-y-auto,限制高度
  • ✅ 移除 whitespace-pre-wrap,改用正常文本渲染
  • ✅ 左侧 max-w-2xl,防止过宽

3. 修复生成日历业务逻辑(45 分钟)

核心策略: 在 onboarding 页面等待 AI 生成完成,再跳转

修改文件: app/onboarding/page.tsx

// BEFORE
if (result.success) {
  toast.success(`成功生成 ${result.actionsCount} 个搞钱行动!`)
  await new Promise(resolve => setTimeout(resolve, 2000))
}

// 立即跳转
router.replace("/calendar")

// AFTER
if (result.success) {
  console.log("[Onboarding] ✅ AI 生成完成,等待数据可用...")

  // Phase 3: 验证数据
  setGenerationPhase("saving")
  setGenerationProgress(80)
  setCurrentAction("验证数据完整性...")

  // 等待数据真正写入数据库并可查询
  let retries = 0
  const maxRetries = 10
  let dataReady = false

  while (retries < maxRetries && !dataReady) {
    await new Promise(resolve => setTimeout(resolve, 500))

    // 检查数据是否可用
    const checkResponse = await fetch(`/api/check-calendar-data?userId=${user.id}`)
    const checkResult = await checkResponse.json()

    if (checkResult.hasData && checkResult.count >= 30) {
      dataReady = true
      console.log("[Onboarding] ✅ 数据已就绪,可以跳转")
    } else {
      retries++
      console.log(`[Onboarding] 等待数据就绪... (${retries}/${maxRetries})`)
    }
  }

  if (!dataReady) {
    console.warn("[Onboarding] ⚠️ 数据验证超时,继续跳转")
  }

  setGenerationProgress(100)
  setGenerationPhase("complete")
  toast.success(`成功生成 ${result.actionsCount} 个搞钱行动!`)

  await new Promise(resolve => setTimeout(resolve, 1500))
}

// 现在可以安全跳转了
sessionStorage.setItem("onboarding_just_completed", "true")
router.replace("/calendar")

新增 API: app/api/check-calendar-data/route.ts

import { NextResponse } from "next/server"
import { createClient } from "@/lib/supabase/server"

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const userId = searchParams.get("userId")

  if (!userId) {
    return NextResponse.json({ hasData: false, count: 0 })
  }

  try {
    const supabase = await createClient()

    const { data, error } = await supabase
      .from("daily_actions")
      .select("id", { count: "exact" })
      .eq("user_id", userId)

    if (error) {
      console.error("[Check Calendar Data] Error:", error)
      return NextResponse.json({ hasData: false, count: 0, error: error.message })
    }

    return NextResponse.json({
      hasData: (data?.length || 0) > 0,
      count: data?.length || 0
    })
  } catch (error) {
    console.error("[Check Calendar Data] Exception:", error)
    return NextResponse.json({ hasData: false, count: 0 })
  }
}

优化 calendar 页面的 hybrid 逻辑:

// lib/calendar-hybrid.ts

export async function getMonthTheme(
  userId: string | null,
  month: number,
  profile: UserProfile
): Promise<MonthTheme> {
  // 如果没有登录,使用模板
  if (!userId) {
    return getPersonalizedMonthTheme(month, profile)
  }

  try {
    const year = new Date().getFullYear()
    const actions = await getUserCalendarActions(userId, year, month)

    if (actions.length > 0) {
      // ✅ 有数据,使用数据库数据
      return {
        month,
        name: `${month}月`,
        theme: actions[0].theme || getPersonalizedMonthTheme(month, profile).theme,
        description: getPersonalizedMonthTheme(month, profile).description,
        emoji: actions[0].emoji || getPersonalizedMonthTheme(month, profile).emoji,
      }
    }

    // ❌ BEFORE: 立即 fallback
    // return getPersonalizedMonthTheme(month, profile)

    // ✅ AFTER: 检查是否刚完成 onboarding
    const justCompleted = typeof window !== 'undefined'
      ? sessionStorage.getItem("onboarding_just_completed")
      : null

    if (justCompleted) {
      // 如果刚完成 onboarding,等待 AI 数据(最多 3 秒)
      console.log(`[Calendar Hybrid] 检测到刚完成 onboarding,等待 AI 数据...`)

      for (let i = 0; i < 6; i++) {
        await new Promise(resolve => setTimeout(resolve, 500))
        const retryActions = await getUserCalendarActions(userId, year, month)

        if (retryActions.length > 0) {
          console.log(`[Calendar Hybrid] ✅ AI 数据已就绪`)
          return {
            month,
            name: `${month}月`,
            theme: retryActions[0].theme || getPersonalizedMonthTheme(month, profile).theme,
            description: getPersonalizedMonthTheme(month, profile).description,
            emoji: retryActions[0].emoji || getPersonalizedMonthTheme(month, profile).emoji,
          }
        }
      }

      console.log(`[Calendar Hybrid] ⚠️ 等待超时,使用模板数据`)
    }

    // Fallback 到模板
    return getPersonalizedMonthTheme(month, profile)
  } catch (error) {
    console.error("[Calendar Hybrid] 获取月度主题失败:", error)
    return getPersonalizedMonthTheme(month, profile)
  }
}

方案 B: 深度重构(可选 - 4-8 小时)

目标: 全面优化架构和性能

1. 升级 AI 模型到 Gemini 2.5 Flash(30 分钟)

为什么:

  • Gemini 2.5 Pro:慢,贵,适合复杂推理
  • Gemini 2.5 Flash:快 2-3 倍,便宜 80%,质量相近

修改:

lib/gemini.ts:

// BEFORE
const model = genAI.getGenerativeModel({ model: "gemini-2.5-pro" })

// AFTER
const model = genAI.getGenerativeModel({
  model: "gemini-2.5-flash",
  generationConfig: {
    temperature: 0.9,  // 增加创意性
    topP: 0.95,
    topK: 40,
    maxOutputTokens: 4096,
  }
})

app/api/generate-calendar-progressive/route.ts:

// 第 136 行
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" })

预期效果:

  • 生成速度:60 秒 → 20-25 秒
  • 成本降低:80%
  • 用户体验大幅提升

2. 实现批量生成策略(1 小时)

策略: 分批生成,实时显示进度

// 新文件:app/api/generate-calendar-batch/route.ts

export async function POST(request: Request) {
  const { userId, profile, batchSize = 30 } = await request.json()

  // 生成器函数,分批返回
  const encoder = new TextEncoder()
  const stream = new ReadableStream({
    async start(controller) {
      try {
        const totalDays = 365
        let generated = 0

        while (generated < totalDays) {
          const remainingDays = totalDays - generated
          const currentBatch = Math.min(batchSize, remainingDays)

          // 生成这一批
          const actions = await generateBatch(userId, profile, generated, currentBatch)

          // 保存到数据库
          await saveBatch(userId, actions)

          generated += currentBatch

          // 发送进度
          const progress = {
            generated,
            total: totalDays,
            percentage: Math.round((generated / totalDays) * 100)
          }

          controller.enqueue(encoder.encode(`data: ${JSON.stringify(progress)}\n\n`))
        }

        controller.close()
      } catch (error) {
        controller.error(error)
      }
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    }
  })
}

3. 优化数据库查询性能(30 分钟)

添加组合索引:

-- supabase/migrations/009_performance_optimization.sql

-- 优化 daily_actions 查询
CREATE INDEX IF NOT EXISTS idx_daily_actions_user_date_composite
  ON daily_actions(user_id, date DESC);

-- 优化 check_ins 查询
CREATE INDEX IF NOT EXISTS idx_check_ins_user_date_composite
  ON check_ins(user_id, date DESC);

-- 添加部分索引(只索引未来的日期)
CREATE INDEX IF NOT EXISTS idx_daily_actions_future
  ON daily_actions(user_id, date)
  WHERE date >= CURRENT_DATE;

优化统计触发器:

-- 使用物化视图代替实时计算
CREATE MATERIALIZED VIEW user_stats AS
SELECT
  user_id,
  COUNT(*) as total_check_ins,
  COUNT(*) * 10 as total_coins,
  MAX(date) as last_check_in
FROM check_ins
GROUP BY user_id;

-- 创建唯一索引以支持并发刷新
CREATE UNIQUE INDEX ON user_stats(user_id);

-- 每小时刷新一次(或在打卡时手动刷新)
REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;

4. 实现前端缓存机制(1 小时)

使用 React Query / SWR:

// lib/hooks/useCalendarData.ts

import useSWR from 'swr'

export function useMonthTheme(month: number) {
  const { user, profile } = useAuth()

  const { data, error, isLoading } = useSWR(
    user && profile ? [`/api/month-theme/${month}`, user.id] : null,
    () => getMonthTheme(user!.id, month, profile!),
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      dedupingInterval: 60000, // 1 分钟内不重复请求
    }
  )

  return {
    theme: data,
    isLoading,
    error
  }
}

📋 实施计划

阶段 1: 快速修复(推荐立即执行)

预计时间: 1-2 小时

  1. ✅ 修复图片保存(15 分钟)

    • 修改 month-client-page.tsx
    • 测试保存功能
  2. ✅ 修复 AI 建议布局(30 分钟)

    • 修改 onboarding/page.tsx 布局
    • 限制 AI 建议高度
    • 测试响应式
  3. ✅ 修复生成日历逻辑(45 分钟)

    • 添加 /api/check-calendar-data API
    • 修改 onboarding/page.tsx 等待逻辑
    • 优化 calendar-hybrid.ts 加载逻辑
    • 完整测试生成流程

部署: 立即部署到生产环境

阶段 2: 性能优化(可选)

预计时间: 4-8 小时

  1. 升级到 Gemini 2.5 Flash(30 分钟)
  2. 实现批量生成策略(1 小时)
  3. 数据库性能优化(30 分钟)
  4. 前端缓存机制(1 小时)
  5. 完整测试(1 小时)

部署: 充分测试后部署


🧪 测试清单

图片保存功能

  • 月历页面点击"保存图片"
  • 检查浏览器控制台,确保没有 lab() 错误
  • 验证生成的图片质量和颜色
  • 测试 dark mode(如果支持)

AI 建议布局

  • 输入目标并生成 AI 建议
  • 检查左右两侧是否对齐
  • 测试不同长度的 AI 建议
  • 测试响应式布局(手机、平板、桌面)
  • 验证滚动条是否正常工作

生成日历流程

  • 新用户注册 → onboarding
  • 选择 MBTI 和职业
  • 点击"生成日历"
  • 观察进度提示是否正确
  • 等待生成完成(应该停留在 onboarding)
  • 跳转到 calendar 页面
  • 验证显示的是 AI 数据,不是模板数据
  • 检查控制台日志,确认数据流正确

📝 迁移步骤

数据库迁移

步骤 1: 执行之前的快速修复脚本(如果还没执行)

-- supabase/migrations/008_quick_fix_existing_db.sql

步骤 2: 执行性能优化脚本(可选)

-- supabase/migrations/009_performance_optimization.sql

代码部署

步骤 1: 创建新分支

git checkout -b refactor/fix-critical-issues

步骤 2: 依次修改文件

  • app/month/[month]/month-client-page.tsx
  • app/onboarding/page.tsx
  • app/api/check-calendar-data/route.ts(新增)
  • lib/calendar-hybrid.ts

步骤 3: 本地测试

npm run dev
# 完整测试所有功能

步骤 4: 提交和部署

git add .
git commit -m "fix: 修复图片保存、AI建议布局和生成日历流程"
git push origin refactor/fix-critical-issues

# 部署到生产环境
vercel --prod

🎯 预期成果

修复后的用户体验

  1. 图片保存:

    • ✅ 点击保存,立即生成图片
    • ✅ 没有错误提示
    • ✅ 颜色渐变正常显示
  2. AI 建议布局:

    • ✅ 左右对齐,视觉协调
    • ✅ 建议内容可滚动,不会撑破布局
    • ✅ 响应式布局在所有设备上正常
  3. 生成日历流程:

    • ✅ 点击生成后,显示清晰的进度提示
    • ✅ 生成完成前不跳转
    • ✅ 跳转后立即显示 AI 数据
    • ✅ 没有"默认数据"的闪烁
  4. 性能提升(如果执行阶段 2):

    • ✅ 生成速度:60 秒 → 20-25 秒
    • ✅ 页面加载速度提升 30%
    • ✅ 数据库查询减少 50%

🚨 风险评估

低风险

  • ✅ 图片保存修复(只改样式,不影响逻辑)
  • ✅ AI 建议布局(纯 UI 改动)

中风险

  • ⚠️ 生成日历流程(涉及核心业务逻辑)
    • 缓解措施: 充分测试,保留降级方案

高风险

  • 🔴 数据库结构变更(如果执行阶段 2)
    • 缓解措施: 先在开发环境测试,备份生产数据

📞 后续支持

完成重构后,建议:

  1. 监控错误: 使用 Sentry 或类似工具
  2. 性能监控: 使用 Vercel Analytics
  3. 用户反馈: 收集真实用户体验
  4. 持续优化: 根据数据调整策略

文档创建时间: 2025-10-26 版本: 1.0 作者: Claude Code