Bun + Elysia + Prisma + tRPC
🚀 技术栈选择
- Bun: 超快的 JavaScript 运行时 (比 Node.js 快 3-4 倍)
- Elysia: 专为 Bun 设计的 Web 框架 (260k+ req/s)
- Prisma: 类型安全的 ORM,自动事务管理
- tRPC: 端到端类型安全的 API (可选)
- MySQL 8: 地理位置查询、全文搜索
📦 项目初始化
1. 创建后端项目
bash
# 在项目根目录创建 backend 文件夹
mkdir backend
cd backend
# 初始化 Bun 项目
bun init -y
# 安装依赖
bun add elysia @elysiajs/cors @elysiajs/swagger
bun add @prisma/client
bun add -d prisma2. 初始化 Prisma
bash
bunx prisma init🗄️ 数据库配置
1. 配置环境变量 (.env)
env
# MySQL 连接字符串
DATABASE_URL="mysql://root:@localhost:3306/shiyu"
# 服务器端口
PORT=3000
# CORS 允许的源
ALLOWED_ORIGINS="exp://192.168.1.100:8081,http://localhost:8081"2. 配置 Prisma Schema (prisma/schema.prisma)
prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// 诗词表
model Poem {
id Int @id @default(autoincrement())
title String @db.VarChar(100)
content String @db.Text
author String @db.VarChar(50)
dynasty String @db.VarChar(20)
latitude Decimal @db.Decimal(10, 8)
longitude Decimal @db.Decimal(11, 8)
favoriteCount Int @default(0) @map("favorite_count")
checkInCount Int @default(0) @map("check_in_count")
createdAt DateTime @default(now()) @map("created_at")
favorites UserFavorite[]
checkIns CheckIn[]
@@index([latitude, longitude])
@@map("poems")
}
// 用户表
model User {
id Int @id @default(autoincrement())
username String @unique @db.VarChar(50)
email String? @unique @db.VarChar(100)
password String @db.VarChar(255)
totalFavorites Int @default(0) @map("total_favorites")
points Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
favorites UserFavorite[]
checkIns CheckIn[]
@@map("users")
}
// 收藏表
model UserFavorite {
id Int @id @default(autoincrement())
userId Int @map("user_id")
poemId Int @map("poem_id")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
poem Poem @relation(fields: [poemId], references: [id], onDelete: Cascade)
@@unique([userId, poemId])
@@index([userId])
@@index([poemId])
@@map("user_favorites")
}
// 打卡表
model CheckIn {
id Int @id @default(autoincrement())
userId Int @map("user_id")
poemId Int @map("poem_id")
latitude Decimal @db.Decimal(10, 8)
longitude Decimal @db.Decimal(11, 8)
photo String? @db.VarChar(500)
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
poem Poem @relation(fields: [poemId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([poemId])
@@map("check_ins")
}3. 从现有数据库生成模型
bash
# 如果已有 MySQL 数据库和表
bunx prisma db pull
# 生成 Prisma Client
bunx prisma generate4. 或创建新数据库
bash
# 创建迁移并应用
bunx prisma migrate dev --name init
# 生成 Prisma Client
bunx prisma generate🏗️ 项目结构
backend/
├── src/
│ ├── index.ts # 主入口
│ ├── db/
│ │ └── prisma.ts # Prisma Client 实例
│ ├── routes/
│ │ ├── poems.ts # 诗词相关路由
│ │ ├── users.ts # 用户相关路由
│ │ └── favorites.ts # 收藏相关路由
│ ├── services/
│ │ ├── poem.service.ts
│ │ └── user.service.ts
│ └── types/
│ └── index.ts # 类型定义
├── prisma/
│ └── schema.prisma # 数据库模型
├── .env # 环境变量
├── package.json
└── tsconfig.json💻 核心代码实现
1. Prisma Client (src/db/prisma.ts)
typescript
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}2. 诗词服务 (src/services/poem.service.ts)
typescript
import { prisma } from '../db/prisma'
export class PoemService {
// 获取附近诗词
async getNearbyPoems(lat: number, lng: number, radius: number = 5000) {
const poems = await prisma.$queryRaw<any[]>`
SELECT
id, title, content, author, dynasty,
latitude, longitude, favorite_count, check_in_count,
ST_Distance_Sphere(
POINT(longitude, latitude),
POINT(${lng}, ${lat})
) as distance
FROM poems
WHERE ST_Distance_Sphere(
POINT(longitude, latitude),
POINT(${lng}, ${lat})
) <= ${radius}
ORDER BY distance
LIMIT 50
`
return poems
}
// 搜索诗词
async searchPoems(keyword: string) {
return await prisma.poem.findMany({
where: {
OR: [
{ title: { contains: keyword } },
{ content: { contains: keyword } },
{ author: { contains: keyword } }
]
},
take: 50,
orderBy: { favoriteCount: 'desc' }
})
}
// 获取诗词详情
async getPoemById(id: number) {
return await prisma.poem.findUnique({
where: { id },
include: {
favorites: {
take: 10,
include: { user: { select: { username: true } } }
},
checkIns: {
take: 10,
include: { user: { select: { username: true } } }
}
}
})
}
// 按朝代获取诗词
async getPoemsByDynasty(dynasty: string, page: number = 1, limit: number = 20) {
const skip = (page - 1) * limit
const [poems, total] = await Promise.all([
prisma.poem.findMany({
where: { dynasty },
skip,
take: limit,
orderBy: { favoriteCount: 'desc' }
}),
prisma.poem.count({ where: { dynasty } })
])
return { poems, total, page, totalPages: Math.ceil(total / limit) }
}
}3. 用户服务 (src/services/user.service.ts)
typescript
import { prisma } from '../db/prisma'
export class UserService {
// 收藏诗词 (自动事务)
async addFavorite(userId: number, poemId: number) {
return await prisma.$transaction(async (tx) => {
// 检查是否已收藏
const existing = await tx.userFavorite.findUnique({
where: {
userId_poemId: { userId, poemId }
}
})
if (existing) {
throw new Error('Already favorited')
}
// 创建收藏
const favorite = await tx.userFavorite.create({
data: { userId, poemId },
include: { poem: true }
})
// 更新计数
await tx.poem.update({
where: { id: poemId },
data: { favoriteCount: { increment: 1 } }
})
await tx.user.update({
where: { id: userId },
data: { totalFavorites: { increment: 1 } }
})
return favorite
})
}
// 取消收藏 (自动事务)
async removeFavorite(userId: number, poemId: number) {
return await prisma.$transaction(async (tx) => {
const favorite = await tx.userFavorite.findUnique({
where: {
userId_poemId: { userId, poemId }
}
})
if (!favorite) {
throw new Error('Favorite not found')
}
await tx.userFavorite.delete({
where: { id: favorite.id }
})
await tx.poem.update({
where: { id: poemId },
data: { favoriteCount: { decrement: 1 } }
})
await tx.user.update({
where: { id: userId },
data: { totalFavorites: { decrement: 1 } }
})
return { success: true }
})
}
// 打卡 (自动事务)
async checkIn(userId: number, poemId: number, lat: number, lng: number, photo?: string) {
return await prisma.$transaction(async (tx) => {
const checkIn = await tx.checkIn.create({
data: {
userId,
poemId,
latitude: lat,
longitude: lng,
photo
},
include: { poem: true }
})
// 更新打卡次数
await tx.poem.update({
where: { id: poemId },
data: { checkInCount: { increment: 1 } }
})
// 增加用户积分
await tx.user.update({
where: { id: userId },
data: { points: { increment: 10 } }
})
return checkIn
})
}
// 获取用户收藏列表
async getUserFavorites(userId: number, page: number = 1, limit: number = 20) {
const skip = (page - 1) * limit
const [favorites, total] = await Promise.all([
prisma.userFavorite.findMany({
where: { userId },
skip,
take: limit,
include: { poem: true },
orderBy: { createdAt: 'desc' }
}),
prisma.userFavorite.count({ where: { userId } })
])
return { favorites, total, page, totalPages: Math.ceil(total / limit) }
}
}4. 主服务器 (src/index.ts)
typescript
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'
import { swagger } from '@elysiajs/swagger'
import { PoemService } from './services/poem.service'
import { UserService } from './services/user.service'
const poemService = new PoemService()
const userService = new UserService()
const app = new Elysia()
// CORS 配置
.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || true,
credentials: true
}))
// Swagger 文档
.use(swagger({
documentation: {
info: {
title: '诗遇 API',
version: '1.0.0',
description: '诗遇后端 API 文档'
}
}
}))
// 健康检查
.get('/health', () => ({ status: 'ok', timestamp: new Date().toISOString() }))
// ============ 诗词相关 API ============
// 获取附近诗词
.get('/api/poems/nearby', async ({ query }) => {
const { lat, lng, radius = 5000 } = query
if (!lat || !lng) {
return { success: false, error: 'Missing lat or lng' }
}
const poems = await poemService.getNearbyPoems(
parseFloat(lat as string),
parseFloat(lng as string),
parseInt(radius as string)
)
return { success: true, data: poems }
})
// 搜索诗词
.get('/api/poems/search', async ({ query }) => {
const { keyword } = query
if (!keyword) {
return { success: false, error: 'Missing keyword' }
}
const poems = await poemService.searchPoems(keyword as string)
return { success: true, data: poems }
})
// 获取诗词详情
.get('/api/poems/:id', async ({ params }) => {
const poem = await poemService.getPoemById(parseInt(params.id))
if (!poem) {
return { success: false, error: 'Poem not found' }
}
return { success: true, data: poem }
})
// 按朝代获取诗词
.get('/api/poems/dynasty/:dynasty', async ({ params, query }) => {
const { dynasty } = params
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 20
const result = await poemService.getPoemsByDynasty(dynasty, page, limit)
return { success: true, data: result }
})
// ============ 用户相关 API ============
// 收藏诗词
.post('/api/favorites', async ({ body }) => {
try {
const { userId, poemId } = body as { userId: number; poemId: number }
const favorite = await userService.addFavorite(userId, poemId)
return { success: true, data: favorite }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// 取消收藏
.delete('/api/favorites', async ({ body }) => {
try {
const { userId, poemId } = body as { userId: number; poemId: number }
await userService.removeFavorite(userId, poemId)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// 打卡
.post('/api/checkins', async ({ body }) => {
try {
const { userId, poemId, lat, lng, photo } = body as {
userId: number
poemId: number
lat: number
lng: number
photo?: string
}
const checkIn = await userService.checkIn(userId, poemId, lat, lng, photo)
return { success: true, data: checkIn }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// 获取用户收藏列表
.get('/api/users/:userId/favorites', async ({ params, query }) => {
const userId = parseInt(params.userId)
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 20
const result = await userService.getUserFavorites(userId, page, limit)
return { success: true, data: result }
})
// 启动服务器
.listen(process.env.PORT || 3000)
console.log(`🚀 诗遇 API 运行在 http://localhost:${app.server?.port}`)
console.log(`📖 API 文档: http://localhost:${app.server?.port}/swagger`)🔧 常用命令
Prisma 命令
bash
# 生成 Prisma Client (修改 schema 后必须执行)
bunx prisma generate
# 查看数据库 (可视化界面)
bunx prisma studio
# 从数据库拉取 schema
bunx prisma db pull
# 推送 schema 到数据库 (开发环境)
bunx prisma db push
# 创建迁移 (生产环境)
bunx prisma migrate dev --name migration_name
# 应用迁移
bunx prisma migrate deploy
# 重置数据库
bunx prisma migrate reset
# 格式化 schema
bunx prisma format运行服务器
bash
# 开发模式 (热重载)
bun --watch src/index.ts
# 生产模式
bun src/index.ts添加到 package.json
json
{
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"db:generate": "bunx prisma generate",
"db:push": "bunx prisma db push",
"db:studio": "bunx prisma studio",
"db:migrate": "bunx prisma migrate dev"
}
}📱 React Native 客户端调用
typescript
// api/poems.ts
const API_BASE = 'http://192.168.1.100:3000/api'
export async function getNearbyPoems(lat: number, lng: number, radius: number = 5000) {
const response = await fetch(
`${API_BASE}/poems/nearby?lat=${lat}&lng=${lng}&radius=${radius}`
)
return await response.json()
}
export async function searchPoems(keyword: string) {
const response = await fetch(
`${API_BASE}/poems/search?keyword=${encodeURIComponent(keyword)}`
)
return await response.json()
}
export async function addFavorite(userId: number, poemId: number) {
const response = await fetch(`${API_BASE}/favorites`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, poemId })
})
return await response.json()
}
// 使用
const { data: poems } = await getNearbyPoems(39.9042, 116.4074)🚀 性能优化
1. 数据库索引
sql
-- 地理位置索引
CREATE INDEX idx_location ON poems(latitude, longitude);
-- 复合索引
CREATE INDEX idx_dynasty_author ON poems(dynasty, author);
-- 全文索引 (中文搜索)
ALTER TABLE poems ADD FULLTEXT INDEX idx_content (title, content, author) WITH PARSER ngram;2. Prisma 查询优化
typescript
// 使用 select 减少数据传输
const poems = await prisma.poem.findMany({
select: {
id: true,
title: true,
author: true,
// 不查询 content (减少数据量)
}
})
// 使用 include 预加载关联数据 (避免 N+1 查询)
const poem = await prisma.poem.findUnique({
where: { id: 1 },
include: {
favorites: { take: 10 },
checkIns: { take: 10 }
}
})3. 连接池配置
env
# .env
DATABASE_URL="mysql://root:@localhost:3306/shiyu?connection_limit=10&pool_timeout=20"🔐 安全建议
- 使用环境变量: 不要把密码写在代码里
- 添加认证: JWT 或 Session
- 输入验证: 使用 Elysia 的内置验证
- SQL 注入防护: Prisma 自动防护
- CORS 配置: 只允许特定域名
📊 监控和日志
typescript
// 添加请求日志
app.onRequest(({ request }) => {
console.log(`${new Date().toISOString()} ${request.method} ${request.url}`)
})
// 添加错误处理
app.onError(({ error, code }) => {
console.error(`Error ${code}:`, error)
return { success: false, error: error.message }
})🎯 下一步
- ✅ 配置数据库连接
- ✅ 生成 Prisma Client
- ✅ 实现核心 API
- ⬜ 添加用户认证 (JWT)
- ⬜ 添加图片上传 (打卡照片)
- ⬜ 添加 WebSocket (实时通知)
- ⬜ 部署到服务器
相关文档: