feat: 添加文档管理和登录页面功能,包含文档上传、列表展示及用户登录逻辑;新增数据库表结构初始化脚本
This commit is contained in:
101
docs/pb_document_tables.md
Normal file
101
docs/pb_document_tables.md
Normal file
@@ -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` 普通索引
|
||||||
@@ -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/create.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/dictionary/update.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/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/login.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/profile.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/wechat/profile.js`)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require(`${__hooks}/bai_web_pb_hooks/pages/index.js`)
|
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/login.js`)
|
||||||
require(`${__hooks}/bai_web_pb_hooks/pages/page-b.js`)
|
require(`${__hooks}/bai_web_pb_hooks/pages/document-manage.js`)
|
||||||
require(`${__hooks}/bai_web_pb_hooks/pages/dictionary-manage.js`)
|
require(`${__hooks}/bai_web_pb_hooks/pages/dictionary-manage.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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
30
pocket-base/bai_api_pb_hooks/bai_api_routes/document/list.js
Normal file
30
pocket-base/bai_api_pb_hooks/bai_api_routes/document/list.js
Normal file
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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) {
|
function requireAuthOpenid(e) {
|
||||||
if (!e.auth) {
|
if (!e.auth) {
|
||||||
throw createAppError(401, '认证令牌无效或已过期')
|
throw createAppError(401, '认证令牌无效或已过期')
|
||||||
@@ -236,6 +342,15 @@ module.exports = {
|
|||||||
validateDictionaryDetailBody,
|
validateDictionaryDetailBody,
|
||||||
validateDictionaryMutationBody,
|
validateDictionaryMutationBody,
|
||||||
validateDictionaryDeleteBody,
|
validateDictionaryDeleteBody,
|
||||||
|
validateAttachmentListBody,
|
||||||
|
validateAttachmentDetailBody,
|
||||||
|
validateAttachmentDeleteBody,
|
||||||
|
validateAttachmentUploadBody,
|
||||||
|
validateDocumentListBody,
|
||||||
|
validateDocumentDetailBody,
|
||||||
|
validateDocumentMutationBody,
|
||||||
|
validateDocumentDeleteBody,
|
||||||
|
validateDocumentHistoryListBody,
|
||||||
requireAuthOpenid,
|
requireAuthOpenid,
|
||||||
requireAuthUser,
|
requireAuthUser,
|
||||||
duplicateGuard,
|
duplicateGuard,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
429
pocket-base/bai_web_pb_hooks/pages/document-manage.js
Normal file
429
pocket-base/bai_web_pb_hooks/pages/document-manage.js
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
routerAdd('GET', '/manage/document-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, #eef6ff 0%, #f8fafc 100%); color: #1f2937; }
|
||||||
|
.container { max-width: 1280px; margin: 0 auto; padding: 32px 20px 64px; }
|
||||||
|
.panel { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 24px; padding: 24px; }
|
||||||
|
.panel + .panel { margin-top: 24px; }
|
||||||
|
h1, h2 { margin-top: 0; }
|
||||||
|
p { color: #4b5563; line-height: 1.7; }
|
||||||
|
.actions, .form-actions { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
|
.btn-primary { background: #2563eb; color: #fff; }
|
||||||
|
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||||
|
.btn-danger { background: #dc2626; color: #fff; }
|
||||||
|
.status { margin-top: 14px; min-height: 24px; font-size: 14px; }
|
||||||
|
.status.success { color: #15803d; }
|
||||||
|
.status.error { color: #b91c1c; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
||||||
|
.full { grid-column: 1 / -1; }
|
||||||
|
label { display: block; margin-bottom: 8px; color: #334155; font-weight: 600; }
|
||||||
|
input, textarea { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||||
|
textarea { min-height: 96px; resize: vertical; }
|
||||||
|
.hint { color: #64748b; font-size: 12px; margin-top: 6px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
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; }
|
||||||
|
.empty { text-align: center; padding: 24px 16px; color: #64748b; }
|
||||||
|
.muted { color: #64748b; font-size: 12px; }
|
||||||
|
.doc-links a { display: inline-block; margin-right: 10px; color: #2563eb; text-decoration: none; font-weight: 600; }
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.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="panel">
|
||||||
|
<h1>文档管理</h1>
|
||||||
|
<p>页面会先把文件上传到 <code>tbl_attachments</code>,然后把返回的 <code>attachments_id</code> 写入 <code>tbl_document.document_image</code> 或 <code>document_video</code>。文档列表会直接显示 PocketBase 文件流链接。</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-light" id="reloadBtn" type="button">刷新列表</button>
|
||||||
|
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>新增文档</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<label for="documentTitle">文档标题</label>
|
||||||
|
<input id="documentTitle" placeholder="请输入文档标题" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="documentType">文档类型</label>
|
||||||
|
<input id="documentType" placeholder="例如:说明书、新闻、常见故障" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="documentStatus">文档状态</label>
|
||||||
|
<input id="documentStatus" placeholder="默认 active" value="active" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="embeddingStatus">嵌入状态</label>
|
||||||
|
<input id="embeddingStatus" placeholder="默认 pending" value="pending" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="effectDate">生效日期</label>
|
||||||
|
<input id="effectDate" type="date" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="expiryDate">到期日期</label>
|
||||||
|
<input id="expiryDate" type="date" />
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="documentSubtitle">副标题</label>
|
||||||
|
<input id="documentSubtitle" placeholder="可选" />
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="documentSummary">摘要</label>
|
||||||
|
<textarea id="documentSummary" placeholder="请输入文档摘要"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="documentContent">正文内容</label>
|
||||||
|
<textarea id="documentContent" placeholder="支持 Markdown 原文"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="relationModel">关联机型</label>
|
||||||
|
<input id="relationModel" placeholder="可选" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="vectorVersion">向量版本</label>
|
||||||
|
<input id="vectorVersion" placeholder="可选" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="documentKeywords">关键词</label>
|
||||||
|
<input id="documentKeywords" placeholder="多个值用 | 分隔" />
|
||||||
|
<div class="hint">例如:安装|配置|排障</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="productCategories">适用产品类别</label>
|
||||||
|
<input id="productCategories" placeholder="多个值用 | 分隔" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="applicationScenarios">适用场景</label>
|
||||||
|
<input id="applicationScenarios" placeholder="多个值用 | 分隔" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="hotelType">适用酒店类型</label>
|
||||||
|
<input id="hotelType" placeholder="多个值用 | 分隔" />
|
||||||
|
</div>
|
||||||
|
<div class="full">
|
||||||
|
<label for="documentRemark">备注</label>
|
||||||
|
<textarea id="documentRemark" placeholder="可选"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="imageFile">图片附件</label>
|
||||||
|
<input id="imageFile" type="file" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="videoFile">视频附件</label>
|
||||||
|
<input id="videoFile" type="file" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions" style="margin-top:16px;">
|
||||||
|
<button class="btn btn-primary" id="submitBtn" type="button">上传附件并创建文档</button>
|
||||||
|
<button class="btn btn-light" id="resetBtn" type="button">重置表单</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>文档列表</h2>
|
||||||
|
<div style="overflow:auto;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>document_id</th>
|
||||||
|
<th>标题</th>
|
||||||
|
<th>类型/状态</th>
|
||||||
|
<th>附件链接</th>
|
||||||
|
<th>更新时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody">
|
||||||
|
<tr><td colspan="6" class="empty">暂无数据,请先刷新列表。</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const tokenKey = 'pb_manage_token'
|
||||||
|
const statusEl = document.getElementById('status')
|
||||||
|
const tableBody = document.getElementById('tableBody')
|
||||||
|
const fields = {
|
||||||
|
documentTitle: document.getElementById('documentTitle'),
|
||||||
|
documentType: document.getElementById('documentType'),
|
||||||
|
documentStatus: document.getElementById('documentStatus'),
|
||||||
|
embeddingStatus: document.getElementById('embeddingStatus'),
|
||||||
|
effectDate: document.getElementById('effectDate'),
|
||||||
|
expiryDate: document.getElementById('expiryDate'),
|
||||||
|
documentSubtitle: document.getElementById('documentSubtitle'),
|
||||||
|
documentSummary: document.getElementById('documentSummary'),
|
||||||
|
documentContent: document.getElementById('documentContent'),
|
||||||
|
relationModel: document.getElementById('relationModel'),
|
||||||
|
vectorVersion: document.getElementById('vectorVersion'),
|
||||||
|
documentKeywords: document.getElementById('documentKeywords'),
|
||||||
|
productCategories: document.getElementById('productCategories'),
|
||||||
|
applicationScenarios: document.getElementById('applicationScenarios'),
|
||||||
|
hotelType: document.getElementById('hotelType'),
|
||||||
|
documentRemark: document.getElementById('documentRemark'),
|
||||||
|
imageFile: document.getElementById('imageFile'),
|
||||||
|
videoFile: document.getElementById('videoFile'),
|
||||||
|
}
|
||||||
|
const state = { list: [] }
|
||||||
|
|
||||||
|
function setStatus(message, type) {
|
||||||
|
statusEl.textContent = message || ''
|
||||||
|
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem(tokenKey) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestJson(url, payload) {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) {
|
||||||
|
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 || 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')
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
}
|
||||||
|
throw new Error((data && data.msg) || '请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAttachment(file, label) {
|
||||||
|
const token = getToken()
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('attachments_link', file)
|
||||||
|
form.append('attachments_filename', file.name || '')
|
||||||
|
form.append('attachments_filetype', file.type || '')
|
||||||
|
form.append('attachments_size', String(file.size || 0))
|
||||||
|
form.append('attachments_status', 'active')
|
||||||
|
form.append('attachments_remark', 'document-manage:' + label)
|
||||||
|
|
||||||
|
const res = await fetch('/api/attachment/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + token,
|
||||||
|
},
|
||||||
|
body: form,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok || !data || data.code >= 400) {
|
||||||
|
throw new Error((data && data.msg) || ('上传' + label + '失败'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLinks(item) {
|
||||||
|
const links = []
|
||||||
|
if (item.document_image_url) {
|
||||||
|
links.push('<a href="' + escapeHtml(item.document_image_url) + '" target="_blank" rel="noreferrer">图片流</a>')
|
||||||
|
}
|
||||||
|
if (item.document_video_url) {
|
||||||
|
links.push('<a href="' + escapeHtml(item.document_video_url) + '" target="_blank" rel="noreferrer">视频流</a>')
|
||||||
|
}
|
||||||
|
if (!links.length) {
|
||||||
|
return '<span class="muted">无</span>'
|
||||||
|
}
|
||||||
|
return '<div class="doc-links">' + links.join('') + '</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable() {
|
||||||
|
if (!state.list.length) {
|
||||||
|
tableBody.innerHTML = '<tr><td colspan="6" class="empty">暂无文档数据。</td></tr>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tableBody.innerHTML = state.list.map(function (item) {
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td data-label="document_id"><div>' + escapeHtml(item.document_id) + '</div><div class="muted">owner: ' + escapeHtml(item.document_owner) + '</div></td>'
|
||||||
|
+ '<td data-label="标题"><div><strong>' + escapeHtml(item.document_title) + '</strong></div><div class="muted">' + escapeHtml(item.document_subtitle) + '</div></td>'
|
||||||
|
+ '<td data-label="类型/状态"><div>' + escapeHtml(item.document_type) + '</div><div class="muted">' + escapeHtml(item.document_status) + ' / ' + escapeHtml(item.document_embedding_status) + '</div></td>'
|
||||||
|
+ '<td data-label="附件链接">' + renderLinks(item) + '</td>'
|
||||||
|
+ '<td data-label="更新时间"><span class="muted">' + escapeHtml(item.updated) + '</span></td>'
|
||||||
|
+ '<td data-label="操作"><button class="btn btn-danger" type="button" onclick="window.__deleteDocument(\\'' + encodeURIComponent(item.document_id) + '\\')">删除</button></td>'
|
||||||
|
+ '</tr>'
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDocuments() {
|
||||||
|
setStatus('正在加载文档列表...', '')
|
||||||
|
try {
|
||||||
|
const data = await requestJson('/api/document/list', {})
|
||||||
|
state.list = data.items || []
|
||||||
|
renderTable()
|
||||||
|
setStatus('文档列表已刷新,共 ' + state.list.length + ' 条。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '加载列表失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
fields.documentTitle.value = ''
|
||||||
|
fields.documentType.value = ''
|
||||||
|
fields.documentStatus.value = 'active'
|
||||||
|
fields.embeddingStatus.value = 'pending'
|
||||||
|
fields.effectDate.value = ''
|
||||||
|
fields.expiryDate.value = ''
|
||||||
|
fields.documentSubtitle.value = ''
|
||||||
|
fields.documentSummary.value = ''
|
||||||
|
fields.documentContent.value = ''
|
||||||
|
fields.relationModel.value = ''
|
||||||
|
fields.vectorVersion.value = ''
|
||||||
|
fields.documentKeywords.value = ''
|
||||||
|
fields.productCategories.value = ''
|
||||||
|
fields.applicationScenarios.value = ''
|
||||||
|
fields.hotelType.value = ''
|
||||||
|
fields.documentRemark.value = ''
|
||||||
|
fields.imageFile.value = ''
|
||||||
|
fields.videoFile.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDocument() {
|
||||||
|
if (!fields.documentTitle.value.trim()) {
|
||||||
|
setStatus('请先填写文档标题。', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('正在上传附件并创建文档...', '')
|
||||||
|
|
||||||
|
try {
|
||||||
|
let imageAttachment = null
|
||||||
|
let videoAttachment = null
|
||||||
|
const imageFile = fields.imageFile.files && fields.imageFile.files[0]
|
||||||
|
const videoFile = fields.videoFile.files && fields.videoFile.files[0]
|
||||||
|
|
||||||
|
if (imageFile) {
|
||||||
|
imageAttachment = await uploadAttachment(imageFile, 'image')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoFile) {
|
||||||
|
videoAttachment = await uploadAttachment(videoFile, 'video')
|
||||||
|
}
|
||||||
|
|
||||||
|
await requestJson('/api/document/create', {
|
||||||
|
document_title: fields.documentTitle.value.trim(),
|
||||||
|
document_type: fields.documentType.value.trim(),
|
||||||
|
document_status: fields.documentStatus.value.trim(),
|
||||||
|
document_embedding_status: fields.embeddingStatus.value.trim(),
|
||||||
|
document_effect_date: fields.effectDate.value,
|
||||||
|
document_expiry_date: fields.expiryDate.value,
|
||||||
|
document_subtitle: fields.documentSubtitle.value.trim(),
|
||||||
|
document_summary: fields.documentSummary.value.trim(),
|
||||||
|
document_content: fields.documentContent.value.trim(),
|
||||||
|
document_image: imageAttachment ? imageAttachment.attachments_id : '',
|
||||||
|
document_video: videoAttachment ? videoAttachment.attachments_id : '',
|
||||||
|
document_relation_model: fields.relationModel.value.trim(),
|
||||||
|
document_keywords: fields.documentKeywords.value.trim(),
|
||||||
|
document_vector_version: fields.vectorVersion.value.trim(),
|
||||||
|
document_product_categories: fields.productCategories.value.trim(),
|
||||||
|
document_application_scenarios: fields.applicationScenarios.value.trim(),
|
||||||
|
document_hotel_type: fields.hotelType.value.trim(),
|
||||||
|
document_remark: fields.documentRemark.value.trim(),
|
||||||
|
})
|
||||||
|
|
||||||
|
resetForm()
|
||||||
|
await loadDocuments()
|
||||||
|
setStatus('文档创建成功。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '创建文档失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDocument(documentId) {
|
||||||
|
const target = decodeURIComponent(documentId)
|
||||||
|
if (!window.confirm('确认删除文档「' + target + '」吗?')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('正在删除文档...', '')
|
||||||
|
try {
|
||||||
|
await requestJson('/api/document/delete', { document_id: target })
|
||||||
|
await loadDocuments()
|
||||||
|
setStatus('文档删除成功。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '删除文档失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__deleteDocument = deleteDocument
|
||||||
|
|
||||||
|
document.getElementById('reloadBtn').addEventListener('click', loadDocuments)
|
||||||
|
document.getElementById('submitBtn').addEventListener('click', submitDocument)
|
||||||
|
document.getElementById('resetBtn').addEventListener('click', function () {
|
||||||
|
resetForm()
|
||||||
|
setStatus('表单已重置。', 'success')
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
loadDocuments()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
return e.html(200, html)
|
||||||
|
})
|
||||||
@@ -16,41 +16,31 @@ routerAdd('GET', '/manage', function (e) {
|
|||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
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; }
|
.wrap { max-width: 760px; 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; }
|
.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; }
|
h1 { margin: 0 0 20px; font-size: 32px; }
|
||||||
p { color: #4b5563; line-height: 1.8; }
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
|
||||||
.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; text-align: center; }
|
||||||
.card { background: #f8fafc; border: 1px solid #dbe3f0; border-radius: 18px; padding: 22px; }
|
.card h2 { margin: 0 0 14px; font-size: 20px; }
|
||||||
.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; }
|
.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; }
|
.actions { margin-top: 24px; display: flex; justify-content: flex-start; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="wrap">
|
<main class="wrap">
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<h1>管理主页</h1>
|
<h1>管理主页</h1>
|
||||||
<p>该页面仅供已登录的 ManagePlatform 用户使用。你可以从这里跳转到各个管理子页面。</p>
|
|
||||||
<div class="notice">如未登录或 token 无效,系统会自动跳转到登录页。</div>
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h2>字典管理</h2>
|
<h2>字典管理</h2>
|
||||||
<p>维护 tbl_system_dict,支持模糊查询、行内编辑、枚举项弹窗编辑、新增和删除。</p>
|
|
||||||
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<h2>登录页</h2>
|
<h2>文档管理</h2>
|
||||||
<p>登录入口页。若已登录,访问该页会自动跳回管理主页。</p>
|
<a class="btn" href="/pb/manage/document-manage">进入文档管理</a>
|
||||||
<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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:16px;">
|
<div class="actions">
|
||||||
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -68,4 +58,4 @@ routerAdd('GET', '/manage', function (e) {
|
|||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
return e.html(200, html)
|
return e.html(200, html)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -155,4 +155,3 @@ function renderLoginPage(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routerAdd('GET', '/manage/login', renderLoginPage)
|
routerAdd('GET', '/manage/login', renderLoginPage)
|
||||||
routerAdd('GET', '/manage/page-a', renderLoginPage)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
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 刷新、认证落库、错误可观测性、索引策略、字典管理、页面辅助操作、健康检查版本探针、OpenAPI 鉴权与统一响应规范。
|
本次变更覆盖 `pocket-base/` 下 PocketBase hooks 项目的微信登录、平台注册/登录、资料更新、token 刷新、认证落库、错误可观测性、索引策略、字典管理、附件管理、文档管理、文档操作历史、页面辅助操作、健康检查版本探针、OpenAPI 鉴权与统一响应规范。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -144,6 +144,16 @@
|
|||||||
- `POST /api/dictionary/create`
|
- `POST /api/dictionary/create`
|
||||||
- `POST /api/dictionary/update`
|
- `POST /api/dictionary/update`
|
||||||
- `POST /api/dictionary/delete`
|
- `POST /api/dictionary/delete`
|
||||||
|
- `POST /api/attachment/list`
|
||||||
|
- `POST /api/attachment/detail`
|
||||||
|
- `POST /api/attachment/upload`
|
||||||
|
- `POST /api/attachment/delete`
|
||||||
|
- `POST /api/document/list`
|
||||||
|
- `POST /api/document/detail`
|
||||||
|
- `POST /api/document/create`
|
||||||
|
- `POST /api/document/update`
|
||||||
|
- `POST /api/document/delete`
|
||||||
|
- `POST /api/document-history/list`
|
||||||
|
|
||||||
其中平台用户链路补充为:
|
其中平台用户链路补充为:
|
||||||
|
|
||||||
@@ -209,16 +219,100 @@
|
|||||||
- `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。
|
- `dict_word_enum`、`dict_word_description`、`dict_word_sort_order` 已统一改为 JSON 字符串持久化,其中 `dict_word_sort_order` 已改为 `text` 类型。
|
||||||
- 查询时统一聚合为:`items: [{ enum, description, sortOrder }]`
|
- 查询时统一聚合为:`items: [{ enum, description, sortOrder }]`
|
||||||
|
|
||||||
|
### 附件管理接口
|
||||||
|
|
||||||
|
新增 `attachment` 分类接口,统一要求平台管理用户访问:
|
||||||
|
|
||||||
|
- `POST /api/attachment/list`
|
||||||
|
- 支持按 `attachments_id`、`attachments_filename` 模糊搜索
|
||||||
|
- 支持按 `attachments_status` 过滤
|
||||||
|
- 返回附件元数据以及 PocketBase 文件流链接 `attachments_url`
|
||||||
|
- `POST /api/attachment/detail`
|
||||||
|
- 按 `attachments_id` 查询单个附件
|
||||||
|
- 返回文件流链接与下载链接
|
||||||
|
- `POST /api/attachment/upload`
|
||||||
|
- 使用 `multipart/form-data`
|
||||||
|
- 文件字段固定为 `attachments_link`
|
||||||
|
- 上传成功后自动生成 `attachments_id`
|
||||||
|
- 自动写入 `attachments_owner = 当前用户 openid`
|
||||||
|
- `POST /api/attachment/delete`
|
||||||
|
- 按 `attachments_id` 真删除附件
|
||||||
|
- 若该附件已被 `tbl_document.document_image` 或 `document_video` 引用,则拒绝删除
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `tbl_attachments.attachments_link` 为 PocketBase `file` 字段,保存实际文件本体。
|
||||||
|
- 对外查询时会额外补充:
|
||||||
|
- `attachments_url`
|
||||||
|
- `attachments_download_url`
|
||||||
|
|
||||||
|
### 文档管理接口
|
||||||
|
|
||||||
|
新增 `document` 分类接口,统一要求平台管理用户访问:
|
||||||
|
|
||||||
|
- `POST /api/document/list`
|
||||||
|
- 支持按 `document_id`、`document_title`、`document_subtitle`、`document_summary`、`document_keywords` 模糊搜索
|
||||||
|
- 支持按 `document_status`、`document_type` 过滤
|
||||||
|
- 返回时会自动联查 `tbl_attachments`
|
||||||
|
- 额外补充:
|
||||||
|
- `document_image_url`
|
||||||
|
- `document_video_url`
|
||||||
|
- `document_image_attachment`
|
||||||
|
- `document_video_attachment`
|
||||||
|
- `POST /api/document/detail`
|
||||||
|
- 按 `document_id` 查询单条文档
|
||||||
|
- 返回与附件表联动解析后的文件流链接
|
||||||
|
- `POST /api/document/create`
|
||||||
|
- 新增文档
|
||||||
|
- `document_id` 可不传,由服务端自动生成
|
||||||
|
- `document_image`、`document_video` 必须传入已存在的 `attachments_id`
|
||||||
|
- 成功后会写入一条文档操作历史,类型为 `create`
|
||||||
|
- `POST /api/document/update`
|
||||||
|
- 按 `document_id` 更新文档
|
||||||
|
- 若传入附件字段,则会校验对应 `attachments_id` 是否存在
|
||||||
|
- 成功后会写入一条文档操作历史,类型为 `update`
|
||||||
|
- `POST /api/document/delete`
|
||||||
|
- 按 `document_id` 真删除文档
|
||||||
|
- 删除前会写入一条文档操作历史,类型为 `delete`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `document_image`、`document_video` 当前保存的是 `attachments_id`,不是 PocketBase 文件字段。
|
||||||
|
- 文档查询时通过 `attachments_id -> tbl_attachments` 反查实际文件,并返回可直接访问的数据流链接。
|
||||||
|
- `document_owner` 语义为“上传者 openid”。
|
||||||
|
|
||||||
|
### 文档操作历史接口
|
||||||
|
|
||||||
|
新增 `document-history` 分类接口,统一要求平台管理用户访问:
|
||||||
|
|
||||||
|
- `POST /api/document-history/list`
|
||||||
|
- 不传 `document_id` 时返回全部文档历史
|
||||||
|
- 传入 `document_id` 时仅返回该文档历史
|
||||||
|
- 结果按创建时间倒序排列
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 操作历史表为 `tbl_document_operation_history`
|
||||||
|
- 当前由文档新增、修改、删除接口自动写入
|
||||||
|
- 主要字段为:
|
||||||
|
- `doh_document_id`
|
||||||
|
- `doh_operation_type`
|
||||||
|
- `doh_user_id`
|
||||||
|
- `doh_current_count`
|
||||||
|
- `doh_remark`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 七、页面与运维辅助能力新增
|
## 七、页面与运维辅助能力新增
|
||||||
|
|
||||||
### 1. PocketBase 页面
|
### 1. PocketBase 页面
|
||||||
|
|
||||||
新增页面:
|
当前页面入口:
|
||||||
|
|
||||||
- `/web`
|
- `/pb/manage`
|
||||||
- `/web/dictionary-manage`
|
- `/pb/manage/login`
|
||||||
|
- `/pb/manage/dictionary-manage`
|
||||||
|
- `/pb/manage/document-manage`
|
||||||
|
|
||||||
页面能力:
|
页面能力:
|
||||||
|
|
||||||
@@ -231,6 +325,22 @@
|
|||||||
- 弹窗编辑枚举项
|
- 弹窗编辑枚举项
|
||||||
- 新增 / 删除字典
|
- 新增 / 删除字典
|
||||||
- 返回主页
|
- 返回主页
|
||||||
|
- 文档管理页支持:
|
||||||
|
- 先上传附件到 `tbl_attachments`
|
||||||
|
- 再把返回的 `attachments_id` 写入 `tbl_document.document_image` / `document_video`
|
||||||
|
- 新增文档
|
||||||
|
- 查询文档列表
|
||||||
|
- 直接展示 PocketBase 文件流链接
|
||||||
|
- 删除文档
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 原页面 `page-b.js` 已替换为 `document-manage.js`
|
||||||
|
- 页面实际走的接口链路为:
|
||||||
|
- `/api/attachment/upload`
|
||||||
|
- `/api/document/create`
|
||||||
|
- `/api/document/list`
|
||||||
|
- `/api/document/delete`
|
||||||
|
|
||||||
### 2. 健康检查版本探针
|
### 2. 健康检查版本探针
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ info:
|
|||||||
请在 Apifox 环境中统一设置全局 Header:`Authorization: Bearer {{token}}`。
|
请在 Apifox 环境中统一设置全局 Header:`Authorization: Bearer {{token}}`。
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
servers:
|
servers:
|
||||||
- url: https://bai-api.blv-oa.com/pb
|
- url: https://bai-api.blv-oa.com
|
||||||
description: 生产环境
|
description: 生产环境
|
||||||
- url: http://localhost:8090
|
- url: http://localhost:8090
|
||||||
description: PocketBase 本地环境
|
description: PocketBase 本地环境
|
||||||
@@ -22,6 +22,12 @@ tags:
|
|||||||
description: 面向平台用户的认证接口;平台用户会生成 GUID 并写入统一 `openid` 字段。
|
description: 面向平台用户的认证接口;平台用户会生成 GUID 并写入统一 `openid` 字段。
|
||||||
- name: 字典管理
|
- name: 字典管理
|
||||||
description: 面向 ManagePlatform 用户的系统字典维护接口。
|
description: 面向 ManagePlatform 用户的系统字典维护接口。
|
||||||
|
- name: 附件管理
|
||||||
|
description: 面向 ManagePlatform 用户的附件上传、查询与删除接口。
|
||||||
|
- name: 文档管理
|
||||||
|
description: 面向 ManagePlatform 用户的文档新增、查询、修改、删除接口;查询时会自动返回关联附件的 PocketBase 文件流链接。
|
||||||
|
- name: 文档历史
|
||||||
|
description: 面向 ManagePlatform 用户的文档操作历史查询接口。
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
ApiResponse:
|
ApiResponse:
|
||||||
@@ -358,8 +364,286 @@ components:
|
|||||||
dict_name:
|
dict_name:
|
||||||
type: string
|
type: string
|
||||||
example: 用户状态
|
example: 用户状态
|
||||||
|
AttachmentRecord:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pb_id:
|
||||||
|
type: string
|
||||||
|
attachments_id:
|
||||||
|
type: string
|
||||||
|
attachments_link:
|
||||||
|
type: string
|
||||||
|
description: PocketBase 实际存储的文件名
|
||||||
|
attachments_url:
|
||||||
|
type: string
|
||||||
|
description: 附件文件流访问链接
|
||||||
|
attachments_download_url:
|
||||||
|
type: string
|
||||||
|
description: 附件下载链接
|
||||||
|
attachments_filename:
|
||||||
|
type: string
|
||||||
|
attachments_filetype:
|
||||||
|
type: string
|
||||||
|
attachments_size:
|
||||||
|
type: number
|
||||||
|
attachments_owner:
|
||||||
|
type: string
|
||||||
|
attachments_md5:
|
||||||
|
type: string
|
||||||
|
attachments_ocr:
|
||||||
|
type: string
|
||||||
|
attachments_status:
|
||||||
|
type: string
|
||||||
|
attachments_remark:
|
||||||
|
type: string
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
updated:
|
||||||
|
type: string
|
||||||
|
AttachmentListRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
keyword:
|
||||||
|
type: string
|
||||||
|
description: 对 `attachments_id`、`attachments_filename` 的模糊搜索关键字
|
||||||
|
example: 手册
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: 按附件状态过滤
|
||||||
|
example: active
|
||||||
|
AttachmentDetailRequest:
|
||||||
|
type: object
|
||||||
|
required: [attachments_id]
|
||||||
|
properties:
|
||||||
|
attachments_id:
|
||||||
|
type: string
|
||||||
|
example: ATT-1743037200000-abc123
|
||||||
|
AttachmentUploadRequest:
|
||||||
|
type: object
|
||||||
|
required: [attachments_link]
|
||||||
|
properties:
|
||||||
|
attachments_link:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
description: 要上传到 `tbl_attachments` 的单个文件
|
||||||
|
attachments_filename:
|
||||||
|
type: string
|
||||||
|
description: 原始文件名;不传时可由前端直接使用文件名
|
||||||
|
attachments_filetype:
|
||||||
|
type: string
|
||||||
|
description: 文件 MIME 类型
|
||||||
|
attachments_size:
|
||||||
|
type: number
|
||||||
|
description: 文件大小
|
||||||
|
attachments_md5:
|
||||||
|
type: string
|
||||||
|
attachments_ocr:
|
||||||
|
type: string
|
||||||
|
attachments_status:
|
||||||
|
type: string
|
||||||
|
example: active
|
||||||
|
attachments_remark:
|
||||||
|
type: string
|
||||||
|
DocumentRecord:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pb_id:
|
||||||
|
type: string
|
||||||
|
document_id:
|
||||||
|
type: string
|
||||||
|
document_effect_date:
|
||||||
|
type: string
|
||||||
|
document_expiry_date:
|
||||||
|
type: string
|
||||||
|
document_type:
|
||||||
|
type: string
|
||||||
|
document_title:
|
||||||
|
type: string
|
||||||
|
document_subtitle:
|
||||||
|
type: string
|
||||||
|
document_summary:
|
||||||
|
type: string
|
||||||
|
document_content:
|
||||||
|
type: string
|
||||||
|
document_image:
|
||||||
|
type: string
|
||||||
|
description: 关联的 `attachments_id`
|
||||||
|
document_image_url:
|
||||||
|
type: string
|
||||||
|
description: 根据 `document_image -> tbl_attachments` 自动解析出的图片文件流链接
|
||||||
|
document_image_attachment:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/AttachmentRecord'
|
||||||
|
nullable: true
|
||||||
|
document_video:
|
||||||
|
type: string
|
||||||
|
description: 关联的 `attachments_id`
|
||||||
|
document_video_url:
|
||||||
|
type: string
|
||||||
|
description: 根据 `document_video -> tbl_attachments` 自动解析出的视频文件流链接
|
||||||
|
document_video_attachment:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/AttachmentRecord'
|
||||||
|
nullable: true
|
||||||
|
document_owner:
|
||||||
|
type: string
|
||||||
|
description: 上传者 openid
|
||||||
|
document_relation_model:
|
||||||
|
type: string
|
||||||
|
document_keywords:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_share_count:
|
||||||
|
type: number
|
||||||
|
document_download_count:
|
||||||
|
type: number
|
||||||
|
document_favorite_count:
|
||||||
|
type: number
|
||||||
|
document_status:
|
||||||
|
type: string
|
||||||
|
document_embedding_status:
|
||||||
|
type: string
|
||||||
|
document_embedding_error:
|
||||||
|
type: string
|
||||||
|
document_embedding_lasttime:
|
||||||
|
type: string
|
||||||
|
document_vector_version:
|
||||||
|
type: string
|
||||||
|
document_product_categories:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_application_scenarios:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_hotel_type:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_remark:
|
||||||
|
type: string
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
updated:
|
||||||
|
type: string
|
||||||
|
DocumentListRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
keyword:
|
||||||
|
type: string
|
||||||
|
description: 对 `document_id`、`document_title`、`document_subtitle`、`document_summary`、`document_keywords` 的模糊搜索关键字
|
||||||
|
example: 安装
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
example: active
|
||||||
|
document_type:
|
||||||
|
type: string
|
||||||
|
example: 说明书
|
||||||
|
DocumentDetailRequest:
|
||||||
|
type: object
|
||||||
|
required: [document_id]
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
type: string
|
||||||
|
example: DOC-1743037200000-abc123
|
||||||
|
DocumentMutationRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
type: string
|
||||||
|
description: 创建时可不传,由服务端自动生成;更新时必填
|
||||||
|
example: DOC-1743037200000-abc123
|
||||||
|
document_effect_date:
|
||||||
|
type: string
|
||||||
|
description: 支持 `YYYY-MM-DD` 或 PocketBase 可识别日期时间字符串
|
||||||
|
example: 2026-03-27
|
||||||
|
document_expiry_date:
|
||||||
|
type: string
|
||||||
|
description: 支持 `YYYY-MM-DD` 或 PocketBase 可识别日期时间字符串
|
||||||
|
example: 2027-03-27
|
||||||
|
document_type:
|
||||||
|
type: string
|
||||||
|
document_title:
|
||||||
|
type: string
|
||||||
|
document_subtitle:
|
||||||
|
type: string
|
||||||
|
document_summary:
|
||||||
|
type: string
|
||||||
|
document_content:
|
||||||
|
type: string
|
||||||
|
document_image:
|
||||||
|
type: string
|
||||||
|
description: 图片附件的 `attachments_id`
|
||||||
|
document_video:
|
||||||
|
type: string
|
||||||
|
description: 视频附件的 `attachments_id`
|
||||||
|
document_relation_model:
|
||||||
|
type: string
|
||||||
|
document_keywords:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_share_count:
|
||||||
|
type: number
|
||||||
|
document_download_count:
|
||||||
|
type: number
|
||||||
|
document_favorite_count:
|
||||||
|
type: number
|
||||||
|
document_status:
|
||||||
|
type: string
|
||||||
|
document_embedding_status:
|
||||||
|
type: string
|
||||||
|
document_embedding_error:
|
||||||
|
type: string
|
||||||
|
document_embedding_lasttime:
|
||||||
|
type: string
|
||||||
|
document_vector_version:
|
||||||
|
type: string
|
||||||
|
document_product_categories:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_application_scenarios:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_hotel_type:
|
||||||
|
type: string
|
||||||
|
description: 多值字段,使用 `|` 分隔
|
||||||
|
document_remark:
|
||||||
|
type: string
|
||||||
|
DocumentDeleteRequest:
|
||||||
|
type: object
|
||||||
|
required: [document_id]
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
type: string
|
||||||
|
example: DOC-1743037200000-abc123
|
||||||
|
DocumentHistoryRecord:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pb_id:
|
||||||
|
type: string
|
||||||
|
doh_id:
|
||||||
|
type: string
|
||||||
|
doh_document_id:
|
||||||
|
type: string
|
||||||
|
doh_operation_type:
|
||||||
|
type: string
|
||||||
|
doh_user_id:
|
||||||
|
type: string
|
||||||
|
doh_current_count:
|
||||||
|
type: number
|
||||||
|
doh_remark:
|
||||||
|
type: string
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
updated:
|
||||||
|
type: string
|
||||||
|
DocumentHistoryListRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
type: string
|
||||||
|
description: 可选;传入时仅查询指定文档的操作历史
|
||||||
|
example: DOC-1743037200000-abc123
|
||||||
paths:
|
paths:
|
||||||
/api/system/test-helloworld:
|
/pb/api/system/test-helloworld:
|
||||||
post:
|
post:
|
||||||
tags: [系统]
|
tags: [系统]
|
||||||
summary: HelloWorld 测试接口
|
summary: HelloWorld 测试接口
|
||||||
@@ -375,7 +659,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/HelloWorldData'
|
$ref: '#/components/schemas/HelloWorldData'
|
||||||
/api/system/health:
|
/pb/api/system/health:
|
||||||
post:
|
post:
|
||||||
tags: [系统]
|
tags: [系统]
|
||||||
summary: 健康检查
|
summary: 健康检查
|
||||||
@@ -391,7 +675,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/HealthData'
|
$ref: '#/components/schemas/HealthData'
|
||||||
/api/system/users-count:
|
/pb/api/system/users-count:
|
||||||
post:
|
post:
|
||||||
tags: [系统]
|
tags: [系统]
|
||||||
summary: 查询用户总数
|
summary: 查询用户总数
|
||||||
@@ -408,7 +692,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/UsersCountData'
|
$ref: '#/components/schemas/UsersCountData'
|
||||||
/api/system/refresh-token:
|
/pb/api/system/refresh-token:
|
||||||
post:
|
post:
|
||||||
tags: [系统]
|
tags: [系统]
|
||||||
summary: 刷新系统认证 token
|
summary: 刷新系统认证 token
|
||||||
@@ -451,7 +735,7 @@ paths:
|
|||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
/api/wechat/login:
|
/pb/api/wechat/login:
|
||||||
post:
|
post:
|
||||||
tags: [微信认证]
|
tags: [微信认证]
|
||||||
summary: 微信登录/注册合一
|
summary: 微信登录/注册合一
|
||||||
@@ -483,7 +767,7 @@ paths:
|
|||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
'500':
|
'500':
|
||||||
description: 保存 auth 用户失败或服务端内部错误
|
description: 保存 auth 用户失败或服务端内部错误
|
||||||
/api/platform/register:
|
/pb/api/platform/register:
|
||||||
post:
|
post:
|
||||||
tags: [平台认证]
|
tags: [平台认证]
|
||||||
summary: 平台用户注册
|
summary: 平台用户注册
|
||||||
@@ -513,7 +797,7 @@ paths:
|
|||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
/api/platform/login:
|
/pb/api/platform/login:
|
||||||
post:
|
post:
|
||||||
tags: [平台认证]
|
tags: [平台认证]
|
||||||
summary: 平台用户登录
|
summary: 平台用户登录
|
||||||
@@ -575,7 +859,7 @@ paths:
|
|||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
/api/wechat/profile:
|
/pb/api/wechat/profile:
|
||||||
post:
|
post:
|
||||||
tags: [微信认证]
|
tags: [微信认证]
|
||||||
summary: 更新微信用户资料
|
summary: 更新微信用户资料
|
||||||
@@ -604,7 +888,7 @@ paths:
|
|||||||
description: token 无效或当前 auth record 缺少统一身份字段 openid
|
description: token 无效或当前 auth record 缺少统一身份字段 openid
|
||||||
'400':
|
'400':
|
||||||
description: 参数错误、手机号已被注册或资料更新失败
|
description: 参数错误、手机号已被注册或资料更新失败
|
||||||
/api/dictionary/list:
|
/pb/api/dictionary/list:
|
||||||
post:
|
post:
|
||||||
tags: [字典管理]
|
tags: [字典管理]
|
||||||
summary: 查询字典列表
|
summary: 查询字典列表
|
||||||
@@ -640,7 +924,7 @@ paths:
|
|||||||
description: 非 ManagePlatform 用户无权访问
|
description: 非 ManagePlatform 用户无权访问
|
||||||
'415':
|
'415':
|
||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
/api/dictionary/detail:
|
/pb/api/dictionary/detail:
|
||||||
post:
|
post:
|
||||||
tags: [字典管理]
|
tags: [字典管理]
|
||||||
summary: 查询指定字典
|
summary: 查询指定字典
|
||||||
@@ -675,7 +959,7 @@ paths:
|
|||||||
description: 未找到对应字典
|
description: 未找到对应字典
|
||||||
'415':
|
'415':
|
||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
/api/dictionary/create:
|
/pb/api/dictionary/create:
|
||||||
post:
|
post:
|
||||||
tags: [字典管理]
|
tags: [字典管理]
|
||||||
summary: 新增字典
|
summary: 新增字典
|
||||||
@@ -711,7 +995,7 @@ paths:
|
|||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
/api/dictionary/update:
|
/pb/api/dictionary/update:
|
||||||
post:
|
post:
|
||||||
tags: [字典管理]
|
tags: [字典管理]
|
||||||
summary: 修改字典
|
summary: 修改字典
|
||||||
@@ -748,7 +1032,7 @@ paths:
|
|||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
/api/dictionary/delete:
|
/pb/api/dictionary/delete:
|
||||||
post:
|
post:
|
||||||
tags: [字典管理]
|
tags: [字典管理]
|
||||||
summary: 删除字典
|
summary: 删除字典
|
||||||
@@ -788,3 +1072,370 @@ paths:
|
|||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
'429':
|
'429':
|
||||||
description: 重复请求过于频繁
|
description: 重复请求过于频繁
|
||||||
|
/pb/api/attachment/list:
|
||||||
|
post:
|
||||||
|
tags: [附件管理]
|
||||||
|
summary: 查询附件列表
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
支持按 `attachments_id`、`attachments_filename` 模糊搜索,并可按 `attachments_status` 过滤。
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AttachmentListRequest'
|
||||||
|
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/AttachmentRecord'
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
/pb/api/attachment/detail:
|
||||||
|
post:
|
||||||
|
tags: [附件管理]
|
||||||
|
summary: 查询附件详情
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
按 `attachments_id` 查询单条附件,并返回 PocketBase 文件流链接与下载链接。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AttachmentDetailRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/AttachmentRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'404':
|
||||||
|
description: 未找到对应附件
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
/pb/api/attachment/upload:
|
||||||
|
post:
|
||||||
|
tags: [附件管理]
|
||||||
|
summary: 上传附件
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
使用 `multipart/form-data` 上传单个文件到 `tbl_attachments`,服务端会自动生成 `attachments_id`,
|
||||||
|
并返回可直接访问的 PocketBase 文件流链接。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AttachmentUploadRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 上传成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/AttachmentRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、缺少文件或附件保存失败
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
/pb/api/attachment/delete:
|
||||||
|
post:
|
||||||
|
tags: [附件管理]
|
||||||
|
summary: 删除附件
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
按 `attachments_id` 真删除附件;若附件已被 `tbl_document.document_image` 或 `document_video` 引用,则拒绝删除。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AttachmentDetailRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 删除成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
attachments_id:
|
||||||
|
type: string
|
||||||
|
'400':
|
||||||
|
description: 参数错误、附件已被文档引用或删除失败
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'404':
|
||||||
|
description: 未找到待删除附件
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
/pb/api/document/list:
|
||||||
|
post:
|
||||||
|
tags: [文档管理]
|
||||||
|
summary: 查询文档列表
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
支持按标题、摘要、关键词等字段模糊搜索,并可按 `document_status`、`document_type` 过滤。
|
||||||
|
返回结果会自动根据 `document_image`、`document_video` 关联 `tbl_attachments`,
|
||||||
|
额外补充 `document_image_url`、`document_video_url` 以及对应附件对象。
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DocumentListRequest'
|
||||||
|
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/DocumentRecord'
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
/pb/api/document/detail:
|
||||||
|
post:
|
||||||
|
tags: [文档管理]
|
||||||
|
summary: 查询文档详情
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
按 `document_id` 查询单条文档,并返回与附件表联动解析后的文件流链接。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DocumentDetailRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/DocumentRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'404':
|
||||||
|
description: 未找到对应文档
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
/pb/api/document/create:
|
||||||
|
post:
|
||||||
|
tags: [文档管理]
|
||||||
|
summary: 新增文档
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
`document_id` 可选;未传时服务端自动生成。
|
||||||
|
`document_image`、`document_video` 需传入已存在于 `tbl_attachments` 的 `attachments_id`。
|
||||||
|
成功后会同步写入一条 `tbl_document_operation_history`,操作类型为 `create`。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DocumentMutationRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 新增成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/DocumentRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、附件不存在或文档创建失败
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
/pb/api/document/update:
|
||||||
|
post:
|
||||||
|
tags: [文档管理]
|
||||||
|
summary: 修改文档
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
按 `document_id` 定位现有文档并更新;若传入 `document_image`、`document_video`,则必须是已存在的 `attachments_id`。
|
||||||
|
成功后会同步写入一条 `tbl_document_operation_history`,操作类型为 `update`。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DocumentMutationRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 修改成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/DocumentRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、附件不存在或修改失败
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'404':
|
||||||
|
description: 未找到待修改文档
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
/pb/api/document/delete:
|
||||||
|
post:
|
||||||
|
tags: [文档管理]
|
||||||
|
summary: 删除文档
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
按 `document_id` 真删除文档,并在删除前同步写入一条 `tbl_document_operation_history`,操作类型为 `delete`。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DocumentDeleteRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 删除成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ApiResponse'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
type: string
|
||||||
|
'400':
|
||||||
|
description: 参数错误或删除失败
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'404':
|
||||||
|
description: 未找到待删除文档
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
/pb/api/document-history/list:
|
||||||
|
post:
|
||||||
|
tags: [文档历史]
|
||||||
|
summary: 查询文档操作历史
|
||||||
|
description: |
|
||||||
|
仅允许 `ManagePlatform` 用户访问。
|
||||||
|
若 body 传入 `document_id`,则仅查询该文档的历史;否则返回全部文档操作历史,按创建时间倒序排列。
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/DocumentHistoryListRequest'
|
||||||
|
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/DocumentHistoryRecord'
|
||||||
|
'401':
|
||||||
|
description: token 无效或已过期
|
||||||
|
'403':
|
||||||
|
description: 非 ManagePlatform 用户无权访问
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"main": "pocketbase.js",
|
"main": "pocketbase.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"init:newpb": "node pocketbase.newpb.js"
|
"init:newpb": "node pocketbase.newpb.js",
|
||||||
|
"init:documents": "node pocketbase.documents.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
272
script/pocketbase.documents.js
Normal file
272
script/pocketbase.documents.js
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { createRequire } from 'module';
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
let runtimeConfig = {};
|
||||||
|
try {
|
||||||
|
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
|
||||||
|
} catch (_error) {
|
||||||
|
runtimeConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, '');
|
||||||
|
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
|
||||||
|
|
||||||
|
if (!AUTH_TOKEN) {
|
||||||
|
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行建表。');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pb = new PocketBase(PB_URL);
|
||||||
|
|
||||||
|
const collections = [
|
||||||
|
{
|
||||||
|
name: 'tbl_attachments',
|
||||||
|
type: 'base',
|
||||||
|
fields: [
|
||||||
|
{ name: 'attachments_id', type: 'text', required: true },
|
||||||
|
{ name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 104857600, mimeTypes: [] },
|
||||||
|
{ name: 'attachments_filename', type: 'text' },
|
||||||
|
{ name: 'attachments_filetype', type: 'text' },
|
||||||
|
{ name: 'attachments_size', type: 'number' },
|
||||||
|
{ name: 'attachments_owner', type: 'text' },
|
||||||
|
{ name: 'attachments_md5', type: 'text' },
|
||||||
|
{ name: 'attachments_ocr', type: 'text' },
|
||||||
|
{ name: 'attachments_status', type: 'text' },
|
||||||
|
{ name: 'attachments_remark', type: 'text' },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE UNIQUE INDEX idx_tbl_attachments_attachments_id ON tbl_attachments (attachments_id)',
|
||||||
|
'CREATE INDEX idx_tbl_attachments_attachments_owner ON tbl_attachments (attachments_owner)',
|
||||||
|
'CREATE INDEX idx_tbl_attachments_attachments_status ON tbl_attachments (attachments_status)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tbl_document',
|
||||||
|
type: 'base',
|
||||||
|
fields: [
|
||||||
|
{ name: 'document_id', type: 'text', required: true },
|
||||||
|
{ name: 'document_effect_date', type: 'date' },
|
||||||
|
{ name: 'document_expiry_date', type: 'date' },
|
||||||
|
{ name: 'document_type', type: 'text' },
|
||||||
|
{ name: 'document_title', type: 'text' },
|
||||||
|
{ name: 'document_subtitle', type: 'text' },
|
||||||
|
{ name: 'document_summary', type: 'text' },
|
||||||
|
{ name: 'document_content', type: 'text' },
|
||||||
|
{ name: 'document_image', type: 'text' },
|
||||||
|
{ name: 'document_video', type: 'text' },
|
||||||
|
{ name: 'document_owner', type: 'text' },
|
||||||
|
{ name: 'document_relation_model', type: 'text' },
|
||||||
|
{ name: 'document_keywords', type: 'text' },
|
||||||
|
{ name: 'document_share_count', type: 'number' },
|
||||||
|
{ name: 'document_download_count', type: 'number' },
|
||||||
|
{ name: 'document_favorite_count', type: 'number' },
|
||||||
|
{ name: 'document_status', type: 'text' },
|
||||||
|
{ name: 'document_embedding_status', type: 'text' },
|
||||||
|
{ name: 'document_embedding_error', type: 'text' },
|
||||||
|
{ name: 'document_embedding_lasttime', type: 'date' },
|
||||||
|
{ name: 'document_vector_version', type: 'text' },
|
||||||
|
{ name: 'document_product_categories', type: 'text' },
|
||||||
|
{ name: 'document_application_scenarios', type: 'text' },
|
||||||
|
{ name: 'document_hotel_type', type: 'text' },
|
||||||
|
{ name: 'document_remark', type: 'text' },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)',
|
||||||
|
'CREATE INDEX idx_tbl_document_document_owner ON tbl_document (document_owner)',
|
||||||
|
'CREATE INDEX idx_tbl_document_document_type ON tbl_document (document_type)',
|
||||||
|
'CREATE INDEX idx_tbl_document_document_status ON tbl_document (document_status)',
|
||||||
|
'CREATE INDEX idx_tbl_document_document_embedding_status ON tbl_document (document_embedding_status)',
|
||||||
|
'CREATE INDEX idx_tbl_document_document_effect_date ON tbl_document (document_effect_date)',
|
||||||
|
'CREATE INDEX idx_tbl_document_document_expiry_date ON tbl_document (document_expiry_date)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tbl_document_operation_history',
|
||||||
|
type: 'base',
|
||||||
|
fields: [
|
||||||
|
{ name: 'doh_id', type: 'text', required: true },
|
||||||
|
{ name: 'doh_document_id', type: 'text', required: true },
|
||||||
|
{ name: 'doh_operation_type', type: 'text' },
|
||||||
|
{ name: 'doh_user_id', type: 'text' },
|
||||||
|
{ name: 'doh_current_count', type: 'number' },
|
||||||
|
{ name: 'doh_remark', type: 'text' },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE UNIQUE INDEX idx_tbl_document_operation_history_doh_id ON tbl_document_operation_history (doh_id)',
|
||||||
|
'CREATE INDEX idx_tbl_document_operation_history_doh_document_id ON tbl_document_operation_history (doh_document_id)',
|
||||||
|
'CREATE INDEX idx_tbl_document_operation_history_doh_user_id ON tbl_document_operation_history (doh_user_id)',
|
||||||
|
'CREATE INDEX idx_tbl_document_operation_history_doh_operation_type ON tbl_document_operation_history (doh_operation_type)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeFieldPayload(field, existingField) {
|
||||||
|
const payload = existingField
|
||||||
|
? Object.assign({}, existingField)
|
||||||
|
: {
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingField && existingField.id) {
|
||||||
|
payload.id = existingField.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.name = field.name;
|
||||||
|
payload.type = field.type;
|
||||||
|
|
||||||
|
if (typeof field.required !== 'undefined') {
|
||||||
|
payload.required = field.required;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'file') {
|
||||||
|
payload.maxSelect = typeof field.maxSelect === 'number' ? field.maxSelect : 0;
|
||||||
|
payload.maxSize = typeof field.maxSize === 'number' ? field.maxSize : 0;
|
||||||
|
payload.mimeTypes = Array.isArray(field.mimeTypes) && field.mimeTypes.length ? field.mimeTypes : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCollectionPayload(collectionData, existingCollection) {
|
||||||
|
if (!existingCollection) {
|
||||||
|
return {
|
||||||
|
name: collectionData.name,
|
||||||
|
type: collectionData.type,
|
||||||
|
fields: collectionData.fields.map((field) => normalizeFieldPayload(field, null)),
|
||||||
|
indexes: collectionData.indexes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field]));
|
||||||
|
const fields = (existingCollection.fields || []).map((existingField) => {
|
||||||
|
const targetField = targetFieldMap.get(existingField.name);
|
||||||
|
if (!targetField) {
|
||||||
|
return existingField;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFieldMap.delete(existingField.name);
|
||||||
|
return normalizeFieldPayload(targetField, existingField);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const field of targetFieldMap.values()) {
|
||||||
|
fields.push(normalizeFieldPayload(field, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: collectionData.name,
|
||||||
|
type: collectionData.type,
|
||||||
|
fields: fields,
|
||||||
|
indexes: collectionData.indexes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFieldList(fields) {
|
||||||
|
return (fields || []).map((field) => ({
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrUpdateCollection(collectionData) {
|
||||||
|
console.log(`🔄 正在处理表: ${collectionData.name} ...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await pb.collections.getOne(collectionData.name);
|
||||||
|
await pb.collections.update(existing.id, buildCollectionPayload(collectionData, existing));
|
||||||
|
console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
try {
|
||||||
|
await pb.collections.create(buildCollectionPayload(collectionData, null));
|
||||||
|
console.log(`✅ ${collectionData.name} 创建完成。`);
|
||||||
|
return;
|
||||||
|
} catch (createError) {
|
||||||
|
console.error(`❌ 创建集合 ${collectionData.name} 失败:`, {
|
||||||
|
status: createError.status,
|
||||||
|
message: createError.message,
|
||||||
|
response: createError.response?.data,
|
||||||
|
});
|
||||||
|
throw createError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`❌ 处理集合 ${collectionData.name} 失败:`, {
|
||||||
|
status: error.status,
|
||||||
|
message: error.message,
|
||||||
|
response: error.response?.data,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCollections(targetCollections) {
|
||||||
|
console.log('\n🔍 开始校验文档相关表结构与索引...');
|
||||||
|
|
||||||
|
for (const target of targetCollections) {
|
||||||
|
const remote = await pb.collections.getOne(target.name);
|
||||||
|
const remoteFields = normalizeFieldList(remote.fields);
|
||||||
|
const targetFields = normalizeFieldList(target.fields);
|
||||||
|
const remoteFieldMap = new Map(remoteFields.map((field) => [field.name, field.type]));
|
||||||
|
const missingFields = [];
|
||||||
|
const mismatchedTypes = [];
|
||||||
|
|
||||||
|
for (const field of targetFields) {
|
||||||
|
if (!remoteFieldMap.has(field.name)) {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteFieldMap.get(field.name) !== field.type) {
|
||||||
|
mismatchedTypes.push(`${field.name}:${remoteFieldMap.get(field.name)}!=${field.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteIndexes = new Set(remote.indexes || []);
|
||||||
|
const missingIndexes = target.indexes.filter((indexSql) => !remoteIndexes.has(indexSql));
|
||||||
|
|
||||||
|
if (remote.type !== target.type) {
|
||||||
|
throw new Error(`${target.name} 类型不匹配,期望 ${target.type},实际 ${remote.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missingFields.length && !mismatchedTypes.length && !missingIndexes.length) {
|
||||||
|
console.log(`✅ ${target.name} 校验通过。`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`❌ ${target.name} 校验失败:`);
|
||||||
|
if (missingFields.length) {
|
||||||
|
console.log(` - 缺失字段: ${missingFields.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (mismatchedTypes.length) {
|
||||||
|
console.log(` - 字段类型不匹配: ${mismatchedTypes.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (missingIndexes.length) {
|
||||||
|
console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${target.name} 结构与预期不一致`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
|
||||||
|
pb.authStore.save(AUTH_TOKEN, null);
|
||||||
|
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
|
||||||
|
|
||||||
|
for (const collectionData of collections) {
|
||||||
|
await createOrUpdateCollection(collectionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await verifyCollections(collections);
|
||||||
|
console.log('\n🎉 文档相关表结构初始化并校验完成!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 初始化失败:', error.response?.data || error.message);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
Reference in New Issue
Block a user