@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.
Files changed (102) hide show
  1. package/dist/index.js +13 -6
  2. package/dist/index.mjs +13 -6
  3. package/package.json +3 -1
  4. package/templates/apps/admin/next-env.d.ts +2 -0
  5. package/templates/apps/admin/next.config.js +15 -0
  6. package/templates/apps/admin/package.json.tpl +54 -0
  7. package/templates/apps/admin/postcss.config.js +6 -0
  8. package/templates/apps/admin/src/app/globals.css +37 -0
  9. package/templates/apps/admin/src/app/layout.tsx +19 -0
  10. package/templates/apps/admin/src/app/login/page.tsx +96 -0
  11. package/templates/apps/admin/src/app/page.tsx +8 -0
  12. package/templates/apps/admin/src/app/users/page.tsx +165 -0
  13. package/templates/apps/admin/src/components/ui/switch.tsx +29 -0
  14. package/templates/apps/admin/src/hooks/useAPI.ts +130 -0
  15. package/templates/apps/admin/src/lib/api-client.ts +112 -0
  16. package/templates/apps/admin/src/lib/api-server.ts +95 -0
  17. package/templates/apps/admin/tailwind.config.js +54 -0
  18. package/templates/apps/admin/tsconfig.json +22 -0
  19. package/templates/apps/backend/.env.example +29 -0
  20. package/templates/apps/backend/nest-cli.json +8 -0
  21. package/templates/apps/backend/package.json.tpl +57 -0
  22. package/templates/apps/backend/prisma/schema.prisma +72 -0
  23. package/templates/apps/backend/prisma/seed.ts +32 -0
  24. package/templates/apps/backend/src/app.controller.ts +15 -0
  25. package/templates/apps/backend/src/app.module.ts +85 -0
  26. package/templates/apps/backend/src/app.service.ts +12 -0
  27. package/templates/apps/backend/src/auth/auth.controller.ts +31 -0
  28. package/templates/apps/backend/src/auth/auth.module.ts +27 -0
  29. package/templates/apps/backend/src/auth/auth.service.ts +89 -0
  30. package/templates/apps/backend/src/auth/jwt-auth.guard.ts +5 -0
  31. package/templates/apps/backend/src/auth/jwt.strategy.ts +27 -0
  32. package/templates/apps/backend/src/config/env.schema.ts +35 -0
  33. package/templates/apps/backend/src/main.ts +51 -0
  34. package/templates/apps/backend/src/object-storage/object-storage.controller.ts +114 -0
  35. package/templates/apps/backend/src/object-storage/object-storage.module.ts +7 -0
  36. package/templates/apps/backend/src/prisma/prisma.module.ts +9 -0
  37. package/templates/apps/backend/src/prisma/prisma.service.ts +13 -0
  38. package/templates/apps/backend/src/user/user.controller.ts +50 -0
  39. package/templates/apps/backend/src/user/user.module.ts +12 -0
  40. package/templates/apps/backend/src/user/user.service.ts +117 -0
  41. package/templates/apps/backend/tsconfig.json +23 -0
  42. package/templates/apps/mobile/babel.config.js +8 -0
  43. package/templates/apps/mobile/config/index.ts +65 -0
  44. package/templates/apps/mobile/package.json.tpl +48 -0
  45. package/templates/apps/mobile/project.config.json.tpl +17 -0
  46. package/templates/apps/mobile/src/app.config.ts +9 -0
  47. package/templates/apps/mobile/src/app.scss +4 -0
  48. package/templates/apps/mobile/src/app.ts +8 -0
  49. package/templates/apps/mobile/src/hooks/useAPI.ts +285 -0
  50. package/templates/apps/mobile/src/pages/index/index.scss +7 -0
  51. package/templates/apps/mobile/src/pages/index/index.tsx +49 -0
  52. package/templates/apps/mobile/src/services/api.ts +155 -0
  53. package/templates/apps/mobile/src/services/upload.service.ts +41 -0
  54. package/templates/apps/mobile/tsconfig.json +21 -0
  55. package/templates/configs/authz.config.ts +10 -0
  56. package/templates/configs/cache.config.ts +14 -0
  57. package/templates/configs/oauth.config.ts +20 -0
  58. package/templates/configs/payment.config.ts +44 -0
  59. package/templates/configs/queue.config.ts +21 -0
  60. package/templates/configs/rate-limit.config.ts +16 -0
  61. package/templates/configs/sms.config.ts +11 -0
  62. package/templates/configs/storage.config.ts +14 -0
  63. package/templates/examples/README.md +258 -0
  64. package/templates/examples/authz/README.md +273 -0
  65. package/templates/examples/authz/roles.guard.ts +37 -0
  66. package/templates/examples/authz/user.controller.ts +116 -0
  67. package/templates/examples/cache/README.md +82 -0
  68. package/templates/examples/cache/user.controller.ts +42 -0
  69. package/templates/examples/cache/user.service.ts +78 -0
  70. package/templates/examples/oauth/README.md +192 -0
  71. package/templates/examples/oauth/auth.controller.ts +99 -0
  72. package/templates/examples/oauth/auth.service.ts +97 -0
  73. package/templates/examples/payment/README.md +151 -0
  74. package/templates/examples/payment/order.controller.ts +56 -0
  75. package/templates/examples/payment/order.service.ts +132 -0
  76. package/templates/examples/payment/webhook.controller.ts +73 -0
  77. package/templates/examples/queue/README.md +134 -0
  78. package/templates/examples/queue/email.controller.ts +34 -0
  79. package/templates/examples/queue/email.processor.ts +68 -0
  80. package/templates/examples/queue/email.service.ts +64 -0
  81. package/templates/examples/rate-limit/README.md +249 -0
  82. package/templates/examples/rate-limit/api.controller.ts +113 -0
  83. package/templates/examples/sms/README.md +121 -0
  84. package/templates/examples/sms/sms.service.ts +69 -0
  85. package/templates/examples/sms/verification.controller.ts +100 -0
  86. package/templates/examples/storage/README.md +224 -0
  87. package/templates/examples/storage/upload.controller.ts +117 -0
  88. package/templates/examples/storage/upload.service.ts +123 -0
  89. package/templates/packages/types/package.json.tpl +16 -0
  90. package/templates/packages/types/src/api.ts +88 -0
  91. package/templates/packages/types/src/common.ts +89 -0
  92. package/templates/packages/types/src/index.ts +3 -0
  93. package/templates/packages/types/tsconfig.json +16 -0
  94. package/templates/skills/authz.skill.md +42 -0
  95. package/templates/skills/base.skill.md +57 -0
  96. package/templates/skills/cache.skill.md +88 -0
  97. package/templates/skills/oauth.skill.md +41 -0
  98. package/templates/skills/payment.skill.md +129 -0
  99. package/templates/skills/queue.skill.md +140 -0
  100. package/templates/skills/rate-limit.skill.md +38 -0
  101. package/templates/skills/sms.skill.md +39 -0
  102. package/templates/skills/storage.skill.md +42 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "miniprogramRoot": "dist/",
3
+ "projectname": "{{PROJECT_NAME}}",
4
+ "description": "{{PROJECT_NAME}} 小程序",
5
+ "appid": "",
6
+ "setting": {
7
+ "urlCheck": true,
8
+ "es6": false,
9
+ "enhance": false,
10
+ "compileHotReLoad": false,
11
+ "postcss": false,
12
+ "minified": false,
13
+ "bundle": false,
14
+ "nodeModules": false
15
+ },
16
+ "compileType": "miniprogram"
17
+ }
@@ -0,0 +1,9 @@
1
+ export default defineAppConfig({
2
+ pages: ['pages/index/index'],
3
+ window: {
4
+ backgroundTextStyle: 'light',
5
+ navigationBarBackgroundColor: '#fff',
6
+ navigationBarTitleText: '首页',
7
+ navigationBarTextStyle: 'black',
8
+ },
9
+ });
@@ -0,0 +1,4 @@
1
+ page {
2
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
3
+ background-color: #f5f5f5;
4
+ }
@@ -0,0 +1,8 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import './app.scss';
3
+
4
+ function App({ children }: PropsWithChildren) {
5
+ return children;
6
+ }
7
+
8
+ export default App;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Mobile 端 useAPI Hook
3
+ * 适配 Taro 的 React Hook
4
+ *
5
+ * 类型定义通过 {{ORG_NAME}}/types 扩展 @svton/api-client 的全局类型
6
+ */
7
+
8
+ import { useDidShow } from '@tarojs/taro';
9
+ import { useState, useEffect, useRef } from 'react';
10
+ import { usePersistFn } from '@svton/hooks';
11
+ import type { ApiName, ApiParams, ApiResponse } from '@svton/api-client';
12
+ import { apiAsync } from '../services/api';
13
+ // 引入类型定义以启用模块增强
14
+ import '{{ORG_NAME}}/types';
15
+
16
+ /**
17
+ * useAPI Hook 配置
18
+ */
19
+ interface UseAPIConfig {
20
+ /**
21
+ * 是否立即加载
22
+ */
23
+ immediate?: boolean;
24
+ /**
25
+ * 页面显示时是否重新加载
26
+ */
27
+ refreshOnShow?: boolean;
28
+ /**
29
+ * 成功回调
30
+ */
31
+ onSuccess?: (data: any) => void;
32
+ /**
33
+ * 错误回调
34
+ */
35
+ onError?: (error: Error) => void;
36
+ }
37
+
38
+ /**
39
+ * useAPI 返回类型
40
+ */
41
+ interface UseAPIReturn<K extends ApiName> {
42
+ data: ApiResponse<K> | null;
43
+ loading: boolean;
44
+ error: Error | null;
45
+ refresh: () => Promise<void>;
46
+ }
47
+
48
+ export function useAPI<K extends ApiName>(
49
+ apiName: K,
50
+ params?: ApiParams<K>,
51
+ config: UseAPIConfig = {},
52
+ ): UseAPIReturn<K> {
53
+ const { immediate = true, refreshOnShow = false, onSuccess, onError } = config;
54
+
55
+ const [data, setData] = useState<ApiResponse<K> | null>(null);
56
+ const [loading, setLoading] = useState(immediate);
57
+ const [error, setError] = useState<Error | null>(null);
58
+ const mountedRef = useRef(true);
59
+
60
+ const fetchData = usePersistFn(async () => {
61
+ if (!mountedRef.current) return;
62
+
63
+ setLoading(true);
64
+ setError(null);
65
+
66
+ try {
67
+ const result = (
68
+ params !== undefined
69
+ ? await (apiAsync as any)(apiName, params)
70
+ : await (apiAsync as any)(apiName)
71
+ ) as ApiResponse<K>;
72
+
73
+ if (mountedRef.current) {
74
+ setData(result);
75
+ onSuccess?.(result);
76
+ }
77
+ } catch (err: unknown) {
78
+ if (mountedRef.current) {
79
+ const error = err instanceof Error ? err : new Error(String(err));
80
+ setError(error);
81
+ onError?.(error);
82
+ }
83
+ } finally {
84
+ if (mountedRef.current) {
85
+ setLoading(false);
86
+ }
87
+ }
88
+ });
89
+
90
+ useEffect(() => {
91
+ if (immediate || params !== undefined) {
92
+ fetchData();
93
+ }
94
+
95
+ return () => {
96
+ mountedRef.current = false;
97
+ };
98
+ }, [apiName, JSON.stringify(params), fetchData, immediate, params]);
99
+
100
+ useDidShow(() => {
101
+ if (refreshOnShow && data) {
102
+ fetchData();
103
+ }
104
+ });
105
+
106
+ return {
107
+ data,
108
+ loading,
109
+ error,
110
+ refresh: fetchData,
111
+ };
112
+ }
113
+
114
+ interface UseMutationReturn<K extends ApiName> {
115
+ trigger: (params?: ApiParams<K>) => Promise<ApiResponse<K>>;
116
+ loading: boolean;
117
+ error: Error | null;
118
+ data: ApiResponse<K> | null;
119
+ reset: () => void;
120
+ }
121
+
122
+ export function useMutation<K extends ApiName>(apiName: K): UseMutationReturn<K> {
123
+ const [loading, setLoading] = useState(false);
124
+ const [error, setError] = useState<Error | null>(null);
125
+ const [data, setData] = useState<ApiResponse<K> | null>(null);
126
+
127
+ const trigger = async (params?: ApiParams<K>): Promise<ApiResponse<K>> => {
128
+ setLoading(true);
129
+ setError(null);
130
+
131
+ try {
132
+ const result = (
133
+ params !== undefined
134
+ ? await (apiAsync as any)(apiName, params)
135
+ : await (apiAsync as any)(apiName)
136
+ ) as ApiResponse<K>;
137
+ setData(result);
138
+ return result;
139
+ } catch (err: unknown) {
140
+ const error = err instanceof Error ? err : new Error(String(err));
141
+ setError(error);
142
+ throw error;
143
+ } finally {
144
+ setLoading(false);
145
+ }
146
+ };
147
+
148
+ const reset = () => {
149
+ setLoading(false);
150
+ setError(null);
151
+ setData(null);
152
+ };
153
+
154
+ return {
155
+ trigger,
156
+ loading,
157
+ error,
158
+ data,
159
+ reset,
160
+ };
161
+ }
162
+
163
+ type ExtractItemType<T> = T extends { items: (infer Item)[] }
164
+ ? Item
165
+ : T extends { data: (infer Item)[] }
166
+ ? Item
167
+ : T extends Array<infer Item>
168
+ ? Item
169
+ : unknown;
170
+
171
+ type PaginationParams<T> = T extends void
172
+ ? { pageSize?: number }
173
+ : Omit<T, 'page'> & { pageSize?: number };
174
+
175
+ interface UsePaginationReturn<TItem> {
176
+ data: TItem[];
177
+ loading: boolean;
178
+ error: Error | null;
179
+ page: number;
180
+ hasMore: boolean;
181
+ loadMore: () => Promise<void>;
182
+ refresh: () => Promise<void>;
183
+ }
184
+
185
+ export function usePagination<K extends ApiName>(
186
+ apiName: K,
187
+ initialParams?: PaginationParams<ApiParams<K>>,
188
+ ): UsePaginationReturn<ExtractItemType<ApiResponse<K>>> {
189
+ type ItemType = ExtractItemType<ApiResponse<K>>;
190
+
191
+ const [data, setData] = useState<ItemType[]>([]);
192
+ const [loading, setLoading] = useState(false);
193
+ const [error, setError] = useState<Error | null>(null);
194
+ const [page, setPage] = useState(1);
195
+ const [hasMore, setHasMore] = useState(true);
196
+ const pageSize = initialParams?.pageSize || 10;
197
+
198
+ const paramsRef = useRef(initialParams);
199
+ paramsRef.current = initialParams;
200
+
201
+ const isFirstRenderRef = useRef(true);
202
+ const isRefreshingRef = useRef(false);
203
+
204
+ const loadMore = usePersistFn(async () => {
205
+ if (loading || !hasMore) return;
206
+
207
+ setLoading(true);
208
+ setError(null);
209
+
210
+ try {
211
+ const currentPage = isRefreshingRef.current ? 1 : page;
212
+
213
+ const paginationParams = {
214
+ ...(paramsRef.current || {}),
215
+ page: currentPage,
216
+ pageSize,
217
+ };
218
+
219
+ const result = await (apiAsync as any)(apiName, paginationParams);
220
+
221
+ let newItems: ItemType[] = [];
222
+ if (result && typeof result === 'object') {
223
+ if ('items' in result && Array.isArray((result as any).items)) {
224
+ newItems = (result as any).items as ItemType[];
225
+ } else if ('data' in result && Array.isArray((result as any).data)) {
226
+ newItems = (result as any).data as ItemType[];
227
+ } else if ('items' in (result as any)?.data && Array.isArray((result as any).data?.items)) {
228
+ newItems = (result as any).data.items as ItemType[];
229
+ } else if (Array.isArray(result)) {
230
+ newItems = result as ItemType[];
231
+ }
232
+ }
233
+
234
+ if (isRefreshingRef.current) {
235
+ setData(newItems);
236
+ setPage(2);
237
+ isRefreshingRef.current = false;
238
+ } else {
239
+ setData((prev: ItemType[]) => [...prev, ...newItems]);
240
+ setPage((p: number) => p + 1);
241
+ }
242
+
243
+ if (newItems.length < pageSize) {
244
+ setHasMore(false);
245
+ }
246
+ } catch (err: unknown) {
247
+ const error = err instanceof Error ? err : new Error(String(err));
248
+ setError(error);
249
+ } finally {
250
+ setLoading(false);
251
+ }
252
+ });
253
+
254
+ const refresh = usePersistFn(async () => {
255
+ isRefreshingRef.current = true;
256
+ setData([]);
257
+ setHasMore(true);
258
+ await loadMore();
259
+ });
260
+
261
+ useEffect(() => {
262
+ loadMore();
263
+ // eslint-disable-next-line react-hooks/exhaustive-deps
264
+ }, []);
265
+
266
+ useEffect(() => {
267
+ if (isFirstRenderRef.current) {
268
+ isFirstRenderRef.current = false;
269
+ return;
270
+ }
271
+
272
+ refresh();
273
+ // eslint-disable-next-line react-hooks/exhaustive-deps
274
+ }, [JSON.stringify(initialParams)]);
275
+
276
+ return {
277
+ data,
278
+ loading,
279
+ error,
280
+ page,
281
+ hasMore,
282
+ loadMore,
283
+ refresh,
284
+ };
285
+ }
@@ -0,0 +1,7 @@
1
+ .index {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ min-height: 100vh;
7
+ }
@@ -0,0 +1,49 @@
1
+ import { View } from '@tarojs/components';
2
+ import { useState } from 'react';
3
+ import { usePersistFn, useMount } from '@svton/hooks';
4
+ import { NavBar, StatusBar, Loading, Empty } from '@svton/taro-ui';
5
+ import type { ContentVo } from '{{ORG_NAME}}/types';
6
+ import './index.scss';
7
+
8
+ export default function Index() {
9
+ const [loading, setLoading] = useState(true);
10
+ const [contents, setContents] = useState<ContentVo[]>([]);
11
+
12
+ const fetchContents = usePersistFn(async () => {
13
+ try {
14
+ setLoading(true);
15
+ // 这里应该使用 @svton/api-client 的 API
16
+ // const response = await apiClient.contents.list({ page: 1, pageSize: 10 });
17
+ // setContents(response.data.list);
18
+
19
+ // 模拟数据
20
+ await new Promise((resolve) => setTimeout(resolve, 1000));
21
+ setContents([]);
22
+ } catch (error) {
23
+ console.error('获取内容列表失败', error);
24
+ } finally {
25
+ setLoading(false);
26
+ }
27
+ });
28
+
29
+ useMount(() => {
30
+ fetchContents();
31
+ });
32
+
33
+ return (
34
+ <View className="index">
35
+ <StatusBar />
36
+ <NavBar title="首页" />
37
+
38
+ <View className="content">
39
+ {loading && <Loading text="加载中..." />}
40
+ {!loading && contents.length === 0 && <Empty text="暂无内容" />}
41
+ {!loading && contents.length > 0 && (
42
+ <View className="content-list">
43
+ {/* 内容列表 */}
44
+ </View>
45
+ )}
46
+ </View>
47
+ </View>
48
+ );
49
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Mobile 端 API Client
3
+ * 使用 @svton/api-client 系统
4
+ */
5
+
6
+ import Taro from '@tarojs/taro';
7
+ import { createApiClient, createTokenInterceptor } from '@svton/api-client';
8
+ // 引入类型定义以启用模块增强
9
+ import '{{ORG_NAME}}/types';
10
+
11
+ // API 基础 URL(从 Taro 配置文件的 defineConstants 中注入)
12
+ // 注意:API_BASE_URL 是一个全局常量,在编译时由 Taro 配置文件注入
13
+ declare const API_BASE_URL: string;
14
+
15
+ /**
16
+ * Taro 适配器
17
+ */
18
+ const taroAdapter = {
19
+ async request(config: any) {
20
+ const response = await Taro.request({
21
+ url: config.url,
22
+ method: config.method as any,
23
+ data: config.method === 'GET' ? config.params : config.data,
24
+ header: config.headers,
25
+ });
26
+
27
+ const result = response.data;
28
+
29
+ // 后端统一包装格式:{ code: 200, message: 'success', data: T, ... }
30
+ if (result && typeof result === 'object' && 'code' in result) {
31
+ if ((result as any).code !== 200) {
32
+ const error: any = new Error((result as any).message || 'Request failed');
33
+ error.code = (result as any).code;
34
+ error.response = {
35
+ status: (result as any).code,
36
+ data: result,
37
+ };
38
+ throw error;
39
+ }
40
+ return (result as any).data;
41
+ }
42
+
43
+ // 兼容未包装响应
44
+ return result;
45
+ },
46
+ };
47
+
48
+ /**
49
+ * 刷新 Token
50
+ */
51
+ let isRefreshing = false;
52
+ let refreshSubscribers: Array<(token: string) => void> = [];
53
+
54
+ const subscribeTokenRefresh = (cb: (token: string) => void) => {
55
+ refreshSubscribers.push(cb);
56
+ };
57
+
58
+ const onRefreshed = (token: string) => {
59
+ refreshSubscribers.forEach((cb) => cb(token));
60
+ refreshSubscribers = [];
61
+ };
62
+
63
+ const refreshAccessToken = async (): Promise<string | null> => {
64
+ const refreshToken = Taro.getStorageSync('refreshToken');
65
+ if (!refreshToken) {
66
+ return null;
67
+ }
68
+
69
+ try {
70
+ const response = await Taro.request({
71
+ url: `${API_BASE_URL}/auth/refresh`,
72
+ method: 'POST',
73
+ data: { refreshToken },
74
+ header: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ });
78
+
79
+ const result = response.data as any;
80
+
81
+ if (result && result.code === 200 && result.data && result.data.accessToken) {
82
+ const newToken = result.data.accessToken as string;
83
+ const newRefreshToken = result.data.refreshToken as string | undefined;
84
+
85
+ Taro.setStorageSync('token', newToken);
86
+ if (newRefreshToken) {
87
+ Taro.setStorageSync('refreshToken', newRefreshToken);
88
+ }
89
+
90
+ return newToken;
91
+ }
92
+
93
+ return null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Token 刷新拦截器(错误处理)
101
+ */
102
+ const createTokenRefreshInterceptor = () => {
103
+ return async (error: any): Promise<void> => {
104
+ if (error.code === 401 || error.code === '401') {
105
+ const token = Taro.getStorageSync('token');
106
+ const refreshToken = Taro.getStorageSync('refreshToken');
107
+
108
+ if (!token || !refreshToken) {
109
+ throw error;
110
+ }
111
+
112
+ if (!isRefreshing) {
113
+ isRefreshing = true;
114
+ const newToken = await refreshAccessToken();
115
+ isRefreshing = false;
116
+
117
+ if (newToken) {
118
+ onRefreshed(newToken);
119
+ return;
120
+ }
121
+
122
+ throw error;
123
+ }
124
+
125
+ return new Promise((resolve) => {
126
+ subscribeTokenRefresh(() => {
127
+ resolve();
128
+ });
129
+ });
130
+ }
131
+
132
+ throw error;
133
+ };
134
+ };
135
+
136
+ /**
137
+ * 创建 API 客户端实例
138
+ */
139
+ const { api, apiAsync, runGenerator } = createApiClient(taroAdapter, {
140
+ baseURL: API_BASE_URL,
141
+ interceptors: {
142
+ request: [
143
+ createTokenInterceptor(() => {
144
+ try {
145
+ return Taro.getStorageSync('token');
146
+ } catch {
147
+ return null;
148
+ }
149
+ }),
150
+ ],
151
+ error: [createTokenRefreshInterceptor()],
152
+ },
153
+ });
154
+
155
+ export { api, apiAsync, runGenerator };
@@ -0,0 +1,41 @@
1
+ import Taro from '@tarojs/taro';
2
+
3
+ // API 基础 URL(从 Taro 配置文件注入)
4
+ declare const API_BASE_URL: string;
5
+
6
+ export interface UploadResult {
7
+ url: string;
8
+ }
9
+
10
+ export async function uploadImage(filePath: string): Promise<UploadResult> {
11
+ const token = Taro.getStorageSync('token');
12
+
13
+ const res = await Taro.uploadFile({
14
+ url: `${API_BASE_URL}/upload/image`,
15
+ filePath,
16
+ name: 'file',
17
+ header: {
18
+ Authorization: token ? `Bearer ${token}` : '',
19
+ },
20
+ });
21
+
22
+ if (res.statusCode !== 200 && res.statusCode !== 201) {
23
+ throw new Error(`上传失败: ${res.statusCode}`);
24
+ }
25
+
26
+ let parsed: any;
27
+ try {
28
+ parsed = JSON.parse(res.data);
29
+ } catch {
30
+ parsed = res.data;
31
+ }
32
+
33
+ if (parsed && typeof parsed === 'object' && 'code' in parsed) {
34
+ if (parsed.code !== 200) {
35
+ throw new Error(parsed.message || '上传失败');
36
+ }
37
+ return { url: parsed.data?.url };
38
+ }
39
+
40
+ return { url: parsed?.url };
41
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "jsx": "react-jsx",
9
+ "allowSyntheticDefaultImports": true,
10
+ "resolveJsonModule": true,
11
+ "typeRoots": ["node_modules/@types"],
12
+ "noEmit": true,
13
+ "skipLibCheck": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "@/*": ["./src/*"]
17
+ }
18
+ },
19
+ "include": ["src/**/*", "types/**/*", "config/**/*"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }
@@ -0,0 +1,10 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { AuthzModuleOptions } from '@svton/nestjs-authz';
3
+
4
+ export const useAuthzConfig = (
5
+ configService: ConfigService,
6
+ ): AuthzModuleOptions => ({
7
+ // 权限配置
8
+ roles: ['admin', 'user', 'guest'],
9
+ defaultRole: 'guest',
10
+ });
@@ -0,0 +1,14 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { CacheModuleOptions } from '@svton/nestjs-cache';
3
+
4
+ export const useCacheConfig = (
5
+ configService: ConfigService,
6
+ ): CacheModuleOptions => ({
7
+ ttl: 3600, // 默认缓存时间 1 小时
8
+ prefix: 'cache', // 缓存 key 前缀
9
+ redis: {
10
+ host: configService.get('REDIS_HOST', 'localhost'),
11
+ port: parseInt(configService.get('REDIS_PORT', '6379'), 10),
12
+ password: configService.get('REDIS_PASSWORD'),
13
+ },
14
+ });
@@ -0,0 +1,20 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { OAuthModuleOptions } from '@svton/nestjs-oauth';
3
+
4
+ export const useOAuthConfig = (
5
+ configService: ConfigService,
6
+ ): OAuthModuleOptions => ({
7
+ wechat: [
8
+ {
9
+ platform: 'open',
10
+ appId: configService.get('WECHAT_OPEN_APP_ID'),
11
+ appSecret: configService.get('WECHAT_OPEN_APP_SECRET'),
12
+ callbackUrl: configService.get('WECHAT_OPEN_CALLBACK_URL'),
13
+ },
14
+ {
15
+ platform: 'miniprogram',
16
+ appId: configService.get('WECHAT_MINI_APP_ID'),
17
+ appSecret: configService.get('WECHAT_MINI_APP_SECRET'),
18
+ },
19
+ ],
20
+ });
@@ -0,0 +1,44 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import { PaymentModuleOptions } from '@svton/nestjs-payment';
3
+ import * as fs from 'fs';
4
+
5
+ /**
6
+ * 读取密钥文件
7
+ * @param filePath 文件路径
8
+ * @returns 文件内容
9
+ */
10
+ function readKeyFile(filePath: string): string {
11
+ try {
12
+ if (fs.existsSync(filePath)) {
13
+ return fs.readFileSync(filePath, 'utf-8');
14
+ }
15
+ throw new Error(`Key file not found: ${filePath}`);
16
+ } catch (error) {
17
+ throw new Error(
18
+ `Failed to read key file: ${filePath}. Error: ${error instanceof Error ? error.message : String(error)}`,
19
+ );
20
+ }
21
+ }
22
+
23
+ export const usePaymentConfig = (
24
+ configService: ConfigService,
25
+ ): PaymentModuleOptions => ({
26
+ wechat: {
27
+ mchId: configService.getOrThrow('WECHAT_MCH_ID'),
28
+ privateKey: readKeyFile(
29
+ configService.get('WECHAT_PRIVATE_KEY', './certs/apiclient_key.pem'),
30
+ ),
31
+ serialNo: configService.getOrThrow('WECHAT_SERIAL_NO'),
32
+ apiV3Key: configService.getOrThrow('WECHAT_API_V3_KEY'),
33
+ appId: configService.getOrThrow('WECHAT_APP_ID'),
34
+ },
35
+ alipay: {
36
+ appId: configService.getOrThrow('ALIPAY_APP_ID'),
37
+ privateKey: readKeyFile(
38
+ configService.get('ALIPAY_PRIVATE_KEY', './certs/alipay_private_key.pem'),
39
+ ),
40
+ alipayPublicKey: readKeyFile(
41
+ configService.get('ALIPAY_PUBLIC_KEY', './certs/alipay_public_key.pem'),
42
+ ),
43
+ },
44
+ });