refactor: 数据层迁往服务端 (Express+DuckDB), APP 变瘦客户端只留缓存
- server: 新增 DuckDB 数据层 (db.js) 与鉴权 (auth.js: 验证码+token), 挂载 /api 业务路由 (衣物/搭配/日历/统计/MCP), 错误统一中间件 - 前端 src/api/index.ts 全部业务调用改为 HTTP (Bearer token), 客户端不再持有数据库 - 删除 Rust 数据层 (db.rs/auth.rs/mcp.rs/models.rs), Cargo.toml 裁剪 duckdb/chrono 等依赖 - Dockerfile: 基镜像 alpine→slim (@duckdb/node-api glibc 预编译), DB 落 /app/data 卷 - docker-build.yml: 移除 prune --volumes, 挂具名卷 wardrobe-data 持久化 - Landing/Profile 文案改为云端同步口径; 退出登录调服务端会话销毁
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// 业务 REST API: 鉴权 + 衣物/搭配/日历/统计/MCP
|
||||
// 客户端不再持有数据库, 全部数据经此接口读写 (瘦客户端)
|
||||
import { Router } from 'express'
|
||||
import * as auth from '../auth.js'
|
||||
import {
|
||||
dbCreateClothing, dbListClothing, getClothing, dbUpdateClothing, dbDeleteClothing,
|
||||
dbCreateOutfit, dbListOutfits, dbUpsertDaily, dbListDaily, dbGetStats,
|
||||
mcpTools, mcpCall,
|
||||
} from '../db.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const asyncH = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next)
|
||||
|
||||
// ── 鉴权中间件: Authorization: Bearer <token> ──
|
||||
async function requireAuth(req, res, next) {
|
||||
try {
|
||||
const h = req.headers.authorization || ''
|
||||
const token = h.startsWith('Bearer ') ? h.slice(7).trim() : null
|
||||
const user = await auth.resolveSession(token)
|
||||
if (!user) return res.status(401).json({ error: '未登录或登录已过期' })
|
||||
req.user = user
|
||||
next()
|
||||
} catch (e) {
|
||||
next(e)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auth ──
|
||||
router.post('/auth/send-code', asyncH(async (req, res) => {
|
||||
const target = String(req.body?.target || '').trim()
|
||||
if (!target) return res.status(400).json({ error: '请提供邮箱或手机号' })
|
||||
res.json({ message: auth.sendVerification(target) })
|
||||
}))
|
||||
|
||||
router.post('/auth/register', asyncH(async (req, res) => {
|
||||
const { email, phone, code, nickname } = req.body || {}
|
||||
res.json(await auth.register({ email: email || null, phone: phone || null, code, nickname }))
|
||||
}))
|
||||
|
||||
router.post('/auth/login', asyncH(async (req, res) => {
|
||||
const { email, phone, code } = req.body || {}
|
||||
res.json(await auth.login({ email: email || null, phone: phone || null, code }))
|
||||
}))
|
||||
|
||||
router.post('/auth/logout', requireAuth, asyncH(async (req, res) => {
|
||||
const h = req.headers.authorization || ''
|
||||
const token = h.startsWith('Bearer ') ? h.slice(7).trim() : null
|
||||
await auth.logout(token)
|
||||
res.json({ success: true })
|
||||
}))
|
||||
|
||||
router.get('/auth/me', requireAuth, asyncH(async (req, res) => {
|
||||
res.json({ user: req.user })
|
||||
}))
|
||||
|
||||
// ── Clothing ──
|
||||
router.get('/clothing', requireAuth, asyncH(async (req, res) => {
|
||||
res.json(await dbListClothing(req.user.id, req.query.category || null))
|
||||
}))
|
||||
|
||||
router.post('/clothing', requireAuth, asyncH(async (req, res) => {
|
||||
const b = req.body || {}
|
||||
if (!b.name || !b.category || !b.color) return res.status(400).json({ error: '缺少必填字段' })
|
||||
res.json(await dbCreateClothing(req.user.id, b))
|
||||
}))
|
||||
|
||||
router.get('/clothing/:id', requireAuth, asyncH(async (req, res) => {
|
||||
res.json(await getClothing(req.user.id, req.params.id))
|
||||
}))
|
||||
|
||||
router.put('/clothing/:id', requireAuth, asyncH(async (req, res) => {
|
||||
const b = req.body || {}
|
||||
await dbUpdateClothing(req.user.id, req.params.id, b.name, b.category, b.color)
|
||||
res.json({ success: true })
|
||||
}))
|
||||
|
||||
router.delete('/clothing/:id', requireAuth, asyncH(async (req, res) => {
|
||||
await dbDeleteClothing(req.user.id, req.params.id)
|
||||
res.json({ success: true })
|
||||
}))
|
||||
|
||||
// ── Outfits ──
|
||||
router.get('/outfits', requireAuth, asyncH(async (req, res) => {
|
||||
res.json(await dbListOutfits(req.user.id))
|
||||
}))
|
||||
|
||||
router.post('/outfits', requireAuth, asyncH(async (req, res) => {
|
||||
const b = req.body || {}
|
||||
if (!b.name || !Array.isArray(b.item_ids)) return res.status(400).json({ error: '缺少必填字段' })
|
||||
res.json(await dbCreateOutfit(req.user.id, b))
|
||||
}))
|
||||
|
||||
// ── Daily Records ──
|
||||
router.get('/daily', requireAuth, asyncH(async (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 30, 500)
|
||||
res.json(await dbListDaily(req.user.id, limit))
|
||||
}))
|
||||
|
||||
router.post('/daily', requireAuth, asyncH(async (req, res) => {
|
||||
const b = req.body || {}
|
||||
if (!b.date) return res.status(400).json({ error: '缺少 date' })
|
||||
res.json(await dbUpsertDaily(req.user.id, b))
|
||||
}))
|
||||
|
||||
// ── Stats ──
|
||||
router.get('/stats', requireAuth, asyncH(async (req, res) => {
|
||||
res.json(await dbGetStats(req.user.id))
|
||||
}))
|
||||
|
||||
// ── MCP ──
|
||||
router.get('/mcp/tools', requireAuth, asyncH(async (req, res) => {
|
||||
res.json(mcpTools())
|
||||
}))
|
||||
|
||||
router.post('/mcp/call', requireAuth, asyncH(async (req, res) => {
|
||||
const { tool_name, args } = req.body || {}
|
||||
if (!tool_name) return res.status(400).json({ error: '缺少 tool_name' })
|
||||
const r = await mcpCall(tool_name, args || {}, req.user.id)
|
||||
res.json(r.value)
|
||||
}))
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user