项目背景
“死亡不是终点,遗忘才是”
家史云谱 ——基于 Next.js 的家族人物生平记忆系统
Family Chronicle
开发历程
初始化
数据库连接
项目会用到 upstash 和 supabase ,因此需要配置好环境变量,并下载好连接数据库的客户端程序:
npm i dotenv
npm i @upstash/redis
npm install @supabase/supabase-js
|
下面会在 lib 库中,导出客户端变量:
import { Redis } from '@upstash/redis'
export const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, })
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true, }, })
|
除了配置环境变量之外,一般都会写一个脚本来测试连接是否成功,脚本一般写于 scripts 目录下:
import { Redis } from '@upstash/redis'
require('dotenv').config({ path: '.env.local' })
async function runTests() { console.log('🔍 Testing Redis connection...\n')
const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, })
try { console.log('1. Testing basic connection...') const ping = await redis.ping() console.log(`✅ Ping response: ${ping}`)
console.log('\n2. Testing read/write operations...') const testKey = 'test_' + Date.now() await redis.set(testKey, 'Hello Redis!') const value = await redis.get(testKey) await redis.del(testKey) console.log(`✅ Write/read successful: ${value}`)
console.log('\n3. Testing performance...') const start = Date.now() for (let i = 0; i < 10; i++) { await redis.ping() } const avgLatency = (Date.now() - start) / 10 console.log(`✅ Average latency: ${avgLatency.toFixed(2)}ms`)
console.log('\n🎉 All tests passed! Redis is working correctly.') process.exit(0) } catch (error: any) { console.error('\n❌ Test failed:', error.message) process.exit(1) } }
runTests()
|
import { createClient } from '@supabase/supabase-js' import dotenv from 'dotenv' import { join } from 'path'
dotenv.config({ path: join(__dirname, '..', '.env.local') })
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
async function testSupabaseConnection() { console.log('🔍 开始测试 Supabase 连接...\n')
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { console.error('❌ 缺少 Supabase 环境变量:') console.error(` SUPABASE_URL: ${SUPABASE_URL ? '已设置' : '未设置'}`) console.error( ` SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY ? '已设置' : '未设置'}` ) process.exit(1) }
console.log('✅ 环境变量检查通过') console.log(` URL: ${SUPABASE_URL.substring(0, 30)}...`) console.log(` Key: ${SUPABASE_ANON_KEY.substring(0, 10)}...\n`)
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { auth: { persistSession: false, }, })
try { console.log('1. 测试基础连接...') const { data: serverInfo, error: serverError } = await supabase.auth.getSession()
if (serverError) { console.log(` ⚠️ 会话获取失败(可能正常): ${serverError.message}`) } else { console.log( ` ✅ 连接成功,用户: ${serverInfo.session?.user?.email || '未登录'}` ) }
console.log('\n2. 测试数据库查询...')
const testTables = ['profiles', 'users', 'test_table', 'todos']
for (const table of testTables) { const { data, error, count } = await supabase .from(table) .select('*', { count: 'exact', head: true }) .limit(1)
if (error) { if (error.code === '42P01') { continue } console.log(` ⚠️ 表 ${table} 查询错误: ${error.message}`) } else { console.log(` ✅ 表 ${table} 连接成功,记录数: ${count || 0}`) break } }
console.log('\n3. 测试实时订阅...') const channel = supabase .channel('test-connection') .on('system' as any, { event: '*' }, (payload: any) => { console.log(` ✅ 实时连接成功: ${payload.event}`) }) .subscribe((status: string) => { console.log(` ✅ 订阅状态: ${status}`) })
setTimeout(() => { supabase.removeChannel(channel) console.log(' ✅ 实时测试完成') }, 2000)
console.log('\n4. 测试存储连接...') const { data: buckets, error: bucketsError } = await supabase.storage.listBuckets()
if (bucketsError) { console.log(` ⚠️ 存储连接错误: ${bucketsError.message}`) } else { console.log(` ✅ 存储连接成功,Bucket 数量: ${buckets.length}`) }
console.log('\n🎉 Supabase 连接测试完成!') console.log('--------------------------------') console.log('✅ 连接状态: 成功') console.log(`✅ 项目URL: ${SUPABASE_URL}`) console.log(`✅ 服务可用: Auth, Database, Realtime, Storage`) console.log('--------------------------------\n')
process.exit(0) } catch (error: any) { console.error('\n❌ Supabase 连接失败:') console.error(` 错误: ${error.message}`) console.error(` 堆栈: ${error.stack?.split('\n')[1]}`)
console.log('\n🔧 调试建议:') console.log(' 1. 检查环境变量是否正确') console.log(' 2. 检查 Supabase 项目是否启动') console.log(' 3. 检查网络连接') console.log(' 4. 检查 CORS 设置(项目 Settings > API)')
process.exit(1) } }
testSupabaseConnection()
|
为了在开发环境执行测试脚本,需要安装 tsx 这个库,使用方式如下:
npm install --save-dev tsx
npx tsx .\scripts\test-redis.tsx
npx tsx .\scripts\test-supabase.tsx
|
对象存储服务
本项目使用火山引擎的 TOS 服务,根据官方文档,需要安装如下的 SDK:
npm i @volcengine/tos-sdk
|
接着需要在环境变量中配置 TOS 的 AKSK:
TOS_ACCESS_KEY=AKTPYmI1Z**** TOS_SECRET_KEY=T1dJM01UU****
|
然后在 lib/tos.tsx 导出客户端:
import { TosClient } from '@volcengine/tos-sdk'
const connectionTimeout = 10000 const requestTimeout = 120000
export const tosClient = new TosClient({ accessKeyId: process.env.TOS_ACCESS_KEY!, accessKeySecret: process.env.TOS_SECRET_KEY!, region: 'cn-guangzhou', endpoint: 'tos-cn-guangzhou.volces.com', connectionTimeout, requestTimeout, })
|
中间件
在 NextJS 16 中,需要在项目的根目录下创建一个 proxy.tsx 文件,并进行配置,配置实例如下:
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) { return if (request.nextUrl.pathname === '/demo') { return }
if (!request.nextUrl.pathname.startsWith('/login')) { const token = request.cookies?.get('token')?.value console.log('token: ', token) if (!token) { return NextResponse.redirect(new URL('/login', request.url)) } } }
export const config = { matcher: [
'/((?!api|_next/static|_next/image|bg.png|favicon.ico|sitemap.xml|robots.txt).*)', ], }
|
人脸识别服务
本次用到了华为云的 FRS 服务,具体参考文档
功能
用户
- 编辑人物事迹、评价人物
- 查看人物事迹(有一个时间轴)
- 缅怀人物(为人物献花)
数据库
数据字典
实体
系统的角色大概分为如下三类:
- 超级管理员:目前主要是负责用户端的展示页面
- 管理员:可以将用户添加进家族中,家族中的用户可以相互编写人物事迹和人物评价,但是事迹需要管理员审核通过才行
- 用户:可以为家族中的人物编写事迹和评价,也可以访问其他家族的人物事迹(前提是该人物事迹被设置为公开可见)