@sisin/egg-client 1.0.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.
Files changed (57) hide show
  1. package/README.md +267 -0
  2. package/app/constants/code.js +9 -0
  3. package/app/constants/crypt.js +10 -0
  4. package/app/constants/error.js +20 -0
  5. package/app/constants/redis.js +17 -0
  6. package/app/controller/authSystem/auth.js +119 -0
  7. package/app/controller/common/health.js +15 -0
  8. package/app/controller/home.js +10 -0
  9. package/app/core/database/database-health.js +39 -0
  10. package/app/core/database/database-manager.js +43 -0
  11. package/app/core/database/index.js +19 -0
  12. package/app/core/database/prisma-logging.js +38 -0
  13. package/app/core/database/transaction-manager.js +9 -0
  14. package/app/core/repository/auth/user_repository.js +14 -0
  15. package/app/core/repository/base-repository.js +26 -0
  16. package/app/extend/context.js +57 -0
  17. package/app/extend/helper.js +24 -0
  18. package/app/middleware/auth.js +16 -0
  19. package/app/middleware/check_ready.js +11 -0
  20. package/app/middleware/error_handler.js +28 -0
  21. package/app/middleware/jwt_auth.js +27 -0
  22. package/app/middleware/login_limit.js +4 -0
  23. package/app/middleware/permission.js +8 -0
  24. package/app/middleware/request_log.js +28 -0
  25. package/app/middleware/upload_limit.js +17 -0
  26. package/app/model/redis/redis-auth.js +42 -0
  27. package/app/redis/index.js +25 -0
  28. package/app/redis/redis-manager.js +46 -0
  29. package/app/router/auth.js +17 -0
  30. package/app/router/common.js +11 -0
  31. package/app/router/index.js +13 -0
  32. package/app/router.js +11 -0
  33. package/app/service/authSystem/auth.js +144 -0
  34. package/app/service/authSystem/permission.js +4 -0
  35. package/app/service/authSystem/token.js +4 -0
  36. package/app/service/redis/auth.js +4 -0
  37. package/app/utils/common.js +58 -0
  38. package/app/utils/encrypt.js +33 -0
  39. package/app/utils/jwt.js +23 -0
  40. package/app/utils/logger.js +209 -0
  41. package/app/utils/permission.js +2 -0
  42. package/app/utils/prisma-manager.js +127 -0
  43. package/app/utils/prisma.js +21 -0
  44. package/app/validate/user.js +6 -0
  45. package/app.js +21 -0
  46. package/config/config.default.js +137 -0
  47. package/config/logging.js +26 -0
  48. package/config/plugin.default.js +3 -0
  49. package/index.js +22 -0
  50. package/init/index.js +39 -0
  51. package/init/init-auth.js +37 -0
  52. package/init/init-database.js +59 -0
  53. package/init/init-logger.js +18 -0
  54. package/init/init-redis.js +26 -0
  55. package/init/init-websocket.js +10 -0
  56. package/init/ready.js +8 -0
  57. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # @niceguy/egg-client
