@svton/cli 1.2.1 → 1.2.2
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/dist/index.js +13 -6
- package/dist/index.mjs +13 -6
- package/package.json +3 -1
- package/templates/apps/admin/next-env.d.ts +2 -0
- package/templates/apps/admin/next.config.js +15 -0
- package/templates/apps/admin/package.json.tpl +54 -0
- package/templates/apps/admin/postcss.config.js +6 -0
- package/templates/apps/admin/src/app/globals.css +37 -0
- package/templates/apps/admin/src/app/layout.tsx +19 -0
- package/templates/apps/admin/src/app/login/page.tsx +96 -0
- package/templates/apps/admin/src/app/page.tsx +8 -0
- package/templates/apps/admin/src/app/users/page.tsx +165 -0
- package/templates/apps/admin/src/components/ui/switch.tsx +29 -0
- package/templates/apps/admin/src/hooks/useAPI.ts +130 -0
- package/templates/apps/admin/src/lib/api-client.ts +112 -0
- package/templates/apps/admin/src/lib/api-server.ts +95 -0
- package/templates/apps/admin/tailwind.config.js +54 -0
- package/templates/apps/admin/tsconfig.json +22 -0
- package/templates/apps/backend/.env.example +29 -0
- package/templates/apps/backend/nest-cli.json +8 -0
- package/templates/apps/backend/package.json.tpl +57 -0
- package/templates/apps/backend/prisma/schema.prisma +72 -0
- package/templates/apps/backend/prisma/seed.ts +32 -0
- package/templates/apps/backend/src/app.controller.ts +15 -0
- package/templates/apps/backend/src/app.module.ts +85 -0
- package/templates/apps/backend/src/app.service.ts +12 -0
- package/templates/apps/backend/src/auth/auth.controller.ts +31 -0
- package/templates/apps/backend/src/auth/auth.module.ts +27 -0
- package/templates/apps/backend/src/auth/auth.service.ts +89 -0
- package/templates/apps/backend/src/auth/jwt-auth.guard.ts +5 -0
- package/templates/apps/backend/src/auth/jwt.strategy.ts +27 -0
- package/templates/apps/backend/src/config/env.schema.ts +35 -0
- package/templates/apps/backend/src/main.ts +51 -0
- package/templates/apps/backend/src/object-storage/object-storage.controller.ts +114 -0
- package/templates/apps/backend/src/object-storage/object-storage.module.ts +7 -0
- package/templates/apps/backend/src/prisma/prisma.module.ts +9 -0
- package/templates/apps/backend/src/prisma/prisma.service.ts +13 -0
- package/templates/apps/backend/src/user/user.controller.ts +50 -0
- package/templates/apps/backend/src/user/user.module.ts +12 -0
- package/templates/apps/backend/src/user/user.service.ts +117 -0
- package/templates/apps/backend/tsconfig.json +23 -0
- package/templates/apps/mobile/babel.config.js +8 -0
- package/templates/apps/mobile/config/index.ts +65 -0
- package/templates/apps/mobile/package.json.tpl +48 -0
- package/templates/apps/mobile/project.config.json.tpl +17 -0
- package/templates/apps/mobile/src/app.config.ts +9 -0
- package/templates/apps/mobile/src/app.scss +4 -0
- package/templates/apps/mobile/src/app.ts +8 -0
- package/templates/apps/mobile/src/hooks/useAPI.ts +285 -0
- package/templates/apps/mobile/src/pages/index/index.scss +7 -0
- package/templates/apps/mobile/src/pages/index/index.tsx +49 -0
- package/templates/apps/mobile/src/services/api.ts +155 -0
- package/templates/apps/mobile/src/services/upload.service.ts +41 -0
- package/templates/apps/mobile/tsconfig.json +21 -0
- package/templates/configs/authz.config.ts +10 -0
- package/templates/configs/cache.config.ts +14 -0
- package/templates/configs/oauth.config.ts +20 -0
- package/templates/configs/payment.config.ts +44 -0
- package/templates/configs/queue.config.ts +21 -0
- package/templates/configs/rate-limit.config.ts +16 -0
- package/templates/configs/sms.config.ts +11 -0
- package/templates/configs/storage.config.ts +14 -0
- package/templates/examples/README.md +258 -0
- package/templates/examples/authz/README.md +273 -0
- package/templates/examples/authz/roles.guard.ts +37 -0
- package/templates/examples/authz/user.controller.ts +116 -0
- package/templates/examples/cache/README.md +82 -0
- package/templates/examples/cache/user.controller.ts +42 -0
- package/templates/examples/cache/user.service.ts +78 -0
- package/templates/examples/oauth/README.md +192 -0
- package/templates/examples/oauth/auth.controller.ts +99 -0
- package/templates/examples/oauth/auth.service.ts +97 -0
- package/templates/examples/payment/README.md +151 -0
- package/templates/examples/payment/order.controller.ts +56 -0
- package/templates/examples/payment/order.service.ts +132 -0
- package/templates/examples/payment/webhook.controller.ts +73 -0
- package/templates/examples/queue/README.md +134 -0
- package/templates/examples/queue/email.controller.ts +34 -0
- package/templates/examples/queue/email.processor.ts +68 -0
- package/templates/examples/queue/email.service.ts +64 -0
- package/templates/examples/rate-limit/README.md +249 -0
- package/templates/examples/rate-limit/api.controller.ts +113 -0
- package/templates/examples/sms/README.md +121 -0
- package/templates/examples/sms/sms.service.ts +69 -0
- package/templates/examples/sms/verification.controller.ts +100 -0
- package/templates/examples/storage/README.md +224 -0
- package/templates/examples/storage/upload.controller.ts +117 -0
- package/templates/examples/storage/upload.service.ts +123 -0
- package/templates/packages/types/package.json.tpl +16 -0
- package/templates/packages/types/src/api.ts +88 -0
- package/templates/packages/types/src/common.ts +89 -0
- package/templates/packages/types/src/index.ts +3 -0
- package/templates/packages/types/tsconfig.json +16 -0
- package/templates/skills/authz.skill.md +42 -0
- package/templates/skills/base.skill.md +57 -0
- package/templates/skills/cache.skill.md +88 -0
- package/templates/skills/oauth.skill.md +41 -0
- package/templates/skills/payment.skill.md +129 -0
- package/templates/skills/queue.skill.md +140 -0
- package/templates/skills/rate-limit.skill.md +38 -0
- package/templates/skills/sms.skill.md +39 -0
- package/templates/skills/storage.skill.md +42 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin 端 API Client
|
|
3
|
+
* 使用优化后的 @svton/api-client 系统
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import {
|
|
8
|
+
createApiClient,
|
|
9
|
+
createTokenInterceptor,
|
|
10
|
+
createUnauthorizedInterceptor,
|
|
11
|
+
} from '@svton/api-client';
|
|
12
|
+
// 引入类型定义以启用模块增强
|
|
13
|
+
import '{{ORG_NAME}}/types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Axios 适配器
|
|
17
|
+
* 后端使用统一响应格式:{ code, data, message }
|
|
18
|
+
*/
|
|
19
|
+
const axiosAdapter = {
|
|
20
|
+
async request(config: any) {
|
|
21
|
+
const response = await axios.request(config);
|
|
22
|
+
const result = response.data;
|
|
23
|
+
|
|
24
|
+
// 后端统一包装格式:{ code: 200, message: 'success', data: T, ... }
|
|
25
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
26
|
+
if ((result as any).code !== 200) {
|
|
27
|
+
const error: any = new Error((result as any).message || 'Request failed');
|
|
28
|
+
error.code = (result as any).code;
|
|
29
|
+
error.response = {
|
|
30
|
+
status: (result as any).code,
|
|
31
|
+
data: result,
|
|
32
|
+
};
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return (result as any).data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 兼容未包装响应
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 创建 API 客户端实例
|
|
45
|
+
*/
|
|
46
|
+
const { api, apiAsync, runGenerator } = createApiClient(axiosAdapter, {
|
|
47
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
|
|
48
|
+
interceptors: {
|
|
49
|
+
// 请求拦截器 - 添加 Token
|
|
50
|
+
request: [
|
|
51
|
+
createTokenInterceptor(() => {
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
// 从 zustand persist 存储中读取 token
|
|
54
|
+
const authStorage = localStorage.getItem('auth-storage');
|
|
55
|
+
if (authStorage) {
|
|
56
|
+
try {
|
|
57
|
+
const { state } = JSON.parse(authStorage);
|
|
58
|
+
return state?.accessToken || null;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error('[API Client] Failed to parse auth-storage:', e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
// 错误拦截器 - 处理 401
|
|
68
|
+
error: [
|
|
69
|
+
createUnauthorizedInterceptor(() => {
|
|
70
|
+
if (typeof window !== 'undefined') {
|
|
71
|
+
// 清除 zustand persist 存储
|
|
72
|
+
localStorage.removeItem('auth-storage');
|
|
73
|
+
window.location.href = '/login';
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 导出 API 函数
|
|
81
|
+
export { api, apiAsync, runGenerator };
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 使用示例:
|
|
85
|
+
*
|
|
86
|
+
* // Generator 方式(推荐)
|
|
87
|
+
* function* loadDashboard() {
|
|
88
|
+
* const user = yield* api('GET:/auth/me')
|
|
89
|
+
* const contents = yield* api('GET:/contents', { page: 1, pageSize: 10 })
|
|
90
|
+
* return { user, contents }
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* const data = await runGenerator(loadDashboard())
|
|
94
|
+
*
|
|
95
|
+
* // Promise 方式
|
|
96
|
+
* const user = await apiAsync('GET:/auth/me')
|
|
97
|
+
* const contents = await apiAsync('GET:/contents', { page: 1 })
|
|
98
|
+
*
|
|
99
|
+
* // 创建内容
|
|
100
|
+
* const newContent = await apiAsync('POST:/contents', {
|
|
101
|
+
* title: '标题',
|
|
102
|
+
* body: '内容',
|
|
103
|
+
* contentType: 'article',
|
|
104
|
+
* categoryId: 1,
|
|
105
|
+
* })
|
|
106
|
+
*
|
|
107
|
+
* // 更新内容
|
|
108
|
+
* const updated = await apiAsync('PUT:/contents/:id', {
|
|
109
|
+
* id: 123,
|
|
110
|
+
* data: { title: '新标题' }
|
|
111
|
+
* })
|
|
112
|
+
*/
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务端 API 客户端
|
|
3
|
+
* 基于 @svton/api-client
|
|
4
|
+
* 用于 Next.js 服务端组件中发起认证请求
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cookies } from 'next/headers';
|
|
8
|
+
import { createApiClient, createTokenInterceptor } from '@svton/api-client';
|
|
9
|
+
// 引入类型定义以启用模块增强
|
|
10
|
+
import '{{ORG_NAME}}/types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetch 适配器(用于服务端)
|
|
14
|
+
* 后端使用统一响应格式:{ code, data, message }
|
|
15
|
+
*/
|
|
16
|
+
const fetchAdapter = {
|
|
17
|
+
async request(config: any) {
|
|
18
|
+
const { method, url, data, params, headers } = config;
|
|
19
|
+
|
|
20
|
+
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
|
21
|
+
const fullUrl = `${url}${queryString}`;
|
|
22
|
+
|
|
23
|
+
const response = await fetch(fullUrl, {
|
|
24
|
+
method,
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...headers,
|
|
28
|
+
},
|
|
29
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
30
|
+
cache: 'no-store',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = await response.json().catch(() => null);
|
|
34
|
+
|
|
35
|
+
// 兼容非 JSON 响应
|
|
36
|
+
if (result === null) {
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error: any = new Error(`HTTP ${response.status}`);
|
|
39
|
+
error.code = response.status;
|
|
40
|
+
error.response = { status: response.status, data: null };
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 后端统一包装格式:{ code: 200, message: 'success', data: T, ... }
|
|
47
|
+
if (result && typeof result === 'object' && 'code' in result) {
|
|
48
|
+
if ((result as any).code !== 200) {
|
|
49
|
+
const error: any = new Error((result as any).message || 'Request failed');
|
|
50
|
+
error.code = (result as any).code;
|
|
51
|
+
error.response = {
|
|
52
|
+
status: (result as any).code,
|
|
53
|
+
data: result,
|
|
54
|
+
};
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
return (result as any).data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const error: any = new Error((result as any)?.message || `HTTP ${response.status}`);
|
|
62
|
+
error.code = response.status;
|
|
63
|
+
error.response = { status: response.status, data: result };
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 创建服务端 API 客户端实例
|
|
73
|
+
*/
|
|
74
|
+
const {
|
|
75
|
+
api: serverApi,
|
|
76
|
+
apiAsync: serverApiAsync,
|
|
77
|
+
runGenerator: runServerGenerator,
|
|
78
|
+
} = createApiClient(fetchAdapter, {
|
|
79
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
|
|
80
|
+
interceptors: {
|
|
81
|
+
// 请求拦截器 - 从 Cookie 读取 Token
|
|
82
|
+
request: [
|
|
83
|
+
createTokenInterceptor(async () => {
|
|
84
|
+
try {
|
|
85
|
+
const cookieStore = await cookies();
|
|
86
|
+
return cookieStore.get('token')?.value || cookieStore.get('accessToken')?.value || null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export { serverApi, serverApiAsync, runServerGenerator };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
darkMode: ['class'],
|
|
4
|
+
content: [
|
|
5
|
+
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
6
|
+
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
7
|
+
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
8
|
+
],
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {
|
|
11
|
+
colors: {
|
|
12
|
+
border: 'hsl(var(--border))',
|
|
13
|
+
input: 'hsl(var(--input))',
|
|
14
|
+
ring: 'hsl(var(--ring))',
|
|
15
|
+
background: 'hsl(var(--background))',
|
|
16
|
+
foreground: 'hsl(var(--foreground))',
|
|
17
|
+
primary: {
|
|
18
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
19
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
20
|
+
},
|
|
21
|
+
secondary: {
|
|
22
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
23
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
24
|
+
},
|
|
25
|
+
destructive: {
|
|
26
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
27
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
28
|
+
},
|
|
29
|
+
muted: {
|
|
30
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
31
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
32
|
+
},
|
|
33
|
+
accent: {
|
|
34
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
35
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
36
|
+
},
|
|
37
|
+
popover: {
|
|
38
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
39
|
+
foreground: 'hsl(var(--popover-foreground))',
|
|
40
|
+
},
|
|
41
|
+
card: {
|
|
42
|
+
DEFAULT: 'hsl(var(--card))',
|
|
43
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
borderRadius: {
|
|
47
|
+
lg: 'var(--radius)',
|
|
48
|
+
md: 'calc(var(--radius) - 2px)',
|
|
49
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
plugins: [require('tailwindcss-animate')],
|
|
54
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [{ "name": "next" }],
|
|
16
|
+
"paths": {
|
|
17
|
+
"@/*": ["./src/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
21
|
+
"exclude": ["node_modules"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 应用配置
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
PORT=3000
|
|
4
|
+
|
|
5
|
+
# 数据库
|
|
6
|
+
DATABASE_URL="mysql://root:password@localhost:3306/mydb"
|
|
7
|
+
|
|
8
|
+
# Redis
|
|
9
|
+
REDIS_URL=redis://localhost:6379
|
|
10
|
+
# 或者分开配置
|
|
11
|
+
# REDIS_HOST=localhost
|
|
12
|
+
# REDIS_PORT=6379
|
|
13
|
+
# REDIS_PASSWORD=
|
|
14
|
+
|
|
15
|
+
# JWT
|
|
16
|
+
JWT_SECRET=your-jwt-secret-key-must-be-at-least-32-chars
|
|
17
|
+
JWT_EXPIRES_IN=7d
|
|
18
|
+
|
|
19
|
+
# 七牛云对象存储(可选)
|
|
20
|
+
# QINIU_ACCESS_KEY=your-access-key
|
|
21
|
+
# QINIU_SECRET_KEY=your-secret-key
|
|
22
|
+
# QINIU_BUCKET=your-bucket
|
|
23
|
+
# QINIU_REGION=z0
|
|
24
|
+
# QINIU_CDN_URL=https://cdn.example.com
|
|
25
|
+
|
|
26
|
+
# 阿里云短信(可选)
|
|
27
|
+
# ALIYUN_ACCESS_KEY_ID=your-access-key-id
|
|
28
|
+
# ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret
|
|
29
|
+
# SMS_SIGN_NAME=your-sign-name
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ORG_NAME}}/backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "{{PROJECT_NAME}} 后端 API",
|
|
5
|
+
"prisma": {
|
|
6
|
+
"seed": "ts-node prisma/seed.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "nest build",
|
|
10
|
+
"dev": "NODE_ENV=development nest start --watch",
|
|
11
|
+
"start": "NODE_ENV=production node dist/main",
|
|
12
|
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"type-check": "tsc --noEmit",
|
|
15
|
+
"prisma:generate": "prisma generate",
|
|
16
|
+
"prisma:migrate": "prisma migrate dev",
|
|
17
|
+
"prisma:migrate:deploy": "prisma migrate deploy",
|
|
18
|
+
"prisma:seed": "ts-node prisma/seed.ts",
|
|
19
|
+
"prisma:studio": "prisma studio",
|
|
20
|
+
"db:init": "pnpm prisma:generate && pnpm prisma:migrate && pnpm prisma:seed",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@nestjs/common": "^10.3.0",
|
|
25
|
+
"@nestjs/config": "^3.1.1",
|
|
26
|
+
"@nestjs/core": "^10.3.0",
|
|
27
|
+
"@nestjs/jwt": "^10.2.0",
|
|
28
|
+
"@nestjs/passport": "^10.0.3",
|
|
29
|
+
"@nestjs/platform-express": "^10.3.0",
|
|
30
|
+
"@nestjs/swagger": "^7.1.17",
|
|
31
|
+
"@prisma/client": "^5.7.1",
|
|
32
|
+
"{{ORG_NAME}}/types": "workspace:*",
|
|
33
|
+
"bcrypt": "^5.1.1",
|
|
34
|
+
"class-transformer": "^0.5.1",
|
|
35
|
+
"class-validator": "^0.14.0",
|
|
36
|
+
"ioredis": "^5.3.2",
|
|
37
|
+
"passport": "^0.7.0",
|
|
38
|
+
"passport-jwt": "^4.0.1",
|
|
39
|
+
"reflect-metadata": "^0.2.1",
|
|
40
|
+
"rxjs": "^7.8.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@nestjs/cli": "^10.2.1",
|
|
44
|
+
"@nestjs/schematics": "^10.0.3",
|
|
45
|
+
"@nestjs/testing": "^10.2.10",
|
|
46
|
+
"@types/bcrypt": "^5.0.2",
|
|
47
|
+
"@types/express": "^4.17.21",
|
|
48
|
+
"@types/jest": "^29.5.11",
|
|
49
|
+
"@types/node": "^20.10.0",
|
|
50
|
+
"@types/passport-jwt": "^3.0.13",
|
|
51
|
+
"jest": "^29.7.0",
|
|
52
|
+
"prisma": "^5.7.0",
|
|
53
|
+
"ts-jest": "^29.1.1",
|
|
54
|
+
"ts-node": "^10.9.2",
|
|
55
|
+
"typescript": "^5.3.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "mysql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 用户模型
|
|
11
|
+
model User {
|
|
12
|
+
id Int @id @default(autoincrement())
|
|
13
|
+
phone String @unique
|
|
14
|
+
password String
|
|
15
|
+
nickname String
|
|
16
|
+
avatar String?
|
|
17
|
+
email String?
|
|
18
|
+
bio String? @db.Text
|
|
19
|
+
role String @default("user") // user, admin, super_admin
|
|
20
|
+
status Int @default(1) // 0: 禁用, 1: 启用
|
|
21
|
+
lastLoginAt DateTime?
|
|
22
|
+
createdAt DateTime @default(now())
|
|
23
|
+
updatedAt DateTime @updatedAt
|
|
24
|
+
|
|
25
|
+
contents Content[]
|
|
26
|
+
comments Comment[]
|
|
27
|
+
|
|
28
|
+
@@index([phone])
|
|
29
|
+
@@index([role])
|
|
30
|
+
@@index([status])
|
|
31
|
+
@@map("users")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 内容模型
|
|
35
|
+
model Content {
|
|
36
|
+
id Int @id @default(autoincrement())
|
|
37
|
+
title String
|
|
38
|
+
content String @db.Text
|
|
39
|
+
images String? @db.Text // JSON array
|
|
40
|
+
status String @default("draft") // draft, pending, published, rejected
|
|
41
|
+
viewCount Int @default(0)
|
|
42
|
+
likeCount Int @default(0)
|
|
43
|
+
commentCount Int @default(0)
|
|
44
|
+
authorId Int
|
|
45
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
46
|
+
createdAt DateTime @default(now())
|
|
47
|
+
updatedAt DateTime @updatedAt
|
|
48
|
+
|
|
49
|
+
comments Comment[]
|
|
50
|
+
|
|
51
|
+
@@index([authorId])
|
|
52
|
+
@@index([status])
|
|
53
|
+
@@index([createdAt])
|
|
54
|
+
@@map("contents")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 评论模型
|
|
58
|
+
model Comment {
|
|
59
|
+
id Int @id @default(autoincrement())
|
|
60
|
+
content String @db.Text
|
|
61
|
+
contentId Int
|
|
62
|
+
content_ Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
|
|
63
|
+
authorId Int
|
|
64
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
65
|
+
likeCount Int @default(0)
|
|
66
|
+
createdAt DateTime @default(now())
|
|
67
|
+
updatedAt DateTime @updatedAt
|
|
68
|
+
|
|
69
|
+
@@index([contentId])
|
|
70
|
+
@@index([authorId])
|
|
71
|
+
@@map("comments")
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
|
|
3
|
+
const prisma = new PrismaClient();
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
console.log('🌱 开始初始化数据...');
|
|
7
|
+
|
|
8
|
+
// 创建管理员用户
|
|
9
|
+
const admin = await prisma.user.upsert({
|
|
10
|
+
where: { phone: '13800000000' },
|
|
11
|
+
update: {},
|
|
12
|
+
create: {
|
|
13
|
+
phone: '13800000000',
|
|
14
|
+
password: '$2b$10$example', // 需要用 bcrypt 生成
|
|
15
|
+
nickname: '管理员',
|
|
16
|
+
role: 'admin',
|
|
17
|
+
status: 1,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
console.log('✅ 管理员用户:', admin);
|
|
22
|
+
console.log('🎉 数据初始化完成!');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
main()
|
|
26
|
+
.catch((e) => {
|
|
27
|
+
console.error(e);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
})
|
|
30
|
+
.finally(async () => {
|
|
31
|
+
await prisma.$disconnect();
|
|
32
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Controller, Get } from '@nestjs/common';
|
|
2
|
+
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
|
3
|
+
import { AppService } from './app.service';
|
|
4
|
+
|
|
5
|
+
@ApiTags('健康检查')
|
|
6
|
+
@Controller()
|
|
7
|
+
export class AppController {
|
|
8
|
+
constructor(private readonly appService: AppService) {}
|
|
9
|
+
|
|
10
|
+
@Get()
|
|
11
|
+
@ApiOperation({ summary: '健康检查' })
|
|
12
|
+
getHealth() {
|
|
13
|
+
return this.appService.getHealth();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
3
|
+
import { AppController } from './app.controller';
|
|
4
|
+
import { AppService } from './app.service';
|
|
5
|
+
import { PrismaModule } from './prisma/prisma.module';
|
|
6
|
+
import { AuthModule } from './auth/auth.module';
|
|
7
|
+
import { UserModule } from './user/user.module';
|
|
8
|
+
|
|
9
|
+
// Svton 基础设施包(按需启用)
|
|
10
|
+
// import { createZodValidate } from '@svton/nestjs-config-schema';
|
|
11
|
+
// import { HttpModule } from '@svton/nestjs-http';
|
|
12
|
+
// import { LoggerModule } from '@svton/nestjs-logger';
|
|
13
|
+
// import { RedisModule } from '@svton/nestjs-redis';
|
|
14
|
+
// import { AuthzModule } from '@svton/nestjs-authz';
|
|
15
|
+
// import { ObjectStorageModule } from '@svton/nestjs-object-storage';
|
|
16
|
+
// import { createQiniuAdapter } from '@svton/nestjs-object-storage-qiniu-kodo';
|
|
17
|
+
// import { envSchema } from './config/env.schema';
|
|
18
|
+
|
|
19
|
+
@Module({
|
|
20
|
+
imports: [
|
|
21
|
+
// 配置模块(可启用 schema 验证)
|
|
22
|
+
ConfigModule.forRoot({
|
|
23
|
+
isGlobal: true,
|
|
24
|
+
// validate: createZodValidate(envSchema),
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
// HTTP 规范化(统一响应/异常格式)
|
|
28
|
+
// HttpModule.forRoot({
|
|
29
|
+
// successCode: 0,
|
|
30
|
+
// successMessage: 'success',
|
|
31
|
+
// excludePaths: ['/health', '/api-docs'],
|
|
32
|
+
// }),
|
|
33
|
+
|
|
34
|
+
// 日志模块
|
|
35
|
+
// LoggerModule.forRootAsync({
|
|
36
|
+
// imports: [ConfigModule],
|
|
37
|
+
// inject: [ConfigService],
|
|
38
|
+
// useFactory: (config: ConfigService) => ({
|
|
39
|
+
// appName: 'backend',
|
|
40
|
+
// env: config.get('NODE_ENV'),
|
|
41
|
+
// prettyPrint: config.get('NODE_ENV') !== 'production',
|
|
42
|
+
// }),
|
|
43
|
+
// }),
|
|
44
|
+
|
|
45
|
+
// Redis 模块
|
|
46
|
+
// RedisModule.forRootAsync({
|
|
47
|
+
// imports: [ConfigModule],
|
|
48
|
+
// inject: [ConfigService],
|
|
49
|
+
// useFactory: (config: ConfigService) => ({
|
|
50
|
+
// url: config.get('REDIS_URL'),
|
|
51
|
+
// keyPrefix: 'app:',
|
|
52
|
+
// }),
|
|
53
|
+
// }),
|
|
54
|
+
|
|
55
|
+
// RBAC 权限模块
|
|
56
|
+
// AuthzModule.forRoot({
|
|
57
|
+
// userRoleField: 'role',
|
|
58
|
+
// enableGlobalGuard: false,
|
|
59
|
+
// }),
|
|
60
|
+
|
|
61
|
+
// 对象存储模块(七牛云示例)
|
|
62
|
+
// ObjectStorageModule.forRootAsync({
|
|
63
|
+
// imports: [ConfigModule],
|
|
64
|
+
// inject: [ConfigService],
|
|
65
|
+
// useFactory: (config: ConfigService) => ({
|
|
66
|
+
// defaultBucket: config.get('QINIU_BUCKET'),
|
|
67
|
+
// publicBaseUrl: config.get('QINIU_CDN_URL'),
|
|
68
|
+
// adapter: createQiniuAdapter({
|
|
69
|
+
// accessKey: config.get('QINIU_ACCESS_KEY'),
|
|
70
|
+
// secretKey: config.get('QINIU_SECRET_KEY'),
|
|
71
|
+
// bucket: config.get('QINIU_BUCKET'),
|
|
72
|
+
// region: config.get('QINIU_REGION'),
|
|
73
|
+
// publicDomain: config.get('QINIU_CDN_URL'),
|
|
74
|
+
// }),
|
|
75
|
+
// }),
|
|
76
|
+
// }),
|
|
77
|
+
|
|
78
|
+
PrismaModule,
|
|
79
|
+
AuthModule,
|
|
80
|
+
UserModule,
|
|
81
|
+
],
|
|
82
|
+
controllers: [AppController],
|
|
83
|
+
providers: [AppService],
|
|
84
|
+
})
|
|
85
|
+
export class AppModule {}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Controller, Post, Body, Get, UseGuards, Request } from '@nestjs/common';
|
|
2
|
+
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
3
|
+
import { AuthService } from './auth.service';
|
|
4
|
+
import { JwtAuthGuard } from './jwt-auth.guard';
|
|
5
|
+
import type { LoginDto, RegisterDto } from '{{ORG_NAME}}/types';
|
|
6
|
+
|
|
7
|
+
@ApiTags('认证')
|
|
8
|
+
@Controller('auth')
|
|
9
|
+
export class AuthController {
|
|
10
|
+
constructor(private authService: AuthService) {}
|
|
11
|
+
|
|
12
|
+
@Post('login')
|
|
13
|
+
@ApiOperation({ summary: '登录' })
|
|
14
|
+
async login(@Body() dto: LoginDto) {
|
|
15
|
+
return this.authService.login(dto);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Post('register')
|
|
19
|
+
@ApiOperation({ summary: '注册' })
|
|
20
|
+
async register(@Body() dto: RegisterDto) {
|
|
21
|
+
return this.authService.register(dto);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Get('me')
|
|
25
|
+
@UseGuards(JwtAuthGuard)
|
|
26
|
+
@ApiBearerAuth()
|
|
27
|
+
@ApiOperation({ summary: '获取当前用户信息' })
|
|
28
|
+
async getProfile(@Request() req: any) {
|
|
29
|
+
return this.authService.validateUser(req.user.sub);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
3
|
+
import { PassportModule } from '@nestjs/passport';
|
|
4
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
5
|
+
import { AuthController } from './auth.controller';
|
|
6
|
+
import { AuthService } from './auth.service';
|
|
7
|
+
import { JwtStrategy } from './jwt.strategy';
|
|
8
|
+
import { PrismaModule } from '../prisma/prisma.module';
|
|
9
|
+
|
|
10
|
+
@Module({
|
|
11
|
+
imports: [
|
|
12
|
+
PrismaModule,
|
|
13
|
+
PassportModule.register({ defaultStrategy: 'jwt' }),
|
|
14
|
+
JwtModule.registerAsync({
|
|
15
|
+
imports: [ConfigModule],
|
|
16
|
+
useFactory: async (configService: ConfigService) => ({
|
|
17
|
+
secret: configService.get<string>('JWT_SECRET'),
|
|
18
|
+
signOptions: { expiresIn: '7d' },
|
|
19
|
+
}),
|
|
20
|
+
inject: [ConfigService],
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
controllers: [AuthController],
|
|
24
|
+
providers: [AuthService, JwtStrategy],
|
|
25
|
+
exports: [AuthService],
|
|
26
|
+
})
|
|
27
|
+
export class AuthModule {}
|