feat: 添加字典管理功能,包括字典的增删改查接口和管理页面
- 新增字典删除接口 `/api/dictionary/delete` - 新增字典详情接口 `/api/dictionary/detail` - 新增字典列表接口 `/api/dictionary/list` - 新增字典更新接口 `/api/dictionary/update` - 新增字典服务 `dictionaryService.js`,实现字典的创建、更新、删除和查询功能 - 新增字典管理页面 `dictionary-manage.js`,支持字典的增删改查操作 - 新增管理主页 `index.js`,提供字典管理入口 - 新增示例页面 `page-b.js`,用于验证页面跳转
This commit is contained in:
@@ -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` 不是运行必需项。
|
||||
|
||||
## 额外要求
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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) || {},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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) || {},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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) || {},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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) || {},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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) || {},
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -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', */
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
468
pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js
Normal file
468
pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js
Normal file
@@ -0,0 +1,468 @@
|
||||
routerAdd('GET', '/manage/dictionary-manage', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>字典管理</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
||||
.container { max-width: 1240px; margin: 0 auto; padding: 32px 20px 60px; }
|
||||
.topbar, .panel, .modal-card { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); }
|
||||
.topbar { border-radius: 24px; padding: 24px; margin-bottom: 24px; }
|
||||
.topbar h1 { margin: 0 0 8px; font-size: 30px; }
|
||||
.topbar p { margin: 0; color: #4b5563; line-height: 1.7; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-secondary { background: #e0e7ff; color: #1e3a8a; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.panel { border-radius: 24px; padding: 24px; }
|
||||
.toolbar { display: grid; grid-template-columns: 1.3fr 1fr 1fr auto auto; gap: 12px; margin-bottom: 18px; }
|
||||
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||
textarea { min-height: 88px; resize: vertical; }
|
||||
table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 16px; }
|
||||
thead { background: #eff6ff; }
|
||||
th, td { padding: 14px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||
th { font-size: 13px; color: #475569; }
|
||||
tr:hover td { background: #f8fafc; }
|
||||
.badge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.badge-on { background: #dcfce7; color: #166534; }
|
||||
.badge-off { background: #fee2e2; color: #991b1b; }
|
||||
.muted { color: #64748b; font-size: 13px; }
|
||||
.inline-input { min-width: 120px; }
|
||||
.link { color: #2563eb; text-decoration: none; font-weight: 600; }
|
||||
.status { margin: 14px 0 0; min-height: 24px; font-size: 14px; }
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.auth-box { display: grid; grid-template-columns: 1fr auto; gap: 12px; margin-top: 18px; }
|
||||
.modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(15, 23, 42, 0.4); padding: 20px; }
|
||||
.modal.show { display: flex; }
|
||||
.modal-card { width: min(920px, 100%); border-radius: 24px; padding: 24px; max-height: 90vh; overflow: auto; }
|
||||
.modal-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
||||
.modal-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
||||
.full { grid-column: 1 / -1; }
|
||||
.item-table { margin-top: 16px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
||||
.item-table table { border-radius: 0; }
|
||||
.modal-actions { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
|
||||
.empty { text-align: center; padding: 32px 16px; color: #64748b; }
|
||||
@media (max-width: 960px) {
|
||||
.toolbar, .auth-box, .modal-grid { grid-template-columns: 1fr; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
||||
td { display: flex; justify-content: space-between; gap: 12px; }
|
||||
td::before { content: attr(data-label); font-weight: 700; color: #475569; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<section class="topbar">
|
||||
<h1>字典管理</h1>
|
||||
<p>页面仅允许 ManagePlatform 用户操作。当前请求会自动使用登录页保存到 localStorage 的 token。</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<a class="btn btn-light" href="/pb/manage/login">登录页</a>
|
||||
<button class="btn btn-primary" id="createBtn" type="button">新增字典</button>
|
||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<input id="keywordInput" placeholder="按 dict_name 模糊搜索" />
|
||||
<input id="detailInput" placeholder="查询指定 dict_name" />
|
||||
<button class="btn btn-secondary" id="listBtn" type="button">查询全部</button>
|
||||
<button class="btn btn-light" id="detailBtn" type="button">查询指定</button>
|
||||
<button class="btn btn-light" id="resetBtn" type="button">重置</button>
|
||||
</div>
|
||||
<div style="overflow:auto;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>dict_name</th>
|
||||
<th>启用</th>
|
||||
<th>备注</th>
|
||||
<th>parent_id</th>
|
||||
<th>枚举项</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<tr><td colspan="7" class="empty">暂无数据,请先查询。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="editorModal">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h2 id="modalTitle" style="margin:0;">新增字典</h2>
|
||||
<div class="muted">三个聚合字段将以 JSON 字符串形式保存,并在读取时自动还原为 items。</div>
|
||||
</div>
|
||||
<button class="btn btn-light" id="closeModalBtn" type="button">关闭</button>
|
||||
</div>
|
||||
<div class="modal-grid">
|
||||
<div>
|
||||
<label>dict_name</label>
|
||||
<input id="dictNameInput" />
|
||||
</div>
|
||||
<div>
|
||||
<label>dict_word_parent_id</label>
|
||||
<input id="parentIdInput" />
|
||||
</div>
|
||||
<div>
|
||||
<label>dict_word_is_enabled</label>
|
||||
<select id="enabledInput">
|
||||
<option value="true">启用</option>
|
||||
<option value="false">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="full">
|
||||
<label>dict_word_remark</label>
|
||||
<textarea id="remarkInput"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>枚举值</th>
|
||||
<th>描述</th>
|
||||
<th>排序</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="itemsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<div>
|
||||
<button class="btn btn-secondary" id="addItemBtn" type="button">新增枚举项</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-light" id="cancelBtn" type="button">取消</button>
|
||||
<button class="btn btn-primary" id="saveBtn" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/pb/api'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const state = {
|
||||
list: [],
|
||||
mode: 'create',
|
||||
editingOriginalName: '',
|
||||
items: [],
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status')
|
||||
const keywordInput = document.getElementById('keywordInput')
|
||||
const detailInput = document.getElementById('detailInput')
|
||||
const tableBody = document.getElementById('tableBody')
|
||||
const editorModal = document.getElementById('editorModal')
|
||||
const modalTitle = document.getElementById('modalTitle')
|
||||
const dictNameInput = document.getElementById('dictNameInput')
|
||||
const parentIdInput = document.getElementById('parentIdInput')
|
||||
const enabledInput = document.getElementById('enabledInput')
|
||||
const remarkInput = document.getElementById('remarkInput')
|
||||
const itemsBody = document.getElementById('itemsBody')
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(tokenKey) || ''
|
||||
}
|
||||
|
||||
async function request(url, payload) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: JSON.stringify(payload || {}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (!res.ok || data.code >= 400) {
|
||||
if (res.status === 401 || res.status === 403 || data.code === 401 || data.code === 403) {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
throw new Error(data.msg || '请求失败')
|
||||
}
|
||||
|
||||
return data.data
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function renderItemsPreview(items) {
|
||||
if (!items || !items.length) return '<span class="muted">无</span>'
|
||||
return items.map(function (item) {
|
||||
return '<div><strong>' + escapeHtml(item.enum) + '</strong> → ' + escapeHtml(item.description) + '(排序 ' + escapeHtml(item.sortOrder) + ')</div>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderTable(list) {
|
||||
if (!list.length) {
|
||||
tableBody.innerHTML = '<tr><td colspan="7" class="empty">暂无匹配数据。</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
tableBody.innerHTML = list.map(function (item) {
|
||||
const enabledClass = item.dict_word_is_enabled ? 'badge badge-on' : 'badge badge-off'
|
||||
const enabledText = item.dict_word_is_enabled ? '启用' : '禁用'
|
||||
return '<tr>'
|
||||
+ '<td data-label="dict_name"><input class="inline-input" data-field="dict_name" data-name="' + escapeHtml(item.dict_name) + '" value="' + escapeHtml(item.dict_name) + '" /></td>'
|
||||
+ '<td data-label="启用"><select class="inline-input" data-field="dict_word_is_enabled" data-name="' + escapeHtml(item.dict_name) + '"><option value="true"' + (item.dict_word_is_enabled ? ' selected' : '') + '>启用</option><option value="false"' + (!item.dict_word_is_enabled ? ' selected' : '') + '>禁用</option></select><div class="' + enabledClass + '" style="margin-top:8px;">' + enabledText + '</div></td>'
|
||||
+ '<td data-label="备注"><textarea class="inline-input" data-field="dict_word_remark" data-name="' + escapeHtml(item.dict_name) + '">' + escapeHtml(item.dict_word_remark) + '</textarea></td>'
|
||||
+ '<td data-label="parent_id"><input class="inline-input" data-field="dict_word_parent_id" data-name="' + escapeHtml(item.dict_name) + '" value="' + escapeHtml(item.dict_word_parent_id) + '" /></td>'
|
||||
+ '<td data-label="枚举项">' + renderItemsPreview(item.items) + '</td>'
|
||||
+ '<td data-label="创建时间"><span class="muted">' + escapeHtml(item.created) + '</span></td>'
|
||||
+ '<td data-label="操作">'
|
||||
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;">'
|
||||
+ '<button class="btn btn-light" type="button" onclick="window.__editItems(\\'' + encodeURIComponent(item.dict_name) + '\\')">编辑枚举</button>'
|
||||
+ '<button class="btn btn-secondary" type="button" onclick="window.__saveInline(\\'' + encodeURIComponent(item.dict_name) + '\\')">保存行</button>'
|
||||
+ '<button class="btn btn-danger" type="button" onclick="window.__deleteRow(\\'' + encodeURIComponent(item.dict_name) + '\\')">删除</button>'
|
||||
+ '</div>'
|
||||
+ '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function openModal(mode, record) {
|
||||
state.mode = mode
|
||||
state.editingOriginalName = record ? record.dict_name : ''
|
||||
modalTitle.textContent = mode === 'create' ? '新增字典' : '编辑枚举项'
|
||||
dictNameInput.value = record ? record.dict_name : ''
|
||||
parentIdInput.value = record ? (record.dict_word_parent_id || '') : ''
|
||||
enabledInput.value = record && !record.dict_word_is_enabled ? 'false' : 'true'
|
||||
remarkInput.value = record ? (record.dict_word_remark || '') : ''
|
||||
state.items = record && Array.isArray(record.items) && record.items.length
|
||||
? record.items.map(function (item) { return { enum: item.enum, description: item.description, sortOrder: item.sortOrder } })
|
||||
: [{ enum: '', description: '', sortOrder: 1 }]
|
||||
renderItemsEditor()
|
||||
editorModal.classList.add('show')
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
editorModal.classList.remove('show')
|
||||
}
|
||||
|
||||
function renderItemsEditor() {
|
||||
itemsBody.innerHTML = state.items.map(function (item, index) {
|
||||
return '<tr>'
|
||||
+ '<td><input data-item-field="enum" data-index="' + index + '" value="' + escapeHtml(item.enum) + '" /></td>'
|
||||
+ '<td><input data-item-field="description" data-index="' + index + '" value="' + escapeHtml(item.description) + '" /></td>'
|
||||
+ '<td><input type="number" data-item-field="sortOrder" data-index="' + index + '" value="' + escapeHtml(item.sortOrder) + '" /></td>'
|
||||
+ '<td><button class="btn btn-danger" type="button" onclick="window.__removeItem(' + index + ')">删除</button></td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function collectItemsFromEditor() {
|
||||
const rows = Array.from(itemsBody.querySelectorAll('tr'))
|
||||
return rows.map(function (row, index) {
|
||||
const enumInput = row.querySelector('[data-item-field="enum"]')
|
||||
const descriptionInput = row.querySelector('[data-item-field="description"]')
|
||||
const sortOrderInput = row.querySelector('[data-item-field="sortOrder"]')
|
||||
return {
|
||||
enum: enumInput.value.trim(),
|
||||
description: descriptionInput.value.trim(),
|
||||
sortOrder: Number(sortOrderInput.value || index + 1),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
setStatus('正在查询字典列表...', '')
|
||||
try {
|
||||
const data = await request(API_BASE + '/dictionary/list', { keyword: keywordInput.value.trim() })
|
||||
state.list = data.items || []
|
||||
renderTable(state.list)
|
||||
setStatus('查询成功,共 ' + state.list.length + ' 条。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '查询失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
const dictName = detailInput.value.trim()
|
||||
if (!dictName) {
|
||||
setStatus('请输入 dict_name', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('正在查询字典详情...', '')
|
||||
try {
|
||||
const data = await request(API_BASE + '/dictionary/detail', { dict_name: dictName })
|
||||
state.list = [data]
|
||||
renderTable(state.list)
|
||||
setStatus('查询详情成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '查询失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveModalRecord() {
|
||||
const items = collectItemsFromEditor()
|
||||
const payload = {
|
||||
dict_name: dictNameInput.value.trim(),
|
||||
original_dict_name: state.editingOriginalName,
|
||||
dict_word_parent_id: parentIdInput.value.trim(),
|
||||
dict_word_is_enabled: enabledInput.value === 'true',
|
||||
dict_word_remark: remarkInput.value.trim(),
|
||||
items: items,
|
||||
}
|
||||
|
||||
setStatus('正在保存字典...', '')
|
||||
try {
|
||||
await request(state.mode === 'create' ? API_BASE + '/dictionary/create' : API_BASE + '/dictionary/update', payload)
|
||||
closeModal()
|
||||
await loadList()
|
||||
setStatus(state.mode === 'create' ? '新增成功。' : '修改成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveInline(dictName) {
|
||||
const targetName = decodeURIComponent(dictName)
|
||||
const row = Array.from(tableBody.querySelectorAll('[data-name]')).filter(function (el) {
|
||||
return el.getAttribute('data-name') === targetName
|
||||
})
|
||||
const record = state.list.find(function (item) { return item.dict_name === targetName })
|
||||
|
||||
if (!record) {
|
||||
setStatus('未找到对应记录', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
original_dict_name: targetName,
|
||||
dict_name: (row.find(function (el) { return el.getAttribute('data-field') === 'dict_name' }) || {}).value || targetName,
|
||||
dict_word_is_enabled: ((row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_is_enabled' }) || {}).value || 'true') === 'true',
|
||||
dict_word_remark: (row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_remark' }) || {}).value || '',
|
||||
dict_word_parent_id: (row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_parent_id' }) || {}).value || '',
|
||||
items: record.items || [],
|
||||
}
|
||||
|
||||
setStatus('正在保存行数据...', '')
|
||||
try {
|
||||
await request(API_BASE + '/dictionary/update', payload)
|
||||
await loadList()
|
||||
setStatus('行内保存成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '保存失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRow(dictName) {
|
||||
const targetName = decodeURIComponent(dictName)
|
||||
if (!window.confirm('确认删除字典「' + targetName + '」吗?此操作不可恢复。')) {
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('正在删除字典...', '')
|
||||
try {
|
||||
await request(API_BASE + '/dictionary/delete', { dict_name: targetName })
|
||||
await loadList()
|
||||
setStatus('删除成功。', 'success')
|
||||
} catch (err) {
|
||||
setStatus(err.message || '删除失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
window.__editItems = function (dictName) {
|
||||
const targetName = decodeURIComponent(dictName)
|
||||
const record = state.list.find(function (item) { return item.dict_name === targetName })
|
||||
if (!record) {
|
||||
setStatus('未找到对应字典', 'error')
|
||||
return
|
||||
}
|
||||
openModal('edit', record)
|
||||
}
|
||||
|
||||
window.__saveInline = saveInline
|
||||
window.__deleteRow = deleteRow
|
||||
window.__removeItem = function (index) {
|
||||
state.items.splice(index, 1)
|
||||
if (!state.items.length) {
|
||||
state.items.push({ enum: '', description: '', sortOrder: 1 })
|
||||
}
|
||||
renderItemsEditor()
|
||||
}
|
||||
|
||||
document.getElementById('listBtn').addEventListener('click', loadList)
|
||||
document.getElementById('detailBtn').addEventListener('click', loadDetail)
|
||||
document.getElementById('resetBtn').addEventListener('click', function () {
|
||||
keywordInput.value = ''
|
||||
detailInput.value = ''
|
||||
state.list = []
|
||||
renderTable([])
|
||||
setStatus('已重置查询条件。', 'success')
|
||||
})
|
||||
document.getElementById('createBtn').addEventListener('click', function () { openModal('create') })
|
||||
document.getElementById('closeModalBtn').addEventListener('click', closeModal)
|
||||
document.getElementById('cancelBtn').addEventListener('click', closeModal)
|
||||
document.getElementById('addItemBtn').addEventListener('click', function () {
|
||||
state.items.push({ enum: '', description: '', sortOrder: state.items.length + 1 })
|
||||
renderItemsEditor()
|
||||
})
|
||||
document.getElementById('saveBtn').addEventListener('click', saveModalRecord)
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
71
pocket-base/bai_web_pb_hooks/pages/index.js
Normal file
71
pocket-base/bai_web_pb_hooks/pages/index.js
Normal file
@@ -0,0 +1,71 @@
|
||||
routerAdd('GET', '/manage', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理主页</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
||||
.wrap { max-width: 960px; margin: 0 auto; padding: 48px 20px; }
|
||||
.hero { background: #ffffff; border-radius: 24px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 36px; border: 1px solid #e5e7eb; }
|
||||
h1 { margin: 0 0 12px; font-size: 32px; }
|
||||
p { color: #4b5563; line-height: 1.8; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-top: 28px; }
|
||||
.card { background: #f8fafc; border: 1px solid #dbe3f0; border-radius: 18px; padding: 22px; }
|
||||
.card h2 { margin: 0 0 10px; font-size: 20px; }
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px; border-radius: 12px; text-decoration: none; background: #2563eb; color: #fff; font-weight: 600; margin-top: 12px; }
|
||||
.notice { margin-top: 16px; padding: 12px 14px; border-radius: 12px; background: #eff6ff; color: #1d4ed8; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="hero">
|
||||
<h1>管理主页</h1>
|
||||
<p>该页面仅供已登录的 ManagePlatform 用户使用。你可以从这里跳转到各个管理子页面。</p>
|
||||
<div class="notice">如未登录或 token 无效,系统会自动跳转到登录页。</div>
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h2>字典管理</h2>
|
||||
<p>维护 tbl_system_dict,支持模糊查询、行内编辑、枚举项弹窗编辑、新增和删除。</p>
|
||||
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>登录页</h2>
|
||||
<p>登录入口页。若已登录,访问该页会自动跳回管理主页。</p>
|
||||
<a class="btn" href="/pb/manage/login">打开登录页</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>页面二</h2>
|
||||
<p>第二个示例子页面,便于验证主页跳转与 hooks 页面导航。</p>
|
||||
<a class="btn" href="/pb/manage/page-b">进入页面二</a>
|
||||
</article>
|
||||
</div>
|
||||
<div style="margin-top:16px;">
|
||||
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
@@ -1,52 +1,158 @@
|
||||
routerAdd('GET', '/web/page-a', function (e) {
|
||||
function renderLoginPage(e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>页面一</title>
|
||||
<title>登录</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (token && isLoggedIn) {
|
||||
window.location.replace('/pb/manage')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #f8fafc;
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
||||
color: #1f2937;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 760px;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
padding: 72px 20px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 18px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
p { line-height: 1.8; color: #4b5563; }
|
||||
.actions { margin-top: 20px; }
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #2563eb;
|
||||
margin-right: 16px;
|
||||
p { line-height: 1.8; color: #4b5563; margin-bottom: 20px; }
|
||||
.field { margin-bottom: 14px; }
|
||||
.field label { display: block; margin-bottom: 8px; color: #334155; font-weight: 600; }
|
||||
.field input {
|
||||
width: 100%;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 11px 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.status { margin-top: 12px; min-height: 22px; font-size: 14px; }
|
||||
.status.error { color: #b91c1c; }
|
||||
.status.success { color: #15803d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="card">
|
||||
<h1>页面一</h1>
|
||||
<p>这里是 page-a 页面。后续你可以把它扩展成介绍页、帮助页、数据展示页或简单表单页。</p>
|
||||
<div class="actions">
|
||||
<a href="/web">返回首页</a>
|
||||
<a href="/web/page-b">跳转到页面二</a>
|
||||
</div>
|
||||
<h1>后台登录</h1>
|
||||
<p>请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。</p>
|
||||
<form id="loginForm">
|
||||
<div class="field">
|
||||
<label for="account">登录账号</label>
|
||||
<input id="account" name="account" placeholder="邮箱或手机号" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">密码</label>
|
||||
<input id="password" name="password" type="password" placeholder="请输入密码" required />
|
||||
</div>
|
||||
<button class="btn" id="submitBtn" type="submit">登录</button>
|
||||
</form>
|
||||
<div id="status" class="status"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var API_BASE = '/pb/api'
|
||||
var form = document.getElementById('loginForm')
|
||||
var accountInput = document.getElementById('account')
|
||||
var passwordInput = document.getElementById('password')
|
||||
var statusEl = document.getElementById('status')
|
||||
var submitBtn = document.getElementById('submitBtn')
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault()
|
||||
|
||||
var loginAccount = accountInput.value.trim()
|
||||
var password = passwordInput.value
|
||||
if (!loginAccount || !password) {
|
||||
setStatus('请填写账号和密码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
submitBtn.disabled = true
|
||||
submitBtn.textContent = '登录中...'
|
||||
setStatus('正在登录,请稍候...', '')
|
||||
|
||||
try {
|
||||
var response = await fetch(API_BASE + '/platform/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
login_account: loginAccount,
|
||||
password: password,
|
||||
}),
|
||||
})
|
||||
|
||||
var result = await response.json()
|
||||
if (!response.ok || !result || result.code >= 400) {
|
||||
throw new Error((result && result.msg) || '登录失败')
|
||||
}
|
||||
|
||||
var token = result.token || (result.data && result.data.token) || ''
|
||||
if (!token) {
|
||||
throw new Error('登录成功但未获取到 token')
|
||||
}
|
||||
|
||||
localStorage.setItem('pb_manage_token', token)
|
||||
localStorage.setItem('pb_manage_logged_in', '1')
|
||||
localStorage.setItem('pb_manage_login_account', loginAccount)
|
||||
localStorage.setItem('pb_manage_login_time', new Date().toISOString())
|
||||
|
||||
setStatus('登录成功,正在跳转...', 'success')
|
||||
window.location.replace('/pb/manage')
|
||||
} catch (error) {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
setStatus(error.message || '登录失败', 'error')
|
||||
} finally {
|
||||
submitBtn.disabled = false
|
||||
submitBtn.textContent = '登录'
|
||||
}
|
||||
})
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
}
|
||||
|
||||
routerAdd('GET', '/manage/login', renderLoginPage)
|
||||
routerAdd('GET', '/manage/page-a', renderLoginPage)
|
||||
|
||||
62
pocket-base/bai_web_pb_hooks/pages/page-b.js
Normal file
62
pocket-base/bai_web_pb_hooks/pages/page-b.js
Normal file
@@ -0,0 +1,62 @@
|
||||
routerAdd('GET', '/manage/page-b', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>页面二</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
||||
color: #1f2937;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 18px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
h1 { margin-top: 0; }
|
||||
p { line-height: 1.8; color: #4b5563; }
|
||||
.actions { margin-top: 20px; display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<section class="card">
|
||||
<h1>页面二</h1>
|
||||
<p>这里是 page-b 页面。当前用于验证 PocketBase hooks 页面路由是否已经正确注册,并可从 manage 首页完成跳转。</p>
|
||||
<div class="actions">
|
||||
<a href="/pb/manage">返回首页</a>
|
||||
<a href="/pb/manage/login">打开登录页</a>
|
||||
<a href="/pb/manage/dictionary-manage">进入字典管理</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 范围
|
||||
|
||||
本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、平台注册/登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略与运行规范。
|
||||
本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、平台注册/登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略、字典管理、页面辅助操作、健康检查版本探针、OpenAPI 鉴权与统一响应规范。
|
||||
|
||||
---
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
### 1. 认证体系
|
||||
|
||||
- 保持 PocketBase 原生 auth token 作为唯一正式认证令牌。
|
||||
- 登录与刷新响应统一通过 `$apis.recordAuthResponse(...)` 返回 PocketBase 原生认证结果。
|
||||
- 登录与刷新响应统一为项目标准结构:`code`、`msg`、`data`,认证成功时额外返回顶层 `token`。
|
||||
- `authMethod` 统一使用空字符串 `''`,避免触发不必要的 MFA / login alerts 校验。
|
||||
|
||||
### 2. Header 规则
|
||||
@@ -139,6 +139,11 @@
|
||||
- `POST /api/platform/login`
|
||||
- `POST /api/wechat/login`
|
||||
- `POST /api/wechat/profile`
|
||||
- `POST /api/dictionary/list`
|
||||
- `POST /api/dictionary/detail`
|
||||
- `POST /api/dictionary/create`
|
||||
- `POST /api/dictionary/update`
|
||||
- `POST /api/dictionary/delete`
|
||||
|
||||
其中平台用户链路补充为:
|
||||
|
||||
@@ -147,14 +152,15 @@
|
||||
- body 必填:`users_name`、`users_phone`、`password`、`passwordConfirm`、`users_picture`
|
||||
- 自动生成 GUID 并写入统一身份字段 `openid`
|
||||
- 写入 `users_idtype = ManagePlatform`
|
||||
- 成功时返回 PocketBase 原生 token + auth record + meta
|
||||
- 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token`
|
||||
|
||||
### `POST /api/platform/login`
|
||||
|
||||
- body 必填:`users_phone`、`password`
|
||||
- body 必填:`login_account`、`password`
|
||||
- 仅允许 `users_idtype = ManagePlatform`
|
||||
- 前端使用手机号+密码提交
|
||||
- 服务端内部仍通过 PocketBase 原生 password auth 返回原生 token
|
||||
- 前端使用邮箱或手机号 + 密码提交
|
||||
- 服务端先通过 PocketBase `auth-with-password` 校验身份,再由当前 hooks 进程签发正式 token
|
||||
- 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token`
|
||||
|
||||
其中:
|
||||
|
||||
@@ -164,7 +170,7 @@
|
||||
- 自动以微信 code 换取微信侧 `openid` 并写入统一身份字段
|
||||
- 若不存在 auth 用户则尝试创建 `tbl_auth_users` 记录
|
||||
- 写入 `users_idtype = WeChat`
|
||||
- 成功时返回 PocketBase 原生 token + auth record + meta
|
||||
- 成功时返回统一结构:`code`、`msg`、`data`,并在顶层额外返回 `token`
|
||||
|
||||
### `POST /api/wechat/profile`
|
||||
|
||||
@@ -179,25 +185,137 @@
|
||||
- 若 token 仍有效:基于当前 auth record 续签
|
||||
- 若 token 已过期:回退到微信 code 重签流程
|
||||
- 若 token 已过期且未提供 `users_wx_code`,返回:`token已过期,请上传users_wx_code`
|
||||
- 返回精简结构,仅返回新 token(不返回完整登录用户信息)
|
||||
- 返回统一结构:`code`、`msg`、`data`,并在顶层额外返回新 `token`
|
||||
- 属于系统级通用认证能力,不限定为微信专属接口
|
||||
|
||||
### 字典管理接口
|
||||
|
||||
新增 `dictionary` 分类接口,统一要求平台管理用户访问:
|
||||
|
||||
- `POST /api/dictionary/list`
|
||||
- 支持按 `dict_name` 模糊搜索
|
||||
- 返回字典全量信息,并将 `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 组装为 `items`
|
||||
- `POST /api/dictionary/detail`
|
||||
- 按 `dict_name` 查询单条字典
|
||||
- `POST /api/dictionary/create`
|
||||
- 新增字典,`system_dict_id` 自动生成,`dict_name` 唯一
|
||||
- `POST /api/dictionary/update`
|
||||
- 按 `original_dict_name` / `dict_name` 更新字典
|
||||
- `POST /api/dictionary/delete`
|
||||
- 按 `dict_name` 真删除字典
|
||||
|
||||
说明:
|
||||
|
||||
- `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。
|
||||
- 查询时统一聚合为:`items: [{ enum, description, sortOrder }]`
|
||||
|
||||
---
|
||||
|
||||
## 七、当前已知边界
|
||||
## 七、页面与运维辅助能力新增
|
||||
|
||||
### 1. PocketBase 页面
|
||||
|
||||
新增页面:
|
||||
|
||||
- `/web`
|
||||
- `/web/dictionary-manage`
|
||||
|
||||
页面能力:
|
||||
|
||||
- 首页支持跳转到子页面
|
||||
- 字典管理页支持:
|
||||
- Bearer Token 粘贴与本地保存
|
||||
- `dict_name` 模糊搜索
|
||||
- 指定字典查询
|
||||
- 行内编辑基础字段
|
||||
- 弹窗编辑枚举项
|
||||
- 新增 / 删除字典
|
||||
- 返回主页
|
||||
|
||||
### 2. 健康检查版本探针
|
||||
|
||||
`POST /api/system/health` 新增:
|
||||
|
||||
- `data.version`
|
||||
|
||||
用途:
|
||||
|
||||
- 通过修改 `APP_VERSION` 判断 hooks 是否已成功部署并生效
|
||||
|
||||
配置来源:
|
||||
|
||||
- 进程环境变量 `APP_VERSION`
|
||||
- 或 `runtime.js`
|
||||
|
||||
---
|
||||
|
||||
## 八、OpenAPI 与 Apifox 调试策略调整
|
||||
|
||||
### 1. 统一返回结构
|
||||
|
||||
所有对外接口统一返回:
|
||||
|
||||
- `code`
|
||||
- `msg`
|
||||
- `data`
|
||||
|
||||
认证成功类接口额外返回:
|
||||
|
||||
- `token`
|
||||
|
||||
不再返回以下顶层字段:
|
||||
|
||||
- `record`
|
||||
- `meta`
|
||||
|
||||
### 2. 鉴权文档策略
|
||||
|
||||
OpenAPI 文档中已取消 `bearerAuth` 鉴权组件与接口级重复 `Authorization` 参数。
|
||||
|
||||
统一约定:
|
||||
|
||||
- 在 Apifox 环境中配置全局 Header:`Authorization: Bearer {{token}}`
|
||||
- 不再依赖文档中的 Bearer 组件自动注入
|
||||
|
||||
此举目的是:
|
||||
|
||||
- 避免接口页重复出现局部 `Authorization`
|
||||
- 统一依赖环境变量完成鉴权注入
|
||||
|
||||
---
|
||||
|
||||
## 九、当前已知边界
|
||||
|
||||
1. `tbl_auth_users` 为 PocketBase `auth` 集合,因此仍受 PocketBase auth 内置规则影响。
|
||||
2. 自定义字段“除 openid 外均可空”已在脚本层按目标放宽,但 auth 集合结构更新仍可能触发 PocketBase 服务端限制。
|
||||
3. 若线上仍返回 PocketBase 默认 400,需要确保最新 hooks 已部署并重启生效。
|
||||
4. 平台登录通过回源 PocketBase REST 完成密码校验,因此 `POCKETBASE_API_URL` 必须配置为 PocketBase 进程/容器内部可达地址,不应使用外部 HTTPS 域名。
|
||||
5. Apifox 环境中需自行维护全局 Header:`Authorization: Bearer {{token}}`,否则鉴权接口不会自动携带 token。
|
||||
|
||||
---
|
||||
|
||||
## 八、归档建议
|
||||
## 十、归档建议
|
||||
|
||||
部署时至少同步以下文件:
|
||||
|
||||
- `pocket-base/bai-api-main.pb.js`
|
||||
- `pocket-base/bai-web-main.pb.js`
|
||||
- `pocket-base/bai_api_pb_hooks/`
|
||||
- `script/pocketbase.newpb.js`
|
||||
- `pocket-base/bai_web_pb_hooks/`
|
||||
- `pocket-base/spec/openapi.yaml`
|
||||
- `script/pocketbase.js`
|
||||
|
||||
并在 PocketBase 环境中执行 schema 同步后重启服务,再进行接口验证。
|
||||
|
||||
建议归档后的发布核验顺序:
|
||||
|
||||
1. `POST /api/system/health`:确认 `data.version`
|
||||
2. `POST /api/platform/login`:确认返回统一结构与顶层 `token`
|
||||
3. `POST /api/dictionary/list`:确认鉴权与字典接口可用
|
||||
|
||||
---
|
||||
|
||||
## 十一、归档状态
|
||||
|
||||
- 已将本轮字典管理、PocketBase 页面、统一响应、平台登录兼容修复、健康检查版本探针、OpenAPI/Apifox 鉴权策略调整并入本变更记录。
|
||||
- 本记录可作为当前 PocketBase hooks 阶段性归档基线继续维护。
|
||||
|
||||
@@ -6,6 +6,7 @@ info:
|
||||
当前 `tbl_auth_users.openid` 已被定义为全平台统一身份锚点:
|
||||
- 微信用户:`openid = 微信 openid`
|
||||
- 平台用户:`openid = 服务端生成的 GUID`
|
||||
请在 Apifox 环境中统一设置全局 Header:`Authorization: Bearer {{token}}`。
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: https://bai-api.blv-oa.com/pb
|
||||
@@ -19,12 +20,9 @@ tags:
|
||||
description: 面向微信用户的认证接口;认证成功后仍统一使用全平台 `openid` 与 PocketBase 原生 token。
|
||||
- name: 平台认证
|
||||
description: 面向平台用户的认证接口;平台用户会生成 GUID 并写入统一 `openid` 字段。
|
||||
- name: 字典管理
|
||||
description: 面向 ManagePlatform 用户的系统字典维护接口。
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: PocketBaseAuthToken
|
||||
schemas:
|
||||
ApiResponse:
|
||||
type: object
|
||||
@@ -45,6 +43,10 @@ components:
|
||||
status:
|
||||
type: string
|
||||
example: healthy
|
||||
version:
|
||||
type: string
|
||||
description: 当前已部署 hooks 版本号,用于确认发布是否生效
|
||||
example: 2026.03.26-health-probe.1
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -94,7 +96,7 @@ components:
|
||||
users_id_number:
|
||||
type: string
|
||||
users_status:
|
||||
type: number
|
||||
type: string
|
||||
users_rank_level:
|
||||
type: number
|
||||
users_auth_type:
|
||||
@@ -132,34 +134,55 @@ components:
|
||||
PocketBaseAuthResponse:
|
||||
type: object
|
||||
description: |
|
||||
PocketBase 原生认证响应。
|
||||
客户端可直接使用返回的 `token` 与 PocketBase SDK 或当前 hooks 接口交互。
|
||||
项目统一认证响应。
|
||||
所有对外接口统一返回 `code`、`msg`、`data`,认证成功时额外返回顶层 `token`。
|
||||
properties:
|
||||
token:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
msg:
|
||||
type: string
|
||||
description: PocketBase 原生 auth token
|
||||
record:
|
||||
type: object
|
||||
description: PocketBase auth record 原始对象
|
||||
meta:
|
||||
example: 登录成功
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
msg:
|
||||
status:
|
||||
type: string
|
||||
example: 登录成功
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [register_success, login_success]
|
||||
is_info_complete:
|
||||
type: boolean
|
||||
user:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
enum: [register_success, login_success]
|
||||
is_info_complete:
|
||||
type: boolean
|
||||
user:
|
||||
$ref: '#/components/schemas/UserInfo'
|
||||
token:
|
||||
type: string
|
||||
description: PocketBase 原生 auth token;仅认证类接口在成功时额外返回
|
||||
example:
|
||||
code: 200
|
||||
msg: 登录成功
|
||||
data:
|
||||
status: login_success
|
||||
is_info_complete: true
|
||||
user:
|
||||
pb_id: vtukf6agem2xbcv
|
||||
users_id: U202603260001
|
||||
users_idtype: ManagePlatform
|
||||
users_name: momo
|
||||
users_phone: '13509214696'
|
||||
users_phone_masked: '135****4696'
|
||||
users_status: ''
|
||||
users_rank_level: 0
|
||||
users_auth_type: 0
|
||||
users_type: 注册用户
|
||||
users_picture: ''
|
||||
openid: app_momo
|
||||
company_id: ''
|
||||
users_parent_id: ''
|
||||
users_promo_code: ''
|
||||
usergroups_id: ''
|
||||
company: null
|
||||
created: ''
|
||||
updated: ''
|
||||
token: eyJhbGciOi...
|
||||
WechatLoginRequest:
|
||||
type: object
|
||||
required: [users_wx_code]
|
||||
@@ -227,12 +250,13 @@ components:
|
||||
type: string
|
||||
PlatformLoginRequest:
|
||||
type: object
|
||||
required: [users_phone, password]
|
||||
description: 平台用户登录请求体;前端使用手机号+密码提交,服务端内部转换为 PocketBase 原生 password auth。
|
||||
required: [login_account, password]
|
||||
description: 平台用户登录请求体;前端使用邮箱或手机号 + 密码提交,服务端内部转换为 PocketBase 原生 password auth。
|
||||
properties:
|
||||
users_phone:
|
||||
login_account:
|
||||
type: string
|
||||
example: 13800138000
|
||||
description: 支持邮箱或手机号
|
||||
example: admin@example.com
|
||||
password:
|
||||
type: string
|
||||
example: 12345678
|
||||
@@ -251,10 +275,89 @@ components:
|
||||
example: 0a1b2c3d4e5f6g
|
||||
RefreshTokenData:
|
||||
type: object
|
||||
properties: {}
|
||||
DictionaryItem:
|
||||
type: object
|
||||
required: [enum, description, sortOrder]
|
||||
properties:
|
||||
token:
|
||||
enum:
|
||||
type: string
|
||||
description: 新签发的 PocketBase 原生 auth token
|
||||
example: enabled
|
||||
description:
|
||||
type: string
|
||||
example: 启用
|
||||
sortOrder:
|
||||
type: integer
|
||||
example: 1
|
||||
DictionaryRecord:
|
||||
type: object
|
||||
properties:
|
||||
pb_id:
|
||||
type: string
|
||||
system_dict_id:
|
||||
type: string
|
||||
dict_name:
|
||||
type: string
|
||||
dict_word_is_enabled:
|
||||
type: boolean
|
||||
dict_word_parent_id:
|
||||
type: string
|
||||
dict_word_remark:
|
||||
type: string
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DictionaryItem'
|
||||
created:
|
||||
type: string
|
||||
updated:
|
||||
type: string
|
||||
DictionaryListRequest:
|
||||
type: object
|
||||
properties:
|
||||
keyword:
|
||||
type: string
|
||||
description: 对 `dict_name` 的模糊搜索关键字
|
||||
example: 状态
|
||||
DictionaryDetailRequest:
|
||||
type: object
|
||||
required: [dict_name]
|
||||
properties:
|
||||
dict_name:
|
||||
type: string
|
||||
example: 用户状态
|
||||
DictionaryMutationRequest:
|
||||
type: object
|
||||
required: [dict_name, items]
|
||||
properties:
|
||||
original_dict_name:
|
||||
type: string
|
||||
description: 更新时用于定位原始记录;新增时可不传
|
||||
example: 用户状态
|
||||
dict_name:
|
||||
type: string
|
||||
example: 用户状态
|
||||
dict_word_is_enabled:
|
||||
type: boolean
|
||||
example: true
|
||||
dict_word_parent_id:
|
||||
type: string
|
||||
example: ''
|
||||
dict_word_remark:
|
||||
type: string
|
||||
example: 系统状态字典
|
||||
items:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: '#/components/schemas/DictionaryItem'
|
||||
DictionaryDeleteRequest:
|
||||
type: object
|
||||
required: [dict_name]
|
||||
properties:
|
||||
dict_name:
|
||||
type: string
|
||||
example: 用户状态
|
||||
paths:
|
||||
/api/system/test-helloworld:
|
||||
post:
|
||||
@@ -314,13 +417,6 @@ paths:
|
||||
1) 若 `Authorization` 对应 token 仍有效:直接按当前 auth record 续签(不调用微信接口)。
|
||||
2) 若 token 已过期:仅在 body 提供 `users_wx_code` 时才走微信 code 重新签发。
|
||||
返回体仅包含新的 `token`,不返回完整登录用户信息。
|
||||
parameters:
|
||||
- in: header
|
||||
name: Authorization
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: 可选。建议传入旧 token(`Bearer <token>`)以优先走有效 token 续签路径。
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@@ -337,8 +433,14 @@ paths:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/RefreshTokenData'
|
||||
token:
|
||||
type: string
|
||||
description: 新签发的 PocketBase 原生 auth token
|
||||
example:
|
||||
code: 200
|
||||
msg: 刷新成功
|
||||
data: {}
|
||||
token: eyJhbGciOi...
|
||||
'400':
|
||||
description: 参数错误或微信侧身份换取失败
|
||||
'401':
|
||||
@@ -416,9 +518,9 @@ paths:
|
||||
tags: [平台认证]
|
||||
summary: 平台用户登录
|
||||
description: |
|
||||
前端使用平台注册时保存的 `users_phone + password` 登录。
|
||||
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
||||
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
||||
服务端会先按手机号定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||
登录成功后直接返回 PocketBase 原生 auth token。
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -426,6 +528,9 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PlatformLoginRequest'
|
||||
example:
|
||||
login_account: 13509214696
|
||||
password: Momo123456
|
||||
responses:
|
||||
'200':
|
||||
description: 登录成功
|
||||
@@ -433,6 +538,33 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PocketBaseAuthResponse'
|
||||
example:
|
||||
code: 200
|
||||
msg: 登录成功
|
||||
data:
|
||||
status: login_success
|
||||
is_info_complete: false
|
||||
user:
|
||||
pb_id: vtukf6agem2xbcv
|
||||
users_id: ''
|
||||
users_idtype: ManagePlatform
|
||||
users_name: momo
|
||||
users_phone: '13509214696'
|
||||
users_phone_masked: '135****4696'
|
||||
users_status: ''
|
||||
users_rank_level: 0
|
||||
users_auth_type: 0
|
||||
users_type: ''
|
||||
users_picture: ''
|
||||
openid: app_momo
|
||||
company_id: ''
|
||||
users_parent_id: ''
|
||||
users_promo_code: ''
|
||||
usergroups_id: ''
|
||||
company: null
|
||||
created: ''
|
||||
updated: ''
|
||||
token: eyJhbGciOi...
|
||||
'400':
|
||||
description: 参数错误、密码错误或用户类型不匹配
|
||||
'404':
|
||||
@@ -450,8 +582,6 @@ paths:
|
||||
description: |
|
||||
基于当前 `Authorization` 对应 auth record 中的统一 `openid` 定位当前微信用户。
|
||||
当前接口仍用于微信资料完善场景。
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -474,3 +604,187 @@ paths:
|
||||
description: token 无效或当前 auth record 缺少统一身份字段 openid
|
||||
'400':
|
||||
description: 参数错误、手机号已被注册或资料更新失败
|
||||
/api/dictionary/list:
|
||||
post:
|
||||
tags: [字典管理]
|
||||
summary: 查询字典列表
|
||||
description: |
|
||||
仅允许 `ManagePlatform` 用户访问。
|
||||
支持按 `dict_name` 模糊搜索,返回字典全量信息,并将三个聚合字段组装为 `items`。
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DictionaryListRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DictionaryRecord'
|
||||
'401':
|
||||
description: token 无效或已过期
|
||||
'403':
|
||||
description: 非 ManagePlatform 用户无权访问
|
||||
'415':
|
||||
description: 请求体必须为 application/json
|
||||
/api/dictionary/detail:
|
||||
post:
|
||||
tags: [字典管理]
|
||||
summary: 查询指定字典
|
||||
description: |
|
||||
仅允许 `ManagePlatform` 用户访问。
|
||||
按唯一键 `dict_name` 查询单条字典,并返回组装后的 `items`。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DictionaryDetailRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DictionaryRecord'
|
||||
'400':
|
||||
description: 参数错误
|
||||
'401':
|
||||
description: token 无效或已过期
|
||||
'403':
|
||||
description: 非 ManagePlatform 用户无权访问
|
||||
'404':
|
||||
description: 未找到对应字典
|
||||
'415':
|
||||
description: 请求体必须为 application/json
|
||||
/api/dictionary/create:
|
||||
post:
|
||||
tags: [字典管理]
|
||||
summary: 新增字典
|
||||
description: |
|
||||
仅允许 `ManagePlatform` 用户访问。
|
||||
`system_dict_id` 由服务端自动生成;`dict_name` 必须唯一;
|
||||
`items` 会分别序列化写入 `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 三个字段。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DictionaryMutationRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 新增成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DictionaryRecord'
|
||||
'400':
|
||||
description: 参数错误或 dict_name 已存在
|
||||
'401':
|
||||
description: token 无效或已过期
|
||||
'403':
|
||||
description: 非 ManagePlatform 用户无权访问
|
||||
'415':
|
||||
description: 请求体必须为 application/json
|
||||
'429':
|
||||
description: 重复请求过于频繁
|
||||
/api/dictionary/update:
|
||||
post:
|
||||
tags: [字典管理]
|
||||
summary: 修改字典
|
||||
description: |
|
||||
仅允许 `ManagePlatform` 用户访问。
|
||||
根据 `original_dict_name`(未传时回退为 `dict_name`)定位原记录并更新。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DictionaryMutationRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 修改成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/DictionaryRecord'
|
||||
'400':
|
||||
description: 参数错误或 dict_name 冲突
|
||||
'401':
|
||||
description: token 无效或已过期
|
||||
'403':
|
||||
description: 非 ManagePlatform 用户无权访问
|
||||
'404':
|
||||
description: 未找到待修改字典
|
||||
'415':
|
||||
description: 请求体必须为 application/json
|
||||
'429':
|
||||
description: 重复请求过于频繁
|
||||
/api/dictionary/delete:
|
||||
post:
|
||||
tags: [字典管理]
|
||||
summary: 删除字典
|
||||
description: |
|
||||
仅允许 `ManagePlatform` 用户访问。
|
||||
按 `dict_name` 真删除对应记录。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DictionaryDeleteRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 删除成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
dict_name:
|
||||
type: string
|
||||
'400':
|
||||
description: 参数错误或删除失败
|
||||
'401':
|
||||
description: token 无效或已过期
|
||||
'403':
|
||||
description: 非 ManagePlatform 用户无权访问
|
||||
'404':
|
||||
description: 未找到待删除字典
|
||||
'415':
|
||||
description: 请求体必须为 application/json
|
||||
'429':
|
||||
description: 重复请求过于频繁
|
||||
|
||||
@@ -15,16 +15,17 @@ const collections = [
|
||||
type: 'base',
|
||||
fields: [
|
||||
{ name: 'system_dict_id', type: 'text', required: true },
|
||||
{ name: 'dict_name', type: 'text' },
|
||||
{ name: 'dict_name', type: 'text', required: true },
|
||||
{ name: 'dict_word_enum', type: 'text' },
|
||||
{ name: 'dict_word_description', type: 'text' },
|
||||
{ name: 'dict_word_is_enabled', type: 'bool' },
|
||||
{ name: 'dict_word_sort_order', type: 'number' },
|
||||
{ name: 'dict_word_sort_order', type: 'text' },
|
||||
{ name: 'dict_word_parent_id', type: 'text' },
|
||||
{ name: 'dict_word_remark', type: 'text' }
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_system_dict_id ON tbl_system_dict (system_dict_id)',
|
||||
'CREATE UNIQUE INDEX idx_dict_name ON tbl_system_dict (dict_name)',
|
||||
'CREATE INDEX idx_dict_word_parent_id ON tbl_system_dict (dict_word_parent_id)'
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user