字典管理
-维护 tbl_system_dict,支持模糊查询、行内编辑、枚举项弹窗编辑、新增和删除。
进入字典管理diff --git a/docs/pb_document_tables.md b/docs/pb_document_tables.md new file mode 100644 index 0000000..a82d52d --- /dev/null +++ b/docs/pb_document_tables.md @@ -0,0 +1,101 @@ +# PocketBase 文档相关表结构 + +本方案新增 3 张 PocketBase `base collection`,统一采用业务 id 字段进行关联,不直接依赖 PocketBase 自动生成的 `id` 作为业务主键。 + +补充约定: + +- `document_image`、`document_video` 只保存关联的 `attachments_id`,不直接存文件。 +- `document_keywords`、`document_product_categories`、`document_application_scenarios`、`document_hotel_type` 统一使用竖线分隔字符串,例如:`分类A|分类B|分类C`。 +- `document_owner` 的业务含义为“上传者openid”。 +- `attachments_link` 是 PocketBase `file` 字段,保存附件本体;访问链接由 PocketBase 根据记录和文件名生成。 + +--- + +## 1. `tbl_attachments` 附件表 + +**类型:** Base Collection + +| 字段名 | 类型 | 备注 | +| :--- | :--- | :--- | +| attachments_id | text | 附件业务 id,唯一标识符 | +| attachments_link | file | 附件本体,单文件,不限制文件类型,单文件上限 100MB | +| attachments_filename | text | 原始文件名 | +| attachments_filetype | text | 文件类型/MIME | +| attachments_size | number | 附件大小 | +| attachments_owner | text | 上传者业务 id | +| attachments_md5 | text | 附件 MD5 码 | +| attachments_ocr | text | OCR 识别结果 | +| attachments_status | text | 附件状态 | +| attachments_remark | text | 备注 | + +**索引规划:** + +- `attachments_id` 唯一索引 +- `attachments_owner` 普通索引 +- `attachments_status` 普通索引 + +--- + +## 2. `tbl_document` 文档表 + +**类型:** Base Collection + +| 字段名 | 类型 | 备注 | +| :--- | :--- | :--- | +| document_id | text | 文档业务 id,唯一标识符 | +| document_effect_date | date | 文档生效日期 | +| document_expiry_date | date | 文档到期日期 | +| document_type | text | 文档类型 | +| document_title | text | 文档标题 | +| document_subtitle | text | 文档副标题 | +| document_summary | text | 文档摘要 | +| document_content | text | 正文内容,保存 Markdown 原文 | +| document_image | text | 关联 `attachments_id` | +| document_video | text | 关联 `attachments_id` | +| document_owner | text | 上传者openid | +| document_relation_model | text | 关联机型/模型标识 | +| document_keywords | text | 关键词,竖线分隔 | +| document_share_count | number | 分享次数 | +| document_download_count | number | 下载次数 | +| document_favorite_count | number | 收藏次数 | +| document_status | text | 文档状态 | +| document_embedding_status | text | 文档嵌入状态 | +| document_embedding_error | text | 文档错误原因 | +| document_embedding_lasttime | date | 最后更新日期 | +| document_vector_version | text | 向量版本号或模型名称 | +| document_product_categories | text | 适用产品类别,竖线分隔 | +| document_application_scenarios | text | 适用场景,竖线分隔 | +| document_hotel_type | text | 适用酒店类型,竖线分隔 | +| document_remark | text | 备注 | + +**索引规划:** + +- `document_id` 唯一索引 +- `document_owner` 普通索引 +- `document_type` 普通索引 +- `document_status` 普通索引 +- `document_embedding_status` 普通索引 +- `document_effect_date` 普通索引 +- `document_expiry_date` 普通索引 + +--- + +## 3. `tbl_document_operation_history` 文档操作历史表 + +**类型:** Base Collection + +| 字段名 | 类型 | 备注 | +| :--- | :--- | :--- | +| doh_id | text | 文档操作历史业务 id,唯一标识符 | +| doh_document_id | text | 关联 `document_id` | +| doh_operation_type | text | 操作类型 | +| doh_user_id | text | 操作人业务 id | +| doh_current_count | number | 本次操作对应次数 | +| doh_remark | text | 备注 | + +**索引规划:** + +- `doh_id` 唯一索引 +- `doh_document_id` 普通索引 +- `doh_user_id` 普通索引 +- `doh_operation_type` 普通索引 diff --git a/pocket-base/bai-api-main.pb.js b/pocket-base/bai-api-main.pb.js index d16ebde..e53ff5e 100644 --- a/pocket-base/bai-api-main.pb.js +++ b/pocket-base/bai-api-main.pb.js @@ -28,5 +28,15 @@ 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/attachment/list.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/attachment/detail.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/attachment/upload.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/attachment/delete.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document/list.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document/detail.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document/create.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document/update.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document/delete.js`) +require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document-history/list.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 5ed467d..916f15c 100644 --- a/pocket-base/bai-web-main.pb.js +++ b/pocket-base/bai-web-main.pb.js @@ -1,4 +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/login.js`) +require(`${__hooks}/bai_web_pb_hooks/pages/document-manage.js`) require(`${__hooks}/bai_web_pb_hooks/pages/dictionary-manage.js`) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/delete.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/delete.js new file mode 100644 index 0000000..7f72323 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/delete.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/attachment/delete', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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.validateAttachmentDeleteBody(e) + const data = documentService.deleteAttachment(payload.attachments_id) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/detail.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/detail.js new file mode 100644 index 0000000..351be84 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/detail.js @@ -0,0 +1,28 @@ +routerAdd('POST', '/api/attachment/detail', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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.validateAttachmentDetailBody(e) + const data = documentService.getAttachmentDetail(payload.attachments_id) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/list.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/list.js new file mode 100644 index 0000000..9e31f1c --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/list.js @@ -0,0 +1,30 @@ +routerAdd('POST', '/api/attachment/list', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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.validateAttachmentListBody(e) + const data = documentService.listAttachments(payload) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/upload.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/upload.js new file mode 100644 index 0000000..886901a --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/attachment/upload.js @@ -0,0 +1,28 @@ +routerAdd('POST', '/api/attachment/upload', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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 { + const authState = guards.requireManagePlatformUser(e) + const payload = guards.validateAttachmentUploadBody(e) + const files = e.findUploadedFiles('attachments_link') || [] + const file = files.length ? files[0] : null + const data = documentService.uploadAttachment(authState.openid, payload, file) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/document-history/list.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/document-history/list.js new file mode 100644 index 0000000..1d126eb --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/document-history/list.js @@ -0,0 +1,30 @@ +routerAdd('POST', '/api/document-history/list', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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.validateDocumentHistoryListBody(e) + const data = documentService.listDocumentHistories(payload) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/document/create.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/create.js new file mode 100644 index 0000000..a71bc27 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/create.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/document/create', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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) + const authState = guards.requireManagePlatformUser(e) + guards.duplicateGuard(e) + + const payload = guards.validateDocumentMutationBody(e, false) + const data = documentService.createDocument(authState.openid, 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/document/delete.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/delete.js new file mode 100644 index 0000000..6bb0acf --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/delete.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/document/delete', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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) + const authState = guards.requireManagePlatformUser(e) + guards.duplicateGuard(e) + + const payload = guards.validateDocumentDeleteBody(e) + const data = documentService.deleteDocument(authState.openid, payload.document_id) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/document/detail.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/detail.js new file mode 100644 index 0000000..80f4754 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/detail.js @@ -0,0 +1,28 @@ +routerAdd('POST', '/api/document/detail', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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.validateDocumentDetailBody(e) + const data = documentService.getDocumentDetail(payload.document_id) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/document/list.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/list.js new file mode 100644 index 0000000..923f059 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/list.js @@ -0,0 +1,30 @@ +routerAdd('POST', '/api/document/list', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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.validateDocumentListBody(e) + const data = documentService.listDocuments(payload) + + 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) || {}, + }) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/document/update.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/update.js new file mode 100644 index 0000000..35bbb9c --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/document/update.js @@ -0,0 +1,29 @@ +routerAdd('POST', '/api/document/update', function (e) { + const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`) + const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.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) + const authState = guards.requireManagePlatformUser(e) + guards.duplicateGuard(e) + + const payload = guards.validateDocumentMutationBody(e, true) + const data = documentService.updateDocument(authState.openid, 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) || {}, + }) + } +}) 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 b61c981..513fa31 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 @@ -167,6 +167,112 @@ function validateDictionaryDeleteBody(e) { } } +function validateAttachmentListBody(e) { + const payload = parseBody(e) + + return { + keyword: payload.keyword || '', + status: payload.status || '', + } +} + +function validateAttachmentDetailBody(e) { + const payload = parseBody(e) + if (!payload.attachments_id) { + throw createAppError(400, 'attachments_id 为必填项') + } + + return { + attachments_id: payload.attachments_id, + } +} + +function validateAttachmentDeleteBody(e) { + return validateAttachmentDetailBody(e) +} + +function validateAttachmentUploadBody(e) { + const payload = sanitizePayload(e.requestInfo().body || {}) + + return { + attachments_filename: payload.attachments_filename || '', + attachments_filetype: payload.attachments_filetype || '', + attachments_size: payload.attachments_size || 0, + attachments_md5: payload.attachments_md5 || '', + attachments_ocr: payload.attachments_ocr || '', + attachments_status: payload.attachments_status || 'active', + attachments_remark: payload.attachments_remark || '', + } +} + +function validateDocumentListBody(e) { + const payload = parseBody(e) + + return { + keyword: payload.keyword || '', + status: payload.status || '', + document_type: payload.document_type || '', + } +} + +function validateDocumentDetailBody(e) { + const payload = parseBody(e) + if (!payload.document_id) { + throw createAppError(400, 'document_id 为必填项') + } + + return { + document_id: payload.document_id, + } +} + +function validateDocumentMutationBody(e, isUpdate) { + const payload = parseBody(e) + + if (!payload.document_id && isUpdate) { + throw createAppError(400, 'document_id 为必填项') + } + + return { + document_id: payload.document_id || '', + document_effect_date: payload.document_effect_date || '', + document_expiry_date: payload.document_expiry_date || '', + document_type: payload.document_type || '', + document_title: payload.document_title || '', + document_subtitle: payload.document_subtitle || '', + document_summary: payload.document_summary || '', + document_content: payload.document_content || '', + document_image: payload.document_image || '', + document_video: payload.document_video || '', + document_relation_model: payload.document_relation_model || '', + document_keywords: payload.document_keywords || '', + document_share_count: payload.document_share_count || 0, + document_download_count: payload.document_download_count || 0, + document_favorite_count: payload.document_favorite_count || 0, + document_status: payload.document_status || '', + document_embedding_status: payload.document_embedding_status || '', + document_embedding_error: payload.document_embedding_error || '', + document_embedding_lasttime: payload.document_embedding_lasttime || '', + document_vector_version: payload.document_vector_version || '', + document_product_categories: payload.document_product_categories || '', + document_application_scenarios: payload.document_application_scenarios || '', + document_hotel_type: payload.document_hotel_type || '', + document_remark: payload.document_remark || '', + } +} + +function validateDocumentDeleteBody(e) { + return validateDocumentDetailBody(e) +} + +function validateDocumentHistoryListBody(e) { + const payload = parseBody(e) + + return { + document_id: payload.document_id || '', + } +} + function requireAuthOpenid(e) { if (!e.auth) { throw createAppError(401, '认证令牌无效或已过期') @@ -236,6 +342,15 @@ module.exports = { validateDictionaryDetailBody, validateDictionaryMutationBody, validateDictionaryDeleteBody, + validateAttachmentListBody, + validateAttachmentDetailBody, + validateAttachmentDeleteBody, + validateAttachmentUploadBody, + validateDocumentListBody, + validateDocumentDetailBody, + validateDocumentMutationBody, + validateDocumentDeleteBody, + validateDocumentHistoryListBody, requireAuthOpenid, requireAuthUser, duplicateGuard, diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js new file mode 100644 index 0000000..df2769f --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js @@ -0,0 +1,473 @@ +const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`) +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`) + +function buildBusinessId(prefix) { + return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6) +} + +function normalizePublicBaseUrl() { + const base = String(env.appBaseUrl || '').replace(/\/+$/, '') + + if (!base) return '/pb' + if (base.toLowerCase().endsWith('/pb')) return base + + return base + '/pb' +} + +function buildFileUrl(collectionName, recordId, filename, download) { + if (!filename) return '' + + const base = normalizePublicBaseUrl() + const url = base + '/api/files/' + encodeURIComponent(collectionName) + '/' + encodeURIComponent(recordId) + '/' + encodeURIComponent(filename) + + return download ? (url + '?download=1') : url +} + +function normalizeDateValue(value) { + const text = String(value || '').replace(/^\s+|\s+$/g, '') + if (!text) return '' + + if (/^\d{4}-\d{2}-\d{2}$/.test(text)) { + return text + ' 00:00:00.000Z' + } + + if (/^\d{4}-\d{2}-\d{2}\s/.test(text) || text.indexOf('T') !== -1) { + return text + } + + throw createAppError(400, '日期字段格式错误') +} + +function normalizeNumberValue(value, fieldName) { + if (value === '' || value === null || typeof value === 'undefined') { + return 0 + } + + const num = Number(value) + if (!Number.isFinite(num)) { + throw createAppError(400, fieldName + ' 必须为数字') + } + + return num +} + +function exportAttachmentRecord(record) { + const storedFilename = record.getString('attachments_link') + + return { + pb_id: record.id, + attachments_id: record.getString('attachments_id'), + attachments_link: storedFilename, + attachments_url: buildFileUrl('tbl_attachments', record.id, storedFilename, false), + attachments_download_url: buildFileUrl('tbl_attachments', record.id, storedFilename, true), + attachments_filename: record.getString('attachments_filename'), + attachments_filetype: record.getString('attachments_filetype'), + attachments_size: record.get('attachments_size'), + attachments_owner: record.getString('attachments_owner'), + attachments_md5: record.getString('attachments_md5'), + attachments_ocr: record.getString('attachments_ocr'), + attachments_status: record.getString('attachments_status'), + attachments_remark: record.getString('attachments_remark'), + created: String(record.created || ''), + updated: String(record.updated || ''), + } +} + +function findAttachmentRecordByAttachmentId(attachmentId) { + if (!attachmentId) return null + + const records = $app.findRecordsByFilter('tbl_attachments', 'attachments_id = {:attachmentsId}', '', 1, 0, { + attachmentsId: attachmentId, + }) + + return records.length ? records[0] : null +} + +function exportDocumentRecord(record) { + const imageAttachmentId = record.getString('document_image') + const videoAttachmentId = record.getString('document_video') + const imageAttachmentRecord = findAttachmentRecordByAttachmentId(imageAttachmentId) + const videoAttachmentRecord = findAttachmentRecordByAttachmentId(videoAttachmentId) + const imageAttachment = imageAttachmentRecord ? exportAttachmentRecord(imageAttachmentRecord) : null + const videoAttachment = videoAttachmentRecord ? exportAttachmentRecord(videoAttachmentRecord) : null + + return { + pb_id: record.id, + document_id: record.getString('document_id'), + document_effect_date: String(record.get('document_effect_date') || ''), + document_expiry_date: String(record.get('document_expiry_date') || ''), + document_type: record.getString('document_type'), + document_title: record.getString('document_title'), + document_subtitle: record.getString('document_subtitle'), + document_summary: record.getString('document_summary'), + document_content: record.getString('document_content'), + document_image: imageAttachmentId, + document_image_url: imageAttachment ? imageAttachment.attachments_url : '', + document_image_attachment: imageAttachment, + document_video: videoAttachmentId, + document_video_url: videoAttachment ? videoAttachment.attachments_url : '', + document_video_attachment: videoAttachment, + document_owner: record.getString('document_owner'), + document_relation_model: record.getString('document_relation_model'), + document_keywords: record.getString('document_keywords'), + document_share_count: record.get('document_share_count'), + document_download_count: record.get('document_download_count'), + document_favorite_count: record.get('document_favorite_count'), + document_status: record.getString('document_status'), + document_embedding_status: record.getString('document_embedding_status'), + document_embedding_error: record.getString('document_embedding_error'), + document_embedding_lasttime: String(record.get('document_embedding_lasttime') || ''), + document_vector_version: record.getString('document_vector_version'), + document_product_categories: record.getString('document_product_categories'), + document_application_scenarios: record.getString('document_application_scenarios'), + document_hotel_type: record.getString('document_hotel_type'), + document_remark: record.getString('document_remark'), + created: String(record.created || ''), + updated: String(record.updated || ''), + } +} + +function findDocumentRecordByDocumentId(documentId) { + const records = $app.findRecordsByFilter('tbl_document', 'document_id = {:documentId}', '', 1, 0, { + documentId: documentId, + }) + + return records.length ? records[0] : null +} + +function exportHistoryRecord(record) { + return { + pb_id: record.id, + doh_id: record.getString('doh_id'), + doh_document_id: record.getString('doh_document_id'), + doh_operation_type: record.getString('doh_operation_type'), + doh_user_id: record.getString('doh_user_id'), + doh_current_count: record.get('doh_current_count'), + doh_remark: record.getString('doh_remark'), + created: String(record.created || ''), + updated: String(record.updated || ''), + } +} + +function createHistoryRecord(txApp, payload) { + const collection = txApp.findCollectionByNameOrId('tbl_document_operation_history') + const record = new Record(collection) + + record.set('doh_id', buildBusinessId('DOH')) + record.set('doh_document_id', payload.documentId) + record.set('doh_operation_type', payload.operationType) + record.set('doh_user_id', payload.userOpenid || '') + record.set('doh_current_count', normalizeNumberValue(payload.currentCount, 'doh_current_count')) + record.set('doh_remark', payload.remark || '') + + txApp.save(record) +} + +function ensureAttachmentExists(attachmentId, fieldName) { + if (!attachmentId) return + + const record = findAttachmentRecordByAttachmentId(attachmentId) + if (!record) { + throw createAppError(400, fieldName + ' 对应的附件不存在') + } +} + +function listAttachments(payload) { + const allRecords = $app.findRecordsByFilter('tbl_attachments', '', '', 500, 0) + const keyword = String(payload.keyword || '').toLowerCase() + const status = String(payload.status || '') + const result = [] + + for (let i = 0; i < allRecords.length; i += 1) { + const item = exportAttachmentRecord(allRecords[i]) + const matchedKeyword = !keyword + || item.attachments_id.toLowerCase().indexOf(keyword) !== -1 + || item.attachments_filename.toLowerCase().indexOf(keyword) !== -1 + const matchedStatus = !status || item.attachments_status === status + + if (matchedKeyword && matchedStatus) { + result.push(item) + } + } + + return result +} + +function getAttachmentDetail(attachmentId) { + const record = findAttachmentRecordByAttachmentId(attachmentId) + if (!record) { + throw createAppError(404, '未找到对应附件') + } + + return exportAttachmentRecord(record) +} + +function uploadAttachment(userOpenid, payload, file) { + if (!file) { + throw createAppError(400, 'attachments_link 为必填文件') + } + + const collection = $app.findCollectionByNameOrId('tbl_attachments') + const record = new Record(collection) + + record.set('attachments_id', buildBusinessId('ATT')) + record.set('attachments_link', file) + record.set('attachments_filename', payload.attachments_filename || '') + record.set('attachments_filetype', payload.attachments_filetype || '') + record.set('attachments_size', normalizeNumberValue(payload.attachments_size, 'attachments_size')) + record.set('attachments_owner', userOpenid || '') + record.set('attachments_md5', payload.attachments_md5 || '') + record.set('attachments_ocr', payload.attachments_ocr || '') + record.set('attachments_status', payload.attachments_status || 'active') + record.set('attachments_remark', payload.attachments_remark || '') + + try { + $app.save(record) + } catch (err) { + throw createAppError(400, '上传附件失败', { + originalMessage: (err && err.message) || '未知错误', + originalData: (err && err.data) || {}, + }) + } + + logger.info('附件上传成功', { + attachments_id: record.getString('attachments_id'), + attachments_owner: userOpenid || '', + }) + + return exportAttachmentRecord(record) +} + +function deleteAttachment(attachmentId) { + const record = findAttachmentRecordByAttachmentId(attachmentId) + if (!record) { + throw createAppError(404, '未找到待删除的附件') + } + + const quotedAttachmentId = '"' + attachmentId.replace(/"/g, '\\"') + '"' + const usedByDocument = $app.findRecordsByFilter('tbl_document', 'document_image = ' + quotedAttachmentId + ' || document_video = ' + quotedAttachmentId, '', 1, 0) + if (usedByDocument.length) { + throw createAppError(400, '附件已被文档引用,无法删除') + } + + try { + $app.delete(record) + } catch (err) { + throw createAppError(400, '删除附件失败', { + originalMessage: (err && err.message) || '未知错误', + originalData: (err && err.data) || {}, + }) + } + + logger.info('附件删除成功', { + attachments_id: attachmentId, + }) + + return { + attachments_id: attachmentId, + } +} + +function listDocuments(payload) { + const allRecords = $app.findRecordsByFilter('tbl_document', '', '', 500, 0) + const keyword = String(payload.keyword || '').toLowerCase() + const status = String(payload.status || '') + const type = String(payload.document_type || '') + const result = [] + + for (let i = 0; i < allRecords.length; i += 1) { + const item = exportDocumentRecord(allRecords[i]) + const matchedKeyword = !keyword + || item.document_id.toLowerCase().indexOf(keyword) !== -1 + || item.document_title.toLowerCase().indexOf(keyword) !== -1 + || item.document_subtitle.toLowerCase().indexOf(keyword) !== -1 + || item.document_summary.toLowerCase().indexOf(keyword) !== -1 + || item.document_keywords.toLowerCase().indexOf(keyword) !== -1 + const matchedStatus = !status || item.document_status === status + const matchedType = !type || item.document_type === type + + if (matchedKeyword && matchedStatus && matchedType) { + result.push(item) + } + } + + result.sort(function (a, b) { + return String(b.updated || '').localeCompare(String(a.updated || '')) + }) + + return result +} + +function getDocumentDetail(documentId) { + const record = findDocumentRecordByDocumentId(documentId) + if (!record) { + throw createAppError(404, '未找到对应文档') + } + + return exportDocumentRecord(record) +} + +function createDocument(userOpenid, payload) { + ensureAttachmentExists(payload.document_image, 'document_image') + ensureAttachmentExists(payload.document_video, 'document_video') + + const targetDocumentId = payload.document_id || buildBusinessId('DOC') + const duplicated = findDocumentRecordByDocumentId(targetDocumentId) + if (duplicated) { + throw createAppError(400, 'document_id 已存在') + } + + return $app.runInTransaction(function (txApp) { + const collection = txApp.findCollectionByNameOrId('tbl_document') + const record = new Record(collection) + + record.set('document_id', targetDocumentId) + record.set('document_effect_date', normalizeDateValue(payload.document_effect_date)) + record.set('document_expiry_date', normalizeDateValue(payload.document_expiry_date)) + record.set('document_type', payload.document_type || '') + record.set('document_title', payload.document_title || '') + record.set('document_subtitle', payload.document_subtitle || '') + record.set('document_summary', payload.document_summary || '') + record.set('document_content', payload.document_content || '') + record.set('document_image', payload.document_image || '') + record.set('document_video', payload.document_video || '') + record.set('document_owner', userOpenid || '') + record.set('document_relation_model', payload.document_relation_model || '') + record.set('document_keywords', payload.document_keywords || '') + record.set('document_share_count', normalizeNumberValue(payload.document_share_count, 'document_share_count')) + record.set('document_download_count', normalizeNumberValue(payload.document_download_count, 'document_download_count')) + record.set('document_favorite_count', normalizeNumberValue(payload.document_favorite_count, 'document_favorite_count')) + record.set('document_status', payload.document_status || 'active') + record.set('document_embedding_status', payload.document_embedding_status || 'pending') + record.set('document_embedding_error', payload.document_embedding_error || '') + record.set('document_embedding_lasttime', normalizeDateValue(payload.document_embedding_lasttime)) + record.set('document_vector_version', payload.document_vector_version || '') + record.set('document_product_categories', payload.document_product_categories || '') + record.set('document_application_scenarios', payload.document_application_scenarios || '') + record.set('document_hotel_type', payload.document_hotel_type || '') + record.set('document_remark', payload.document_remark || '') + + txApp.save(record) + + createHistoryRecord(txApp, { + documentId: record.getString('document_id'), + operationType: 'create', + userOpenid: userOpenid, + currentCount: 1, + remark: '文档创建', + }) + + return exportDocumentRecord(record) + }) +} + +function updateDocument(userOpenid, payload) { + const record = findDocumentRecordByDocumentId(payload.document_id) + if (!record) { + throw createAppError(404, '未找到待修改的文档') + } + + ensureAttachmentExists(payload.document_image, 'document_image') + ensureAttachmentExists(payload.document_video, 'document_video') + + return $app.runInTransaction(function (txApp) { + const target = txApp.findRecordById('tbl_document', record.id) + + target.set('document_effect_date', normalizeDateValue(payload.document_effect_date)) + target.set('document_expiry_date', normalizeDateValue(payload.document_expiry_date)) + target.set('document_type', payload.document_type || '') + target.set('document_title', payload.document_title || '') + target.set('document_subtitle', payload.document_subtitle || '') + target.set('document_summary', payload.document_summary || '') + target.set('document_content', payload.document_content || '') + target.set('document_image', payload.document_image || '') + target.set('document_video', payload.document_video || '') + target.set('document_relation_model', payload.document_relation_model || '') + target.set('document_keywords', payload.document_keywords || '') + target.set('document_share_count', normalizeNumberValue(payload.document_share_count, 'document_share_count')) + target.set('document_download_count', normalizeNumberValue(payload.document_download_count, 'document_download_count')) + target.set('document_favorite_count', normalizeNumberValue(payload.document_favorite_count, 'document_favorite_count')) + target.set('document_status', payload.document_status || '') + target.set('document_embedding_status', payload.document_embedding_status || '') + target.set('document_embedding_error', payload.document_embedding_error || '') + target.set('document_embedding_lasttime', normalizeDateValue(payload.document_embedding_lasttime)) + target.set('document_vector_version', payload.document_vector_version || '') + target.set('document_product_categories', payload.document_product_categories || '') + target.set('document_application_scenarios', payload.document_application_scenarios || '') + target.set('document_hotel_type', payload.document_hotel_type || '') + target.set('document_remark', payload.document_remark || '') + + txApp.save(target) + + createHistoryRecord(txApp, { + documentId: target.getString('document_id'), + operationType: 'update', + userOpenid: userOpenid, + currentCount: 1, + remark: '文档更新', + }) + + return exportDocumentRecord(target) + }) +} + +function deleteDocument(userOpenid, documentId) { + const record = findDocumentRecordByDocumentId(documentId) + if (!record) { + throw createAppError(404, '未找到待删除的文档') + } + + return $app.runInTransaction(function (txApp) { + createHistoryRecord(txApp, { + documentId: documentId, + operationType: 'delete', + userOpenid: userOpenid, + currentCount: 1, + remark: '文档删除', + }) + + const target = txApp.findRecordById('tbl_document', record.id) + txApp.delete(target) + + logger.info('文档删除成功', { + document_id: documentId, + }) + + return { + document_id: documentId, + } + }) +} + +function listDocumentHistories(payload) { + const allRecords = payload.document_id + ? $app.findRecordsByFilter('tbl_document_operation_history', 'doh_document_id = {:documentId}', '', 500, 0, { + documentId: payload.document_id, + }) + : $app.findRecordsByFilter('tbl_document_operation_history', '', '', 500, 0) + + const result = [] + for (let i = 0; i < allRecords.length; i += 1) { + result.push(exportHistoryRecord(allRecords[i])) + } + + result.sort(function (a, b) { + return String(b.created || '').localeCompare(String(a.created || '')) + }) + + return result +} + +module.exports = { + listAttachments, + getAttachmentDetail, + uploadAttachment, + deleteAttachment, + listDocuments, + getDocumentDetail, + createDocument, + updateDocument, + deleteDocument, + listDocumentHistories, +} diff --git a/pocket-base/bai_web_pb_hooks/pages/document-manage.js b/pocket-base/bai_web_pb_hooks/pages/document-manage.js new file mode 100644 index 0000000..3b2f416 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/pages/document-manage.js @@ -0,0 +1,429 @@ +routerAdd('GET', '/manage/document-manage', function (e) { + const html = ` + +
+ + +页面会先把文件上传到 tbl_attachments,然后把返回的 attachments_id 写入 tbl_document.document_image 或 document_video。文档列表会直接显示 PocketBase 文件流链接。
| document_id | +标题 | +类型/状态 | +附件链接 | +更新时间 | +操作 | +
|---|---|---|---|---|---|
| 暂无数据,请先刷新列表。 | |||||
该页面仅供已登录的 ManagePlatform 用户使用。你可以从这里跳转到各个管理子页面。
-维护 tbl_system_dict,支持模糊查询、行内编辑、枚举项弹窗编辑、新增和删除。
进入字典管理登录入口页。若已登录,访问该页会自动跳回管理主页。
- 打开登录页 -第二个示例子页面,便于验证主页跳转与 hooks 页面导航。
- 进入页面二 +这里是 page-b 页面。当前用于验证 PocketBase hooks 页面路由是否已经正确注册,并可从 manage 首页完成跳转。
- -