字典管理
+维护 tbl_system_dict,支持模糊查询、行内编辑、枚举项弹窗编辑、新增和删除。
+ 进入字典管理 +diff --git a/pocket-base/README.md b/pocket-base/README.md index c8bbc6a..c695eec 100644 --- a/pocket-base/README.md +++ b/pocket-base/README.md @@ -58,7 +58,7 @@ pocket-base/ - 登录接口:`POST /api/platform/login` - 平台用户注册时会自动生成 GUID 并写入 `tbl_auth_users.openid` - 同时写入 `users_idtype = ManagePlatform` -- 平台登录对前端暴露为 `users_phone + password` +- 平台登录对前端暴露为 `login_account + password`,其中 `login_account` 支持邮箱或手机号 - 服务端内部仍使用 PocketBase 原生 password auth,以确保返回原生 `token` ### 通用认证能力 @@ -120,6 +120,7 @@ PocketBase JSVM 不会自动读取 `back-end/.env`。当前 Hook 运行配置来 - 旧 `back-end/.env` 中的这些值可以作为来源参考,但 **不会被 pb_hooks 自动读取**。 - 如果不方便改 PocketBase 进程环境,可在服务器创建:`pb_hooks/bai_api_pb_hooks/bai_api_shared/config/runtime.js`。 - `runtime.js` 的内容可直接参考 `runtime.example.js`。 +- 若 hooks 内部需要回源调用 PocketBase REST(如 `auth-with-password`),`POCKETBASE_API_URL` 不应使用外部 HTTPS 域名,建议使用 PocketBase 进程/容器内可达地址,例如 `http://127.0.0.1:8080`,以避免 Nginx 443 回环失败。 - 当前 Hook 代码已不依赖自定义 JWT,因此 `JWT_SECRET`、`JWT_EXPIRES_IN` 不是运行必需项。 ## 额外要求 diff --git a/pocket-base/bai-api-main.pb.js b/pocket-base/bai-api-main.pb.js index dda3fae..d16ebde 100644 --- a/pocket-base/bai-api-main.pb.js +++ b/pocket-base/bai-api-main.pb.js @@ -23,5 +23,10 @@ require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/users-count.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/system/refresh-token.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/platform/login.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/platform/register.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/list.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/detail.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/create.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/update.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/delete.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/login.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/profile.js`) diff --git a/pocket-base/bai-web-main.pb.js b/pocket-base/bai-web-main.pb.js index 8f1d652..5ed467d 100644 --- a/pocket-base/bai-web-main.pb.js +++ b/pocket-base/bai-web-main.pb.js @@ -1,3 +1,4 @@ require(`${__hooks}/bai_web_pb_hooks/pages/index.js`) require(`${__hooks}/bai_web_pb_hooks/pages/page-a.js`) require(`${__hooks}/bai_web_pb_hooks/pages/page-b.js`) +require(`${__hooks}/bai_web_pb_hooks/pages/dictionary-manage.js`) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/create.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/create.js new file mode 100644 index 0000000..de7ad1e --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/create.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/dictionary/create', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`) + const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) + + try { + guards.requireJson(e) + guards.requireManagePlatformUser(e) + guards.duplicateGuard(e) + + const payload = guards.validateDictionaryMutationBody(e, false) + const data = dictionaryService.createDictionary(payload) + + return success(e, '新增字典成功', data) + } catch (err) { + const status = (err && err.statusCode) || (err && err.status) || 400 + logger.error('新增字典失败', { + status: status, + message: (err && err.message) || '未知错误', + data: (err && err.data) || {}, + }) + return e.json(status, { + code: status, + msg: (err && err.message) || '新增字典失败', + data: (err && err.data) || {}, + }) + } +}) \ No newline at end of file diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/delete.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/delete.js new file mode 100644 index 0000000..c77fce1 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/delete.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/dictionary/delete', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`) + const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) + + try { + guards.requireJson(e) + guards.requireManagePlatformUser(e) + guards.duplicateGuard(e) + + const payload = guards.validateDictionaryDeleteBody(e) + const data = dictionaryService.deleteDictionary(payload.dict_name) + + return success(e, '删除字典成功', data) + } catch (err) { + const status = (err && err.statusCode) || (err && err.status) || 400 + logger.error('删除字典失败', { + status: status, + message: (err && err.message) || '未知错误', + data: (err && err.data) || {}, + }) + return e.json(status, { + code: status, + msg: (err && err.message) || '删除字典失败', + data: (err && err.data) || {}, + }) + } +}) \ No newline at end of file diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/detail.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/detail.js new file mode 100644 index 0000000..53499b2 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/detail.js @@ -0,0 +1,28 @@ +routerAdd('POST', '/api/dictionary/detail', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`) + const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) + + try { + guards.requireJson(e) + guards.requireManagePlatformUser(e) + + const payload = guards.validateDictionaryDetailBody(e) + const data = dictionaryService.getDictionaryByName(payload.dict_name) + + return success(e, '查询字典详情成功', data) + } catch (err) { + const status = (err && err.statusCode) || (err && err.status) || 400 + logger.error('查询字典详情失败', { + status: status, + message: (err && err.message) || '未知错误', + data: (err && err.data) || {}, + }) + return e.json(status, { + code: status, + msg: (err && err.message) || '查询字典详情失败', + data: (err && err.data) || {}, + }) + } +}) \ No newline at end of file diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/list.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/list.js new file mode 100644 index 0000000..faea7a3 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/list.js @@ -0,0 +1,30 @@ +routerAdd('POST', '/api/dictionary/list', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`) + const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) + + try { + guards.requireJson(e) + guards.requireManagePlatformUser(e) + + const payload = guards.validateDictionaryListBody(e) + const data = dictionaryService.listDictionaries(payload.keyword) + + return success(e, '查询字典列表成功', { + items: data, + }) + } catch (err) { + const status = (err && err.statusCode) || (err && err.status) || 400 + logger.error('查询字典列表失败', { + status: status, + message: (err && err.message) || '未知错误', + data: (err && err.data) || {}, + }) + return e.json(status, { + code: status, + msg: (err && err.message) || '查询字典列表失败', + data: (err && err.data) || {}, + }) + } +}) \ No newline at end of file diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/update.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/update.js new file mode 100644 index 0000000..f682b89 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/dictionary/update.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/dictionary/update', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`) + const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) + + try { + guards.requireJson(e) + guards.requireManagePlatformUser(e) + guards.duplicateGuard(e) + + const payload = guards.validateDictionaryMutationBody(e, true) + const data = dictionaryService.updateDictionary(payload) + + return success(e, '修改字典成功', data) + } catch (err) { + const status = (err && err.statusCode) || (err && err.status) || 400 + logger.error('修改字典失败', { + status: status, + message: (err && err.message) || '未知错误', + data: (err && err.data) || {}, + }) + return e.json(status, { + code: status, + msg: (err && err.message) || '修改字典失败', + data: (err && err.data) || {}, + }) + } +}) \ No newline at end of file diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/login.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/login.js index 05cbc27..05c46fa 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/login.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/login.js @@ -2,6 +2,7 @@ routerAdd('POST', '/api/platform/login', function (e) { const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`) const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { successWithToken } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) try { guards.requireJson(e) @@ -10,8 +11,7 @@ routerAdd('POST', '/api/platform/login', function (e) { const payload = guards.validatePlatformLoginBody(e) const data = userService.authenticatePlatformUser(payload) - $apis.recordAuthResponse(e, data.authRecord, data.authMethod, data.meta) - return + return successWithToken(e, '登录成功', data.meta.data, userService.ensureAuthToken(data.token)) } catch (err) { const status = (err && typeof err.statusCode === 'number' && err.statusCode) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/register.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/register.js index df9b597..6b0a983 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/register.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/platform/register.js @@ -2,6 +2,7 @@ routerAdd('POST', '/api/platform/register', function (e) { const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`) const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { successWithToken } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) try { guards.requireJson(e) @@ -9,9 +10,9 @@ routerAdd('POST', '/api/platform/register', function (e) { const payload = guards.validatePlatformRegisterBody(e) const data = userService.registerPlatformUser(payload) + const token = userService.issueAuthToken(data.authRecord) - $apis.recordAuthResponse(e, data.authRecord, data.authMethod, data.meta) - return + return successWithToken(e, '注册成功', data.meta.data, token) } catch (err) { const status = (err && typeof err.statusCode === 'number' && err.statusCode) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/system/health.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/system/health.js index b7dc484..230ed98 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_routes/system/health.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/system/health.js @@ -1,8 +1,10 @@ routerAdd('POST', '/api/system/health', function (e) { + const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`) const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) return success(e, '服务运行正常', { status: 'healthy', + version: env.appVersion, timestamp: new Date().toISOString(), }) }) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/system/refresh-token.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/system/refresh-token.js index a5415cd..2f38248 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_routes/system/refresh-token.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/system/refresh-token.js @@ -2,6 +2,7 @@ routerAdd('POST', '/api/system/refresh-token', function (e) { const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`) const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { successWithToken } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) try { guards.requireJson(e) @@ -14,13 +15,7 @@ routerAdd('POST', '/api/system/refresh-token', function (e) { const data = userService.refreshAuthToken(openid) const token = userService.issueAuthToken(data.authRecord) - return e.json(200, { - code: 200, - msg: '刷新成功', - data: { - token: token, - }, - }) + return successWithToken(e, '刷新成功', {}, token) } if (!payload.users_wx_code) { @@ -34,13 +29,7 @@ routerAdd('POST', '/api/system/refresh-token', function (e) { const authData = userService.authenticateWechatUser(payload) const token = userService.issueAuthToken(authData.authRecord) - return e.json(200, { - code: 200, - msg: '刷新成功', - data: { - token: token, - }, - }) + return successWithToken(e, '刷新成功', {}, token) } catch (err) { const status = (err && typeof err.statusCode === 'number' && err.statusCode) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/wechat/login.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/wechat/login.js index 68b2c3f..32004b3 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_routes/wechat/login.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/wechat/login.js @@ -2,6 +2,7 @@ routerAdd('POST', '/api/wechat/login', function (e) { const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`) const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + const { successWithToken } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`) try { guards.requireJson(e) @@ -9,9 +10,9 @@ routerAdd('POST', '/api/wechat/login', function (e) { const payload = guards.validateLoginBody(e) const data = userService.authenticateWechatUser(payload) + const token = userService.issueAuthToken(data.authRecord) - $apis.recordAuthResponse(e, data.authRecord, data.authMethod, data.meta) - return + return successWithToken(e, data.status === 'register_success' ? '注册成功' : '登录成功', data.meta.data, token) } catch (err) { const status = (err && typeof err.statusCode === 'number' && err.statusCode) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/env.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/env.js index b54acab..66fde6b 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/env.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/env.js @@ -20,6 +20,7 @@ function pick(key, fallback) { module.exports = { nodeEnv: pick('NODE_ENV', 'production'), apiPrefix: '/api', + appVersion: pick('APP_VERSION', 'dev-local'), wechatAppId: pick('WECHAT_APPID', ''), wechatSecret: pick('WECHAT_SECRET', ''), pocketbaseApiUrl: pick('POCKETBASE_API_URL', ''), diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.example.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.example.js index ea426f0..b16b32c 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.example.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.example.js @@ -1,7 +1,8 @@ module.exports = { NODE_ENV: 'production', + APP_VERSION: 'temp-dev-local', APP_BASE_URL: 'https://bai-api.blv-oa.com', - POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb/', + POCKETBASE_API_URL: 'http://127.0.0.1:8090', POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTc3NDEwMTc4NiwiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.67DCKhqRQYF3ClPU_9mBgON_9ZDEy-NzqTeS50rGGZo', /* WECHAT_APPID: 'wx3bd7a7b19679da7a', WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1', */ diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js index 729da21..64b691c 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js @@ -1,7 +1,8 @@ module.exports = { NODE_ENV: 'production', + APP_VERSION: '0.1.21', APP_BASE_URL: 'https://bai-api.blv-oa.com', - POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb/', + POCKETBASE_API_URL: 'http://127.0.0.1:8090', POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo', WECHAT_APPID: 'wx3bd7a7b19679da7a', WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1', diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js index f0d3347..b61c981 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js @@ -50,10 +50,15 @@ function validatePlatformRegisterBody(e) { function validatePlatformLoginBody(e) { const payload = parseBody(e) - if (!payload.users_phone) throw createAppError(400, 'users_phone 为必填项') + const loginAccount = payload.login_account || payload.users_phone || payload.email || '' + + if (!loginAccount) throw createAppError(400, 'login_account 为必填项,支持邮箱或手机号') if (!payload.password) throw createAppError(400, 'password 为必填项') - return payload + return { + login_account: loginAccount, + password: payload.password, + } } function validateSystemRefreshBody(e) { @@ -74,6 +79,94 @@ function validateSystemRefreshBody(e) { } } +function requireManagePlatformUser(e) { + const authUser = requireAuthUser(e) + const idType = authUser.authRecord.getString('users_idtype') + + if (idType !== 'ManagePlatform') { + throw createAppError(403, '仅平台管理用户可访问') + } + + return authUser +} + +function validateDictionaryListBody(e) { + const payload = parseBody(e) + return { + keyword: payload.keyword || '', + } +} + +function validateDictionaryDetailBody(e) { + const payload = parseBody(e) + if (!payload.dict_name) { + throw createAppError(400, 'dict_name 为必填项') + } + + return { + dict_name: payload.dict_name, + } +} + +function normalizeDictionaryItem(item, index) { + const current = sanitizePayload(item || {}) + if (!current.enum) { + throw createAppError(400, '第 ' + (index + 1) + ' 项 enum 为必填项') + } + + if (!current.description) { + throw createAppError(400, '第 ' + (index + 1) + ' 项 description 为必填项') + } + + const sortOrderNumber = Number(current.sortOrder) + if (!Number.isFinite(sortOrderNumber)) { + throw createAppError(400, '第 ' + (index + 1) + ' 项 sortOrder 必须为数字') + } + + return { + enum: String(current.enum), + description: String(current.description), + sortOrder: sortOrderNumber, + } +} + +function validateDictionaryMutationBody(e, isUpdate) { + const payload = parseBody(e) + + if (!payload.dict_name) { + throw createAppError(400, 'dict_name 为必填项') + } + + const items = Array.isArray(payload.items) ? payload.items : [] + if (!items.length) { + throw createAppError(400, 'items 至少需要一项') + } + + const normalizedItems = items.map(function (item, index) { + return normalizeDictionaryItem(item, index) + }) + + return { + dict_name: payload.dict_name, + dict_word_is_enabled: typeof payload.dict_word_is_enabled === 'boolean' ? payload.dict_word_is_enabled : true, + dict_word_parent_id: payload.dict_word_parent_id || '', + dict_word_remark: payload.dict_word_remark || '', + items: normalizedItems, + original_dict_name: isUpdate ? (payload.original_dict_name || payload.dict_name) : '', + } +} + +function validateDictionaryDeleteBody(e) { + const payload = parseBody(e) + if (!payload.dict_name) { + throw createAppError(400, 'dict_name 为必填项') + } + + return { + dict_name: payload.dict_name, + } +} + function requireAuthOpenid(e) { if (!e.auth) { throw createAppError(401, '认证令牌无效或已过期') @@ -138,6 +231,11 @@ module.exports = { validatePlatformRegisterBody, validatePlatformLoginBody, validateSystemRefreshBody, + requireManagePlatformUser, + validateDictionaryListBody, + validateDictionaryDetailBody, + validateDictionaryMutationBody, + validateDictionaryDeleteBody, requireAuthOpenid, requireAuthUser, duplicateGuard, diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js new file mode 100644 index 0000000..8e8c0cb --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js @@ -0,0 +1,200 @@ +const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`) +const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) + +function buildSystemDictId() { + return 'DICT-' + new Date().getTime() + '-' + $security.randomString(6) +} + +function safeJsonParse(text, fallback) { + if (!text) return fallback + + try { + return JSON.parse(text) + } catch (err) { + return fallback + } +} + +function normalizeItemsFromRecord(record) { + const enums = safeJsonParse(record.getString('dict_word_enum'), []) + const descriptions = safeJsonParse(record.getString('dict_word_description'), []) + const sortOrders = safeJsonParse(record.getString('dict_word_sort_order'), []) + const maxLength = Math.max(enums.length, descriptions.length, sortOrders.length) + const items = [] + + for (let i = 0; i < maxLength; i += 1) { + if (typeof enums[i] === 'undefined' && typeof descriptions[i] === 'undefined' && typeof sortOrders[i] === 'undefined') { + continue + } + + items.push({ + enum: typeof enums[i] === 'undefined' ? '' : String(enums[i]), + description: typeof descriptions[i] === 'undefined' ? '' : String(descriptions[i]), + sortOrder: Number(sortOrders[i] || 0), + }) + } + + return items +} + +function exportDictionaryRecord(record) { + return { + pb_id: record.id, + system_dict_id: record.getString('system_dict_id'), + dict_name: record.getString('dict_name'), + dict_word_is_enabled: !!record.get('dict_word_is_enabled'), + dict_word_parent_id: record.getString('dict_word_parent_id'), + dict_word_remark: record.getString('dict_word_remark'), + items: normalizeItemsFromRecord(record), + created: String(record.created || ''), + updated: String(record.updated || ''), + } +} + +function findDictionaryByName(dictName) { + const records = $app.findRecordsByFilter('tbl_system_dict', 'dict_name = {:dictName}', '', 1, 0, { + dictName: dictName, + }) + + return records.length ? records[0] : null +} + +function ensureDictionaryNameUnique(dictName, excludeId) { + const existing = findDictionaryByName(dictName) + if (existing && existing.id !== excludeId) { + throw createAppError(400, 'dict_name 已存在') + } +} + +function fillDictionaryItems(record, items) { + const enums = [] + const descriptions = [] + const sortOrders = [] + + for (let i = 0; i < items.length; i += 1) { + enums.push(items[i].enum) + descriptions.push(items[i].description) + sortOrders.push(items[i].sortOrder) + } + + record.set('dict_word_enum', JSON.stringify(enums)) + record.set('dict_word_description', JSON.stringify(descriptions)) + record.set('dict_word_sort_order', JSON.stringify(sortOrders)) +} + +function listDictionaries(keyword) { + const allRecords = $app.findRecordsByFilter('tbl_system_dict', '', 'dict_name', 500, 0) + const normalizedKeyword = String(keyword || '').toLowerCase() + const result = [] + + for (let i = 0; i < allRecords.length; i += 1) { + const item = exportDictionaryRecord(allRecords[i]) + if (!normalizedKeyword || item.dict_name.toLowerCase().indexOf(normalizedKeyword) !== -1) { + result.push(item) + } + } + + return result +} + +function getDictionaryByName(dictName) { + const record = findDictionaryByName(dictName) + if (!record) { + throw createAppError(404, '未找到对应字典') + } + + return exportDictionaryRecord(record) +} + +function createDictionary(payload) { + ensureDictionaryNameUnique(payload.dict_name) + + const collection = $app.findCollectionByNameOrId('tbl_system_dict') + const record = new Record(collection) + record.set('system_dict_id', buildSystemDictId()) + record.set('dict_name', payload.dict_name) + record.set('dict_word_is_enabled', payload.dict_word_is_enabled) + record.set('dict_word_parent_id', payload.dict_word_parent_id) + record.set('dict_word_remark', payload.dict_word_remark) + fillDictionaryItems(record, payload.items) + + try { + $app.save(record) + } catch (err) { + throw createAppError(400, '创建字典失败', { + originalMessage: (err && err.message) || '未知错误', + originalData: (err && err.data) || {}, + }) + } + + logger.info('字典创建成功', { + dict_name: payload.dict_name, + system_dict_id: record.getString('system_dict_id'), + }) + + return exportDictionaryRecord(record) +} + +function updateDictionary(payload) { + const record = findDictionaryByName(payload.original_dict_name) + if (!record) { + throw createAppError(404, '未找到待修改的字典') + } + + ensureDictionaryNameUnique(payload.dict_name, record.id) + + record.set('dict_name', payload.dict_name) + record.set('dict_word_is_enabled', payload.dict_word_is_enabled) + record.set('dict_word_parent_id', payload.dict_word_parent_id) + record.set('dict_word_remark', payload.dict_word_remark) + fillDictionaryItems(record, payload.items) + + try { + $app.save(record) + } catch (err) { + throw createAppError(400, '修改字典失败', { + originalMessage: (err && err.message) || '未知错误', + originalData: (err && err.data) || {}, + }) + } + + logger.info('字典修改成功', { + dict_name: payload.dict_name, + original_dict_name: payload.original_dict_name, + }) + + return exportDictionaryRecord(record) +} + +function deleteDictionary(dictName) { + const record = findDictionaryByName(dictName) + if (!record) { + throw createAppError(404, '未找到待删除的字典') + } + + try { + $app.delete(record) + } catch (err) { + throw createAppError(400, '删除字典失败', { + originalMessage: (err && err.message) || '未知错误', + originalData: (err && err.data) || {}, + }) + } + + logger.info('字典删除成功', { + dict_name: dictName, + system_dict_id: record.getString('system_dict_id'), + }) + + return { + dict_name: dictName, + } +} + +module.exports = { + listDictionaries, + getDictionaryByName, + createDictionary, + updateDictionary, + deleteDictionary, +} \ No newline at end of file diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js index 55118f2..bea8bc1 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js @@ -1,6 +1,7 @@ const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`) const wechatService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/wechatService.js`) +const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`) const GUEST_USER_TYPE = '游客' const REGISTERED_USER_TYPE = '注册用户' @@ -64,6 +65,48 @@ function findUserByPhone(usersPhone) { return records.length ? records[0] : null } +function findUserByEmail(email) { + const records = $app.findRecordsByFilter('tbl_auth_users', 'email = {:email}', '', 1, 0, { + email: email, + }) + return records.length ? records[0] : null +} + +function isEmail(value) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || '')) +} + +function parseHttpJsonResponse(response) { + if (!response) { + throw createAppError(500, 'PocketBase 认证响应为空') + } + + if (response.json && typeof response.json === 'object') { + return response.json + } + + if (typeof response.body === 'string' && response.body) { + return JSON.parse(response.body) + } + + if (response.body && typeof response.body === 'object') { + return response.body + } + + if (typeof response.data === 'string' && response.data) { + return JSON.parse(response.data) + } + + if (response.data && typeof response.data === 'object') { + return response.data + } + + throw createAppError(500, 'PocketBase 认证响应体为空', { + statusCode: response.statusCode || 0, + responseKeys: Object.keys(response || {}), + }) +} + function getCompanyByCompanyId(companyId) { if (!companyId) return null const records = $app.findRecordsByFilter('tbl_company', 'company_id = {:companyId}', '', 1, 0, { @@ -248,7 +291,7 @@ function registerPlatformUser(payload) { record.set('users_promo_code', payload.users_promo_code || '') record.set('usergroups_id', payload.usergroups_id || '') record.set('users_auth_type', 0) - record.set('email', platformOpenid + '@manage.local') + record.set('email', payload.email || (platformOpenid + '@manage.local')) record.setPassword(payload.password) record.set('passwordConfirm', payload.passwordConfirm) @@ -278,14 +321,18 @@ function registerPlatformUser(payload) { } function authenticatePlatformUser(payload) { - return withUserLock('platform-login:' + payload.users_phone, function () { - const userRecord = findUserByPhone(payload.users_phone) + return withUserLock('platform-login:' + payload.login_account, function () { + const loginAccount = String(payload.login_account || '') + const userRecord = isEmail(loginAccount) + ? findUserByEmail(loginAccount) + : findUserByPhone(loginAccount) + if (!userRecord) { throw createAppError(404, '平台用户不存在') } if (userRecord.getString('users_idtype') !== MANAGE_PLATFORM_ID_TYPE) { - throw createAppError(400, '当前手机号对应的不是平台用户') + throw createAppError(400, '当前登录账号对应的不是平台用户') } const identity = userRecord.getString('email') @@ -293,8 +340,32 @@ function authenticatePlatformUser(payload) { throw createAppError(500, '平台用户缺少原生登录标识') } - const authData = $apis.recordAuthWithPassword('tbl_auth_users', identity, payload.password) - const authRecord = authData.record || userRecord + const baseUrl = String(env.pocketbaseApiUrl || '').replace(/\/+$/, '') + if (!baseUrl) { + throw createAppError(500, '缺少 POCKETBASE_API_URL 配置') + } + + const authResponse = $http.send({ + url: baseUrl + '/api/collections/tbl_auth_users/auth-with-password', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identity: identity, + password: payload.password, + }), + }) + + if (!authResponse || authResponse.statusCode < 200 || authResponse.statusCode >= 300) { + throw createAppError(400, '平台登录失败', { + statusCode: authResponse ? authResponse.statusCode : 0, + body: authResponse ? String(authResponse.body || '') : '', + }) + } + + const authData = parseHttpJsonResponse(authResponse) + const authRecord = userRecord const user = enrichUser(authRecord) @@ -302,6 +373,7 @@ function authenticatePlatformUser(payload) { users_id: user.users_id, openid: user.openid, users_idtype: user.users_idtype, + login_account: loginAccount, }) return { @@ -310,6 +382,8 @@ function authenticatePlatformUser(payload) { user: user, authRecord: authRecord, authMethod: '', + token: issueAuthToken(authRecord), + pbRecord: authData.record || null, meta: buildAuthMeta({ status: 'login_success', is_info_complete: isInfoComplete(authRecord), @@ -409,9 +483,18 @@ function issueAuthToken(authRecord) { return token } +function ensureAuthToken(token) { + if (!token) { + throw createAppError(500, '签发令牌失败:生成 token 为空') + } + + return token +} + module.exports = { authenticateWechatUser, authenticatePlatformUser, + ensureAuthToken, updateWechatUserProfile, refreshAuthToken, issueAuthToken, diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js index 7eda0b3..2ac2286 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js @@ -6,6 +6,22 @@ function success(e, msg, data, code) { }) } +function successWithToken(e, msg, data, token, code) { + const statusCode = code || 200 + const payload = { + code: statusCode, + msg: msg || '操作成功', + data: data || {}, + } + + if (token) { + payload.token = token + } + + return e.json(statusCode, payload) +} + module.exports = { success, + successWithToken, } diff --git a/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js b/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js new file mode 100644 index 0000000..f7136bf --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js @@ -0,0 +1,468 @@ +routerAdd('GET', '/manage/dictionary-manage', function (e) { + const html = ` + +
+ + +| dict_name | +启用 | +备注 | +parent_id | +枚举项 | +创建时间 | +操作 | +
|---|---|---|---|---|---|---|
| 暂无数据,请先查询。 | ||||||
| 枚举值 | +描述 | +排序 | +操作 | +
|---|
该页面仅供已登录的 ManagePlatform 用户使用。你可以从这里跳转到各个管理子页面。
+维护 tbl_system_dict,支持模糊查询、行内编辑、枚举项弹窗编辑、新增和删除。
+ 进入字典管理 +登录入口页。若已登录,访问该页会自动跳回管理主页。
+ 打开登录页 +第二个示例子页面,便于验证主页跳转与 hooks 页面导航。
+ 进入页面二 +这里是 page-a 页面。后续你可以把它扩展成介绍页、帮助页、数据展示页或简单表单页。
- +请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。
+ +这里是 page-b 页面。当前用于验证 PocketBase hooks 页面路由是否已经正确注册,并可从 manage 首页完成跳转。
+ +