家史云谱 ——基于 Next.js 的家族人物生平记忆系统

项目背景

“死亡不是终点,遗忘才是”

家史云谱 ——基于 Next.js 的家族人物生平记忆系统

Family Chronicle

开发历程

初始化

数据库连接

项目会用到 upstash 和 supabase ,因此需要配置好环境变量,并下载好连接数据库的客户端程序:

# 读取环境变量
npm i dotenv

# redis 客户端
npm i @upstash/redis

# postgreSQL 客户端
npm install @supabase/supabase-js

下面会在 lib 库中,导出客户端变量:

// lib/redis.tsx
import { Redis } from '@upstash/redis'

export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})




// lib/supabase.tsx
import { createClient } from '@supabase/supabase-js'

// 环境变量命名(Vercel推荐)
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 目录下:

// scripts/test-redis.ts
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 {
// 测试 1: 基础连接
console.log('1. Testing basic connection...')
const ping = await redis.ping()
console.log(`✅ Ping response: ${ping}`)

// 测试 2: 读写操作
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}`)

// 测试 3: 性能测试
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()

// scripts/test-supabase.ts
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 {
// 测试 1: 获取服务器信息
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 || '未登录'}`
)
}

// 测试 2: 从公共表查询数据
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
}
}

// 测试 3: 实时订阅测试
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)

// 测试 4: 存储桶测试(如果有存储功能)
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')) {
// 并且没有 token
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: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!api|_next/static|_next/image|bg.png|favicon.ico|sitemap.xml|robots.txt).*)',
],
}

人脸识别服务

本次用到了华为云的 FRS 服务,具体参考文档

功能

用户

  1. 编辑人物事迹、评价人物
  2. 查看人物事迹(有一个时间轴)
  3. 缅怀人物(为人物献花)

数据库

数据字典

实体

系统的角色大概分为如下三类:

  • 超级管理员:目前主要是负责用户端的展示页面
  • 管理员:可以将用户添加进家族中,家族中的用户可以相互编写人物事迹和人物评价,但是事迹需要管理员审核通过才行
  • 用户:可以为家族中的人物编写事迹和评价,也可以访问其他家族的人物事迹(前提是该人物事迹被设置为公开可见)