2
+
3
+ Egg.js 核心框架包 — 将 egg-example 中沉淀的通用基础设施封装为可复用的 Egg 框架,新建项目只需依赖此包即可直接使用中间件、数据层、认证、日志、Redis 等能力,业务项目只需关注业务层面。
4
+
5
+ ## 核心特性
6
+
7
+ - **即拉即用**:安装依赖 + 配置框架名,所有封装自动生效
8
+ - **独立迭代**:框架包可独立发版更新,业务项目同步升级即可
9
+ - **多项目复用**:一套核心逻辑应用于多个业务项目
10
+ - **Egg 原生集成**:基于 Egg.js Framework 机制,完美继承 loadUnit 加载顺序
11
+
12
+ ## 安装
13
+
14
+ `ash
15
+ npm install @niceguy/egg-client
16
+ `
17
+
18
+ ## 使用方式
19
+
20
+ ### 1. 新建 Egg.js 项目
21
+
22
+ 创建标准 Egg.js 项目结构后,在 package.json 中指定框架:
23
+
24
+ `json
25
+ {
26
+ "name": "my-business-project",
27
+ "egg": {
28
+ "framework": "@niceguy/egg-client"
29
+ },
30
+ "dependencies": {
31
+ "@niceguy/egg-client": "^1.0.0",
32
+ "egg": "^3.17.5"
33
+ }
34
+ }
35
+ `
36
+
37
+ ### 2. 项目结构
38
+
39
+ `
40
+ my-business-project/
41
+ ├── package.json # egg.framework: '@niceguy/egg-client'
42
+ ├── app.js # (可选)业务项目初始化逻辑
43
+ ├── config/
44
+ │ ├── config.default.js # 覆盖框架默认配置(数据库、Redis 等)
45
+ │ └── plugin.js # 追加业务插件
46
+ ├── app/
47
+ │ ├── controller/ # 业务控制器
48
+ │ ├── router/ # 业务路由
49
+ │ ├── service/ # 业务服务
50
+ │ └── model/ # 业务 Prisma 模型
51
+ ├── prisma/
52
+ │ ├── schema.prisma # 数据表定义
53
+ │ └── generated/ # prisma generate 产物
54
+ │ ├── fx/ # 数据库名称(与 config.Dbs key 对应)
55
+ │ └── business/
56
+ └── package.json
57
+ `
58
+
59
+ ### 3. 配置数据库 & Redis
60
+
61
+ 在你的项目 config/config.default.js 中覆盖连接信息:
62
+
63
+ `js
64
+ module.exports = (appInfo) => {
65
+ return {
66
+ Dbs: {
67
+ fx: {
68
+ type: 'mysql',
69
+ host: '127.0.0.1',
70
+ port: 3306,
71
+ user: 'root',
72
+ password: '123456',
73
+ database: 'db_base',
74
+ connectionLimit: 5,
75
+ },
76
+ business: {
77
+ type: 'mysql',
78
+ host: '127.0.0.1',
79
+ port: 3306,
80
+ user: 'root',
81
+ password: '123456',
82
+ database: 'db_business',
83
+ connectionLimit: 5,
84
+ },
85
+ },
86
+ redis: {
87
+ fx: { host: '127.0.0.1', port: 6379, password: '123456', db: 0 },
88
+ business: { host: '127.0.0.1', port: 6379, password: '123456', db: 1 },
89
+ },
90
+ };
91
+ };
92
+ `
93
+
94
+ ### 4. 编写路由和控制器
95
+
96
+ `js
97
+ // app/router/user.js
98
+ module.exports = (app) => {
99
+ const { router, controller } = app;
100
+ router.get('/api/user/info', controller.user.info);
101
+ };
102
+
103
+ // app/controller/user.js
104
+ const Controller = require('egg').Controller;
105
+ class UserController extends Controller {
106
+ async info() {
107
+ const { ctx } = this;
108
+ const user = await ctx.service.authSystem.auth.findByUser({ username: 'admin' });
109
+ ctx.api.success(user);
110
+ }
111
+ }
112
+ module.exports = UserController;
113
+ `
114
+
115
+ ## 框架包目录结构
116
+
117
+ `
118
+ packages/egg-client/
119
+ ├── package.json # 框架声明(egg.framework: true)
120
+ ├── app.js # 框架生命周期:初始化数据库、Redis、日志、认证
121
+ ├── config/
122
+ │ ├── config.default.js # 默认配置(合并策略:项目配置优先)
123
+ │ └── plugin.default.js # 默认插件
124
+ ├── app/
125
+ │ ├── constants/ # 状态码、错误码、密码常量、Redis key
126
+ │ ├── core/ # 数据库客户端管理、Repository、事务封装
127
+ │ ├── extend/ # ctx.api / ctx.payload / ctx.repository / helper
128
+ │ ├── middleware/ # jwt_auth / error_handler / check_ready / etc.
129
+ │ ├── model/redis/ # RedisAuth 模型(Session CRUD)
130
+ │ ├── redis/ # Redis 客户端工厂 & 管理器
131
+ │ ├── service/authSystem/ # 认证服务(登录/登出/刷新Token/改密)
132
+ │ ├── utils/ # JWT / 加密 / 日志 / 通用工具
133
+ │ └── validate/ # 参数校验规则
134
+ └── init/ # 初始化编排脚本(挂载日志→数据库→Redis→认证)
135
+ `
136
+
137
+ ## 框架提供的能力清单
138
+
139
+ | 模块 | 提供能力 | 使用方式 |
140
+ |------|---------|---------|
141
+ | **中间件** | checkReady / errorHandler / requestLog / jwtAuth / auth / uploadLimit / loginLimit / permission | 自动注册 checkReady, errorHandler, requestLog, jwtAuth |
142
+ | **Context 扩展** | ctx.api.success/fail/page / ctx.payload / ctx.repository / ctx.encrypt | 直接使用 |
143
+ | **Helper** | ormatTime / ormDateExp / imeUtil / snakeToCamel / camelToSnake | ctx.helper.* |
144
+ | **认证服务** | login / logout / erifyAccessToken /
145
+ | **Redis 管理** | 多实例客户端 / get/set/hset/hget/del | pp.redis.getClient(name) |
146
+ | **数据库** | Prisma 多库管理 / 事务 / 通用 CRUD Repository | ctx.repository.fx.Raw('table') |
147
+ | **日志** | 文件按日切割 / 终端彩显 / 请求日志 / Prisma SQL 日志 | pp.logger.* / ctx.logger.* |
148
+ | **WebSocket** | 可选启用 | pp.config.websocket.enable = true |
149
+
150
+ ## 本地开发
151
+
152
+ ### 安装依赖
153
+
154
+ ```bash
155
+ # 在 monorepo 根目录
156
+ cd niceguy-core-web
157
+ npm install
158
+ ```
159
+
160
+ ### 测试
161
+
162
+ 单元测试(不依赖外部服务):
163
+
164
+ ```bash
165
+ # 在 packages/egg-client 目录
166
+ cd packages/egg-client
167
+ npm run test:unit
168
+
169
+ # 带覆盖率
170
+ npx c8 --reporter=text --reporter=html mocha test/unit/**/*.test.js --timeout 10000
171
+ ```
172
+
173
+ 集成测试(依赖 egg-mock,无需真实数据库):
174
+
175
+ ```bash
176
+ npm test
177
+ ```
178
+
179
+ ### 发布流程
180
+
181
+ ```bash
182
+ # 1. 构建检查:确保测试通过
183
+ npm run test:unit
184
+
185
+ # 2. 发布 patch 版本(0.0.1 → 0.0.2)
186
+ npm run publish:patch
187
+
188
+ # 3. 发布 minor 版本(0.0.1 → 0.1.0)
189
+ npm run publish:minor
190
+
191
+ # 4. 发布 major 版本(0.0.1 → 1.0.0)
192
+ npm run publish:major
193
+ ```
194
+
195
+ ## 使用本地开发版本(link 模式)
196
+
197
+ 在业务项目中直接使用本地的 egg-client:
198
+
199
+ ```bash
200
+ # 在 egg-client 目录注册全局 link
201
+ cd niceguy-core-web/packages/egg-client
202
+ npm link
203
+
204
+ # 在业务项目(如 egg-example)中使用本地版本
205
+ cd egg-example
206
+ npm link @niceguy/egg-client
207
+
208
+ # 恢复为 npm 版本
209
+ npm unlink @niceguy/egg-client && npm install @niceguy/egg-client
210
+ ```
211
+
212
+ 或者使用 npm workspaces(推荐):
213
+
214
+ ```json
215
+ {
216
+ "workspaces": [
217
+ "packages/*"
218
+ ]
219
+ }
220
+ ```
221
+
222
+ 然后在业务项目的 package.json 中:
223
+
224
+ ```json
225
+ {
226
+ "egg": {
227
+ "framework": "@niceguy/egg-client"
228
+ },
229
+ "dependencies": {
230
+ "@niceguy/egg-client": "^1.0.0"
231
+ }
232
+ }
233
+ ```
234
+
235
+ ## 框架目录结构
236
+
237
+ ```
238
+ packages/egg-client/
239
+ ├── index.js # 框架入口,导出工具函数
240
+ ├── app.js # 框架生命周期:初始化数据库、Redis、日志、认证
241
+ ├── package.json # 框架声明(egg.framework: true)
242
+ ├── .npmignore # npm 发布排除文件
243
+ ├── .mocharc.yml # 测试配置
244
+ ├── config/
245
+ │ ├── config.default.js # 默认配置(合并策略:项目配置优先)
246
+ │ └── plugin.default.js # 默认插件
247
+ ├── app/
248
+ │ ├── constants/ # 状态码、错误码、密码常量、Redis key
249
+ │ ├── core/ # 数据库客户端管理、Repository、事务封装
250
+ │ ├── extend/ # ctx.api / ctx.payload / ctx.repository / helper
251
+ │ ├── middleware/ # jwt_auth / error_handler / check_ready 等
252
+ │ ├── model/redis/ # RedisAuth 模型(Session CRUD)
253
+ │ ├── redis/ # Redis 客户端工厂 & 管理器
254
+ │ ├── service/authSystem/ # 认证服务(登录/登出/刷新Token/改密)
255
+ │ ├── utils/ # JWT / 加密 / 日志 / 通用 / PrismaManager
256
+ │ └── validate/ # 参数校验规则
257
+ ├── init/ # 初始化编排脚本
258
+ └── test/ # 测试
259
+ ├── config/
260
+ │ └── config.default.js # 测试环境配置
261
+ ├── app/controller/ # 集成测试
262
+ └── unit/ # 单元测试
263
+ ```
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ SUCCESS: 0, // 成功
3
+ BAD_REQUEST: 400, // 请求错误
4
+ UNAUTHORIZED: 401, // 未授权,无权限
5
+ NOT_ALLOWED: 405, // 不允许的请求方法
6
+ FORBIDDEN: 403, // 禁止访问
7
+ NOT_FOUND: 404, // 未找到
8
+ SERVER_ERROR: 500, // 服务器错误
9
+ };
@@ -0,0 +1,10 @@
1
+ // JWT 配置
2
+ module.exports = {
3
+ ALG: 'HS256',
4
+ ACCESS_SECRET: 'niceguy328597wndvhefn8935nx',
5
+ REFRESH_SECRET: 'niceguyondgnos0sdfodf0ssfs34',
6
+ SESSION_EXPIRES_IN: 7 * 24 * 60, // 会话最长有效时长 7天,单位 分钟
7
+ TOKEN_EXPIRES_IN: 1, // jwt过期时长 1小时,单位 分钟
8
+ REFRESH_TOKEN_EXPIRES_IN: 7 * 24 * 60, // 刷新token过期时长 7天,单位 分钟
9
+ SESSION_COOKIE_NAME: 'sessionId',
10
+ };
@@ -0,0 +1,20 @@
1
+ // 错误状态码
2
+ module.exports = {
3
+ 200: { code: 200, parent: null, msg: '请求成功' },
4
+ 400: { code: 200, parent: 400, msg: '请求错误/操作失败' },
5
+ 401: { code: 401, parent: null, msg: '用户未登录或登录过期' },
6
+ 40101: { code: 40101, parent: 401, msg: 'access登录已过期' },
7
+ 40102: { code: 40102, parent: 401, msg: 'refresh登录已过期' },
8
+ 40103: { code: 40103, parent: 401, msg: '非法TOKEN登录' },
9
+ 40104: { code: 40104, parent: 401, msg: '登录已失效,请重新登录' },
10
+ 40105: { code: 40105, parent: 401, msg: '用户不存在' },
11
+ 40106: { code: 40106, parent: 401, msg: '密码错误' },
12
+ 40107: { code: 40107, parent: 401, msg: '暂不支持当前平台登录' },
13
+ 403: { code: 403, parent: null, msg: '权限不足,禁止访问' },
14
+ 40301: { code: 40301, parent: 403, msg: '账号禁用' },
15
+ 40302: { code: 40302, parent: 403, msg: '权限不足' },
16
+ 404: { code: 404, parent: null, msg: '未找到资源' },
17
+ 405: { code: 405, parent: null, msg: '非法请求' },
18
+ 422: { code: 422, parent: null, msg: '数据校验不通过' },
19
+ 500: { code: 500, parent: null, msg: '服务器错误' },
20
+ };
@@ -0,0 +1,17 @@
1
+ // 各模块的 redis key 常量
2
+ module.exports = {
3
+ USER_LOGIN: 'user:login',
4
+ USER_LOGIN_TOKEN: 'login:token',
5
+ USER_LOGIN_REFRESH_TOKEN: 'login:refresh',
6
+ USER_LOGIN_SESSION: 'login:session',
7
+ USER_SESSION: 'user:session',
8
+ AUTH_SESSION: 'auth:session',
9
+ AUTH_USER_SESSIONS: 'auth:user:sessions',
10
+ AUTH_REFRESH_LOCK: 'auth:refresh:lock',
11
+
12
+ DEVICE_OPTION: {
13
+ PC: 'pc', APP: 'app', MINIAPP: 'miniapp',
14
+ WXAPP: 'wxapp', ALIPAY: 'alipay', BAIDU: 'baidu',
15
+ TIKTOK: 'tiktok', QQ: 'qq', DOUYIN: 'douyin', H5: 'h5',
16
+ },
17
+ };
@@ -0,0 +1,119 @@
1
+ const Controller = require("egg").Controller;
2
+ const errorCode = require("../../constants/error");
3
+ const {
4
+ REFRESH_TOKEN_EXPIRES_IN,
5
+ SESSION_COOKIE_NAME,
6
+ } = require("../../constants/crypt");
7
+
8
+ class AuthController extends Controller {
9
+ /**
10
+ * 登录:校验 → 调用 service → 返回 accessToken + 设置 refreshToken cookie
11
+ */
12
+ async login() {
13
+ const { ctx } = this;
14
+ ctx.validate(
15
+ {
16
+ username: { type: "string", required: true, min: 3, max: 20 },
17
+ password: { type: "string", required: true, min: 6, max: 20 },
18
+ },
19
+ ctx.payload,
20
+ );
21
+
22
+ const result = await ctx.service.authSystem.auth.login(ctx.payload);
23
+ if (!result.ok) {
24
+ return ctx.api.fail(errorCode[result.code]?.msg, result.code, null);
25
+ }
26
+
27
+ // 设置 refreshToken 为 HTTP-only cookie
28
+ const maxAge = REFRESH_TOKEN_EXPIRES_IN * 60 * 1000;
29
+ ctx.cookies.set(SESSION_COOKIE_NAME, result.data.refreshToken, {
30
+ httpOnly: true,
31
+ sameSite: "Strict",
32
+ path: "/",
33
+ maxAge,
34
+ signed: false,
35
+ });
36
+
37
+ return ctx.api.success({ accessToken: result.data.accessToken });
38
+ }
39
+
40
+ /**
41
+ * 刷新 accessToken:从 cookie 获取 refreshToken → 调用 service → 返回新 token
42
+ */
43
+ async refresh() {
44
+ const { ctx } = this;
45
+ const token = ctx.cookies.get(SESSION_COOKIE_NAME, { signed: false });
46
+ if (!token) {
47
+ return ctx.api.fail(errorCode[401]?.msg, 401, null);
48
+ }
49
+
50
+ const result = await ctx.service.authSystem.auth.refreshToken({ token });
51
+ if (!result.ok) {
52
+ ctx.cookies.set(SESSION_COOKIE_NAME, null, { maxAge: 0, path: "/" });
53
+ return ctx.api.fail(errorCode[result.code]?.msg, result.code, null);
54
+ }
55
+
56
+ // 设置新 refreshToken cookie
57
+ const maxAge = REFRESH_TOKEN_EXPIRES_IN * 60 * 1000;
58
+ ctx.cookies.set(SESSION_COOKIE_NAME, result.data.refreshToken, {
59
+ httpOnly: true,
60
+ sameSite: "Strict",
61
+ path: "/",
62
+ maxAge,
63
+ signed: false,
64
+ });
65
+
66
+ return ctx.api.success({ accessToken: result.data.accessToken });
67
+ }
68
+
69
+ /**
70
+ * 登出:调用 service → 清除 cookie
71
+ */
72
+ async logout() {
73
+ const { ctx } = this;
74
+ const { uid, sid } = ctx.user;
75
+ await ctx.service.authSystem.auth.logout({ uid, sid });
76
+ ctx.cookies.set(SESSION_COOKIE_NAME, null, { maxAge: 0, path: "/" });
77
+ return ctx.api.success(null);
78
+ }
79
+
80
+ /**
81
+ * 用户信息查询(保留,仅做代理)
82
+ */
83
+ async getUserInfo() {
84
+ const { ctx } = this;
85
+ const { username } = ctx.payload;
86
+ const data = await ctx.service.authSystem.auth.findByUser({ username });
87
+ return ctx.api.success(data);
88
+ }
89
+
90
+ /**
91
+ * 修改密码:校验 → 调用 service → 清除 cookie
92
+ */
93
+ async changePassword() {
94
+ const { ctx } = this;
95
+ ctx.validate(
96
+ {
97
+ oldPassword: { type: "string", required: true, min: 6, max: 20 },
98
+ newPassword: { type: "string", required: true, min: 6, max: 20 },
99
+ },
100
+ ctx.payload,
101
+ );
102
+
103
+ const { uid } = ctx.user;
104
+ const result = await ctx.service.authSystem.auth.changePassword({
105
+ uid,
106
+ oldPassword: ctx.payload.oldPassword,
107
+ newPassword: ctx.payload.newPassword,
108
+ });
109
+ if (!result.ok) {
110
+ return ctx.api.fail(errorCode[result.code]?.msg, result.code, null);
111
+ }
112
+
113
+ // 密码已改,全端已登出 → 清除 cookie
114
+ ctx.cookies.set(SESSION_COOKIE_NAME, null, { maxAge: 0, path: "/" });
115
+ return ctx.api.success(null);
116
+ }
117
+ }
118
+
119
+ module.exports = AuthController;
@@ -0,0 +1,15 @@
1
+ const Controller = require('egg').Controller;
2
+
3
+ class HealthController extends Controller {
4
+ async index() {
5
+ const { ctx } = this;
6
+
7
+ return ctx.api.success({
8
+ ready: this.app.initStatus.ready,
9
+ tasks: this.app.initStatus.tasks,
10
+ uptime: process.uptime(),
11
+ });
12
+ }
13
+ }
14
+
15
+ module.exports = HealthController;
@@ -0,0 +1,10 @@
1
+ const { Controller } = require('egg');
2
+
3
+ class HomeController extends Controller {
4
+ async index() {
5
+ const { ctx } = this;
6
+ ctx.body = 'hi, egg';
7
+ }
8
+ }
9
+
10
+ module.exports = HomeController;
@@ -0,0 +1,39 @@
1
+ // 数据库健康检查
2
+ const { initDatabases } = require('./index');
3
+
4
+ async function checkClient(name, prisma, query) {
5
+ const startedAt = Date.now();
6
+ try {
7
+ await prisma.\();
8
+ const result = await query(prisma);
9
+ return { name, ok: true, duration: Date.now() - startedAt, detail: result };
10
+ } catch (error) {
11
+ return { name, ok: false, duration: Date.now() - startedAt, detail: error instanceof Error ? error.message : String(error) };
12
+ } finally {
13
+ await prisma.\();
14
+ }
15
+ }
16
+
17
+ function formatResult(result) {
18
+ const status = result.ok ? 'PASS' : 'FAIL';
19
+ return '[' + status + '] ' + result.name + ' (' + result.duration + 'ms) ' + result.detail;
20
+ }
21
+
22
+ async function runDbChecks(app) {
23
+ const clients = initDatabases(app);
24
+ const checks = [];
25
+ for (const key in clients) {
26
+ if (!clients[key]) continue;
27
+ checks.push(
28
+ checkClient(key + ':connect-and-query', clients[key], async (prisma) => {
29
+ await prisma.\\SELECT 1 AS ok\;
30
+ return 'connected';
31
+ })
32
+ );
33
+ }
34
+ const results = await Promise.all(checks);
35
+ const failed = results.filter((r) => !r.ok);
36
+ return { ok: failed.length === 0, results };
37
+ }
38
+
39
+ module.exports = { formatResult, runDbChecks };
@@ -0,0 +1,43 @@
1
+ // 数据库实例管理器
2
+ // 根据 app.config.Dbs 动态创建 PrismaClient 实例
3
+ // Prisma 生成客户端从业务项目的 prisma/generated/{key} 加载
4
+ const path = require('path');
5
+ const { PrismaMariaDb } = require('@prisma/adapter-mariadb');
6
+ const { buildPrismaLogDefinitions } = require('./prisma-logging');
7
+ const { buildDataBaseUrl } = require('../../utils/prisma');
8
+
9
+ /**
10
+ * 创建所有配置的数据库客户端实例
11
+ * @param {Egg.Application} app
12
+ * @returns {Object} { dbName: PrismaClient }
13
+ */
14
+ function createDatabaseClients(app) {
15
+ const dbConfig = app.config.Dbs || {};
16
+ const loggingConfig = app.config.logging;
17
+ const clients = {};
18
+
19
+ for (const key in dbConfig) {
20
+ if (!dbConfig[key]) continue;
21
+
22
+ let PrismaClient;
23
+ // 优先从业务项目的 prisma/generated/{key} 加载
24
+ const projectPrismaPath = path.join(app.baseDir, 'prisma', 'generated', key);
25
+ try {
26
+ const mod = require(projectPrismaPath);
27
+ PrismaClient = mod.PrismaClient;
28
+ } catch (_) {
29
+ app.logger && app.logger.warn('[数据库] ' + key + ' PrismaClient 未在项目 prisma/generated 中找到: ' + projectPrismaPath);
30
+ continue;
31
+ }
32
+
33
+ const adapter = new PrismaMariaDb(dbConfig[key]);
34
+ clients[key] = new PrismaClient({
35
+ adapter,
36
+ log: buildPrismaLogDefinitions(loggingConfig),
37
+ });
38
+ }
39
+
40
+ return clients;
41
+ }
42
+
43
+ module.exports = createDatabaseClients;
@@ -0,0 +1,19 @@
1
+ // 数据库模块入口
2
+ const createDatabaseClients = require('./database-manager');
3
+ const { setupPrismaLogging } = require('./prisma-logging');
4
+
5
+ /**
6
+ * 初始化所有数据库客户端
7
+ * @param {Egg.Application} app
8
+ * @returns {Object} { dbName: PrismaClient }
9
+ */
10
+ function initDatabases(app) {
11
+ const clients = createDatabaseClients(app);
12
+ const loggingConfig = app.config.logging;
13
+ for (const key in clients) {
14
+ if (clients[key]) setupPrismaLogging(key, clients[key], loggingConfig);
15
+ }
16
+ return clients;
17
+ }
18
+
19
+ module.exports = { initDatabases };
@@ -0,0 +1,38 @@
1
+ // Prisma 日志定义 & 事件监听
2
+ const path = require('path');
3
+ const { createLogger } = require('../../utils/logger');
4
+
5
+ const prismaLevelRank = { query: 10, info: 20, warn: 30, error: 40 };
6
+
7
+ function shouldEnablePrismaLevel(level, config) {
8
+ const configuredRank = prismaLevelRank[config.prisma.level] || prismaLevelRank.warn;
9
+ const currentRank = prismaLevelRank[level];
10
+ return currentRank >= configuredRank;
11
+ }
12
+
13
+ function buildPrismaLogDefinitions(config) {
14
+ const log = [];
15
+ for (const level of ['info', 'warn', 'error']) {
16
+ if (shouldEnablePrismaLevel(level, config)) log.push({ level, emit: 'event' });
17
+ }
18
+ if (config.prisma.sqlTerminal || config.prisma.slowQueryMs >= 0) {
19
+ log.push({ level: 'query', emit: 'event' });
20
+ }
21
+ return log;
22
+ }
23
+
24
+ function setupPrismaLogging(name, prisma, config) {
25
+ const elog = createLogger({ level: config.prisma.level || 'info', filePath: config.prisma.filePath, terminal: config.prisma.sqlTerminal, pretty: config.prisma.sqlTerminal });
26
+ const slog = createLogger({ level: config.prisma.sqlLogLevel, filePath: config.prisma.filePath, terminal: config.prisma.sqlTerminal, pretty: config.prisma.sqlTerminal });
27
+
28
+ prisma.$on('query', (event) => {
29
+ const payload = { component: 'prisma', database: name, query: event.query, params: event.params, duration: event.duration };
30
+ if (typeof slog.debug === 'function') slog.debug('Prisma SQL [' + name + ']', payload);
31
+ if (event.duration > config.prisma.slowQueryMs) elog.warn('Prisma Slow Query [' + name + ']', payload);
32
+ });
33
+ prisma.$on('info', (event) => elog.info('Prisma Info [' + name + ']', { component: 'prisma', database: name, target: event.target, message: event.message }));
34
+ prisma.$on('warn', (event) => elog.warn('Prisma Warn [' + name + ']', { component: 'prisma', database: name, target: event.target, message: event.message }));
35
+ prisma.$on('error', (event) => elog.error('Prisma Error [' + name + ']', { component: 'prisma', database: name, target: event.target, message: event.message }));
36
+ }
37
+
38
+ module.exports = { buildPrismaLogDefinitions, setupPrismaLogging };
@@ -0,0 +1,9 @@
1
+ // 事务封装
2
+ class TransactionManager {
3
+ static async execute(db, callback) {
4
+ return db.$transaction(async (tx) => {
5
+ try { return await callback(tx); } catch (error) { throw error; }
6
+ });
7
+ }
8
+ }
9
+ module.exports = TransactionManager;
@@ -0,0 +1,14 @@
1
+ const BaseRepository = require('../base-repository');
2
+
3
+ class UserRepository extends BaseRepository {
4
+ constructor(ctx) { super(ctx.Dbs.fx.user_info); this.ctx = ctx; }
5
+
6
+ async findOneUser({ userId, enName, password }) {
7
+ const where = {};
8
+ if (userId) where.id = userId;
9
+ if (enName) where.en_name = enName;
10
+ return await this.findOne({ where });
11
+ }
12
+ }
13
+
14
+ module.exports = UserRepository;
@@ -0,0 +1,26 @@
1
+ // 通用 CRUD 封装
2
+ class BaseRepository {
3
+ constructor(model) { this.model = model; }
4
+
5
+ async create(data) { return this.model.create({ data }); }
6
+ async createMany(data) { return this.model.createMany({ data }); }
7
+ async findById(args) { return this.model.findUnique(args); }
8
+ async findOne(args) { return this.model.findFirst(args); }
9
+ async findMany(args) { return this.model.findMany(args); }
10
+
11
+ async paginate(pageIndex = 1, pageSize = 10, args = {}) {
12
+ const skip = (pageIndex - 1) * pageSize;
13
+ const [list, total] = await Promise.all([
14
+ this.model.findMany({ ...args, skip, take: pageSize }),
15
+ this.model.count(args.where ? { where: args.where } : {}),
16
+ ]);
17
+ return { list, total, totalPages: Math.ceil(total / pageSize) };
18
+ }
19
+
20
+ async updateByFilter(args) { return this.model.update(args); }
21
+ async deleteByFilter(args) { return this.model.delete(args); }
22
+ async exists(where = {}) { const count = await this.model.count({ where }); return count > 0; }
23
+ async count(where = {}) { return this.model.count({ where }); }
24
+ }
25
+
26
+ module.exports = BaseRepository;