@arrislink/axon 1.2.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/README.zh-CN.md +10 -9
- package/dist/index.js +49506 -48999
- package/package.json +1 -1
- package/templates/skills/api/rest-crud.md +0 -240
- package/templates/skills/auth/jwt.md +0 -186
- package/templates/skills/database/postgres-schema.md +0 -161
package/package.json
CHANGED
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: RESTful CRUD API
|
|
3
|
-
description: 实现标准 RESTful CRUD 接口
|
|
4
|
-
tags: [api, rest, crud, typescript]
|
|
5
|
-
models: [claude-sonnet-4, gpt-4]
|
|
6
|
-
tokens_avg: 2200
|
|
7
|
-
difficulty: easy
|
|
8
|
-
last_updated: 2026-02-09
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Context
|
|
12
|
-
|
|
13
|
-
实现标准的 RESTful CRUD API,包括列表、详情、创建、更新和删除操作。
|
|
14
|
-
|
|
15
|
-
## Prerequisites
|
|
16
|
-
|
|
17
|
-
- Hono / Express / Fastify
|
|
18
|
-
- TypeScript
|
|
19
|
-
- 数据验证库 (Zod)
|
|
20
|
-
|
|
21
|
-
## Implementation
|
|
22
|
-
|
|
23
|
-
### 1. 路由结构
|
|
24
|
-
|
|
25
|
-
| 方法 | 路径 | 描述 |
|
|
26
|
-
|------|------|------|
|
|
27
|
-
| GET | /api/items | 获取列表 (分页) |
|
|
28
|
-
| GET | /api/items/:id | 获取详情 |
|
|
29
|
-
| POST | /api/items | 创建 |
|
|
30
|
-
| PUT | /api/items/:id | 完整更新 |
|
|
31
|
-
| PATCH | /api/items/:id | 部分更新 |
|
|
32
|
-
| DELETE | /api/items/:id | 删除 |
|
|
33
|
-
|
|
34
|
-
### 2. 使用 Hono 实现
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// src/routes/items.ts
|
|
38
|
-
import { Hono } from 'hono';
|
|
39
|
-
import { zValidator } from '@hono/zod-validator';
|
|
40
|
-
import { z } from 'zod';
|
|
41
|
-
|
|
42
|
-
const app = new Hono();
|
|
43
|
-
|
|
44
|
-
// 验证 Schema
|
|
45
|
-
const createItemSchema = z.object({
|
|
46
|
-
name: z.string().min(1).max(100),
|
|
47
|
-
description: z.string().optional(),
|
|
48
|
-
price: z.number().positive(),
|
|
49
|
-
category: z.enum(['electronics', 'clothing', 'food']),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const updateItemSchema = createItemSchema.partial();
|
|
53
|
-
|
|
54
|
-
const querySchema = z.object({
|
|
55
|
-
page: z.coerce.number().min(1).default(1),
|
|
56
|
-
limit: z.coerce.number().min(1).max(100).default(20),
|
|
57
|
-
sort: z.enum(['newest', 'oldest', 'price_asc', 'price_desc']).default('newest'),
|
|
58
|
-
category: z.string().optional(),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// GET /items - 列表
|
|
62
|
-
app.get('/', zValidator('query', querySchema), async (c) => {
|
|
63
|
-
const { page, limit, sort, category } = c.req.valid('query');
|
|
64
|
-
const offset = (page - 1) * limit;
|
|
65
|
-
|
|
66
|
-
const items = await db.query.items.findMany({
|
|
67
|
-
where: category ? eq(items.category, category) : undefined,
|
|
68
|
-
limit,
|
|
69
|
-
offset,
|
|
70
|
-
orderBy: getSortOrder(sort),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const total = await db.select({ count: count() }).from(items);
|
|
74
|
-
|
|
75
|
-
return c.json({
|
|
76
|
-
data: items,
|
|
77
|
-
pagination: {
|
|
78
|
-
page,
|
|
79
|
-
limit,
|
|
80
|
-
total: total[0].count,
|
|
81
|
-
totalPages: Math.ceil(total[0].count / limit),
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// GET /items/:id - 详情
|
|
87
|
-
app.get('/:id', async (c) => {
|
|
88
|
-
const id = c.req.param('id');
|
|
89
|
-
const item = await db.query.items.findFirst({
|
|
90
|
-
where: eq(items.id, id),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (!item) {
|
|
94
|
-
return c.json({ error: 'Item not found' }, 404);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return c.json({ data: item });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// POST /items - 创建
|
|
101
|
-
app.post('/', zValidator('json', createItemSchema), async (c) => {
|
|
102
|
-
const data = c.req.valid('json');
|
|
103
|
-
|
|
104
|
-
const [item] = await db.insert(items).values(data).returning();
|
|
105
|
-
|
|
106
|
-
return c.json({ data: item }, 201);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// PUT /items/:id - 完整更新
|
|
110
|
-
app.put('/:id', zValidator('json', createItemSchema), async (c) => {
|
|
111
|
-
const id = c.req.param('id');
|
|
112
|
-
const data = c.req.valid('json');
|
|
113
|
-
|
|
114
|
-
const [item] = await db
|
|
115
|
-
.update(items)
|
|
116
|
-
.set({ ...data, updatedAt: new Date() })
|
|
117
|
-
.where(eq(items.id, id))
|
|
118
|
-
.returning();
|
|
119
|
-
|
|
120
|
-
if (!item) {
|
|
121
|
-
return c.json({ error: 'Item not found' }, 404);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return c.json({ data: item });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// PATCH /items/:id - 部分更新
|
|
128
|
-
app.patch('/:id', zValidator('json', updateItemSchema), async (c) => {
|
|
129
|
-
const id = c.req.param('id');
|
|
130
|
-
const data = c.req.valid('json');
|
|
131
|
-
|
|
132
|
-
const [item] = await db
|
|
133
|
-
.update(items)
|
|
134
|
-
.set({ ...data, updatedAt: new Date() })
|
|
135
|
-
.where(eq(items.id, id))
|
|
136
|
-
.returning();
|
|
137
|
-
|
|
138
|
-
if (!item) {
|
|
139
|
-
return c.json({ error: 'Item not found' }, 404);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return c.json({ data: item });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// DELETE /items/:id - 删除
|
|
146
|
-
app.delete('/:id', async (c) => {
|
|
147
|
-
const id = c.req.param('id');
|
|
148
|
-
|
|
149
|
-
const [item] = await db
|
|
150
|
-
.delete(items)
|
|
151
|
-
.where(eq(items.id, id))
|
|
152
|
-
.returning();
|
|
153
|
-
|
|
154
|
-
if (!item) {
|
|
155
|
-
return c.json({ error: 'Item not found' }, 404);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return c.json({ message: 'Deleted successfully' });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
export default app;
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### 3. 响应格式规范
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
// 成功响应
|
|
168
|
-
{
|
|
169
|
-
"data": { ... } | [ ... ],
|
|
170
|
-
"pagination": { ... } // 仅列表接口
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 错误响应
|
|
174
|
-
{
|
|
175
|
-
"error": "Error message",
|
|
176
|
-
"code": "ERROR_CODE",
|
|
177
|
-
"details": { ... } // 可选
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### 4. HTTP 状态码规范
|
|
182
|
-
|
|
183
|
-
| 状态码 | 含义 | 使用场景 |
|
|
184
|
-
|--------|------|----------|
|
|
185
|
-
| 200 | OK | GET, PUT, PATCH 成功 |
|
|
186
|
-
| 201 | Created | POST 成功 |
|
|
187
|
-
| 204 | No Content | DELETE 成功 (无响应体) |
|
|
188
|
-
| 400 | Bad Request | 请求参数错误 |
|
|
189
|
-
| 401 | Unauthorized | 未认证 |
|
|
190
|
-
| 403 | Forbidden | 无权限 |
|
|
191
|
-
| 404 | Not Found | 资源不存在 |
|
|
192
|
-
| 409 | Conflict | 资源冲突 (如重复) |
|
|
193
|
-
| 422 | Unprocessable | 验证失败 |
|
|
194
|
-
| 500 | Internal Error | 服务器错误 |
|
|
195
|
-
|
|
196
|
-
## Tests
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
// tests/routes/items.test.ts
|
|
200
|
-
import { describe, test, expect } from 'bun:test';
|
|
201
|
-
import app from '../../src/routes/items';
|
|
202
|
-
|
|
203
|
-
describe('Items API', () => {
|
|
204
|
-
test('GET /items returns list', async () => {
|
|
205
|
-
const res = await app.request('/');
|
|
206
|
-
expect(res.status).toBe(200);
|
|
207
|
-
|
|
208
|
-
const data = await res.json();
|
|
209
|
-
expect(data.data).toBeArray();
|
|
210
|
-
expect(data.pagination).toBeDefined();
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('POST /items creates item', async () => {
|
|
214
|
-
const res = await app.request('/', {
|
|
215
|
-
method: 'POST',
|
|
216
|
-
headers: { 'Content-Type': 'application/json' },
|
|
217
|
-
body: JSON.stringify({
|
|
218
|
-
name: 'Test Item',
|
|
219
|
-
price: 99.99,
|
|
220
|
-
category: 'electronics',
|
|
221
|
-
}),
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
expect(res.status).toBe(201);
|
|
225
|
-
const data = await res.json();
|
|
226
|
-
expect(data.data.name).toBe('Test Item');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test('GET /items/:id returns 404 for missing', async () => {
|
|
230
|
-
const res = await app.request('/non-existent-id');
|
|
231
|
-
expect(res.status).toBe(404);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Related Skills
|
|
237
|
-
|
|
238
|
-
- api/pagination.md
|
|
239
|
-
- api/error-handling.md
|
|
240
|
-
- api/rate-limiting.md
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: JWT Authentication Implementation
|
|
3
|
-
description: 实现基于 JWT 的用户认证系统
|
|
4
|
-
tags: [auth, jwt, security, typescript]
|
|
5
|
-
models: [claude-sonnet-4, gpt-4]
|
|
6
|
-
tokens_avg: 2500
|
|
7
|
-
difficulty: medium
|
|
8
|
-
last_updated: 2026-02-09
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Context
|
|
12
|
-
|
|
13
|
-
实现一个安全的 JWT 认证系统,包括 token 生成、验证和刷新机制。
|
|
14
|
-
|
|
15
|
-
## Prerequisites
|
|
16
|
-
|
|
17
|
-
- Node.js >= 18 或 Bun
|
|
18
|
-
- jsonwebtoken 库
|
|
19
|
-
- 环境变量: JWT_SECRET, JWT_EXPIRES_IN
|
|
20
|
-
|
|
21
|
-
## Implementation
|
|
22
|
-
|
|
23
|
-
### 1. 安装依赖
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
bun add jsonwebtoken
|
|
27
|
-
bun add -d @types/jsonwebtoken
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### 2. Token 生成工具
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// src/auth/jwt.ts
|
|
34
|
-
import jwt from 'jsonwebtoken';
|
|
35
|
-
|
|
36
|
-
const JWT_SECRET = process.env.JWT_SECRET!;
|
|
37
|
-
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
|
38
|
-
|
|
39
|
-
export interface TokenPayload {
|
|
40
|
-
userId: string;
|
|
41
|
-
email?: string;
|
|
42
|
-
role?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function generateToken(payload: TokenPayload): string {
|
|
46
|
-
return jwt.sign(payload, JWT_SECRET, {
|
|
47
|
-
expiresIn: JWT_EXPIRES_IN,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function verifyToken(token: string): TokenPayload {
|
|
52
|
-
return jwt.verify(token, JWT_SECRET) as TokenPayload;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function decodeToken(token: string): TokenPayload | null {
|
|
56
|
-
const decoded = jwt.decode(token);
|
|
57
|
-
return decoded as TokenPayload | null;
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### 3. 认证中间件
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
// src/auth/middleware.ts
|
|
65
|
-
import type { Request, Response, NextFunction } from 'express';
|
|
66
|
-
import { verifyToken } from './jwt';
|
|
67
|
-
|
|
68
|
-
export function authenticateToken(
|
|
69
|
-
req: Request,
|
|
70
|
-
res: Response,
|
|
71
|
-
next: NextFunction
|
|
72
|
-
) {
|
|
73
|
-
const authHeader = req.headers['authorization'];
|
|
74
|
-
const token = authHeader?.split(' ')[1];
|
|
75
|
-
|
|
76
|
-
if (!token) {
|
|
77
|
-
return res.status(401).json({ error: 'Token required' });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const payload = verifyToken(token);
|
|
82
|
-
req.user = payload;
|
|
83
|
-
next();
|
|
84
|
-
} catch (error) {
|
|
85
|
-
return res.status(403).json({ error: 'Invalid token' });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### 4. 登录接口示例
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
// src/routes/auth.ts
|
|
94
|
-
import { Router } from 'express';
|
|
95
|
-
import { generateToken } from '../auth/jwt';
|
|
96
|
-
|
|
97
|
-
const router = Router();
|
|
98
|
-
|
|
99
|
-
router.post('/login', async (req, res) => {
|
|
100
|
-
const { email, password } = req.body;
|
|
101
|
-
|
|
102
|
-
// 验证用户 (示例)
|
|
103
|
-
const user = await validateUser(email, password);
|
|
104
|
-
if (!user) {
|
|
105
|
-
return res.status(401).json({ error: 'Invalid credentials' });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 生成 token
|
|
109
|
-
const token = generateToken({
|
|
110
|
-
userId: user.id,
|
|
111
|
-
email: user.email,
|
|
112
|
-
role: user.role,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
res.json({ token, expiresIn: process.env.JWT_EXPIRES_IN || '7d' });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
export default router;
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## Tests
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
// tests/auth/jwt.test.ts
|
|
125
|
-
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
126
|
-
import { generateToken, verifyToken, decodeToken } from '../../src/auth/jwt';
|
|
127
|
-
|
|
128
|
-
beforeAll(() => {
|
|
129
|
-
process.env.JWT_SECRET = 'test-secret-key';
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('JWT Authentication', () => {
|
|
133
|
-
test('generates valid token', () => {
|
|
134
|
-
const payload = { userId: 'user123', email: 'test@example.com' };
|
|
135
|
-
const token = generateToken(payload);
|
|
136
|
-
|
|
137
|
-
expect(token).toBeDefined();
|
|
138
|
-
expect(typeof token).toBe('string');
|
|
139
|
-
expect(token.split('.')).toHaveLength(3);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('verifies valid token', () => {
|
|
143
|
-
const payload = { userId: 'user123' };
|
|
144
|
-
const token = generateToken(payload);
|
|
145
|
-
const decoded = verifyToken(token);
|
|
146
|
-
|
|
147
|
-
expect(decoded.userId).toBe('user123');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('rejects invalid token', () => {
|
|
151
|
-
expect(() => {
|
|
152
|
-
verifyToken('invalid-token');
|
|
153
|
-
}).toThrow();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('decodes token without verification', () => {
|
|
157
|
-
const payload = { userId: 'user123' };
|
|
158
|
-
const token = generateToken(payload);
|
|
159
|
-
const decoded = decodeToken(token);
|
|
160
|
-
|
|
161
|
-
expect(decoded?.userId).toBe('user123');
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
## Security Considerations
|
|
167
|
-
|
|
168
|
-
1. **使用强随机 SECRET** - 至少 256 位随机字符串
|
|
169
|
-
2. **设置合理的过期时间** - 建议 15 分钟到 7 天
|
|
170
|
-
3. **使用 HTTPS** - 防止 token 被截获
|
|
171
|
-
4. **Token 刷新机制** - 实现 refresh token 延长会话
|
|
172
|
-
5. **黑名单机制** - 支持主动撤销 token
|
|
173
|
-
|
|
174
|
-
## Environment Variables
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
# .env
|
|
178
|
-
JWT_SECRET=your-256-bit-secret-key-here
|
|
179
|
-
JWT_EXPIRES_IN=7d
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Related Skills
|
|
183
|
-
|
|
184
|
-
- auth/refresh-token.md
|
|
185
|
-
- auth/oauth-integration.md
|
|
186
|
-
- security/input-validation.md
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: PostgreSQL Schema Design
|
|
3
|
-
description: PostgreSQL 数据库表结构设计最佳实践
|
|
4
|
-
tags: [database, postgresql, schema, sql]
|
|
5
|
-
models: [claude-sonnet-4, gpt-4]
|
|
6
|
-
tokens_avg: 2000
|
|
7
|
-
difficulty: medium
|
|
8
|
-
last_updated: 2026-02-09
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Context
|
|
12
|
-
|
|
13
|
-
设计 PostgreSQL 数据库表结构,遵循最佳实践和规范。
|
|
14
|
-
|
|
15
|
-
## Prerequisites
|
|
16
|
-
|
|
17
|
-
- PostgreSQL >= 14
|
|
18
|
-
- 数据库迁移工具 (如 Drizzle, Prisma, Knex)
|
|
19
|
-
|
|
20
|
-
## Implementation
|
|
21
|
-
|
|
22
|
-
### 1. 基础表结构模板
|
|
23
|
-
|
|
24
|
-
```sql
|
|
25
|
-
-- 启用 UUID 扩展
|
|
26
|
-
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
27
|
-
|
|
28
|
-
-- 用户表
|
|
29
|
-
CREATE TABLE users (
|
|
30
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
31
|
-
email VARCHAR(255) NOT NULL UNIQUE,
|
|
32
|
-
password_hash VARCHAR(255) NOT NULL,
|
|
33
|
-
username VARCHAR(50) NOT NULL UNIQUE,
|
|
34
|
-
display_name VARCHAR(100),
|
|
35
|
-
avatar_url TEXT,
|
|
36
|
-
role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')),
|
|
37
|
-
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'banned')),
|
|
38
|
-
email_verified_at TIMESTAMP WITH TIME ZONE,
|
|
39
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
40
|
-
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
-- 创建更新时间触发器
|
|
44
|
-
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
|
45
|
-
RETURNS TRIGGER AS $$
|
|
46
|
-
BEGIN
|
|
47
|
-
NEW.updated_at = NOW();
|
|
48
|
-
RETURN NEW;
|
|
49
|
-
END;
|
|
50
|
-
$$ language 'plpgsql';
|
|
51
|
-
|
|
52
|
-
CREATE TRIGGER update_users_updated_at
|
|
53
|
-
BEFORE UPDATE ON users
|
|
54
|
-
FOR EACH ROW
|
|
55
|
-
EXECUTE FUNCTION update_updated_at_column();
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 2. 索引策略
|
|
59
|
-
|
|
60
|
-
```sql
|
|
61
|
-
-- 常用查询索引
|
|
62
|
-
CREATE INDEX idx_users_email ON users(email);
|
|
63
|
-
CREATE INDEX idx_users_status ON users(status) WHERE status != 'active';
|
|
64
|
-
CREATE INDEX idx_users_created_at ON users(created_at DESC);
|
|
65
|
-
|
|
66
|
-
-- 复合索引 (按查询模式设计)
|
|
67
|
-
CREATE INDEX idx_users_role_status ON users(role, status);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### 3. 关联表设计
|
|
71
|
-
|
|
72
|
-
```sql
|
|
73
|
-
-- 会话表 (一对多)
|
|
74
|
-
CREATE TABLE sessions (
|
|
75
|
-
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
76
|
-
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
77
|
-
token VARCHAR(255) NOT NULL UNIQUE,
|
|
78
|
-
user_agent TEXT,
|
|
79
|
-
ip_address INET,
|
|
80
|
-
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
81
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
|
|
85
|
-
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
|
|
86
|
-
|
|
87
|
-
-- 标签表 (多对多)
|
|
88
|
-
CREATE TABLE tags (
|
|
89
|
-
id SERIAL PRIMARY KEY,
|
|
90
|
-
name VARCHAR(50) NOT NULL UNIQUE,
|
|
91
|
-
slug VARCHAR(50) NOT NULL UNIQUE,
|
|
92
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
CREATE TABLE post_tags (
|
|
96
|
-
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
|
|
97
|
-
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
98
|
-
PRIMARY KEY (post_id, tag_id)
|
|
99
|
-
);
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### 4. JSONB 字段使用
|
|
103
|
-
|
|
104
|
-
```sql
|
|
105
|
-
-- 灵活的元数据存储
|
|
106
|
-
ALTER TABLE users ADD COLUMN metadata JSONB DEFAULT '{}';
|
|
107
|
-
|
|
108
|
-
-- JSONB 索引 (GIN)
|
|
109
|
-
CREATE INDEX idx_users_metadata ON users USING GIN (metadata);
|
|
110
|
-
|
|
111
|
-
-- 查询 JSONB
|
|
112
|
-
SELECT * FROM users WHERE metadata->>'theme' = 'dark';
|
|
113
|
-
SELECT * FROM users WHERE metadata @> '{"preferences": {"newsletter": true}}';
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### 5. Drizzle ORM 示例
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
// src/db/schema.ts
|
|
120
|
-
import { pgTable, uuid, varchar, timestamp, text, jsonb } from 'drizzle-orm/pg-core';
|
|
121
|
-
|
|
122
|
-
export const users = pgTable('users', {
|
|
123
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
124
|
-
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
125
|
-
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
|
126
|
-
username: varchar('username', { length: 50 }).notNull().unique(),
|
|
127
|
-
displayName: varchar('display_name', { length: 100 }),
|
|
128
|
-
avatarUrl: text('avatar_url'),
|
|
129
|
-
role: varchar('role', { length: 20 }).default('user'),
|
|
130
|
-
status: varchar('status', { length: 20 }).default('active'),
|
|
131
|
-
emailVerifiedAt: timestamp('email_verified_at', { withTimezone: true }),
|
|
132
|
-
metadata: jsonb('metadata').default({}),
|
|
133
|
-
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
|
134
|
-
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
export const sessions = pgTable('sessions', {
|
|
138
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
139
|
-
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
140
|
-
token: varchar('token', { length: 255 }).notNull().unique(),
|
|
141
|
-
userAgent: text('user_agent'),
|
|
142
|
-
ipAddress: varchar('ip_address', { length: 45 }),
|
|
143
|
-
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
144
|
-
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Best Practices
|
|
149
|
-
|
|
150
|
-
1. **使用 UUID 作为主键** - 分布式友好,无法猜测
|
|
151
|
-
2. **始终添加 created_at/updated_at** - 审计和调试必需
|
|
152
|
-
3. **使用 CHECK 约束** - 确保数据完整性
|
|
153
|
-
4. **索引策略** - 根据查询模式设计,避免过度索引
|
|
154
|
-
5. **使用 ON DELETE CASCADE** - 简化级联删除
|
|
155
|
-
6. **分区大表** - 超过 1000 万行考虑分区
|
|
156
|
-
|
|
157
|
-
## Related Skills
|
|
158
|
-
|
|
159
|
-
- database/migrations.md
|
|
160
|
-
- database/query-optimization.md
|
|
161
|
-
- database/backup-strategy.md
|