Compare commits

...

3 Commits

Author SHA1 Message Date
e47060f54f feat: 添加微信认证相关接口,包括微信登录和用户资料更新功能
- 新增 wechatLogin 接口,支持使用微信临时凭证 code 登录或注册用户,返回用户信息和 auth token。
- 新增 wechatProfile 接口,支持更新微信用户资料,支持非空字段增量更新。
- 定义相关请求和响应的 schema,包括 WechatLoginRequest、WechatProfileRequest、AuthSuccessResponse 和 WechatProfileResponse。
- 处理不同的响应状态码,包括成功、参数错误、认证失败等。
2026-04-08 09:04:36 +08:00
614c0147e5 Merge branch 'main' of http://blv-rd.tech:3001/Boonlive_RD_Web/Web_BAI_Manage_ApiServer 2026-04-07 20:02:11 +08:00
cd0373be3c feat: 添加系统刷新令牌请求和用户统计响应的 OpenAPI 规范
feat: 添加微信认证相关的 OpenAPI 规范,包括用户信息、登录请求和个人资料请求

feat: 添加 is_delete 字段迁移脚本,支持在集合中添加软删除字段

feat: 添加软删除规则应用脚本,确保所有相关集合的查询规则包含软删除条件

feat: 添加购物车和订单业务 ID 自动生成的迁移脚本,确保字段类型和自动生成规则正确
2026-04-07 20:02:10 +08:00
45 changed files with 8585 additions and 3377 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/back-end/node_modules /back-end/node_modules
/.tmp-upload-probe /.tmp-upload-probe
/.tmp-openapi-validate/node_modules

View File

@@ -0,0 +1,268 @@
const fs = require('fs');
const path = require('path');
const YAML = require('yaml');
const repoRoot = process.cwd();
const rootFile = path.join(repoRoot, 'pocket-base', 'spec', 'openapi-wx.yaml');
const splitDir = path.join(repoRoot, 'pocket-base', 'spec', 'openapi-wx');
const filePaths = [
rootFile,
...fs
.readdirSync(splitDir)
.filter((name) => name.endsWith('.yaml'))
.map((name) => path.join(splitDir, name)),
];
const docs = new Map(
filePaths.map((filePath) => [filePath, YAML.parse(fs.readFileSync(filePath, 'utf8'))]),
);
function decodePointerSegment(segment) {
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
}
function getByPointer(documentData, pointer) {
if (!pointer || pointer === '#' || pointer === '') {
return documentData;
}
const segments = pointer
.replace(/^#/, '')
.split('/')
.filter(Boolean)
.map(decodePointerSegment);
let current = documentData;
for (const segment of segments) {
if (current == null) {
return undefined;
}
current = current[segment];
}
return current;
}
function resolveRef(ref, currentFile) {
const [filePart, hashPart = ''] = ref.split('#');
const targetFile = filePart
? path.resolve(path.dirname(currentFile), filePart)
: currentFile;
const targetDoc = docs.get(targetFile);
if (!targetDoc) {
return { targetFile, targetKey: `${targetFile}#${hashPart}`, schema: undefined };
}
const pointer = hashPart ? `#${hashPart}` : '#';
return {
targetFile,
targetKey: `${targetFile}${pointer}`,
schema: getByPointer(targetDoc, pointer),
};
}
function pickType(typeValue) {
if (Array.isArray(typeValue)) {
return typeValue.find((item) => item !== 'null') || typeValue[0];
}
return typeValue;
}
function cleanLabelText(text, fallback) {
if (!text || typeof text !== 'string') {
return fallback;
}
const normalized = text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.replace(/^[\-\d\.\)\s]+/, '').trim())
.find((line) => line && !/^(可选|必填|说明|注意)[。:]?$/u.test(line));
if (!normalized) {
return fallback;
}
return normalized.replace(/[`"'<>]/g, '').trim() || fallback;
}
function placeholderKey(key) {
return typeof key === 'string' && key.includes('|');
}
function deepMerge(baseValue, nextValue) {
if (
baseValue &&
nextValue &&
typeof baseValue === 'object' &&
typeof nextValue === 'object' &&
!Array.isArray(baseValue) &&
!Array.isArray(nextValue)
) {
const merged = { ...baseValue };
const nextKeys = Object.keys(nextValue);
const hasConcreteNextKeys = nextKeys.some((key) => !placeholderKey(key));
if (hasConcreteNextKeys) {
for (const key of Object.keys(merged)) {
if (placeholderKey(key)) {
delete merged[key];
}
}
}
for (const [key, value] of Object.entries(nextValue)) {
if (key in merged) {
merged[key] = deepMerge(merged[key], value);
} else {
merged[key] = value;
}
}
return merged;
}
return nextValue;
}
function buildLabel(schemaLike, fallback) {
return cleanLabelText(schemaLike?.description || schemaLike?.title, fallback);
}
function fallbackScalar(label, fieldType) {
return `${label}|${fieldType || 'string'}`;
}
function buildExample(schemaLike, currentFile, fieldName = 'field', seenRefs = new Set()) {
if (schemaLike == null) {
return fallbackScalar(fieldName, 'string');
}
if (typeof schemaLike !== 'object' || Array.isArray(schemaLike)) {
return fallbackScalar(fieldName, 'string');
}
if (schemaLike.$ref) {
const { targetFile, targetKey, schema } = resolveRef(schemaLike.$ref, currentFile);
if (!schema) {
return fallbackScalar(fieldName, 'string');
}
if (seenRefs.has(targetKey)) {
return fallbackScalar(fieldName, 'object');
}
const nextSeen = new Set(seenRefs);
nextSeen.add(targetKey);
return buildExample(schema, targetFile, fieldName, nextSeen);
}
if (schemaLike.allOf) {
return schemaLike.allOf.reduce((accumulator, item) => {
const nextValue = buildExample(item, currentFile, fieldName, seenRefs);
return deepMerge(accumulator, nextValue);
}, {});
}
if (schemaLike.oneOf && schemaLike.oneOf.length > 0) {
return buildExample(schemaLike.oneOf[0], currentFile, fieldName, seenRefs);
}
if (schemaLike.anyOf && schemaLike.anyOf.length > 0) {
return buildExample(schemaLike.anyOf[0], currentFile, fieldName, seenRefs);
}
const fieldType =
pickType(schemaLike.type) ||
(schemaLike.properties || schemaLike.additionalProperties ? 'object' : undefined) ||
(schemaLike.items ? 'array' : undefined) ||
'string';
const fieldLabel = buildLabel(schemaLike, fieldName);
if (fieldType === 'object') {
const exampleObject = {};
if (schemaLike.properties && typeof schemaLike.properties === 'object') {
for (const [propertyName, propertySchema] of Object.entries(schemaLike.properties)) {
exampleObject[propertyName] = buildExample(
propertySchema,
currentFile,
propertyName,
seenRefs,
);
}
}
if (Object.keys(exampleObject).length === 0 && schemaLike.additionalProperties) {
exampleObject[`${fieldLabel}字段|string`] = `${fieldLabel}值|string`;
}
if (Object.keys(exampleObject).length === 0) {
exampleObject[`${fieldLabel}|string`] = `${fieldLabel}|string`;
}
return exampleObject;
}
if (fieldType === 'array') {
const itemFieldName = fieldName.endsWith('s') ? fieldName.slice(0, -1) : `${fieldName}_item`;
return [buildExample(schemaLike.items || {}, currentFile, itemFieldName, seenRefs)];
}
return fallbackScalar(fieldLabel, fieldType);
}
function httpMethods(pathItem) {
return ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'].filter(
(method) => pathItem && typeof pathItem[method] === 'object',
);
}
for (const filePath of filePaths.filter((file) => file !== rootFile)) {
const documentData = docs.get(filePath);
if (!documentData || typeof documentData !== 'object') {
continue;
}
if (documentData.components && documentData.components.schemas) {
for (const [schemaName, schemaValue] of Object.entries(documentData.components.schemas)) {
schemaValue.example = buildExample(schemaValue, filePath, schemaName, new Set());
}
}
if (documentData.paths) {
for (const pathItem of Object.values(documentData.paths)) {
for (const method of httpMethods(pathItem)) {
const operation = pathItem[method];
const requestContent = operation?.requestBody?.content;
if (requestContent && typeof requestContent === 'object') {
for (const media of Object.values(requestContent)) {
if (media && media.schema) {
delete media.examples;
media.example = buildExample(media.schema, filePath, 'request', new Set());
}
}
}
const responses = operation?.responses;
if (responses && typeof responses === 'object') {
for (const response of Object.values(responses)) {
const responseContent = response?.content;
if (!responseContent || typeof responseContent !== 'object') {
continue;
}
for (const media of Object.values(responseContent)) {
if (media && media.schema) {
delete media.examples;
media.example = buildExample(media.schema, filePath, 'response', new Set());
}
}
}
}
}
}
}
fs.writeFileSync(filePath, YAML.stringify(documentData, { lineWidth: 0 }), 'utf8');
}
console.log(`Updated examples in ${filePaths.length - 1} split OpenAPI files.`);

View File

@@ -0,0 +1,9 @@
{
"name": "tmp-openapi-validate",
"private": true,
"version": "1.0.0",
"dependencies": {
"@redocly/cli": "^2.25.4",
"yaml": "^2.8.3"
}
}

View File

@@ -23,6 +23,7 @@
| `attachments_ocr` | `text` | 否 | OCR 识别结果 | | `attachments_ocr` | `text` | 否 | OCR 识别结果 |
| `attachments_status` | `text` | 否 | 附件状态 | | `attachments_status` | `text` | 否 | 附件状态 |
| `attachments_remark` | `text` | 否 | 备注 | | `attachments_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -35,5 +36,7 @@
## 补充约定 ## 补充约定
- 图片、视频、普通文件都统一走本表。 - 图片、视频、普通文件都统一走本表。
- `is_delete` 用于软删除控制,附件删除时建议先标记为 `1`,并由后续归档/清理任务处理物理文件。
- 集合默认查询规则已内置 `is_delete = 0`,附件列表/详情默认不返回已软删除数据。
- 业务访问控制不放在本表,而由引用它的业务表与 hooks 接口控制。 - 业务访问控制不放在本表,而由引用它的业务表与 hooks 接口控制。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -17,6 +17,12 @@
| `table_name` | `text` | 是 | 对应数据表名 | | `table_name` | `text` | 是 | 对应数据表名 |
| `column_name` | `text` | 否 | 对应字段名;表级权限时可为空 | | `column_name` | `text` | 否 | 对应字段名;表级权限时可为空 |
| `res_type` | `text` | 是 | 资源类型 | | `res_type` | `text` | 是 | 资源类型 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 补充约定
- `is_delete` 用于软删除控制,资源定义如需停用应优先置为 `1`,避免直接物理删除影响权限审计。
- 集合默认查询规则已内置 `is_delete = 0`,资源列表/详情默认不返回已软删除数据。
## 索引 ## 索引

View File

@@ -18,6 +18,7 @@
| `res_id` | `text` | 是 | 资源业务 ID | | `res_id` | `text` | 是 | 资源业务 ID |
| `access_level` | `number` | 是 | 权限级别 | | `access_level` | `number` | 是 | 权限级别 |
| `priority` | `number` | 否 | 优先级 | | `priority` | `number` | 否 | 优先级 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -27,3 +28,8 @@
| `idx_tbl_auth_role_perms_role_id` | `INDEX` | 加速按角色查询 | | `idx_tbl_auth_role_perms_role_id` | `INDEX` | 加速按角色查询 |
| `idx_tbl_auth_role_perms_res_id` | `INDEX` | 加速按资源查询 | | `idx_tbl_auth_role_perms_res_id` | `INDEX` | 加速按资源查询 |
| `idx_tbl_auth_role_perms_unique_map` | `UNIQUE INDEX` | 保证 `role_id + res_id` 唯一 | | `idx_tbl_auth_role_perms_unique_map` | `UNIQUE INDEX` | 保证 `role_id + res_id` 唯一 |
## 补充约定
- `is_delete` 用于软删除控制,历史角色授权建议通过置 `1` 失效,以保留审计痕迹。
- 集合默认查询规则已内置 `is_delete = 0`,角色权限映射默认不返回已软删除记录。

View File

@@ -18,6 +18,7 @@
| `role_code` | `text` | 否 | 角色编码 | | `role_code` | `text` | 否 | 角色编码 |
| `role_status` | `number` | 否 | 角色状态 | | `role_status` | `number` | 否 | 角色状态 |
| `role_remark` | `text` | 否 | 备注 | | `role_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -26,3 +27,8 @@
| `idx_tbl_auth_roles_role_id` | `UNIQUE INDEX` | 保证 `role_id` 唯一 | | `idx_tbl_auth_roles_role_id` | `UNIQUE INDEX` | 保证 `role_id` 唯一 |
| `idx_tbl_auth_roles_role_name` | `UNIQUE INDEX` | 保证 `role_name` 唯一 | | `idx_tbl_auth_roles_role_name` | `UNIQUE INDEX` | 保证 `role_name` 唯一 |
| `idx_tbl_auth_roles_role_code` | `UNIQUE INDEX` | 保证 `role_code` 唯一 | | `idx_tbl_auth_roles_role_code` | `UNIQUE INDEX` | 保证 `role_code` 唯一 |
## 补充约定
- `is_delete` 用于软删除控制,角色下线时应优先置为 `1`,避免直接物理删除导致历史授权链断裂。
- 集合默认查询规则已内置 `is_delete = 0`,角色列表/详情默认不返回已软删除数据。

View File

@@ -18,6 +18,7 @@
| `target_id` | `text` | 是 | 目标 ID | | `target_id` | `text` | 是 | 目标 ID |
| `table_name` | `text` | 是 | 作用表名 | | `table_name` | `text` | 是 | 作用表名 |
| `filter_sql` | `editor` | 是 | 行级过滤表达式 | | `filter_sql` | `editor` | 是 | 行级过滤表达式 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -27,3 +28,8 @@
| `idx_tbl_auth_row_scopes_target_type` | `INDEX` | 加速按目标类型查询 | | `idx_tbl_auth_row_scopes_target_type` | `INDEX` | 加速按目标类型查询 |
| `idx_tbl_auth_row_scopes_target_id` | `INDEX` | 加速按目标 ID 查询 | | `idx_tbl_auth_row_scopes_target_id` | `INDEX` | 加速按目标 ID 查询 |
| `idx_tbl_auth_row_scopes_table_name` | `INDEX` | 加速按作用表查询 | | `idx_tbl_auth_row_scopes_table_name` | `INDEX` | 加速按作用表查询 |
## 补充约定
- `is_delete` 用于软删除控制,废弃的行级范围规则建议软删除保留,以便问题追溯。
- 集合默认查询规则已内置 `is_delete = 0`,行级范围列表/详情默认隐藏已软删除规则。

View File

@@ -18,6 +18,7 @@
| `res_id` | `text` | 是 | 资源业务 ID | | `res_id` | `text` | 是 | 资源业务 ID |
| `access_level` | `number` | 是 | 权限级别 | | `access_level` | `number` | 是 | 权限级别 |
| `priority` | `number` | 否 | 优先级 | | `priority` | `number` | 否 | 优先级 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -27,3 +28,8 @@
| `idx_tbl_auth_user_overrides_users_convers_id` | `INDEX` | 加速按用户查询 | | `idx_tbl_auth_user_overrides_users_convers_id` | `INDEX` | 加速按用户查询 |
| `idx_tbl_auth_user_overrides_res_id` | `INDEX` | 加速按资源查询 | | `idx_tbl_auth_user_overrides_res_id` | `INDEX` | 加速按资源查询 |
| `idx_tbl_auth_user_overrides_unique_map` | `UNIQUE INDEX` | 保证 `users_convers_id + res_id` 唯一 | | `idx_tbl_auth_user_overrides_unique_map` | `UNIQUE INDEX` | 保证 `users_convers_id + res_id` 唯一 |
## 补充约定
- `is_delete` 用于软删除控制,用户覆盖权限取消时建议软删除保留记录。
- 集合默认查询规则已内置 `is_delete = 0`,用户覆盖权限列表/详情默认隐藏已软删除记录。

View File

@@ -40,6 +40,7 @@
| `users_id_pic_a` | `text` | 否 | 证件照正面附件 ID | | `users_id_pic_a` | `text` | 否 | 证件照正面附件 ID |
| `users_id_pic_b` | `text` | 否 | 证件照反面附件 ID | | `users_id_pic_b` | `text` | 否 | 证件照反面附件 ID |
| `users_tag` | `text` | 否 | 用户标签 | | `users_tag` | `text` | 否 | 用户标签 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -62,6 +63,8 @@
- 本表为 `auth` collection除上述字段外还受 PocketBase 原生鉴权机制约束。 - 本表为 `auth` collection除上述字段外还受 PocketBase 原生鉴权机制约束。
- 图片类字段统一只保存 `tbl_attachments.attachments_id` - 图片类字段统一只保存 `tbl_attachments.attachments_id`
- `is_delete` 用于软删除控制,业务侧删除时应优先将其置为 `1`,而不是直接物理删除记录。
- 集合默认查询规则已内置 `is_delete = 0`,管理侧常规列表/详情默认隐藏已软删除用户。
- 登录接口返回的 token 来源于本表 auth record 的原生签发能力,可直接给 PocketBase SDK 使用。 - 登录接口返回的 token 来源于本表 auth record 的原生签发能力,可直接给 PocketBase SDK 使用。
- 新用户注册时,`users_level` 默认保持为空;已有用户后续登录 / 更新流程也不会自动改写该字段。 - 新用户注册时,`users_level` 默认保持为空;已有用户后续登录 / 更新流程也不会自动改写该字段。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -28,6 +28,7 @@
| `cart_status` | `text` | 是 | 购物车状态,建议值:`有效` / `无效` | | `cart_status` | `text` | 是 | 购物车状态,建议值:`有效` / `无效` |
| `cart_at_price` | `number` | 是 | 加入购物车时的价格,用于后续降价提醒或对比 | | `cart_at_price` | `number` | 是 | 加入购物车时的价格,用于后续降价提醒或对比 |
| `cart_remark` | `text` | 否 | 备注 | | `cart_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -46,6 +47,8 @@
- `cart_owner``cart_product_id` 当前按文本字段保存业务 ID不直接建立 relation便于兼容现有 hooks 业务模型。 - `cart_owner``cart_product_id` 当前按文本字段保存业务 ID不直接建立 relation便于兼容现有 hooks 业务模型。
- `cart_owner` 统一保存 `tbl_auth_users.openid`,便于直接使用微信登录返回 token 做原生访问控制。 - `cart_owner` 统一保存 `tbl_auth_users.openid`,便于直接使用微信登录返回 token 做原生访问控制。
- `is_delete` 用于软删除控制,购物车项删除时建议优先标记为 `1`
- 集合默认查询规则已内置 `is_delete = 0`,常规列表/详情不会返回已软删除数据。
- `cart_product_quantity``cart_at_price` 使用 `number`,数量正整数与价格精度建议在 hooks / API 层统一校验。 - `cart_product_quantity``cart_at_price` 使用 `number`,数量正整数与价格精度建议在 hooks / API 层统一校验。
- 当购物车被清空时,建议业务侧将历史记录 `cart_status` 置为 `无效`,而不是直接覆盖有效记录。 - 当购物车被清空时,建议业务侧将历史记录 `cart_status` 置为 `无效`,而不是直接覆盖有效记录。
- `cart_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。 - `cart_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。

View File

@@ -32,6 +32,7 @@
| `company_level` | `text` | 否 | 公司等级 | | `company_level` | `text` | 否 | 公司等级 |
| `company_owner_openid` | `text` | 否 | 公司所有者 openid | | `company_owner_openid` | `text` | 否 | 公司所有者 openid |
| `company_remark` | `text` | 否 | 备注 | | `company_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -44,5 +45,7 @@
## 补充约定 ## 补充约定
- 微信端原生 PocketBase 接口支持公开创建公司记录。 - 微信端原生 PocketBase 接口支持公开创建公司记录。
- `is_delete` 用于软删除控制,公司资料停用时应优先置为 `1`
- 集合默认查询规则已内置 `is_delete = 0`,公司列表/详情默认不返回已软删除数据。
- `company_id` 已切换为数据库自动生成,客户端不再需要提交。 - `company_id` 已切换为数据库自动生成,客户端不再需要提交。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -40,6 +40,7 @@
| `document_remark` | `text` | 否 | 备注 | | `document_remark` | `text` | 否 | 备注 |
| `document_file` | `text` | 否 | 普通文件附件 ID 集合,底层以 `|` 分隔 | | `document_file` | `text` | 否 | 普通文件附件 ID 集合,底层以 `|` 分隔 |
| `document_create` | `autodate` | 否 | 文档创建时间,由数据库自动生成 | | `document_create` | `autodate` | 否 | 文档创建时间,由数据库自动生成 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -57,6 +58,8 @@
## 补充约定 ## 补充约定
- 三类附件字段都只保存 `attachments_id`,真实文件统一在 `tbl_attachments` - 三类附件字段都只保存 `attachments_id`,真实文件统一在 `tbl_attachments`
- `is_delete` 用于软删除控制,文档删除时建议优先标记为 `1`,避免直接物理删除。
- 集合默认查询规则已内置 `is_delete = 0`,公开列表与详情默认隐藏已软删除文档。
- `document_create` 已作为原生 PocketBase 列表排序字段,推荐使用 `sort=-document_create` - `document_create` 已作为原生 PocketBase 列表排序字段,推荐使用 `sort=-document_create`
- 面向用户填写的字段里,仅 `document_title``document_type` 必填,其余允许为空。 - 面向用户填写的字段里,仅 `document_title``document_type` 必填,其余允许为空。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -19,6 +19,7 @@
| `doh_user_id` | `text` | 否 | 操作人业务 ID | | `doh_user_id` | `text` | 否 | 操作人业务 ID |
| `doh_current_count` | `number` | 否 | 本次操作对应次数 | | `doh_current_count` | `number` | 否 | 本次操作对应次数 |
| `doh_remark` | `text` | 否 | 备注 | | `doh_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -32,4 +33,5 @@
## 补充约定 ## 补充约定
- 本表主要用于管理端审计与追溯,不对匿名用户开放。 - 本表主要用于管理端审计与追溯,不对匿名用户开放。
- `is_delete` 用于软删除控制,历史记录如需屏蔽显示可标记为 `1`,不建议直接物理删除。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -29,6 +29,7 @@
| `order_snap` | `json` | 是 | 订单快照,完整保存订单明细信息 | | `order_snap` | `json` | 是 | 订单快照,完整保存订单明细信息 |
| `order_amount` | `number` | 是 | 订单总金额 | | `order_amount` | `number` | 是 | 订单总金额 |
| `order_remark` | `text` | 否 | 订单备注 | | `order_remark` | `text` | 否 | 订单备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -49,6 +50,8 @@
- 当订单进入 `订单已确定` 及之后状态时,建议业务侧锁定关键字段,不再允许修改订单核心数据。 - 当订单进入 `订单已确定` 及之后状态时,建议业务侧锁定关键字段,不再允许修改订单核心数据。
- `order_owner``order_source_id` 当前按文本字段保存业务 ID不直接建立 relation便于兼容现有 hooks 业务模型。 - `order_owner``order_source_id` 当前按文本字段保存业务 ID不直接建立 relation便于兼容现有 hooks 业务模型。
- `order_owner` 统一保存 `tbl_auth_users.openid`,便于直接使用微信登录返回 token 做原生访问控制。 - `order_owner` 统一保存 `tbl_auth_users.openid`,便于直接使用微信登录返回 token 做原生访问控制。
- `is_delete` 用于软删除控制,订单删除/归档时建议优先标记为 `1`
- 集合默认查询规则已内置 `is_delete = 0`,常规列表/详情不会返回已软删除数据。
- `order_amount` 使用 `number`,货币精度策略建议后续统一为“分”或固定小数位。 - `order_amount` 使用 `number`,货币精度策略建议后续统一为“分”或固定小数位。
- `order_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。 - `order_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -31,6 +31,7 @@
| `prod_list_basic_price` | `number` | 否 | 基础价格 | | `prod_list_basic_price` | `number` | 否 | 基础价格 |
| `prod_list_vip_price` | `json` | 否 | 会员价数组,格式为 `[{"viplevel":"会员等级枚举值","price":1999}]` | | `prod_list_vip_price` | `json` | 否 | 会员价数组,格式为 `[{"viplevel":"会员等级枚举值","price":1999}]` |
| `prod_list_remark` | `text` | 否 | 备注 | | `prod_list_remark` | `text` | 否 | 备注 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -49,6 +50,8 @@
## 补充约定 ## 补充约定
- `prod_list_icon` 仅保存附件业务 ID真实文件统一在 `tbl_attachments`;多图时按上传顺序使用 `|` 聚合。 - `prod_list_icon` 仅保存附件业务 ID真实文件统一在 `tbl_attachments`;多图时按上传顺序使用 `|` 聚合。
- `is_delete` 用于软删除控制,产品停用/删除时建议优先标记为 `1`
- 集合默认查询规则已内置 `is_delete = 0`,产品列表默认不返回已软删除记录。
- 当前预构建脚本中已将 `listRule``viewRule` 设置为空字符串(`""`),对应 PocketBase 的“任何人可查看”。 - 当前预构建脚本中已将 `listRule``viewRule` 设置为空字符串(`""`),对应 PocketBase 的“任何人可查看”。
- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"sort":1,"name":"属性名","value":"属性值"}]` - `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"sort":1,"name":"属性名","value":"属性值"}]`
- `prod_list_parameters.sort` 用于稳定参数展示顺序,约定为正整数;前端未填写时可按当前录入/导入顺序自动补齐。 - `prod_list_parameters.sort` 用于稳定参数展示顺序,约定为正整数;前端未填写时可按当前录入/导入顺序自动补齐。

View File

@@ -22,6 +22,7 @@
| `dict_word_parent_id` | `text` | 否 | 父级字典业务 ID | | `dict_word_parent_id` | `text` | 否 | 父级字典业务 ID |
| `dict_word_remark` | `text` | 否 | 备注 | | `dict_word_remark` | `text` | 否 | 备注 |
| `dict_word_image` | `text` | 否 | 枚举图片附件 ID 集合,和枚举值一一对应 | | `dict_word_image` | `text` | 否 | 枚举图片附件 ID 集合,和枚举值一一对应 |
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
## 索引 ## 索引
@@ -34,5 +35,7 @@
## 补充约定 ## 补充约定
- 业务返回时hooks 会把聚合字段转换成 `items[]` 结构,每个元素包含 `enum``description``image``imageUrl``sortOrder` - 业务返回时hooks 会把聚合字段转换成 `items[]` 结构,每个元素包含 `enum``description``image``imageUrl``sortOrder`
- `is_delete` 用于软删除控制,字典废弃时建议优先标记为 `1`
- 集合默认查询规则已内置 `is_delete = 0`,字典列表/详情默认隐藏已软删除项。
- 字典项图片本体统一存放在 `tbl_attachments`,本表只保存 `attachments_id` - 字典项图片本体统一存放在 `tbl_attachments`,本表只保存 `attachments_id`
- PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。 - PocketBase 系统字段 `created``updated` 仍然存在,只是不在 collection 字段清单里单独声明。

View File

@@ -521,6 +521,7 @@ function validateProductMutationBody(e, isUpdate) {
} }
return { return {
id: payload.id || '',
prod_list_id: payload.prod_list_id || '', prod_list_id: payload.prod_list_id || '',
prod_list_name: payload.prod_list_name || '', prod_list_name: payload.prod_list_name || '',
prod_list_modelnumber: payload.prod_list_modelnumber || '', prod_list_modelnumber: payload.prod_list_modelnumber || '',

View File

@@ -304,47 +304,52 @@ function exportOrderRecord(record) {
} }
function exportAdminCartRecord(record) { function exportAdminCartRecord(record) {
const productInfo = buildProductInfo(findProductRecordByBusinessId(record.cart_product_id)) const productId = record && typeof record.getString === 'function'
? record.getString('cart_product_id')
: String(record && record.cart_product_id || '')
const productInfo = buildProductInfo(findProductRecordByBusinessId(productId))
return { return {
pb_id: String(record.id || ''), pb_id: String(record && record.id || ''),
cart_id: String(record.cart_id || ''), cart_id: record && typeof record.getString === 'function' ? record.getString('cart_id') : String(record && record.cart_id || ''),
cart_number: String(record.cart_number || ''), cart_number: record && typeof record.getString === 'function' ? record.getString('cart_number') : String(record && record.cart_number || ''),
cart_create: String(record.cart_create || ''), cart_create: record && typeof record.get === 'function' ? String(record.get('cart_create') || '') : String(record && record.cart_create || ''),
cart_owner: String(record.cart_owner || ''), cart_owner: record && typeof record.getString === 'function' ? record.getString('cart_owner') : String(record && record.cart_owner || ''),
cart_product_id: String(record.cart_product_id || ''), cart_product_id: productId,
cart_product_quantity: Number(record.cart_product_quantity || 0), cart_product_quantity: record && typeof record.get === 'function' ? Number(record.get('cart_product_quantity') || 0) : Number(record && record.cart_product_quantity || 0),
cart_status: String(record.cart_status || ''), cart_status: record && typeof record.getString === 'function' ? record.getString('cart_status') : String(record && record.cart_status || ''),
cart_at_price: Number(record.cart_at_price || 0), cart_at_price: record && typeof record.get === 'function' ? Number(record.get('cart_at_price') || 0) : Number(record && record.cart_at_price || 0),
cart_remark: String(record.cart_remark || ''), cart_remark: record && typeof record.getString === 'function' ? record.getString('cart_remark') : String(record && record.cart_remark || ''),
product_name: productInfo.prod_list_name, product_name: productInfo.prod_list_name,
product_modelnumber: productInfo.prod_list_modelnumber, product_modelnumber: productInfo.prod_list_modelnumber,
product_basic_price: productInfo.prod_list_basic_price, product_basic_price: productInfo.prod_list_basic_price,
created: String(record.created || ''), created: String(record && record.created || ''),
updated: String(record.updated || ''), updated: String(record && record.updated || ''),
} }
} }
function exportAdminOrderRecord(record) { function exportAdminOrderRecord(record) {
return { return {
pb_id: String(record.id || ''), pb_id: String(record && record.id || ''),
order_id: String(record.order_id || ''), order_id: record && typeof record.getString === 'function' ? record.getString('order_id') : String(record && record.order_id || ''),
order_number: String(record.order_number || ''), order_number: record && typeof record.getString === 'function' ? record.getString('order_number') : String(record && record.order_number || ''),
order_create: String(record.order_create || ''), order_create: record && typeof record.get === 'function' ? String(record.get('order_create') || '') : String(record && record.order_create || ''),
order_owner: String(record.order_owner || ''), order_owner: record && typeof record.getString === 'function' ? record.getString('order_owner') : String(record && record.order_owner || ''),
order_source: String(record.order_source || ''), order_source: record && typeof record.getString === 'function' ? record.getString('order_source') : String(record && record.order_source || ''),
order_status: String(record.order_status || ''), order_status: record && typeof record.getString === 'function' ? record.getString('order_status') : String(record && record.order_status || ''),
order_source_id: String(record.order_source_id || ''), order_source_id: record && typeof record.getString === 'function' ? record.getString('order_source_id') : String(record && record.order_source_id || ''),
order_snap: parseJsonFieldForOutput(record.order_snap), order_snap: record && typeof record.get === 'function' ? parseJsonFieldForOutput(record.get('order_snap')) : parseJsonFieldForOutput(record && record.order_snap),
order_amount: Number(record.order_amount || 0), order_amount: record && typeof record.get === 'function' ? Number(record.get('order_amount') || 0) : Number(record && record.order_amount || 0),
order_remark: String(record.order_remark || ''), order_remark: record && typeof record.getString === 'function' ? record.getString('order_remark') : String(record && record.order_remark || ''),
created: String(record.created || ''), created: String(record && record.created || ''),
updated: String(record.updated || ''), updated: String(record && record.updated || ''),
} }
} }
function exportAdminManageUser(userRecord, groupedCarts, groupedOrders) { function exportAdminManageUser(userRecord, groupedCarts, groupedOrders) {
const openid = String(userRecord.openid || '') const openid = userRecord && typeof userRecord.getString === 'function'
? userRecord.getString('openid')
: String(userRecord && userRecord.openid || '')
const carts = groupedCarts[openid] || [] const carts = groupedCarts[openid] || []
const orders = groupedOrders[openid] || [] const orders = groupedOrders[openid] || []
let cartTotalQuantity = 0 let cartTotalQuantity = 0
@@ -358,40 +363,44 @@ function exportAdminManageUser(userRecord, groupedCarts, groupedOrders) {
} }
return { return {
pb_id: String(userRecord.id || ''), pb_id: String(userRecord && userRecord.id || ''),
openid: openid, openid: openid,
users_id: String(userRecord.users_id || ''), users_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id') : String(userRecord && userRecord.users_id || ''),
users_name: String(userRecord.users_name || ''), users_name: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_name') : String(userRecord && userRecord.users_name || ''),
users_phone: String(userRecord.users_phone || ''), users_phone: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_phone') : String(userRecord && userRecord.users_phone || ''),
users_level: String(userRecord.users_level || ''), users_level: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_level') : String(userRecord && userRecord.users_level || ''),
users_level_name: userService.resolveUserLevelName(String(userRecord.users_level || '')), users_level_name: userService.resolveUserLevelName(userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_level') : String(userRecord && userRecord.users_level || '')),
users_type: String(userRecord.users_type || ''), users_type: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_type') : String(userRecord && userRecord.users_type || ''),
users_idtype: String(userRecord.users_idtype || ''), users_idtype: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_idtype') : String(userRecord && userRecord.users_idtype || ''),
users_id_number: String(userRecord.users_id_number || ''), users_id_number: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id_number') : String(userRecord && userRecord.users_id_number || ''),
users_status: String(userRecord.users_status || ''), users_status: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_status') : String(userRecord && userRecord.users_status || ''),
users_rank_level: userRecord.users_rank_level === null || typeof userRecord.users_rank_level === 'undefined' users_rank_level: !userRecord || typeof userRecord.get !== 'function'
? (userRecord && (userRecord.users_rank_level === null || typeof userRecord.users_rank_level === 'undefined') ? null : Number(userRecord && userRecord.users_rank_level))
: (userRecord.get('users_rank_level') === null || typeof userRecord.get('users_rank_level') === 'undefined')
? null ? null
: Number(userRecord.users_rank_level), : Number(userRecord.get('users_rank_level')),
users_auth_type: userRecord.users_auth_type === null || typeof userRecord.users_auth_type === 'undefined' users_auth_type: !userRecord || typeof userRecord.get !== 'function'
? (userRecord && (userRecord.users_auth_type === null || typeof userRecord.users_auth_type === 'undefined') ? null : Number(userRecord && userRecord.users_auth_type))
: (userRecord.get('users_auth_type') === null || typeof userRecord.get('users_auth_type') === 'undefined')
? null ? null
: Number(userRecord.users_auth_type), : Number(userRecord.get('users_auth_type')),
users_tag: String(userRecord.users_tag || ''), users_tag: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_tag') : String(userRecord && userRecord.users_tag || ''),
company_id: String(userRecord.company_id || ''), company_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('company_id') : String(userRecord && userRecord.company_id || ''),
users_parent_id: String(userRecord.users_parent_id || ''), users_parent_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_parent_id') : String(userRecord && userRecord.users_parent_id || ''),
users_promo_code: String(userRecord.users_promo_code || ''), users_promo_code: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_promo_code') : String(userRecord && userRecord.users_promo_code || ''),
usergroups_id: String(userRecord.usergroups_id || ''), usergroups_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('usergroups_id') : String(userRecord && userRecord.usergroups_id || ''),
users_picture: String(userRecord.users_picture || ''), users_picture: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_picture') : String(userRecord && userRecord.users_picture || ''),
users_id_pic_a: String(userRecord.users_id_pic_a || ''), users_id_pic_a: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id_pic_a') : String(userRecord && userRecord.users_id_pic_a || ''),
users_id_pic_b: String(userRecord.users_id_pic_b || ''), users_id_pic_b: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id_pic_b') : String(userRecord && userRecord.users_id_pic_b || ''),
users_title_picture: String(userRecord.users_title_picture || ''), users_title_picture: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_title_picture') : String(userRecord && userRecord.users_title_picture || ''),
cart_count: carts.length, cart_count: carts.length,
cart_total_quantity: cartTotalQuantity, cart_total_quantity: cartTotalQuantity,
order_count: orders.length, order_count: orders.length,
order_total_amount: orderTotalAmount, order_total_amount: orderTotalAmount,
carts: carts, carts: carts,
orders: orders, orders: orders,
created: String(userRecord.created || ''), created: String(userRecord && userRecord.created || ''),
updated: String(userRecord.updated || ''), updated: String(userRecord && userRecord.updated || ''),
} }
} }
@@ -728,7 +737,9 @@ function listManageUsersCartOrders(payload) {
const groupedOrders = {} const groupedOrders = {}
for (let i = 0; i < cartRecords.length; i += 1) { for (let i = 0; i < cartRecords.length; i += 1) {
const owner = String(cartRecords[i].cart_owner || '') const owner = cartRecords[i] && typeof cartRecords[i].getString === 'function'
? cartRecords[i].getString('cart_owner')
: String(cartRecords[i] && cartRecords[i].cart_owner || '')
if (!groupedCarts[owner]) { if (!groupedCarts[owner]) {
groupedCarts[owner] = [] groupedCarts[owner] = []
} }
@@ -736,7 +747,9 @@ function listManageUsersCartOrders(payload) {
} }
for (let i = 0; i < orderRecords.length; i += 1) { for (let i = 0; i < orderRecords.length; i += 1) {
const owner = String(orderRecords[i].order_owner || '') const owner = orderRecords[i] && typeof orderRecords[i].getString === 'function'
? orderRecords[i].getString('order_owner')
: String(orderRecords[i] && orderRecords[i].order_owner || '')
if (!groupedOrders[owner]) { if (!groupedOrders[owner]) {
groupedOrders[owner] = [] groupedOrders[owner] = []
} }

View File

@@ -171,6 +171,39 @@ function findAttachmentRecordByAttachmentId(attachmentId) {
return records.length ? records[0] : null return records.length ? records[0] : null
} }
function fetchAllRecordsByFilter(collectionName, filter, sort, params) {
const batchSize = 200
const result = []
let offset = 0
while (true) {
const batch = $app.findRecordsByFilter(
collectionName,
filter || '',
sort || '',
batchSize,
offset,
params || {}
)
if (!batch.length) {
break
}
for (let i = 0; i < batch.length; i += 1) {
result.push(batch[i])
}
if (batch.length < batchSize) {
break
}
offset += batch.length
}
return result
}
function resolveAttachmentList(value) { function resolveAttachmentList(value) {
const ids = parseAttachmentIdList(value) const ids = parseAttachmentIdList(value)
const attachments = [] const attachments = []
@@ -402,7 +435,7 @@ function deleteAttachment(attachmentId) {
throw createAppError(404, '未找到待删除的附件') throw createAppError(404, '未找到待删除的附件')
} }
const documentRecords = $app.findRecordsByFilter('tbl_document', '', '', 500, 0) const documentRecords = fetchAllRecordsByFilter('tbl_document', '', '')
for (let i = 0; i < documentRecords.length; i += 1) { for (let i = 0; i < documentRecords.length; i += 1) {
const current = documentRecords[i] const current = documentRecords[i]
const imageIds = parseAttachmentIdList(current.getString('document_image')) const imageIds = parseAttachmentIdList(current.getString('document_image'))
@@ -434,7 +467,7 @@ function deleteAttachment(attachmentId) {
} }
function listDocuments(payload) { function listDocuments(payload) {
const allRecords = $app.findRecordsByFilter('tbl_document', '', '', 500, 0) const allRecords = fetchAllRecordsByFilter('tbl_document', '', '')
const titleKeyword = String(payload.title_keyword || '').toLowerCase().trim() const titleKeyword = String(payload.title_keyword || '').toLowerCase().trim()
const status = String(payload.status || '') const status = String(payload.status || '')
const type = String(payload.document_type || '') const type = String(payload.document_type || '')

View File

@@ -9,10 +9,57 @@ function buildBusinessId(prefix) {
return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6) return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6)
} }
function buildPocketBaseRecordId(raw) {
const normalized = normalizeText(raw).toLowerCase()
if (/^[a-z0-9]{15}$/.test(normalized)) {
return normalized
}
const fallback = String($security.randomString(15) || '').toLowerCase().replace(/[^a-z0-9]/g, 'a')
if (fallback.length >= 15) {
return fallback.slice(0, 15)
}
return (fallback + 'aaaaaaaaaaaaaaa').slice(0, 15)
}
function normalizeText(value) { function normalizeText(value) {
return String(value || '').replace(/^\s+|\s+$/g, '') return String(value || '').replace(/^\s+|\s+$/g, '')
} }
function fetchAllRecordsByFilter(collectionName, filter, sort, params) {
const batchSize = 200
const result = []
let offset = 0
while (true) {
const batch = $app.findRecordsByFilter(
collectionName,
filter || '',
sort || '',
batchSize,
offset,
params || {}
)
if (!batch.length) {
break
}
for (let i = 0; i < batch.length; i += 1) {
result.push(batch[i])
}
if (batch.length < batchSize) {
break
}
offset += batch.length
}
return result
}
function normalizeOptionalNumberValue(value, fieldName) { function normalizeOptionalNumberValue(value, fieldName) {
if (value === '' || value === null || typeof value === 'undefined') { if (value === '' || value === null || typeof value === 'undefined') {
return null return null
@@ -562,7 +609,7 @@ function exportProductRecord(record, extra) {
function listProducts(payload) { function listProducts(payload) {
try { try {
const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0) const allRecords = fetchAllRecordsByFilter('tbl_product_list', '', '')
const keyword = normalizeText(payload.keyword).toLowerCase() const keyword = normalizeText(payload.keyword).toLowerCase()
const status = normalizeText(payload.status) const status = normalizeText(payload.status)
const category = normalizeText(payload.prod_list_category) const category = normalizeText(payload.prod_list_category)
@@ -620,7 +667,7 @@ function getProductDetail(productId) {
throw createAppError(404, '未找到对应产品') throw createAppError(404, '未找到对应产品')
} }
const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0) const allRecords = fetchAllRecordsByFilter('tbl_product_list', '', '')
const allItems = [] const allItems = []
for (let i = 0; i < allRecords.length; i += 1) { for (let i = 0; i < allRecords.length; i += 1) {
allItems.push(exportProductRecord(allRecords[i])) allItems.push(exportProductRecord(allRecords[i]))
@@ -644,6 +691,7 @@ function createProduct(_userOpenid, payload) {
const collection = $app.findCollectionByNameOrId('tbl_product_list') const collection = $app.findCollectionByNameOrId('tbl_product_list')
const record = new Record(collection) const record = new Record(collection)
record.set('id', buildPocketBaseRecordId(payload && payload.id))
record.set('prod_list_id', targetProductId) record.set('prod_list_id', targetProductId)
record.set('prod_list_name', normalizeText(payload.prod_list_name)) record.set('prod_list_name', normalizeText(payload.prod_list_name))
record.set('prod_list_modelnumber', normalizeText(payload.prod_list_modelnumber)) record.set('prod_list_modelnumber', normalizeText(payload.prod_list_modelnumber))

View File

@@ -46,8 +46,26 @@
.summary-label { color: #64748b; font-size: 13px; } .summary-label { color: #64748b; font-size: 13px; }
.summary-value { margin-top: 8px; font-size: 22px; font-weight: 700; color: #0f172a; } .summary-value { margin-top: 8px; font-size: 22px; font-weight: 700; color: #0f172a; }
.profile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; } .profile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
.preview-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
.field-block { display: grid; gap: 6px; } .field-block { display: grid; gap: 6px; }
.field-block.full { grid-column: 1 / -1; }
.field-label { font-size: 13px; color: #64748b; } .field-label { font-size: 13px; color: #64748b; }
.preview-card { border: 1px solid #dbe3f0; border-radius: 14px; padding: 10px 12px; background: #f8fbff; min-width: 0; }
.preview-value { color: #0f172a; font-size: 14px; font-weight: 600; line-height: 1.5; word-break: break-word; }
.user-profile-shell { display: grid; gap: 12px; }
.edit-panel { border-top: 1px dashed #dbe3f0; padding-top: 14px; }
.attachment-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
.attachment-panel { border: 1px solid #dbe3f0; border-radius: 14px; padding: 12px; background: #f8fbff; }
.attachment-panel.compact { padding: 10px; min-width: 0; }
.attachment-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.attachment-actions .btn { padding: 8px 12px; font-size: 13px; }
.attachment-preview { margin-top: 10px; display: flex; align-items: center; gap: 12px; min-height: 72px; }
.attachment-thumb { width: 72px; height: 72px; border-radius: 12px; border: 1px solid #dbe3f0; background: #fff; object-fit: cover; }
.attachment-empty-thumb { width: 72px; height: 72px; border-radius: 12px; border: 1px solid #dbe3f0; background: #fff; color: #94a3b8; display: flex; align-items: center; justify-content: center; font-size: 12px; text-align: center; padding: 8px; }
.attachment-meta { min-width: 0; display: grid; gap: 4px; }
.attachment-meta.compact { gap: 2px; }
.attachment-link { color: #2563eb; text-decoration: none; font-size: 13px; font-weight: 600; word-break: break-all; }
.attachment-hidden-input { display: none; }
.detail-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; } .detail-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
.section + .section { margin-top: 14px; } .section + .section { margin-top: 14px; }
.section-title { margin: 0 0 10px; font-size: 18px; color: #0f172a; } .section-title { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
@@ -60,17 +78,41 @@
@media (max-width: 1080px) { @media (max-width: 1080px) {
.layout { grid-template-columns: 1fr; } .layout { grid-template-columns: 1fr; }
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.preview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.attachment-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.profile-grid { grid-template-columns: 1fr; } .profile-grid { grid-template-columns: 1fr; }
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.toolbar { grid-template-columns: 1fr; } .toolbar { grid-template-columns: 1fr; }
.summary-grid { grid-template-columns: 1fr; } .summary-grid { grid-template-columns: 1fr; }
.preview-grid { grid-template-columns: 1fr; }
.attachment-grid { grid-template-columns: 1fr; }
table, thead, tbody, th, td, tr { display: block; } table, thead, tbody, th, td, tr { display: block; }
thead { display: none; } thead { display: none; }
tr { border-bottom: 1px solid #e5e7eb; } tr { border-bottom: 1px solid #e5e7eb; }
td { display: flex; justify-content: space-between; gap: 10px; } td { display: flex; justify-content: space-between; gap: 10px; }
td::before { content: attr(data-label); font-weight: 700; color: #475569; } td::before { content: attr(data-label); font-weight: 700; color: #475569; }
} }
html[data-theme="dark"] .preview-card,
html[data-theme="dark"] .attachment-panel {
background: rgba(0, 0, 0, 0.88) !important;
border-color: rgba(148, 163, 184, 0.22) !important;
color: #e5e7eb !important;
box-shadow: 0 18px 50px rgba(2, 6, 23, 0.18) !important;
}
html[data-theme="dark"] .preview-value,
html[data-theme="dark"] .attachment-link {
color: #f8fafc !important;
}
html[data-theme="dark"] .attachment-thumb,
html[data-theme="dark"] .attachment-empty-thumb {
background: rgba(15, 23, 42, 0.7) !important;
border-color: rgba(148, 163, 184, 0.22) !important;
color: #94a3b8 !important;
}
html[data-theme="dark"] .edit-panel {
border-top-color: rgba(148, 163, 184, 0.22) !important;
}
</style> </style>
{{ template "theme_head" . }} {{ template "theme_head" . }}
</head> </head>
@@ -112,7 +154,15 @@
const state = { const state = {
users: [], users: [],
selectedOpenid: '', selectedOpenid: '',
isEditMode: false,
userEditDraft: null,
userLevelOptions: [], userLevelOptions: [],
attachmentDetails: {
userUsersPicture: null,
userUsersIdPicA: null,
userUsersIdPicB: null,
userUsersTitlePicture: null,
},
} }
const statusEl = document.getElementById('status') const statusEl = document.getElementById('status')
@@ -172,6 +222,224 @@
}) || null }) || null
} }
function buildUserDraft(user) {
return {
openid: user.openid || '',
users_name: user.users_name || '',
users_phone: user.users_phone || '',
users_level: user.users_level || '',
users_type: user.users_type || '',
users_status: user.users_status || '',
users_rank_level: user.users_rank_level === null || typeof user.users_rank_level === 'undefined' ? '' : user.users_rank_level,
users_auth_type: user.users_auth_type === null || typeof user.users_auth_type === 'undefined' ? '' : user.users_auth_type,
company_id: user.company_id || '',
users_tag: user.users_tag || '',
users_parent_id: user.users_parent_id || '',
users_promo_code: user.users_promo_code || '',
usergroups_id: user.usergroups_id || '',
users_id_number: user.users_id_number || '',
users_picture: user.users_picture || '',
users_id_pic_a: user.users_id_pic_a || '',
users_id_pic_b: user.users_id_pic_b || '',
users_title_picture: user.users_title_picture || '',
}
}
function ensureEditDraft(user) {
if (!user) {
state.userEditDraft = null
return null
}
if (!state.userEditDraft || normalizeText(state.userEditDraft.openid) !== normalizeText(user.openid)) {
state.userEditDraft = buildUserDraft(user)
}
return state.userEditDraft
}
function syncEditDraftFromDom() {
if (!state.isEditMode || !state.userEditDraft) {
return
}
const fieldMap = {
userUsersName: 'users_name',
userUsersPhone: 'users_phone',
userUsersLevel: 'users_level',
userUsersType: 'users_type',
userUsersStatus: 'users_status',
userUsersRankLevel: 'users_rank_level',
userUsersAuthType: 'users_auth_type',
userCompanyId: 'company_id',
userUsersTag: 'users_tag',
userUsersParentId: 'users_parent_id',
userUsersPromoCode: 'users_promo_code',
userUsergroupsId: 'usergroups_id',
userUsersIdNumber: 'users_id_number',
userUsersPicture: 'users_picture',
userUsersIdPicA: 'users_id_pic_a',
userUsersIdPicB: 'users_id_pic_b',
userUsersTitlePicture: 'users_title_picture',
}
Object.keys(fieldMap).forEach(function (id) {
const el = document.getElementById(id)
if (el) {
state.userEditDraft[fieldMap[id]] = el.value
}
})
}
async function parseJsonSafe(res) {
const contentType = String(res.headers.get('content-type') || '').toLowerCase()
const rawText = await res.text()
const isJson = contentType.indexOf('application/json') !== -1
if (!rawText) {
return { json: null, text: '', isJson: false }
}
if (isJson) {
try {
return { json: JSON.parse(rawText), text: rawText, isJson: true }
} catch (_error) {}
}
return { json: null, text: rawText, isJson: false }
}
async function uploadAttachment(file, fieldKey) {
const token = getToken()
if (!token) {
window.location.replace('/pb/manage/login')
throw new Error('登录状态已失效,请重新登录')
}
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', 'cart-order-manage:' + fieldKey)
const res = await fetch(API_BASE + '/attachment/upload', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
},
body: form,
})
const parsed = await parseJsonSafe(res)
const data = parsed.json || {}
if (!res.ok) {
if (res.status === 413) {
throw new Error('上传图片失败:文件过大或服务端 bodyLimit 未生效')
}
if (!parsed.isJson && parsed.text) {
throw new Error('上传图片失败:服务端返回了非 JSON 响应')
}
throw new Error((data && (data.errMsg || data.message)) || '上传图片失败')
}
return data
}
async function loadAttachmentDetail(attachmentId) {
const id = normalizeText(attachmentId)
if (!id) {
return null
}
try {
return await requestJson('/attachment/detail', { attachments_id: id })
} catch (_error) {
return {
attachments_id: id,
attachments_filename: id,
attachments_url: '',
}
}
}
async function refreshSelectedAttachmentDetails() {
const user = getSelectedUser()
if (!user) {
state.attachmentDetails = {
userUsersPicture: null,
userUsersIdPicA: null,
userUsersIdPicB: null,
userUsersTitlePicture: null,
}
renderDetail()
return
}
const currentOpenid = normalizeText(user.openid)
const results = await Promise.all([
loadAttachmentDetail(user.users_picture),
loadAttachmentDetail(user.users_id_pic_a),
loadAttachmentDetail(user.users_id_pic_b),
loadAttachmentDetail(user.users_title_picture),
])
if (normalizeText(state.selectedOpenid) !== currentOpenid) {
return
}
state.attachmentDetails = {
userUsersPicture: results[0],
userUsersIdPicA: results[1],
userUsersIdPicB: results[2],
userUsersTitlePicture: results[3],
}
renderDetail()
}
function renderAttachmentCard(fieldId, label, value, editable) {
const detail = state.attachmentDetails[fieldId] || null
const url = detail && detail.attachments_url ? detail.attachments_url : ''
const filename = detail && (detail.attachments_filename || detail.attachments_id)
? (detail.attachments_filename || detail.attachments_id)
: normalizeText(value)
return '<div class="attachment-panel compact">'
+ '<label class="field-label" for="' + fieldId + '">' + escapeHtml(label) + '</label>'
+ (editable
? '<input id="' + fieldId + '" value="' + escapeHtml(value || '') + '" />'
: '<div class="preview-card" style="margin-top:6px;"><div class="preview-value">' + escapeHtml(value || '-') + '</div></div>')
+ (editable
? '<input class="attachment-hidden-input" id="' + fieldId + 'File" type="file" accept="image/*" data-upload-field="' + escapeHtml(fieldId) + '" />'
: '')
+ (editable
? '<div class="attachment-actions">'
+ '<button class="btn btn-light" type="button" data-upload-trigger="' + escapeHtml(fieldId) + '">上传图片</button>'
+ '<button class="btn btn-light" type="button" data-clear-attachment="' + escapeHtml(fieldId) + '">清空ID</button>'
+ '</div>'
: '')
+ '<div class="attachment-preview">'
+ (url
? '<img class="attachment-thumb" src="' + escapeHtml(url) + '" alt="' + escapeHtml(label) + '" />'
: '<div class="attachment-empty-thumb">暂无预览</div>')
+ '<div class="attachment-meta compact">'
+ '<div class="muted">当前附件ID' + escapeHtml(value || '-') + '</div>'
+ (url
? '<a class="attachment-link" href="' + escapeHtml(url) + '" target="_blank" rel="noreferrer">查看图片:' + escapeHtml(filename || '附件') + '</a>'
: '<div class="muted">上传后将先保存到 tbl_attachments再自动回填附件ID</div>')
+ '</div>'
+ '</div>'
+ '</div>'
}
function renderPreviewField(label, value) {
return '<div class="preview-card">'
+ '<div class="field-label">' + escapeHtml(label) + '</div>'
+ '<div class="preview-value">' + escapeHtml(value || '-') + '</div>'
+ '</div>'
}
function renderUserList() { function renderUserList() {
if (!state.users.length) { if (!state.users.length) {
userListEl.innerHTML = '<div class="empty">暂无匹配用户。</div>' userListEl.innerHTML = '<div class="empty">暂无匹配用户。</div>'
@@ -233,27 +501,53 @@
} }
function renderUserProfileForm(user) { function renderUserProfileForm(user) {
return '<div class="section"><h3 class="section-title">用户信息维护</h3>' const draft = state.isEditMode ? ensureEditDraft(user) : null
+ '<div class="profile-grid">' const source = draft || user
+ '<div class="field-block"><label class="field-label" for="userUsersName">用户名称</label><input id="userUsersName" value="' + escapeHtml(user.users_name || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersPhone">手机号</label><input id="userUsersPhone" value="' + escapeHtml(user.users_phone || '') + '" /></div>' return '<div class="section user-profile-shell"><h3 class="section-title">用户信息维护</h3>'
+ '<div class="field-block"><label class="field-label" for="userUsersLevel">会员等级</label><select id="userUsersLevel">' + renderUserLevelOptions(user.users_level) + '</select></div>' + '<div class="detail-actions">'
+ '<div class="field-block"><label class="field-label" for="userUsersType">用户类型</label><input id="userUsersType" value="' + escapeHtml(user.users_type || '') + '" /></div>' + (state.isEditMode
+ '<div class="field-block"><label class="field-label" for="userUsersStatus">用户状态</label><input id="userUsersStatus" value="' + escapeHtml(user.users_status || '') + '" /></div>' ? '<button class="btn btn-primary" id="saveUserBtn" type="button">保存用户信息</button><button class="btn btn-light" id="cancelEditBtn" type="button">取消编辑</button>'
+ '<div class="field-block"><label class="field-label" for="userUsersRankLevel">用户星级</label><input id="userUsersRankLevel" value="' + escapeHtml(user.users_rank_level === null || typeof user.users_rank_level === 'undefined' ? '' : user.users_rank_level) + '" /></div>' : '<button class="btn btn-primary" id="enterEditBtn" type="button">编辑用户信息</button>')
+ '<div class="field-block"><label class="field-label" for="userUsersAuthType">账户类型</label><input id="userUsersAuthType" value="' + escapeHtml(user.users_auth_type === null || typeof user.users_auth_type === 'undefined' ? '' : user.users_auth_type) + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userCompanyId">公司ID</label><input id="userCompanyId" value="' + escapeHtml(user.company_id || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersTag">用户标签</label><input id="userUsersTag" value="' + escapeHtml(user.users_tag || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersParentId">上级用户ID</label><input id="userUsersParentId" value="' + escapeHtml(user.users_parent_id || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersPromoCode">推广码</label><input id="userUsersPromoCode" value="' + escapeHtml(user.users_promo_code || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsergroupsId">用户组ID</label><input id="userUsergroupsId" value="' + escapeHtml(user.usergroups_id || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersIdNumber">证件号</label><input id="userUsersIdNumber" value="' + escapeHtml(user.users_id_number || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersPicture">头像附件ID</label><input id="userUsersPicture" value="' + escapeHtml(user.users_picture || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersIdPicA">证件正面附件ID</label><input id="userUsersIdPicA" value="' + escapeHtml(user.users_id_pic_a || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersIdPicB">证件反面附件ID</label><input id="userUsersIdPicB" value="' + escapeHtml(user.users_id_pic_b || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersTitlePicture">资质附件ID</label><input id="userUsersTitlePicture" value="' + escapeHtml(user.users_title_picture || '') + '" /></div>'
+ '</div>' + '</div>'
+ '<div class="detail-actions"><button class="btn btn-primary" id="saveUserBtn" type="button">保存用户信息</button></div>' + '<div class="preview-grid">'
+ renderPreviewField('用户名称', user.users_name)
+ renderPreviewField('手机号', user.users_phone)
+ renderPreviewField('会员等级', user.users_level_name || user.users_level)
+ renderPreviewField('用户类型', user.users_type)
+ renderPreviewField('用户状态', user.users_status)
+ renderPreviewField('用户星级', user.users_rank_level)
+ renderPreviewField('账户类型', user.users_auth_type)
+ renderPreviewField('公司ID', user.company_id)
+ renderPreviewField('用户标签', user.users_tag)
+ renderPreviewField('上级用户ID', user.users_parent_id)
+ renderPreviewField('推广码', user.users_promo_code)
+ renderPreviewField('用户组ID', user.usergroups_id)
+ renderPreviewField('证件号', user.users_id_number)
+ '</div>'
+ '<div class="attachment-grid">'
+ renderAttachmentCard('userUsersPicture', '头像附件ID', source.users_picture || '', state.isEditMode)
+ renderAttachmentCard('userUsersIdPicA', '证件正面附件ID', source.users_id_pic_a || '', state.isEditMode)
+ renderAttachmentCard('userUsersIdPicB', '证件反面附件ID', source.users_id_pic_b || '', state.isEditMode)
+ renderAttachmentCard('userUsersTitlePicture', '资质附件ID', source.users_title_picture || '', state.isEditMode)
+ '</div>'
+ (!state.isEditMode ? '' : '<div class="edit-panel">'
+ '<div class="profile-grid">'
+ '<div class="field-block"><label class="field-label" for="userUsersName">用户名称</label><input id="userUsersName" value="' + escapeHtml(source.users_name || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersPhone">手机号</label><input id="userUsersPhone" value="' + escapeHtml(source.users_phone || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersLevel">会员等级</label><select id="userUsersLevel">' + renderUserLevelOptions(source.users_level) + '</select></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersType">用户类型</label><input id="userUsersType" value="' + escapeHtml(source.users_type || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersStatus">用户状态</label><input id="userUsersStatus" value="' + escapeHtml(source.users_status || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersRankLevel">用户星级</label><input id="userUsersRankLevel" value="' + escapeHtml(source.users_rank_level === null || typeof source.users_rank_level === 'undefined' ? '' : source.users_rank_level) + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersAuthType">账户类型</label><input id="userUsersAuthType" value="' + escapeHtml(source.users_auth_type === null || typeof source.users_auth_type === 'undefined' ? '' : source.users_auth_type) + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userCompanyId">公司ID</label><input id="userCompanyId" value="' + escapeHtml(source.company_id || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersTag">用户标签</label><input id="userUsersTag" value="' + escapeHtml(source.users_tag || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersParentId">上级用户ID</label><input id="userUsersParentId" value="' + escapeHtml(source.users_parent_id || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersPromoCode">推广码</label><input id="userUsersPromoCode" value="' + escapeHtml(source.users_promo_code || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsergroupsId">用户组ID</label><input id="userUsergroupsId" value="' + escapeHtml(source.usergroups_id || '') + '" /></div>'
+ '<div class="field-block"><label class="field-label" for="userUsersIdNumber">证件号</label><input id="userUsersIdNumber" value="' + escapeHtml(source.users_id_number || '') + '" /></div>'
+ '</div>'
+ '</div>')
+ '</div>' + '</div>'
} }
@@ -314,6 +608,8 @@
}) })
state.users = Array.isArray(data.items) ? data.items : [] state.users = Array.isArray(data.items) ? data.items : []
state.userLevelOptions = Array.isArray(data.user_level_options) ? data.user_level_options : [] state.userLevelOptions = Array.isArray(data.user_level_options) ? data.user_level_options : []
state.isEditMode = false
state.userEditDraft = null
if (!state.users.length) { if (!state.users.length) {
state.selectedOpenid = '' state.selectedOpenid = ''
} else if (!getSelectedUser()) { } else if (!getSelectedUser()) {
@@ -321,6 +617,7 @@
} }
renderUserList() renderUserList()
renderDetail() renderDetail()
refreshSelectedAttachmentDetails()
setStatus('加载完成,共 ' + state.users.length + ' 位用户。', 'success') setStatus('加载完成,共 ' + state.users.length + ' 位用户。', 'success')
} catch (err) { } catch (err) {
renderUserList() renderUserList()
@@ -336,26 +633,29 @@
return return
} }
syncEditDraftFromDom()
const draft = ensureEditDraft(user)
try { try {
const data = await requestJson('/cart-order/manage-user-update', { const data = await requestJson('/cart-order/manage-user-update', {
openid: user.openid, openid: user.openid,
users_name: document.getElementById('userUsersName').value, users_name: draft.users_name,
users_phone: document.getElementById('userUsersPhone').value, users_phone: draft.users_phone,
users_level: document.getElementById('userUsersLevel').value, users_level: draft.users_level,
users_type: document.getElementById('userUsersType').value, users_type: draft.users_type,
users_status: document.getElementById('userUsersStatus').value, users_status: draft.users_status,
users_rank_level: document.getElementById('userUsersRankLevel').value, users_rank_level: draft.users_rank_level,
users_auth_type: document.getElementById('userUsersAuthType').value, users_auth_type: draft.users_auth_type,
company_id: document.getElementById('userCompanyId').value, company_id: draft.company_id,
users_tag: document.getElementById('userUsersTag').value, users_tag: draft.users_tag,
users_parent_id: document.getElementById('userUsersParentId').value, users_parent_id: draft.users_parent_id,
users_promo_code: document.getElementById('userUsersPromoCode').value, users_promo_code: draft.users_promo_code,
usergroups_id: document.getElementById('userUsergroupsId').value, usergroups_id: draft.usergroups_id,
users_id_number: document.getElementById('userUsersIdNumber').value, users_id_number: draft.users_id_number,
users_picture: document.getElementById('userUsersPicture').value, users_picture: draft.users_picture,
users_id_pic_a: document.getElementById('userUsersIdPicA').value, users_id_pic_a: draft.users_id_pic_a,
users_id_pic_b: document.getElementById('userUsersIdPicB').value, users_id_pic_b: draft.users_id_pic_b,
users_title_picture: document.getElementById('userUsersTitlePicture').value, users_title_picture: draft.users_title_picture,
}) })
const updatedUser = data && data.user ? data.user : null const updatedUser = data && data.user ? data.user : null
@@ -367,6 +667,8 @@
state.users[index] = Object.assign({}, state.users[index], updatedUser) state.users[index] = Object.assign({}, state.users[index], updatedUser)
} }
} }
state.isEditMode = false
state.userEditDraft = null
renderUserList() renderUserList()
renderDetail() renderDetail()
setStatus('用户信息保存成功。', 'success') setStatus('用户信息保存成功。', 'success')
@@ -375,6 +677,46 @@
} }
} }
async function handleAttachmentUpload(fieldId, file) {
if (!file) {
return
}
const input = document.getElementById(fieldId)
if (!input) {
return
}
syncEditDraftFromDom()
const labelMap = {
userUsersPicture: '头像',
userUsersIdPicA: '证件正面',
userUsersIdPicB: '证件反面',
userUsersTitlePicture: '资质附件',
}
try {
setStatus('正在上传' + (labelMap[fieldId] || '附件') + '...', '')
const uploaded = await uploadAttachment(file, fieldId)
input.value = uploaded.attachments_id || ''
if (state.userEditDraft) {
const draftFieldMap = {
userUsersPicture: 'users_picture',
userUsersIdPicA: 'users_id_pic_a',
userUsersIdPicB: 'users_id_pic_b',
userUsersTitlePicture: 'users_title_picture',
}
state.userEditDraft[draftFieldMap[fieldId]] = uploaded.attachments_id || ''
}
state.attachmentDetails[fieldId] = uploaded
renderDetail()
setStatus((labelMap[fieldId] || '附件') + '上传成功,请点击“保存用户信息”完成写入。', 'success')
} catch (err) {
setStatus(err.message || '上传附件失败', 'error')
}
}
userListEl.addEventListener('click', function (event) { userListEl.addEventListener('click', function (event) {
const target = event.target && event.target.closest ? event.target.closest('[data-openid]') : null const target = event.target && event.target.closest ? event.target.closest('[data-openid]') : null
if (!target) { if (!target) {
@@ -383,13 +725,84 @@
state.selectedOpenid = normalizeText(target.getAttribute('data-openid')) state.selectedOpenid = normalizeText(target.getAttribute('data-openid'))
renderUserList() renderUserList()
renderDetail() renderDetail()
refreshSelectedAttachmentDetails()
}) })
detailWrapEl.addEventListener('click', function (event) { detailWrapEl.addEventListener('click', function (event) {
const target = event.target const target = event.target
if (target && target.id === 'enterEditBtn') {
const user = getSelectedUser()
state.isEditMode = true
state.userEditDraft = user ? buildUserDraft(user) : null
renderDetail()
return
}
if (target && target.id === 'cancelEditBtn') {
state.isEditMode = false
state.userEditDraft = null
renderDetail()
return
}
if (target && target.id === 'saveUserBtn') { if (target && target.id === 'saveUserBtn') {
saveSelectedUser() saveSelectedUser()
return
} }
if (target && target.getAttribute) {
const uploadTrigger = target.getAttribute('data-upload-trigger')
if (uploadTrigger) {
const fileInput = document.getElementById(uploadTrigger + 'File')
if (fileInput) {
fileInput.click()
}
return
}
const clearAttachment = target.getAttribute('data-clear-attachment')
if (clearAttachment) {
syncEditDraftFromDom()
const input = document.getElementById(clearAttachment)
const fileInput = document.getElementById(clearAttachment + 'File')
if (input) {
input.value = ''
}
if (fileInput) {
fileInput.value = ''
}
if (state.userEditDraft) {
const draftFieldMap = {
userUsersPicture: 'users_picture',
userUsersIdPicA: 'users_id_pic_a',
userUsersIdPicB: 'users_id_pic_b',
userUsersTitlePicture: 'users_title_picture',
}
state.userEditDraft[draftFieldMap[clearAttachment]] = ''
}
state.attachmentDetails[clearAttachment] = null
renderDetail()
}
}
})
detailWrapEl.addEventListener('input', function () {
syncEditDraftFromDom()
})
detailWrapEl.addEventListener('change', function (event) {
const target = event.target
if (!target || !target.getAttribute) {
return
}
const fieldId = target.getAttribute('data-upload-field')
if (!fieldId) {
return
}
const file = target.files && target.files[0] ? target.files[0] : null
handleAttachmentUpload(fieldId, file)
}) })
document.getElementById('searchBtn').addEventListener('click', loadUsers) document.getElementById('searchBtn').addEventListener('click', loadUsers)

View File

@@ -28,7 +28,7 @@
.btn-danger { background: #dc2626; color: #fff; } .btn-danger { background: #dc2626; color: #fff; }
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; } .btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
.panel { border-radius: 20px; padding: 18px; } .panel { border-radius: 20px; padding: 18px; }
.toolbar { display: grid; grid-template-columns: 1.3fr 1fr 1fr auto auto; gap: 12px; margin-bottom: 18px; } .toolbar { display: grid; grid-template-columns: 1.3fr auto 1fr 1fr auto auto; gap: 12px; margin-bottom: 18px; }
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; } input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
textarea { min-height: 88px; resize: vertical; } textarea { min-height: 88px; resize: vertical; }
table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 16px; } table { width: 100%; border-collapse: collapse; overflow: hidden; border-radius: 16px; }
@@ -113,6 +113,7 @@
<section class="panel"> <section class="panel">
<div class="toolbar"> <div class="toolbar">
<input id="keywordInput" placeholder="按字典名称模糊搜索" /> <input id="keywordInput" placeholder="按字典名称模糊搜索" />
<select id="categorySelect"><option value="">全部分类</option></select>
<input id="detailInput" placeholder="查询指定字典名称" /> <input id="detailInput" placeholder="查询指定字典名称" />
<button class="btn btn-secondary" id="listBtn" type="button">查询全部</button> <button class="btn btn-secondary" id="listBtn" type="button">查询全部</button>
<button class="btn btn-light" id="detailBtn" type="button">查询指定</button> <button class="btn btn-light" id="detailBtn" type="button">查询指定</button>
@@ -217,6 +218,7 @@
const statusEl = document.getElementById('status') const statusEl = document.getElementById('status')
const keywordInput = document.getElementById('keywordInput') const keywordInput = document.getElementById('keywordInput')
const categorySelect = document.getElementById('categorySelect')
const detailInput = document.getElementById('detailInput') const detailInput = document.getElementById('detailInput')
const tableBody = document.getElementById('tableBody') const tableBody = document.getElementById('tableBody')
const editorModal = document.getElementById('editorModal') const editorModal = document.getElementById('editorModal')
@@ -405,6 +407,44 @@
.replace(/'/g, '&#39;') .replace(/'/g, '&#39;')
} }
function extractCategory(dictName) {
var name = String(dictName || '')
if (name.length >= 2) {
return name.substring(0, 2)
}
return ''
}
function populateCategorySelect(list) {
var categories = {}
for (var i = 0; i < list.length; i++) {
var cat = extractCategory(list[i].dict_name)
if (cat) {
categories[cat] = true
}
}
var sorted = Object.keys(categories).sort()
var current = categorySelect.value
categorySelect.innerHTML = '<option value="">全部分类</option>'
for (var j = 0; j < sorted.length; j++) {
var opt = document.createElement('option')
opt.value = sorted[j]
opt.textContent = sorted[j]
categorySelect.appendChild(opt)
}
if (current && categories[current]) {
categorySelect.value = current
}
}
function filterByCategory(list) {
var selected = categorySelect.value
if (!selected) return list
return list.filter(function (item) {
return extractCategory(item.dict_name) === selected
})
}
function randomEnumSeed() { function randomEnumSeed() {
const first = enumChars[Math.floor(Math.random() * enumChars.length)] const first = enumChars[Math.floor(Math.random() * enumChars.length)]
const second = enumChars[Math.floor(Math.random() * enumChars.length)] const second = enumChars[Math.floor(Math.random() * enumChars.length)]
@@ -775,7 +815,8 @@
const data = await request(API_BASE + '/dictionary/list', { keyword: keywordInput.value.trim() }) const data = await request(API_BASE + '/dictionary/list', { keyword: keywordInput.value.trim() })
state.list = data.items || [] state.list = data.items || []
state.expandedPreviewKey = '' state.expandedPreviewKey = ''
renderTable(state.list) populateCategorySelect(state.list)
renderTable(filterByCategory(state.list))
setStatus('查询成功,共 ' + state.list.length + ' 条。', 'success') setStatus('查询成功,共 ' + state.list.length + ' 条。', 'success')
} catch (err) { } catch (err) {
setStatus(err.message || '查询失败', 'error') setStatus(err.message || '查询失败', 'error')
@@ -797,7 +838,8 @@
const data = await request(API_BASE + '/dictionary/detail', { dict_name: dictName }) const data = await request(API_BASE + '/dictionary/detail', { dict_name: dictName })
state.list = [data] state.list = [data]
state.expandedPreviewKey = '' state.expandedPreviewKey = ''
renderTable(state.list) populateCategorySelect(state.list)
renderTable(filterByCategory(state.list))
setStatus('查询详情成功。', 'success') setStatus('查询详情成功。', 'success')
} catch (err) { } catch (err) {
setStatus(err.message || '查询失败', 'error') setStatus(err.message || '查询失败', 'error')
@@ -1108,9 +1150,13 @@
}) })
document.getElementById('listBtn').addEventListener('click', loadList) document.getElementById('listBtn').addEventListener('click', loadList)
categorySelect.addEventListener('change', function () {
renderTable(filterByCategory(state.list))
})
document.getElementById('detailBtn').addEventListener('click', loadDetail) document.getElementById('detailBtn').addEventListener('click', loadDetail)
document.getElementById('resetBtn').addEventListener('click', function () { document.getElementById('resetBtn').addEventListener('click', function () {
keywordInput.value = '' keywordInput.value = ''
categorySelect.value = ''
detailInput.value = '' detailInput.value = ''
state.list = [] state.list = []
renderTable([]) renderTable([])

View File

@@ -84,6 +84,16 @@
.loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; } .loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; }
.loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: docSpin 0.9s linear infinite; } .loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: docSpin 0.9s linear infinite; }
.loading-text { color: #0f172a; font-size: 15px; font-weight: 700; } .loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
.pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
.pagination-info { color: #475569; font-size: 14px; }
.pagination-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.pagination-size { display: inline-flex; align-items: center; gap: 8px; color: #475569; font-size: 14px; }
.pagination-btn { min-width: 42px; padding: 8px 12px; border-radius: 10px; border: 1px solid #dbe3f0; background: #fff; color: #334155; cursor: pointer; font-weight: 600; }
.pagination-select { min-width: 88px; width: auto; padding: 8px 12px; border-radius: 10px; border: 1px solid #dbe3f0; background: #fff; color: #334155; font-size: 14px; }
.pagination-btn.active { background: #2563eb; border-color: #2563eb; color: #fff; }
.pagination-btn:disabled { cursor: not-allowed; color: #94a3b8; background: #f8fafc; }
.pagination-btn:not(:disabled):hover { border-color: #94a3b8; background: #f8fafc; }
.pagination-btn.active:hover { background: #2563eb; border-color: #2563eb; }
html[data-theme="dark"] .editor-banner { background: rgba(30, 41, 59, 0.86); color: #bfdbfe; } html[data-theme="dark"] .editor-banner { background: rgba(30, 41, 59, 0.86); color: #bfdbfe; }
html[data-theme="dark"] .file-box { background: rgba(15, 23, 42, 0.92); border-color: rgba(148, 163, 184, 0.22); box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.08); } html[data-theme="dark"] .file-box { background: rgba(15, 23, 42, 0.92); border-color: rgba(148, 163, 184, 0.22); box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.08); }
html[data-theme="dark"] .choice-switch { background: rgba(15, 23, 42, 0.86); border-color: rgba(148, 163, 184, 0.26); } html[data-theme="dark"] .choice-switch { background: rgba(15, 23, 42, 0.86); border-color: rgba(148, 163, 184, 0.26); }
@@ -97,6 +107,15 @@
html[data-theme="dark"] .file-preview, html[data-theme="dark"] .file-preview,
html[data-theme="dark"] .file-card-icon { background: rgba(2, 6, 23, 0.9); border-color: rgba(148, 163, 184, 0.24); color: #e2e8f0; } html[data-theme="dark"] .file-card-icon { background: rgba(2, 6, 23, 0.9); border-color: rgba(148, 163, 184, 0.24); color: #e2e8f0; }
html[data-theme="dark"] .file-card-icon:hover { background: rgba(30, 41, 59, 0.82); border-color: rgba(148, 163, 184, 0.4); } html[data-theme="dark"] .file-card-icon:hover { background: rgba(30, 41, 59, 0.82); border-color: rgba(148, 163, 184, 0.4); }
html[data-theme="dark"] .pagination-info,
html[data-theme="dark"] .pagination-size { color: #cbd5e1; }
html[data-theme="dark"] .pagination-btn { background: rgba(15, 23, 42, 0.92); border-color: rgba(148, 163, 184, 0.24); color: #e2e8f0; box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); }
html[data-theme="dark"] .pagination-btn:not(:disabled):hover { background: rgba(30, 41, 59, 0.96); border-color: rgba(96, 165, 250, 0.5); color: #f8fafc; }
html[data-theme="dark"] .pagination-btn.active,
html[data-theme="dark"] .pagination-btn.active:hover { background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); border-color: #3b82f6; color: #eff6ff; box-shadow: 0 10px 24px rgba(37, 99, 235, 0.28); }
html[data-theme="dark"] .pagination-btn:disabled { background: rgba(15, 23, 42, 0.58); border-color: rgba(71, 85, 105, 0.45); color: #64748b; box-shadow: none; }
html[data-theme="dark"] .pagination-select { background: rgba(15, 23, 42, 0.92); border-color: rgba(148, 163, 184, 0.24); color: #f8fafc; box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); }
html[data-theme="dark"] .pagination-select:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.18); }
@keyframes docSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes docSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (max-width: 960px) { @media (max-width: 960px) {
.grid, .file-group { grid-template-columns: 1fr; } .grid, .file-group { grid-template-columns: 1fr; }
@@ -295,6 +314,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="pagination" id="listPagination"></div>
</section> </section>
</div> </div>
@@ -342,6 +362,7 @@
const hotelTypeTagsEl = document.getElementById('hotelTypeTags') const hotelTypeTagsEl = document.getElementById('hotelTypeTags')
const listTitleKeywordEl = document.getElementById('listTitleKeyword') const listTitleKeywordEl = document.getElementById('listTitleKeyword')
const listTypeFilterEl = document.getElementById('listTypeFilter') const listTypeFilterEl = document.getElementById('listTypeFilter')
const listPaginationEl = document.getElementById('listPagination')
const imageViewerEl = document.getElementById('imageViewer') const imageViewerEl = document.getElementById('imageViewer')
const imageViewerImgEl = document.getElementById('imageViewerImg') const imageViewerImgEl = document.getElementById('imageViewerImg')
const loadingMaskEl = document.getElementById('loadingMask') const loadingMaskEl = document.getElementById('loadingMask')
@@ -388,6 +409,7 @@
const state = { const state = {
list: [], list: [],
fullList: [], fullList: [],
filteredList: [],
mode: 'idle', mode: 'idle',
editingId: '', editingId: '',
editingSource: null, editingSource: null,
@@ -413,6 +435,12 @@
titleKeyword: '', titleKeyword: '',
type: '', type: '',
}, },
pagination: {
page: 1,
pageSize: 10,
total: 0,
totalPages: 1,
},
} }
function setStatus(message, type) { function setStatus(message, type) {
@@ -1005,6 +1033,102 @@
}) })
} }
function clampPage(page, totalPages) {
const normalizedPage = Number(page) || 1
const normalizedTotalPages = Math.max(1, Number(totalPages) || 1)
return Math.min(Math.max(1, normalizedPage), normalizedTotalPages)
}
function getVisiblePageNumbers(page, totalPages) {
const result = []
const start = Math.max(1, page - 2)
const end = Math.min(totalPages, start + 4)
const adjustedStart = Math.max(1, end - 4)
for (let current = adjustedStart; current <= end; current += 1) {
result.push(current)
}
return result
}
function renderPageSizeSelector() {
const options = [10, 20, 50, 100, 500]
return '<label class="pagination-size">每页'
+ '<select class="pagination-select" onchange="window.__setDocumentPageSize(this.value)">'
+ options.map(function (size) {
return '<option value="' + size + '"' + (Number(state.pagination.pageSize) === size ? ' selected' : '') + '>' + size + '</option>'
}).join('')
+ '</select>'
+ '条</label>'
}
function renderPagination() {
if (!listPaginationEl) {
return
}
const total = Number(state.pagination.total || 0)
const page = Number(state.pagination.page || 1)
const totalPages = Math.max(1, Number(state.pagination.totalPages || 1))
const sizeSelector = renderPageSizeSelector()
if (!total) {
listPaginationEl.innerHTML = '<div class="pagination-info">共 0 条</div>'
+ '<div class="pagination-actions">' + sizeSelector + '</div>'
return
}
const visiblePageNumbers = getVisiblePageNumbers(page, totalPages)
const startIndex = (page - 1) * state.pagination.pageSize + 1
const endIndex = Math.min(total, page * state.pagination.pageSize)
const buttons = []
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoDocumentPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>')
if (visiblePageNumbers[0] > 1) {
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoDocumentPage(1)">1</button>')
if (visiblePageNumbers[0] > 2) {
buttons.push('<button class="pagination-btn" type="button" disabled>...</button>')
}
}
for (let i = 0; i < visiblePageNumbers.length; i += 1) {
const current = visiblePageNumbers[i]
buttons.push('<button class="pagination-btn' + (current === page ? ' active' : '') + '" type="button" onclick="window.__gotoDocumentPage(' + current + ')">' + current + '</button>')
}
if (visiblePageNumbers.length && visiblePageNumbers[visiblePageNumbers.length - 1] < totalPages) {
if (visiblePageNumbers[visiblePageNumbers.length - 1] < totalPages - 1) {
buttons.push('<button class="pagination-btn" type="button" disabled>...</button>')
}
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoDocumentPage(' + totalPages + ')">' + totalPages + '</button>')
}
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoDocumentPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>')
listPaginationEl.innerHTML = '<div class="pagination-info">共 ' + total + ' 条,显示第 ' + startIndex + '-' + endIndex + ' 条,第 ' + page + '/' + totalPages + ' 页</div>'
+ '<div class="pagination-actions">' + sizeSelector + buttons.join('') + '</div>'
}
function updateDocumentListView() {
const filteredList = applyListFiltersToList(state.fullList)
const pageSize = Math.max(1, Number(state.pagination.pageSize || 10))
const total = filteredList.length
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const currentPage = clampPage(state.pagination.page, totalPages)
const startIndex = (currentPage - 1) * pageSize
state.filteredList = filteredList
state.pagination.page = currentPage
state.pagination.total = total
state.pagination.totalPages = totalPages
state.list = filteredList.slice(startIndex, startIndex + pageSize)
renderTable()
renderPagination()
}
function renderTable() { function renderTable() {
if (!state.list.length) { if (!state.list.length) {
tableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无文档数据。</td></tr>' tableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无文档数据。</td></tr>'
@@ -1132,6 +1256,27 @@
} }
} }
function goToDocumentPage(page) {
const nextPage = clampPage(page, state.pagination.totalPages)
if (nextPage === state.pagination.page) {
return
}
state.pagination.page = nextPage
updateDocumentListView()
}
function setDocumentPageSize(pageSize) {
const nextPageSize = Number(pageSize) || 10
if (nextPageSize === state.pagination.pageSize) {
return
}
state.pagination.pageSize = nextPageSize
state.pagination.page = 1
updateDocumentListView()
}
async function loadDocuments() { async function loadDocuments() {
setStatus('正在加载文档列表...', '') setStatus('正在加载文档列表...', '')
showLoading('正在加载文档列表...') showLoading('正在加载文档列表...')
@@ -1140,9 +1285,8 @@
document_type: state.listFilters.type, document_type: state.listFilters.type,
}) })
state.fullList = Array.isArray(data.items) ? data.items : [] state.fullList = Array.isArray(data.items) ? data.items : []
state.list = applyListFiltersToList(state.fullList) updateDocumentListView()
renderTable() setStatus('文档列表已刷新,共 ' + state.pagination.total + ' 条。', 'success')
setStatus('文档列表已刷新,共 ' + state.list.length + ' 条。', 'success')
} catch (err) { } catch (err) {
setStatus(err.message || '加载列表失败', 'error') setStatus(err.message || '加载列表失败', 'error')
} finally { } finally {
@@ -1177,8 +1321,11 @@
} }
} }
async function queryDocumentsWithAutoSave() { async function queryDocumentsWithAutoSave(resetPage) {
syncListFiltersFromInputs() syncListFiltersFromInputs()
if (resetPage) {
state.pagination.page = 1
}
await saveAndHideEditorBeforeListQuery() await saveAndHideEditorBeforeListQuery()
applyListFiltersToInputs() applyListFiltersToInputs()
await loadDocuments() await loadDocuments()
@@ -1490,23 +1637,33 @@
updateSelection(fieldName, target.value, !!target.checked) updateSelection(fieldName, target.value, !!target.checked)
}) })
document.getElementById('reloadBtn').addEventListener('click', queryDocumentsWithAutoSave) window.__gotoDocumentPage = goToDocumentPage
document.getElementById('searchBtn').addEventListener('click', queryDocumentsWithAutoSave) window.__setDocumentPageSize = setDocumentPageSize
document.getElementById('reloadBtn').addEventListener('click', function () {
queryDocumentsWithAutoSave(false)
})
document.getElementById('searchBtn').addEventListener('click', function () {
queryDocumentsWithAutoSave(true)
})
document.getElementById('clearSearchBtn').addEventListener('click', function () { document.getElementById('clearSearchBtn').addEventListener('click', function () {
state.listFilters.titleKeyword = '' state.listFilters.titleKeyword = ''
state.listFilters.type = '' state.listFilters.type = ''
state.pagination.page = 1
applyListFiltersToInputs() applyListFiltersToInputs()
queryDocumentsWithAutoSave() queryDocumentsWithAutoSave(false)
}) })
if (listTitleKeywordEl) { if (listTitleKeywordEl) {
listTitleKeywordEl.addEventListener('keydown', function (event) { listTitleKeywordEl.addEventListener('keydown', function (event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
queryDocumentsWithAutoSave() queryDocumentsWithAutoSave(true)
} }
}) })
} }
if (listTypeFilterEl) { if (listTypeFilterEl) {
listTypeFilterEl.addEventListener('change', queryDocumentsWithAutoSave) listTypeFilterEl.addEventListener('change', function () {
queryDocumentsWithAutoSave(true)
})
} }
document.getElementById('createModeBtn').addEventListener('click', function () { document.getElementById('createModeBtn').addEventListener('click', function () {
enterCreateMode() enterCreateMode()

View File

@@ -76,6 +76,25 @@
.loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; } .loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; }
.loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: spin 0.9s linear infinite; } .loading-spinner { width: 44px; height: 44px; margin: 0 auto 14px; border-radius: 999px; border: 4px solid #dbeafe; border-top-color: #2563eb; animation: spin 0.9s linear infinite; }
.loading-text { color: #0f172a; font-size: 15px; font-weight: 700; } .loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
.pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
.pagination-info { color: #475569; font-size: 14px; }
.pagination-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.pagination-size { display: inline-flex; align-items: center; gap: 8px; color: #475569; font-size: 14px; }
.pagination-btn { min-width: 42px; padding: 8px 12px; border-radius: 10px; border: 1px solid #dbe3f0; background: #fff; color: #334155; cursor: pointer; font-weight: 600; }
.pagination-select { min-width: 88px; width: auto; padding: 8px 12px; border-radius: 10px; border: 1px solid #dbe3f0; background: #fff; color: #334155; font-size: 14px; }
.pagination-btn.active { background: #2563eb; border-color: #2563eb; color: #fff; }
.pagination-btn:disabled { cursor: not-allowed; color: #94a3b8; background: #f8fafc; }
.pagination-btn:not(:disabled):hover { border-color: #94a3b8; background: #f8fafc; }
.pagination-btn.active:hover { background: #2563eb; border-color: #2563eb; }
html[data-theme="dark"] .pagination-info,
html[data-theme="dark"] .pagination-size { color: #cbd5e1; }
html[data-theme="dark"] .pagination-btn { background: rgba(15, 23, 42, 0.92); border-color: rgba(148, 163, 184, 0.24); color: #e2e8f0; box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); }
html[data-theme="dark"] .pagination-btn:not(:disabled):hover { background: rgba(30, 41, 59, 0.96); border-color: rgba(96, 165, 250, 0.5); color: #f8fafc; }
html[data-theme="dark"] .pagination-btn.active,
html[data-theme="dark"] .pagination-btn.active:hover { background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); border-color: #3b82f6; color: #eff6ff; box-shadow: 0 10px 24px rgba(37, 99, 235, 0.28); }
html[data-theme="dark"] .pagination-btn:disabled { background: rgba(15, 23, 42, 0.58); border-color: rgba(71, 85, 105, 0.45); color: #64748b; box-shadow: none; }
html[data-theme="dark"] .pagination-select { background: rgba(15, 23, 42, 0.92); border-color: rgba(148, 163, 184, 0.24); color: #f8fafc; box-shadow: inset 0 1px 0 rgba(255,255,255,0.04); }
html[data-theme="dark"] .pagination-select:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.18); }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (max-width: 960px) { @media (max-width: 960px) {
.grid, .toolbar, .option-list { grid-template-columns: 1fr; } .grid, .toolbar, .option-list { grid-template-columns: 1fr; }
@@ -309,6 +328,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="pagination" id="listPagination"></div>
</section> </section>
</div> </div>
@@ -375,6 +395,7 @@
const statusFilterEl = document.getElementById('statusFilter') const statusFilterEl = document.getElementById('statusFilter')
const categoryFilterEl = document.getElementById('categoryFilter') const categoryFilterEl = document.getElementById('categoryFilter')
const prodListSortHintEl = document.getElementById('prodListSortHint') const prodListSortHintEl = document.getElementById('prodListSortHint')
const listPaginationEl = document.getElementById('listPagination')
const fields = { const fields = {
keyword: document.getElementById('keywordInput'), keyword: document.getElementById('keywordInput'),
@@ -404,6 +425,7 @@
const state = { const state = {
list: [], list: [],
fullList: [],
mode: 'idle', mode: 'idle',
editingId: '', editingId: '',
editingSource: null, editingSource: null,
@@ -423,6 +445,12 @@
currentIconAttachments: [], currentIconAttachments: [],
pendingIconFiles: [], pendingIconFiles: [],
copySourceProductId: '', copySourceProductId: '',
pagination: {
page: 1,
pageSize: 10,
total: 0,
totalPages: 1,
},
} }
const multiFieldConfig = { const multiFieldConfig = {
@@ -609,24 +637,42 @@
} }
} }
async function requestProductListFallback(payload) { async function fetchAllFallbackProductRecords() {
const token = getToken() const token = getToken()
const res = await fetch(getApiUrl('/collections/tbl_product_list/records?page=1&perPage=500&sort=-updated'), { const perPage = 200
method: 'GET', const result = []
headers: { let page = 1
Authorization: token ? ('Bearer ' + token) : '',
},
})
const parsed = await parseJsonSafe(res) while (true) {
if (!res.ok || !parsed.isJson || !parsed.json) { const res = await fetch(getApiUrl('/collections/tbl_product_list/records?page=' + page + '&perPage=' + perPage + '&sort=-updated'), {
throw new Error('产品列表回退查询失败') method: 'GET',
headers: {
Authorization: token ? ('Bearer ' + token) : '',
},
})
const parsed = await parseJsonSafe(res)
if (!res.ok || !parsed.isJson || !parsed.json) {
throw new Error('产品列表回退查询失败')
}
const rawItems = Array.isArray(parsed.json.items) ? parsed.json.items : []
for (let i = 0; i < rawItems.length; i += 1) {
result.push(normalizeFallbackProductRecord(rawItems[i]))
}
if (rawItems.length < perPage) {
break
}
page += 1
} }
const rawItems = Array.isArray(parsed.json.items) ? parsed.json.items : [] return result
const normalized = rawItems.map(function (item) { }
return normalizeFallbackProductRecord(item)
}) async function requestProductListFallback(payload) {
const normalized = await fetchAllFallbackProductRecords()
const keyword = normalizeText(payload && payload.keyword).toLowerCase() const keyword = normalizeText(payload && payload.keyword).toLowerCase()
const status = normalizeText(payload && payload.status) const status = normalizeText(payload && payload.status)
@@ -656,6 +702,101 @@
return filtered return filtered
} }
function clampPage(page, totalPages) {
const normalizedPage = Number(page) || 1
const normalizedTotalPages = Math.max(1, Number(totalPages) || 1)
return Math.min(Math.max(1, normalizedPage), normalizedTotalPages)
}
function getVisiblePageNumbers(page, totalPages) {
const result = []
const start = Math.max(1, page - 2)
const end = Math.min(totalPages, start + 4)
const adjustedStart = Math.max(1, end - 4)
for (let current = adjustedStart; current <= end; current += 1) {
result.push(current)
}
return result
}
function renderPageSizeSelector() {
const options = [10, 20, 50, 100, 500]
return '<label class="pagination-size">每页'
+ '<select class="pagination-select" onchange="window.__setProductPageSize(this.value)">'
+ options.map(function (size) {
return '<option value="' + size + '"' + (Number(state.pagination.pageSize) === size ? ' selected' : '') + '>' + size + '</option>'
}).join('')
+ '</select>'
+ '条</label>'
}
function renderPagination() {
if (!listPaginationEl) {
return
}
const total = Number(state.pagination.total || 0)
const page = Number(state.pagination.page || 1)
const totalPages = Math.max(1, Number(state.pagination.totalPages || 1))
const sizeSelector = renderPageSizeSelector()
if (!total) {
listPaginationEl.innerHTML = '<div class="pagination-info">共 0 条</div>'
+ '<div class="pagination-actions">' + sizeSelector + '</div>'
return
}
const visiblePageNumbers = getVisiblePageNumbers(page, totalPages)
const startIndex = (page - 1) * state.pagination.pageSize + 1
const endIndex = Math.min(total, page * state.pagination.pageSize)
const buttons = []
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoProductPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>')
if (visiblePageNumbers[0] > 1) {
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoProductPage(1)">1</button>')
if (visiblePageNumbers[0] > 2) {
buttons.push('<button class="pagination-btn" type="button" disabled>...</button>')
}
}
for (let i = 0; i < visiblePageNumbers.length; i += 1) {
const current = visiblePageNumbers[i]
buttons.push('<button class="pagination-btn' + (current === page ? ' active' : '') + '" type="button" onclick="window.__gotoProductPage(' + current + ')">' + current + '</button>')
}
if (visiblePageNumbers.length && visiblePageNumbers[visiblePageNumbers.length - 1] < totalPages) {
if (visiblePageNumbers[visiblePageNumbers.length - 1] < totalPages - 1) {
buttons.push('<button class="pagination-btn" type="button" disabled>...</button>')
}
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoProductPage(' + totalPages + ')">' + totalPages + '</button>')
}
buttons.push('<button class="pagination-btn" type="button" onclick="window.__gotoProductPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>')
listPaginationEl.innerHTML = '<div class="pagination-info">共 ' + total + ' 条,显示第 ' + startIndex + '-' + endIndex + ' 条,第 ' + page + '/' + totalPages + ' 页</div>'
+ '<div class="pagination-actions">' + sizeSelector + buttons.join('') + '</div>'
}
function updateProductListView() {
const pageSize = Math.max(1, Number(state.pagination.pageSize || 10))
const total = state.fullList.length
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const currentPage = clampPage(state.pagination.page, totalPages)
const startIndex = (currentPage - 1) * pageSize
state.pagination.page = currentPage
state.pagination.total = total
state.pagination.totalPages = totalPages
state.list = state.fullList.slice(startIndex, startIndex + pageSize)
renderTable()
renderPagination()
renderSortRankHint()
}
async function parseJsonSafe(res) { async function parseJsonSafe(res) {
const contentType = String(res.headers.get('content-type') || '').toLowerCase() const contentType = String(res.headers.get('content-type') || '').toLowerCase()
const rawText = await res.text() const rawText = await res.text()
@@ -713,6 +854,16 @@
return String(value || '').trim() return String(value || '').trim()
} }
function generatePocketBaseRecordId() {
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'
let output = ''
for (let i = 0; i < 15; i += 1) {
const idx = Math.floor(Math.random() * alphabet.length)
output += alphabet[idx]
}
return output
}
function splitPipe(value) { function splitPipe(value) {
return String(value || '') return String(value || '')
.split('|') .split('|')
@@ -858,7 +1009,7 @@
const targetSort = Math.floor(sortValue) const targetSort = Math.floor(sortValue)
const targetId = state.mode === 'edit' ? normalizeText(state.editingId) : '__draft__' const targetId = state.mode === 'edit' ? normalizeText(state.editingId) : '__draft__'
const sameCategoryItems = state.list const sameCategoryItems = state.fullList
.filter(function (item) { .filter(function (item) {
return normalizeText(item.prod_list_category) === category && normalizeText(item.prod_list_id) !== targetId return normalizeText(item.prod_list_category) === category && normalizeText(item.prod_list_id) !== targetId
}) })
@@ -1714,6 +1865,7 @@
function buildCopyPayload(source, nextName, nextModel) { function buildCopyPayload(source, nextName, nextModel) {
return { return {
id: generatePocketBaseRecordId(),
prod_list_id: '', prod_list_id: '',
prod_list_name: normalizeText(nextName), prod_list_name: normalizeText(nextName),
prod_list_modelnumber: normalizeText(nextModel), prod_list_modelnumber: normalizeText(nextModel),
@@ -1761,7 +1913,7 @@
} }
async function copyProduct(productId) { async function copyProduct(productId) {
const source = state.list.find(function (item) { const source = state.fullList.find(function (item) {
return normalizeText(item.prod_list_id) === normalizeText(productId) return normalizeText(item.prod_list_id) === normalizeText(productId)
}) })
@@ -1781,7 +1933,7 @@
return return
} }
const source = state.list.find(function (item) { const source = state.fullList.find(function (item) {
return normalizeText(item.prod_list_id) === sourceId return normalizeText(item.prod_list_id) === sourceId
}) })
if (!source) { if (!source) {
@@ -1830,20 +1982,18 @@
status: payload.status, status: payload.status,
prod_list_category: payload.prod_list_category, prod_list_category: payload.prod_list_category,
}) })
state.list = Array.isArray(data.items) ? data.items : [] state.fullList = Array.isArray(data.items) ? data.items : []
renderTable() updateProductListView()
renderSortRankHint() setStatus('产品列表已刷新,共 ' + state.pagination.total + ' 条。', 'success')
setStatus('产品列表已刷新,共 ' + state.list.length + ' 条。', 'success')
} catch (err) { } catch (err) {
try { try {
state.list = await requestProductListFallback({ state.fullList = await requestProductListFallback({
keyword: normalizeText(fields.keyword.value), keyword: normalizeText(fields.keyword.value),
status: normalizeText(statusFilterEl.value), status: normalizeText(statusFilterEl.value),
prod_list_category: normalizeText(categoryFilterEl.value), prod_list_category: normalizeText(categoryFilterEl.value),
}) })
renderTable() updateProductListView()
renderSortRankHint() setStatus('产品列表已通过回退链路刷新,共 ' + state.pagination.total + ' 条。', 'success')
setStatus('产品列表已通过回退链路刷新,共 ' + state.list.length + ' 条。', 'success')
} catch (_fallbackErr) { } catch (_fallbackErr) {
setStatus(err.message || '加载产品列表失败', 'error') setStatus(err.message || '加载产品列表失败', 'error')
} }
@@ -1878,11 +2028,35 @@
} }
} }
async function queryProductsWithAutoSave() { async function queryProductsWithAutoSave(resetPage) {
if (resetPage) {
state.pagination.page = 1
}
await saveAndHideEditorBeforeProductQuery() await saveAndHideEditorBeforeProductQuery()
await loadProducts() await loadProducts()
} }
function goToProductPage(page) {
const nextPage = clampPage(page, state.pagination.totalPages)
if (nextPage === state.pagination.page) {
return
}
state.pagination.page = nextPage
updateProductListView()
}
function setProductPageSize(pageSize) {
const nextPageSize = Number(pageSize) || 10
if (nextPageSize === state.pagination.pageSize) {
return
}
state.pagination.pageSize = nextPageSize
state.pagination.page = 1
updateProductListView()
}
async function enterEditMode(productId) { async function enterEditMode(productId) {
showLoading('正在加载产品详情...') showLoading('正在加载产品详情...')
try { try {
@@ -1948,6 +2122,7 @@
} }
const payload = { const payload = {
id: state.mode === 'edit' ? '' : generatePocketBaseRecordId(),
prod_list_id: state.mode === 'edit' ? state.editingId : '', prod_list_id: state.mode === 'edit' ? state.editingId : '',
prod_list_name: normalizeText(fields.name.value), prod_list_name: normalizeText(fields.name.value),
prod_list_modelnumber: normalizeText(fields.model.value), prod_list_modelnumber: normalizeText(fields.model.value),
@@ -2218,14 +2393,37 @@
} }
}) })
window.__gotoProductPage = goToProductPage
window.__setProductPageSize = setProductPageSize
document.getElementById('reloadBtn').addEventListener('click', function () { document.getElementById('reloadBtn').addEventListener('click', function () {
queryProductsWithAutoSave() queryProductsWithAutoSave(false)
}) })
document.getElementById('searchBtn').addEventListener('click', function () { document.getElementById('searchBtn').addEventListener('click', function () {
queryProductsWithAutoSave() queryProductsWithAutoSave(true)
}) })
if (fields.keyword) {
fields.keyword.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
queryProductsWithAutoSave(true)
}
})
}
if (statusFilterEl) {
statusFilterEl.addEventListener('change', function () {
queryProductsWithAutoSave(true)
})
}
if (categoryFilterEl) {
categoryFilterEl.addEventListener('change', function () {
queryProductsWithAutoSave(true)
})
}
document.getElementById('createModeBtn').addEventListener('click', function () { document.getElementById('createModeBtn').addEventListener('click', function () {
enterCreateMode() enterCreateMode()
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,263 @@
paths:
attachmentRecords:
get:
operationId: getPocketBaseAttachmentRecords
tags:
- 附件信息
summary: 根据 attachments_id 查询单条或多条附件信息
description: |
使用 PocketBase 原生 records list 接口查询 `tbl_attachments`。
当前线上权限规则:
- `listRule = is_delete = 0`,因此任何客户端都可直接读取未软删除附件
- 原生 `create/update/delete` 仍仅管理员或管理后台用户允许
标准调用方式有两种:
1. 按 `attachments_id` 查询单条:
- `filter=attachments_id="ATT-1774599142438-8n1UcU"`
- `perPage=1`
- `page=1`
2. 按多个 `attachments_id` 批量查询:
- 使用 `||` 组合多个等值条件
- 例如:`filter=attachments_id="ATT-1774599142438-8n1UcU" || attachments_id="ATT-1774599143999-7pQkLm"`
- 传 `perPage` 为预期返回条数,`page=1`
注意:
- 这是 PocketBase 原生返回结构,不是 hooks 统一 `{ statusCode, errMsg, data }` 包装
- `attachments_link` 返回的是 PocketBase 文件字段值,不是完整下载地址
- 若需文件流地址,可按 PocketBase 标准文件路径自行拼接:`/pb/api/files/{collectionId}/{recordId}/{attachments_link}`
parameters:
- name: filter
in: query
required: false
description: |
PocketBase 标准过滤表达式。
- 按 `attachments_id` 精确查询单条:`attachments_id="ATT-1774599142438-8n1UcU"`
- 按多个 `attachments_id` 批量查询:`attachments_id="ATT-1774599142438-8n1UcU" || attachments_id="ATT-1774599143999-7pQkLm"`
- 不传该参数时,返回分页列表
schema:
type: string
example: attachments_id="ATT-1774599142438-8n1UcU"
- name: page
in: query
required: false
description: 页码
schema:
type: integer
minimum: 1
default: 1
- name: perPage
in: query
required: false
description: 每页条数;单查建议为 `1`,批量查询建议设置为预期条数
schema:
type: integer
minimum: 1
default: 20
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseAttachmentListResponse
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: 集合ID|string
collectionName: 集合名称|string
attachments_id: 附件业务 ID|string
attachments_link: PocketBase 文件字段值,可按标准文件路径拼接文件流地址|string
attachments_filename: 原始文件名|string
attachments_filetype: 文件类型 / MIME|string
attachments_size: 文件大小|number
attachments_owner: 上传者业务标识|string
attachments_md5: 文件 MD5|string
attachments_ocr: OCR 识别结果|string
attachments_status: 附件状态|string
attachments_remark: 备注|string
"400":
description: 查询参数错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"403":
description: 集合规则被锁定或服务端权限设置异常
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
components:
schemas:
PocketBaseNativeError:
type: object
properties:
code:
type:
- integer
- string
description: 业务状态码
example: 错误状态码 | integer
message:
type: string
example: PocketBase原生错误信息 | string
data:
description: 业务响应数据
type: object
additionalProperties: true
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
PocketBaseAttachmentRecord:
type: object
properties:
id:
type: string
description: PocketBase 记录主键
example: PocketBase记录主键 | string
collectionId:
type: string
description: 集合ID
example: 集合ID | string
collectionName:
type: string
description: 集合名称
example: 集合名称 | string
attachments_id:
type: string
description: 附件业务 ID
example: 附件业务ID | string
attachments_link:
type: string
description: PocketBase 文件字段值,可按标准文件路径拼接文件流地址
example: PocketBase文件字段值可拼接文件流地址 | string
attachments_filename:
type: string
description: 原始文件名
example: 原始文件名 | string
attachments_filetype:
type: string
description: 文件类型 / MIME
example: 文件类型或MIME | string
attachments_size:
type:
- number
- integer
- string
description: 文件大小
example: 文件大小 | number
attachments_owner:
type: string
description: 上传者业务标识
example: 上传者业务标识 | string
attachments_md5:
type: string
description: 文件 MD5
example: 文件MD5 | string
attachments_ocr:
type: string
description: OCR 识别结果
example: OCR识别结果 | string
attachments_status:
type: string
description: 附件状态
example: 附件状态 | string
attachments_remark:
type: string
description: 备注
example: 备注 | string
example:
id: PocketBase 记录主键|string
collectionId: 集合ID|string
collectionName: 集合名称|string
attachments_id: 附件业务 ID|string
attachments_link: PocketBase 文件字段值,可按标准文件路径拼接文件流地址|string
attachments_filename: 原始文件名|string
attachments_filetype: 文件类型 / MIME|string
attachments_size: 文件大小|number
attachments_owner: 上传者业务标识|string
attachments_md5: 文件 MD5|string
attachments_ocr: OCR 识别结果|string
attachments_status: 附件状态|string
attachments_remark: 备注|string
PocketBaseAttachmentListResponse:
type: object
required:
- page
- perPage
- totalItems
- totalPages
- items
properties:
page:
type:
- integer
- string
example: 页码 | integer
perPage:
type:
- integer
- string
example: 每页条数 | integer
totalItems:
type:
- integer
- string
example: 总记录数 | integer
totalPages:
type:
- integer
- string
example: 总页数 | integer
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseAttachmentRecord
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: 集合ID|string
collectionName: 集合名称|string
attachments_id: 附件业务 ID|string
attachments_link: PocketBase 文件字段值,可按标准文件路径拼接文件流地址|string
attachments_filename: 原始文件名|string
attachments_filetype: 文件类型 / MIME|string
attachments_size: 文件大小|number
attachments_owner: 上传者业务标识|string
attachments_md5: 文件 MD5|string
attachments_ocr: OCR 识别结果|string
attachments_status: 附件状态|string
attachments_remark: 备注|string

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,903 @@
paths:
companyRecords:
post:
operationId: postPocketBaseCompanyRecord
tags:
- 企业信息
summary: 创建公司
description: |
使用 PocketBase 原生 records create 接口向 `tbl_company` 新增一行记录。
当前线上权限规则:
- `createRule = ""`,因此任何客户端都可直接创建
- 其他原生操作中,`update/delete/view` 仅管理员或管理后台用户允许
注意:
- 这是 PocketBase 原生返回结构,不是 hooks 统一 `{ statusCode, errMsg, data }` 包装
- `company_id` 由数据库自动生成,客户端创建时不需要传
- `company_id` 仍带唯一索引,可用于后续按业务 id 查询
requestBody:
required: true
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyCreateRequest
example:
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区 / 县名称|string
company_district_code: 区 / 县编码|string
company_postalcode: 邮编|string
company_add: 地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
responses:
"200":
description: 创建成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
"400":
description: 参数错误或违反当前集合约束
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"403":
description: 集合规则被锁定或服务端权限设置异常
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
get:
operationId: getPocketBaseCompanyRecords
tags:
- 企业信息
summary: 查询整个 tbl_company 列表 / 根据 company_id 查询对应公司信息
description: |
使用 PocketBase 原生 records list 接口查询 `tbl_company`。
当前线上权限规则:
- `listRule = is_delete = 0`,因此默认只返回未软删除数据,且整个列表查询与条件查询都公开可读
- `createRule = ""`,因此创建也公开可调用
- `view/update/delete` 仅管理员或管理后台用户允许
标准调用方式有两种:
1. 根据 `company_id` 查询对应公司信息:
- `filter=company_id="WX-COMPANY-10001"`
- `perPage=1`
- `page=1`
2. 查询整个 `tbl_company` 列表:
- 不传 `filter`
- 按需传 `page`、`perPage`
注意:
- 这是 PocketBase 原生返回结构,不是 hooks 统一 `{ statusCode, errMsg, data }` 包装
- PocketBase 原生标准接口里,“按 `company_id` 查询单条”和“查询整个列表”共用同一个 `GET /records` 路径,因此文档以同一个 GET operation 展示两种调用模式
parameters:
- name: filter
in: query
required: false
description: |
PocketBase 标准过滤表达式。
- 根据 `company_id` 查询单条时:`company_id="WX-COMPANY-10001"`
- 查询整个列表时:不传该参数
schema:
type: string
example: company_id="WX-COMPANY-10001"
- name: page
in: query
required: false
description: 页码
schema:
type: integer
minimum: 1
default: 1
- name: perPage
in: query
required: false
description: 每页条数;按 `company_id` 单查时建议固定为 `1`
schema:
type: integer
minimum: 1
default: 20
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyListResponse
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
"400":
description: 查询参数错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"403":
description: 集合规则被锁定或服务端权限设置异常
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
companyRecordById:
patch:
operationId: patchPocketBaseCompanyRecordByRecordId
tags:
- 企业信息
summary: 通过 company_id 定位后修改公司信息
description: |
这是 PocketBase 原生标准更新接口,实际写入路径参数仍然必须使用记录主键 `recordId`。
如果前端手里只有 `company_id`,标准调用流程是:
1. 先调用 `GET /pb/api/collections/tbl_company/records?filter=company_id="..."&perPage=1&page=1`
2. 从返回结果 `items[0].id` 中取出 PocketBase 原生记录主键
3. 再调用当前 `PATCH /pb/api/collections/tbl_company/records/{recordId}` 完成更新
当前线上权限规则:
- `updateRule` 仅管理员或管理后台用户允许
- 普通公开调用不能直接更新
parameters:
- name: recordId
in: path
required: true
description: 通过 `company_id` 查询结果拿到的 PocketBase 记录主键 `id`
schema:
type: string
example: l2r3nq7rqhuob0h
requestBody:
required: true
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyUpdateRequest
example:
company_id: 所属公司业务 ID|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区 / 县名称|string
company_district_code: 区 / 县编码|string
company_postalcode: 邮编|string
company_add: 地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
responses:
"200":
description: 更新成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
"400":
description: 参数错误或违反集合约束
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"403":
description: 当前调用方没有 update 权限
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"404":
description: 记录不存在
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
components:
schemas:
PocketBaseNativeError:
type: object
properties:
code:
type:
- integer
- string
description: 业务状态码
example: 错误状态码 | integer
message:
type: string
example: PocketBase原生错误信息 | string
data:
description: 业务响应数据
type: object
additionalProperties: true
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
PocketBaseRecordBase:
type: object
required:
- id
- collectionId
- collectionName
- created
- updated
properties:
id:
type: string
description: PocketBase 记录主键
example: PocketBase记录主键 | string
collectionId:
type: string
example: 集合ID | string
collectionName:
type: string
example: 集合名称 | string
created:
type: string
description: 记录创建时间
example: 记录创建时间 | string
updated:
type: string
description: 记录更新时间
example: 记录更新时间 | string
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
CompanyInfo:
anyOf:
- type: object
description: 用户所属公司信息;当用户尚未绑定公司时返回 `null`
properties:
pb_id:
type: string
description: PocketBase 记录主键 id
example: PocketBase记录主键id | string
company_id:
type: string
description: 公司业务 id由数据库自动生成
example: 公司业务id由数据库自动生成 | string
company_name:
type: string
description: 公司名称
example: 公司名称 | string
company_type:
type: string
description: 公司类型
example: 公司类型 | string
company_entity:
type: string
description: 公司法人
example: 公司法人 | string
company_usci:
type: string
description: 统一社会信用代码
example: 统一社会信用代码 | string
company_nationality:
type: string
description: 国家名称
example: 国家名称 | string
company_nationality_code:
type: string
description: 国家编码
example: 国家编码 | string
company_province:
type: string
description: 省份名称
example: 省份名称 | string
company_province_code:
type: string
description: 省份编码
example: 省份编码 | string
company_city:
type: string
description: 城市名称
example: 城市名称 | string
company_city_code:
type: string
description: 城市编码
example: 城市编码 | string
company_district:
type: string
description: 区/县名称
example: 区县名称 | string
company_district_code:
type: string
description: 区/县编码
example: 区县编码 | string
company_postalcode:
type: string
description: 邮政编码
example: 邮政编码 | string
company_add:
type: string
description: 公司地址
example: 公司地址 | string
company_status:
type: string
description: 公司状态
example: 公司状态 | string
company_level:
type: string
description: 公司等级
example: 公司等级 | string
company_owner_openid:
type: string
description: 公司所有者 openid
example: 公司所有者openid | string
company_remark:
type: string
description: 备注
example: 备注 | string
created:
type: string
description: 记录创建时间
example: 记录创建时间 | string
updated:
type: string
description: 记录更新时间
example: 记录更新时间 | string
example:
pb_id: PocketBase记录主键id | string
company_id: 公司业务id由数据库自动生成 | string
company_name: 公司名称 | string
company_type: 公司类型 | string
company_entity: 公司法人 | string
company_usci: 统一社会信用代码 | string
company_nationality: 国家名称 | string
company_nationality_code: 国家编码 | string
company_province: 省份名称 | string
company_province_code: 省份编码 | string
company_city: 城市名称 | string
company_city_code: 城市编码 | string
company_district: 区县名称 | string
company_district_code: 区县编码 | string
company_postalcode: 邮政编码 | string
company_add: 公司地址 | string
company_status: 公司状态 | string
company_level: 公司等级 | string
company_owner_openid: 公司所有者openid | string
company_remark: 备注 | string
created: 记录创建时间 | string
updated: 记录更新时间 | string
- type: "null"
example:
pb_id: PocketBase 记录主键 id|string
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
created: 记录创建时间|string
updated: 记录更新时间|string
PocketBaseCompanyFields:
type: object
properties:
company_id:
type: string
description: 公司业务 id由数据库自动生成
example: 公司业务id由数据库自动生成 | string
company_name:
type: string
description: 公司名称
example: 公司名称 | string
company_type:
type: string
description: 公司类型
example: 公司类型 | string
company_entity:
type: string
description: 公司法人
example: 公司法人 | string
company_usci:
type: string
description: 统一社会信用代码
example: 统一社会信用代码 | string
company_nationality:
type: string
description: 国家名称
example: 国家名称 | string
company_nationality_code:
type: string
description: 国家编码
example: 国家编码 | string
company_province:
type: string
description: 省份名称
example: 省份名称 | string
company_province_code:
type: string
description: 省份编码
example: 省份编码 | string
company_city:
type: string
description: 城市名称
example: 城市名称 | string
company_city_code:
type: string
description: 城市编码
example: 城市编码 | string
company_district:
type: string
description: 区/县名称
example: 区县名称 | string
company_district_code:
type: string
description: 区/县编码
example: 区县编码 | string
company_postalcode:
type: string
description: 邮政编码
example: 邮政编码 | string
company_add:
type: string
description: 公司地址
example: 公司地址 | string
company_status:
type: string
description: 公司状态
example: 公司状态 | string
company_level:
type: string
description: 公司等级
example: 公司等级 | string
company_owner_openid:
type: string
description: 公司所有者 openid
example: 公司所有者openid | string
company_remark:
type: string
description: 备注
example: 备注 | string
example:
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
PocketBaseCompanyRecord:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyFields
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
PocketBaseCompanyCreateRequest:
type: object
properties:
company_name:
description: 公司名称
type: string
company_type:
description: 公司类型
type: string
company_entity:
description: 公司法人
type: string
company_usci:
description: 统一社会信用代码
type: string
company_nationality:
description: 国家名称
type: string
company_nationality_code:
description: 国家编码
type: string
company_province:
description: 省份名称
type: string
company_province_code:
description: 省份编码
type: string
company_city:
description: 城市名称
type: string
company_city_code:
description: 城市编码
type: string
company_district:
description: 区 / 县名称
type: string
company_district_code:
description: 区 / 县编码
type: string
company_postalcode:
description: 邮编
type: string
company_add:
description: 地址
type: string
company_status:
description: 公司状态
type: string
company_level:
description: 公司等级
type: string
company_owner_openid:
description: 公司所有者 openid
type: string
company_remark:
description: 备注
type: string
additionalProperties: false
example:
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区 / 县名称|string
company_district_code: 区 / 县编码|string
company_postalcode: 邮编|string
company_add: 地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
PocketBaseCompanyUpdateRequest:
type: object
properties:
company_id:
description: 所属公司业务 ID
type: string
company_name:
description: 公司名称
type: string
company_type:
description: 公司类型
type: string
company_entity:
description: 公司法人
type: string
company_usci:
description: 统一社会信用代码
type: string
company_nationality:
description: 国家名称
type: string
company_nationality_code:
description: 国家编码
type: string
company_province:
description: 省份名称
type: string
company_province_code:
description: 省份编码
type: string
company_city:
description: 城市名称
type: string
company_city_code:
description: 城市编码
type: string
company_district:
description: 区 / 县名称
type: string
company_district_code:
description: 区 / 县编码
type: string
company_postalcode:
description: 邮编
type: string
company_add:
description: 地址
type: string
company_status:
description: 公司状态
type: string
company_level:
description: 公司等级
type: string
company_owner_openid:
description: 公司所有者 openid
type: string
company_remark:
description: 备注
type: string
additionalProperties: false
example:
company_id: 所属公司业务 ID|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区 / 县名称|string
company_district_code: 区 / 县编码|string
company_postalcode: 邮编|string
company_add: 地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string
PocketBaseCompanyListResponse:
type: object
required:
- page
- perPage
- totalItems
- totalPages
- items
properties:
page:
type:
- integer
- string
example: 页码 | integer
perPage:
type:
- integer
- string
example: 每页条数 | integer
totalItems:
type:
- integer
- string
example: 总记录数 | integer
totalPages:
type:
- integer
- string
example: 总页数 | integer
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
company_id: 公司业务 id由数据库自动生成|string
company_name: 公司名称|string
company_type: 公司类型|string
company_entity: 公司法人|string
company_usci: 统一社会信用代码|string
company_nationality: 国家名称|string
company_nationality_code: 国家编码|string
company_province: 省份名称|string
company_province_code: 省份编码|string
company_city: 城市名称|string
company_city_code: 城市编码|string
company_district: 区/县名称|string
company_district_code: 区/县编码|string
company_postalcode: 邮政编码|string
company_add: 公司地址|string
company_status: 公司状态|string
company_level: 公司等级|string
company_owner_openid: 公司所有者 openid|string
company_remark: 备注|string

View File

@@ -0,0 +1,468 @@
paths:
documentRecords:
get:
operationId: getPocketBaseDocumentRecords
tags:
- 文档信息
summary: 分页查询文档列表 / 按 system_dict_id 与 enum 双条件分页筛选文档
description: |
使用 PocketBase 原生 records list 接口查询 `tbl_document`。
当前线上权限规则:
- `listRule = is_delete = 0`,因此任何客户端都可直接分页查询未软删除文档
- `viewRule = is_delete = 0`,因此单条详情也只可读取未软删除文档
- `create/update/delete` 仍仅管理员或管理后台用户允许
`document_type` 的存储格式为:
- `system_dict_id@dict_word_enum|system_dict_id@dict_word_enum`
业务上这里是两个独立条件,并且查询时两个条件都要满足:
- 条件 1包含某个 `system_dict_id`
- 条件 2包含某个 `enum`
PocketBase 原生标准接口实际只有一个 `filter` 参数,因此应在同一个 `filter` 中写成两个 `contains` 条件,例如:
- `system_dict_id = DICT-1774599144591-hAEFQj`
- `enum = UT1`
- 最终:`document_type ~ "DICT-1774599144591-hAEFQj" && document_type ~ "@UT1"`
这条写法已经按线上真实数据验证通过。
排序说明:
- 当前线上统一按 `document_create` 排序
- 若要“最新上传的排在最前面”,请传 `sort=-document_create`
标准调用方式有两种:
1. 查询整个文档列表:
- 不传 `filter`
- 按需传 `page`、`perPage`
- 若要按最新上传倒序,传 `sort=-document_create`
2. 根据 `system_dict_id` 和 `enum` 两个业务条件分页筛选:
- 直接传 `filter=document_type ~ "<system_dict_id>" && document_type ~ "@<enum>"`
- 传 `page`、`perPage`
- 若要按最新上传倒序,传 `sort=-document_create`
注意:
- 这是 PocketBase 原生返回结构,不是 hooks 统一 `{ statusCode, errMsg, data }` 包装
- 如果需要更复杂的条件组合,可继续使用 PocketBase 原生 `filter` 语法自行扩展
parameters:
- name: filter
in: query
required: false
description: |
PocketBase 标准过滤表达式。
- 查全部列表时:不传
- 按业务条件筛选时,同时写两个 `contains` 条件
- 第二个条件建议带上 `@` 前缀,避免误命中
- 例如:`document_type ~ "DICT-1774599144591-hAEFQj" && document_type ~ "@UT1"`
schema:
type: string
example: document_type ~ "DICT-1774599144591-hAEFQj" && document_type ~ "@UT1"
- name: page
in: query
required: false
description: 页码
schema:
type: integer
minimum: 1
default: 1
- name: perPage
in: query
required: false
description: 每页条数
schema:
type: integer
minimum: 1
default: 20
- name: sort
in: query
required: false
description: |
PocketBase 原生排序表达式。
当前线上建议使用:
- `-document_create`:按最新上传倒序返回
schema:
type: string
example: -document_create
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseDocumentListResponse
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
document_id: 文档业务 ID|string
document_create: 文档创建时间,由数据库自动生成|string
document_effect_date: 文档生效日期|string
document_expiry_date: 文档到期日期|string
document_title: 文档标题|string
document_type: 文档类型,多选时按 system_dict_id@dict_word_enum|... 保存|string
document_subtitle: 文档副标题|string
document_summary: 文档摘要|string
document_content: 正文内容,保存 Markdown|string
document_image: 图片附件 ID 集合,底层以 | 分隔|string
document_video: 视频附件 ID 集合,底层以 | 分隔|string
document_file: 文件附件 ID 集合,底层以 | 分隔|string
document_status: 文档状态,仅 有效 / 过期|string
document_owner: 上传者 openid|string
document_relation_model: 关联机型 / 模型标识|string
document_keywords: 关键词,多选后以 | 分隔|string
document_share_count: 分享次数|number
document_download_count: 下载次数|number
document_favorite_count: 收藏次数|number
document_embedding_status: 文档嵌入状态|string
document_embedding_error: 文档嵌入错误原因|string
document_embedding_lasttime: 最后一次嵌入更新时间|string
document_vector_version: 向量版本号 / 模型名称|string
document_product_categories: 产品关联文档,多选后以 | 分隔|string
document_application_scenarios: 筛选依据,多选后以 | 分隔|string
document_hotel_type: 适用场景,多选后以 | 分隔|string
document_remark: 备注|string
"400":
description: 查询参数错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"403":
description: 集合规则被锁定或服务端权限设置异常
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
components:
schemas:
PocketBaseNativeError:
type: object
properties:
code:
type:
- integer
- string
description: 业务状态码
example: 错误状态码 | integer
message:
type: string
example: PocketBase原生错误信息 | string
data:
description: 业务响应数据
type: object
additionalProperties: true
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
PocketBaseRecordBase:
type: object
required:
- id
- collectionId
- collectionName
- created
- updated
properties:
id:
type: string
description: PocketBase 记录主键
example: PocketBase记录主键 | string
collectionId:
type: string
example: 集合ID | string
collectionName:
type: string
example: 集合名称 | string
created:
type: string
description: 记录创建时间
example: 记录创建时间 | string
updated:
type: string
description: 记录更新时间
example: 记录更新时间 | string
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
PocketBaseDocumentFields:
type: object
properties:
document_id:
type: string
description: 文档业务 ID
example: 文档业务ID | string
document_create:
type: string
description: 文档创建时间,由数据库自动生成
example: 文档创建时间,由数据库自动生成 | string
document_effect_date:
type: string
description: 文档生效日期
example: 文档生效日期 | string
document_expiry_date:
type: string
description: 文档到期日期
example: 文档到期日期 | string
document_title:
type: string
description: 文档标题
example: 文档标题 | string
document_type:
type: string
description: 文档类型,多选时按 system_dict_id@dict_word_enum|... 保存
example: 文档类型按system_dict_id@dict_word_enum保存 | string
document_subtitle:
type: string
description: 文档副标题
example: 文档副标题 | string
document_summary:
type: string
description: 文档摘要
example: 文档摘要 | string
document_content:
type: string
description: 正文内容,保存 Markdown
example: 正文内容 | string
document_image:
type: string
description: 图片附件 ID 集合,底层以 | 分隔
example: 图片附件ID串底层按|分隔 | string
document_video:
type: string
description: 视频附件 ID 集合,底层以 | 分隔
example: 视频附件ID串底层按|分隔 | string
document_file:
type: string
description: 文件附件 ID 集合,底层以 | 分隔
example: 文件附件ID串底层按|分隔 | string
document_status:
type: string
description: 文档状态,仅 `有效` / `过期`
example: 文档状态 | string
document_owner:
type: string
description: 上传者 openid
example: 上传者openid | string
document_relation_model:
type: string
description: 关联机型 / 模型标识
example: 关联机型标识 | string
document_keywords:
type: string
description: 关键词,多选后以 | 分隔
example: 关键词,多选按|分隔 | string
document_share_count:
type: number
description: 分享次数
example: 0
document_download_count:
type: number
description: 下载次数
example: 0
document_favorite_count:
type: number
description: 收藏次数
example: 0
document_embedding_status:
type: string
description: 文档嵌入状态
example: 文档嵌入状态 | string
document_embedding_error:
type: string
description: 文档嵌入错误原因
example: 文档嵌入错误原因 | string
document_embedding_lasttime:
type: string
description: 最后一次嵌入更新时间
example: 最后一次嵌入更新时间 | string
document_vector_version:
type: string
description: 向量版本号 / 模型名称
example: 向量版本号或模型名称 | string
document_product_categories:
type: string
description: 产品关联文档,多选后以 | 分隔
example: 产品关联文档,多选按|分隔 | string
document_application_scenarios:
type: string
description: 筛选依据,多选后以 | 分隔
example: 筛选依据,多选按|分隔 | string
document_hotel_type:
type: string
description: 适用场景,多选后以 | 分隔
example: 适用场景,多选按|分隔 | string
document_remark:
type: string
description: 备注
example: 备注 | string
example:
document_id: 文档业务 ID|string
document_create: 文档创建时间,由数据库自动生成|string
document_effect_date: 文档生效日期|string
document_expiry_date: 文档到期日期|string
document_title: 文档标题|string
document_type: 文档类型,多选时按 system_dict_id@dict_word_enum|... 保存|string
document_subtitle: 文档副标题|string
document_summary: 文档摘要|string
document_content: 正文内容,保存 Markdown|string
document_image: 图片附件 ID 集合,底层以 | 分隔|string
document_video: 视频附件 ID 集合,底层以 | 分隔|string
document_file: 文件附件 ID 集合,底层以 | 分隔|string
document_status: 文档状态,仅 有效 / 过期|string
document_owner: 上传者 openid|string
document_relation_model: 关联机型 / 模型标识|string
document_keywords: 关键词,多选后以 | 分隔|string
document_share_count: 分享次数|number
document_download_count: 下载次数|number
document_favorite_count: 收藏次数|number
document_embedding_status: 文档嵌入状态|string
document_embedding_error: 文档嵌入错误原因|string
document_embedding_lasttime: 最后一次嵌入更新时间|string
document_vector_version: 向量版本号 / 模型名称|string
document_product_categories: 产品关联文档,多选后以 | 分隔|string
document_application_scenarios: 筛选依据,多选后以 | 分隔|string
document_hotel_type: 适用场景,多选后以 | 分隔|string
document_remark: 备注|string
PocketBaseDocumentRecord:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseDocumentFields
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
document_id: 文档业务 ID|string
document_create: 文档创建时间,由数据库自动生成|string
document_effect_date: 文档生效日期|string
document_expiry_date: 文档到期日期|string
document_title: 文档标题|string
document_type: 文档类型,多选时按 system_dict_id@dict_word_enum|... 保存|string
document_subtitle: 文档副标题|string
document_summary: 文档摘要|string
document_content: 正文内容,保存 Markdown|string
document_image: 图片附件 ID 集合,底层以 | 分隔|string
document_video: 视频附件 ID 集合,底层以 | 分隔|string
document_file: 文件附件 ID 集合,底层以 | 分隔|string
document_status: 文档状态,仅 有效 / 过期|string
document_owner: 上传者 openid|string
document_relation_model: 关联机型 / 模型标识|string
document_keywords: 关键词,多选后以 | 分隔|string
document_share_count: 分享次数|number
document_download_count: 下载次数|number
document_favorite_count: 收藏次数|number
document_embedding_status: 文档嵌入状态|string
document_embedding_error: 文档嵌入错误原因|string
document_embedding_lasttime: 最后一次嵌入更新时间|string
document_vector_version: 向量版本号 / 模型名称|string
document_product_categories: 产品关联文档,多选后以 | 分隔|string
document_application_scenarios: 筛选依据,多选后以 | 分隔|string
document_hotel_type: 适用场景,多选后以 | 分隔|string
document_remark: 备注|string
PocketBaseDocumentListResponse:
type: object
required:
- page
- perPage
- totalItems
- totalPages
- items
properties:
page:
type:
- integer
- string
example: 页码 | integer
perPage:
type:
- integer
- string
example: 每页条数 | integer
totalItems:
type:
- integer
- string
example: 总记录数 | integer
totalPages:
type:
- integer
- string
example: 总页数 | integer
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseDocumentRecord
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
document_id: 文档业务 ID|string
document_create: 文档创建时间,由数据库自动生成|string
document_effect_date: 文档生效日期|string
document_expiry_date: 文档到期日期|string
document_title: 文档标题|string
document_type: 文档类型,多选时按 system_dict_id@dict_word_enum|... 保存|string
document_subtitle: 文档副标题|string
document_summary: 文档摘要|string
document_content: 正文内容,保存 Markdown|string
document_image: 图片附件 ID 集合,底层以 | 分隔|string
document_video: 视频附件 ID 集合,底层以 | 分隔|string
document_file: 文件附件 ID 集合,底层以 | 分隔|string
document_status: 文档状态,仅 有效 / 过期|string
document_owner: 上传者 openid|string
document_relation_model: 关联机型 / 模型标识|string
document_keywords: 关键词,多选后以 | 分隔|string
document_share_count: 分享次数|number
document_download_count: 下载次数|number
document_favorite_count: 收藏次数|number
document_embedding_status: 文档嵌入状态|string
document_embedding_error: 文档嵌入错误原因|string
document_embedding_lasttime: 最后一次嵌入更新时间|string
document_vector_version: 向量版本号 / 模型名称|string
document_product_categories: 产品关联文档,多选后以 | 分隔|string
document_application_scenarios: 筛选依据,多选后以 | 分隔|string
document_hotel_type: 适用场景,多选后以 | 分隔|string
document_remark: 备注|string

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,413 @@
paths:
productRecords:
get:
operationId: getPocketBaseProductListRecords
tags:
- 产品信息
summary: 根据产品分类精确筛选并按分类排序值升序返回产品列表
description: |
使用 PocketBase 原生 records list 接口查询 `tbl_product_list`。
当前接口约定:
- 默认仅返回 `is_delete = 0` 的未软删除产品
- 条件:按 `prod_list_category` 精确匹配筛选
- 排序:按 `prod_list_sort` 从小到大排序
标准调用参数建议:
- `filter=prod_list_category="<产品分类>"`
- `sort=prod_list_sort`
注意:
- 这是 PocketBase 原生返回结构,不是 hooks 统一 `{ statusCode, errMsg, data }` 包装
- 若不传 `sort`,将由 PocketBase 默认排序策略决定返回顺序
parameters:
- name: filter
in: query
required: true
description: |
PocketBase 标准过滤表达式,当前要求按产品分类精确值筛选。
推荐写法:`prod_list_category="<产品分类>"`
schema:
type: string
example: prod_list_category="<产品分类>"
- name: page
in: query
required: false
description: 页码
schema:
type: integer
minimum: 1
default: 1
- name: perPage
in: query
required: false
description: 每页条数
schema:
type: integer
minimum: 1
default: 20
- name: sort
in: query
required: false
description: |
PocketBase 原生排序表达式。
当前要求使用:
- `prod_list_sort`:按分类排序值从小到大
schema:
type: string
example: prod_list_sort
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseProductListListResponse
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
prod_list_parameters:
- name: name|string
value: value|string
prod_list_plantype: 产品方案|string
prod_list_category: 产品分类(必填,单选)|string
prod_list_sort: 排序值(同分类内按升序)|number
prod_list_comm_type: 通讯类型|string
prod_list_series: 产品系列|string
prod_list_power_supply: 供电方式|string
prod_list_tags: 产品标签(辅助检索,以 | 聚合)|string
prod_list_status: 产品状态(有效 / 过期 / 主推等)|string
prod_list_basic_price: 基础价格|number
prod_list_vip_price:
- viplevel: viplevel|string
price: price|number
prod_list_remark: 备注|string
"400":
description: 查询参数错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"403":
description: 集合规则被锁定或服务端权限设置异常
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseNativeError
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
components:
schemas:
PocketBaseNativeError:
type: object
properties:
code:
type:
- integer
- string
description: 业务状态码
example: 错误状态码 | integer
message:
type: string
example: PocketBase原生错误信息 | string
data:
description: 业务响应数据
type: object
additionalProperties: true
example:
code: 业务状态码|integer
message: message|string
data:
业务响应数据字段|string: 业务响应数据值|string
PocketBaseRecordBase:
type: object
required:
- id
- collectionId
- collectionName
- created
- updated
properties:
id:
type: string
description: PocketBase 记录主键
example: PocketBase记录主键 | string
collectionId:
type: string
example: 集合ID | string
collectionName:
type: string
example: 集合名称 | string
created:
type: string
description: 记录创建时间
example: 记录创建时间 | string
updated:
type: string
description: 记录更新时间
example: 记录更新时间 | string
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
PocketBaseProductListFields:
type: object
properties:
prod_list_id:
type: string
description: 产品列表业务 ID唯一标识
example: <产品列表业务ID>|<string>
prod_list_name:
type: string
description: 产品名称
example: <产品名称>|<string>
prod_list_modelnumber:
type: string
description: 产品型号
example: <产品型号>|<string>
prod_list_icon:
type: string
description: 产品图标附件 ID保存 `tbl_attachments.attachments_id`
example: <产品图标附件ID>|<string>
prod_list_description:
type: string
description: 产品说明
example: <产品说明>|<string>
prod_list_feature:
type: string
description: 产品特色
example: <产品特色>|<string>
prod_list_parameters:
type: array
description: 产品参数数组,每项包含 name/value
items:
type: object
properties:
name:
type: string
example: <属性名>|<string>
value:
type: string
example: <属性值>|<string>
example:
- name: <属性名>|<string>
value: <属性值>|<string>
prod_list_plantype:
type: string
description: 产品方案
example: <产品方案>|<string>
prod_list_category:
type: string
description: 产品分类(必填,单选)
example: <产品分类>|<string>
prod_list_sort:
type:
- number
- integer
description: 排序值(同分类内按升序)
example: 10
prod_list_comm_type:
type: string
description: 通讯类型
example: <通讯类型>|<string>
prod_list_series:
type: string
description: 产品系列
example: <产品系列>|<string>
prod_list_power_supply:
type: string
description: 供电方式
example: <供电方式>|<string>
prod_list_tags:
type: string
description: 产品标签(辅助检索,以 `|` 聚合)
example: <产品标签>|<string>
prod_list_status:
type: string
description: 产品状态(有效 / 过期 / 主推等)
example: <产品状态>|<string>
prod_list_basic_price:
type:
- number
- integer
description: 基础价格
example: 1999
prod_list_vip_price:
type: array
description: 会员价数组,每项包含会员等级枚举值与价格
items:
type: object
properties:
viplevel:
type: string
example: <会员等级枚举值>|<string>
price:
type:
- number
- integer
example: 1899
example:
- viplevel: VIP1
price: 1899
prod_list_remark:
type: string
description: 备注
example: <备注>|<string>
example:
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
prod_list_parameters:
- name: name|string
value: value|string
prod_list_plantype: 产品方案|string
prod_list_category: 产品分类(必填,单选)|string
prod_list_sort: 排序值(同分类内按升序)|number
prod_list_comm_type: 通讯类型|string
prod_list_series: 产品系列|string
prod_list_power_supply: 供电方式|string
prod_list_tags: 产品标签(辅助检索,以 | 聚合)|string
prod_list_status: 产品状态(有效 / 过期 / 主推等)|string
prod_list_basic_price: 基础价格|number
prod_list_vip_price:
- viplevel: viplevel|string
price: price|number
prod_list_remark: 备注|string
PocketBaseProductListRecord:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase
- $ref: ../openapi-wx.yaml#/components/schemas/PocketBaseProductListFields
example:
id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
prod_list_parameters:
- name: name|string
value: value|string
prod_list_plantype: 产品方案|string
prod_list_category: 产品分类(必填,单选)|string
prod_list_sort: 排序值(同分类内按升序)|number
prod_list_comm_type: 通讯类型|string
prod_list_series: 产品系列|string
prod_list_power_supply: 供电方式|string
prod_list_tags: 产品标签(辅助检索,以 | 聚合)|string
prod_list_status: 产品状态(有效 / 过期 / 主推等)|string
prod_list_basic_price: 基础价格|number
prod_list_vip_price:
- viplevel: viplevel|string
price: price|number
prod_list_remark: 备注|string
PocketBaseProductListListResponse:
type: object
required:
- page
- perPage
- totalItems
- totalPages
- items
properties:
page:
type:
- integer
- string
example: <页码>|<integer>
perPage:
type:
- integer
- string
example: <每页条数>|<integer>
totalItems:
type:
- integer
- string
example: <总记录数>|<integer>
totalPages:
type:
- integer
- string
example: <总页数>|<integer>
items:
type: array
items:
$ref: ../openapi-wx.yaml#/components/schemas/PocketBaseProductListRecord
example:
page: page|integer
perPage: perPage|integer
totalItems: totalItems|integer
totalPages: totalPages|integer
items:
- id: PocketBase 记录主键|string
collectionId: collectionId|string
collectionName: collectionName|string
created: 记录创建时间|string
updated: 记录更新时间|string
prod_list_id: 产品列表业务 ID唯一标识|string
prod_list_name: 产品名称|string
prod_list_modelnumber: 产品型号|string
prod_list_icon: 产品图标附件 ID保存 tbl_attachments.attachments_id|string
prod_list_description: 产品说明|string
prod_list_feature: 产品特色|string
prod_list_parameters:
- name: name|string
value: value|string
prod_list_plantype: 产品方案|string
prod_list_category: 产品分类(必填,单选)|string
prod_list_sort: 排序值(同分类内按升序)|number
prod_list_comm_type: 通讯类型|string
prod_list_series: 产品系列|string
prod_list_power_supply: 供电方式|string
prod_list_tags: 产品标签(辅助检索,以 | 聚合)|string
prod_list_status: 产品状态(有效 / 过期 / 主推等)|string
prod_list_basic_price: 基础价格|number
prod_list_vip_price:
- viplevel: viplevel|string
price: price|number
prod_list_remark: 备注|string

View File

@@ -0,0 +1,253 @@
paths:
usersCount:
post:
security: []
operationId: postSystemUsersCount
tags:
- 系统
summary: 查询用户总数
description: 统计 `tbl_auth_users` 集合中的记录总数。
responses:
"200":
description: 查询成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/UsersCountResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
total_users: total_users|integer
"400":
description: 请求参数错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
refreshToken:
post:
security:
- BearerAuth: []
- {}
operationId: postSystemRefreshToken
tags:
- 系统
summary: 刷新认证 token
description: |
当当前 `Authorization` 仍有效时,直接基于当前 auth 用户续签。
当 token 失效时,可传入 `users_wx_code` 走微信 code 重新签发。
requestBody:
required: false
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/SystemRefreshTokenRequest
example:
users_wx_code: 当前 token 失效时,可通过该 code 重新签发 token。|string
responses:
"200":
description: 刷新成功
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/RefreshTokenResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
token: 新签发的 PocketBase 原生 auth token|string
"400":
description: 参数错误或微信 code 换取失败
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
"401":
description: token 无效,且未提供有效的 `users_wx_code`
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
"404":
description: 当前用户不存在
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
"415":
description: 请求体不是 JSON
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
"429":
description: 请求过于频繁
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
"500":
description: 服务端错误
content:
application/json:
schema:
$ref: ../openapi-wx.yaml#/components/schemas/ErrorResponse
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
components:
schemas:
ApiResponseBase:
type: object
required:
- statusCode
- errMsg
- data
properties:
statusCode:
type:
- integer
- string
description: 业务状态码
example: 业务状态码 | integer
errMsg:
type: string
description: 业务提示信息
example: 业务提示信息 | string
data:
description: 业务响应数据
type: object
additionalProperties: true
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
ErrorResponse:
type: object
required:
- statusCode
- errMsg
- data
properties:
statusCode:
type:
- integer
- string
description: 业务状态码
example: 业务状态码 | integer
errMsg:
type: string
description: 业务提示信息
example: 失败原因提示 | string
data:
description: 业务响应数据
type: object
additionalProperties: true
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
SystemRefreshTokenRequest:
type: object
properties:
users_wx_code:
type:
- string
- "null"
description: |
可选。
当前 token 失效时,可通过该 code 重新签发 token。
example: 0a1b2c3d4e5f6g
example:
users_wx_code: 当前 token 失效时,可通过该 code 重新签发 token。|string
RefreshTokenResponse:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/ApiResponseBase
- type: object
required:
- token
properties:
data:
type: object
additionalProperties: true
description: 业务响应数据
example: {}
token:
type: string
description: 新签发的 PocketBase 原生 auth token
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
业务响应数据字段|string: 业务响应数据值|string
token: 新签发的 PocketBase 原生 auth token|string
UsersCountData:
type: object
properties:
total_users:
type:
- integer
- string
example: 用户总数 | integer
example:
total_users: total_users|integer
UsersCountResponse:
allOf:
- $ref: ../openapi-wx.yaml#/components/schemas/ApiResponseBase
- type: object
properties:
data:
description: 业务响应数据
$ref: ../openapi-wx.yaml#/components/schemas/UsersCountData
example:
statusCode: 业务状态码|integer
errMsg: 业务提示信息|string
data:
total_users: total_users|integer

File diff suppressed because it is too large Load Diff

View File

@@ -89,7 +89,9 @@ async function run() {
const targetFieldType = 'json'; const targetFieldType = 'json';
const existingField = (collection.fields || []).find((field) => field.name === targetFieldName); const existingField = (collection.fields || []).find((field) => field.name === targetFieldName);
if (existingField && existingField.type === targetFieldType) { const hasBrokenCustomIdField = (collection.fields || []).some((field) => field && field.name === 'id');
if (existingField && existingField.type === targetFieldType && !hasBrokenCustomIdField) {
console.log('✅ 字段已存在且类型正确,无需变更。'); console.log('✅ 字段已存在且类型正确,无需变更。');
console.log('✅ 校验完成: tbl_product_list.prod_list_function (json)'); console.log('✅ 校验完成: tbl_product_list.prod_list_function (json)');
return; return;
@@ -100,6 +102,10 @@ async function run() {
for (let i = 0; i < (collection.fields || []).length; i += 1) { for (let i = 0; i < (collection.fields || []).length; i += 1) {
const field = collection.fields[i]; const field = collection.fields[i];
if (field && field.name === 'id') {
// PocketBase system id is implicit; custom required id field will break record creation.
continue;
}
if (field.name === targetFieldName) { if (field.name === targetFieldName) {
nextFields.push(normalizeFieldPayload(field, { name: targetFieldName, type: targetFieldType })); nextFields.push(normalizeFieldPayload(field, { name: targetFieldName, type: targetFieldType }));
patched = true; patched = true;

View File

@@ -10,6 +10,9 @@
"init:product-list": "node pocketbase.product-list.js", "init:product-list": "node pocketbase.product-list.js",
"init:dictionary": "node pocketbase.dictionary.js", "init:dictionary": "node pocketbase.dictionary.js",
"migrate:file-fields": "node pocketbase.file-fields-to-attachments.js", "migrate:file-fields": "node pocketbase.file-fields-to-attachments.js",
"migrate:add-is-delete-field": "node pocketbase.add-is-delete-field.js",
"migrate:apply-soft-delete-rules": "node pocketbase.apply-soft-delete-rules.js",
"migrate:ensure-cart-order-autogen-id": "node pocketbase.ensure-cart-order-autogen-id.js",
"migrate:product-params-array": "node migrate-product-parameters-to-array.js", "migrate:product-params-array": "node migrate-product-parameters-to-array.js",
"migrate:add-product-function-field": "node add-product-function-field.js", "migrate:add-product-function-field": "node add-product-function-field.js",
"test:company-native-api": "node test-tbl-company-native-api.js", "test:company-native-api": "node test-tbl-company-native-api.js",

View File

@@ -0,0 +1,233 @@
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const result = {};
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const index = line.indexOf('=');
if (index === -1) continue;
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
result[key] = value;
}
return result;
}
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
const PB_URL = String(
process.env.PB_URL
|| backendEnv.POCKETBASE_API_URL
|| runtimeConfig.POCKETBASE_API_URL
|| 'http://127.0.0.1:8090'
).replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行 is_delete 字段迁移。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const TARGET_FIELD = {
name: 'is_delete',
type: 'number',
required: false,
presentable: true,
hidden: false,
default: 0,
min: 0,
max: 1,
onlyInt: true,
};
function isSystemCollection(collection) {
return !!(collection && (collection.system || String(collection.name || '').startsWith('_')));
}
function normalizeFieldPayload(field, targetSpec) {
const payload = field ? Object.assign({}, field) : {};
const next = targetSpec || field || {};
if (field && field.id) {
payload.id = field.id;
}
payload.name = next.name;
payload.type = next.type;
if (typeof next.required !== 'undefined') {
payload.required = !!next.required;
}
if (typeof next.presentable !== 'undefined') {
payload.presentable = !!next.presentable;
}
if (typeof next.hidden !== 'undefined') {
payload.hidden = !!next.hidden;
}
if (next.type === 'number') {
if (Object.prototype.hasOwnProperty.call(next, 'default')) payload.default = next.default;
if (Object.prototype.hasOwnProperty.call(next, 'min')) payload.min = next.min;
if (Object.prototype.hasOwnProperty.call(next, 'max')) payload.max = next.max;
if (Object.prototype.hasOwnProperty.call(next, 'onlyInt')) payload.onlyInt = !!next.onlyInt;
}
if (next.type === 'autodate') {
payload.onCreate = typeof next.onCreate === 'boolean' ? next.onCreate : (typeof field?.onCreate === 'boolean' ? field.onCreate : true);
payload.onUpdate = typeof next.onUpdate === 'boolean' ? next.onUpdate : (typeof field?.onUpdate === 'boolean' ? field.onUpdate : false);
}
if (next.type === 'file') {
payload.maxSelect = typeof next.maxSelect === 'number' ? next.maxSelect : (typeof field?.maxSelect === 'number' ? field.maxSelect : 0);
payload.maxSize = typeof next.maxSize === 'number' ? next.maxSize : (typeof field?.maxSize === 'number' ? field.maxSize : 0);
payload.mimeTypes = Array.isArray(next.mimeTypes)
? next.mimeTypes
: (Array.isArray(field?.mimeTypes) ? field.mimeTypes : null);
}
return payload;
}
function buildCollectionPayload(collection, fields) {
return {
name: collection.name,
type: collection.type,
listRule: collection.listRule,
viewRule: collection.viewRule,
createRule: collection.createRule,
updateRule: collection.updateRule,
deleteRule: collection.deleteRule,
fields,
indexes: collection.indexes || [],
};
}
async function addFieldToCollections() {
const collections = await pb.collections.getFullList({ sort: 'name' });
const changed = [];
for (const collection of collections) {
if (isSystemCollection(collection)) continue;
const currentFields = Array.isArray(collection.fields) ? collection.fields : [];
const existingField = currentFields.find((field) => field && field.name === TARGET_FIELD.name);
const nextFields = currentFields
.filter((field) => field && field.name !== 'id')
.map((field) => normalizeFieldPayload(field));
if (existingField) {
for (let i = 0; i < nextFields.length; i += 1) {
if (nextFields[i].name === TARGET_FIELD.name) {
nextFields[i] = normalizeFieldPayload(existingField, TARGET_FIELD);
break;
}
}
} else {
nextFields.push(normalizeFieldPayload(null, TARGET_FIELD));
}
await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields));
changed.push({ name: collection.name, action: existingField ? 'normalized' : 'added' });
}
return changed;
}
async function backfillRecords() {
const collections = await pb.collections.getFullList({ sort: 'name' });
const summary = [];
for (const collection of collections) {
if (isSystemCollection(collection)) continue;
let page = 1;
let patched = 0;
const perPage = 200;
while (true) {
const list = await pb.collection(collection.name).getList(page, perPage, {
fields: 'id,is_delete',
skipTotal: false,
});
const items = Array.isArray(list.items) ? list.items : [];
for (const item of items) {
const value = item && Object.prototype.hasOwnProperty.call(item, 'is_delete') ? item.is_delete : null;
if (value === 0 || value === '0') continue;
await pb.collection(collection.name).update(item.id, { is_delete: 0 });
patched += 1;
}
if (page >= list.totalPages) break;
page += 1;
}
summary.push({ name: collection.name, backfilledRecords: patched });
}
return summary;
}
async function verifyCollections() {
const collections = await pb.collections.getFullList({ sort: 'name' });
const failed = [];
for (const collection of collections) {
if (isSystemCollection(collection)) continue;
const field = (collection.fields || []).find((item) => item && item.name === TARGET_FIELD.name);
if (!field || field.type !== 'number') failed.push(collection.name);
}
if (failed.length) {
throw new Error('以下集合缺少 is_delete 字段或类型不正确: ' + failed.join(', '));
}
}
async function main() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const changed = await addFieldToCollections();
console.log('📝 已更新集合:');
console.log(JSON.stringify(changed, null, 2));
const backfilled = await backfillRecords();
console.log('📝 已回填记录:');
console.log(JSON.stringify(backfilled, null, 2));
await verifyCollections();
console.log('✅ 校验通过:所有业务集合已包含 is_delete(number, default=0) 字段。');
console.log('🎉 is_delete 字段迁移完成!');
} catch (error) {
console.error('❌ is_delete 字段迁移失败:', error.response?.data || error.message || error);
process.exitCode = 1;
}
}
main();

View File

@@ -0,0 +1,176 @@
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const result = {};
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const index = line.indexOf('=');
if (index === -1) continue;
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
result[key] = value;
}
return result;
}
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
const PB_URL = String(
process.env.PB_URL
|| backendEnv.POCKETBASE_API_URL
|| runtimeConfig.POCKETBASE_API_URL
|| 'http://127.0.0.1:8090'
).replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
const SOFT_DELETE_RULE = 'is_delete = 0';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行软删除规则迁移。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
function isSystemCollection(collection) {
return !!(collection && (collection.system || String(collection.name || '').startsWith('_')));
}
function normalizeFieldPayload(field) {
const payload = Object.assign({}, field);
if (field.type === 'number') {
if (Object.prototype.hasOwnProperty.call(field, 'default')) payload.default = field.default;
if (Object.prototype.hasOwnProperty.call(field, 'min')) payload.min = field.min;
if (Object.prototype.hasOwnProperty.call(field, 'max')) payload.max = field.max;
if (Object.prototype.hasOwnProperty.call(field, 'onlyInt')) payload.onlyInt = !!field.onlyInt;
}
if (field.type === 'autodate') {
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
}
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 : null;
}
return payload;
}
function mergeRuleWithSoftDelete(rule) {
const currentRule = typeof rule === 'string' ? rule.trim() : '';
if (!currentRule) return SOFT_DELETE_RULE;
if (currentRule === SOFT_DELETE_RULE) return currentRule;
if (currentRule.includes(SOFT_DELETE_RULE)) return currentRule;
return `(${currentRule}) && ${SOFT_DELETE_RULE}`;
}
function buildCollectionPayload(collection) {
return {
name: collection.name,
type: collection.type,
listRule: mergeRuleWithSoftDelete(collection.listRule),
viewRule: mergeRuleWithSoftDelete(collection.viewRule),
createRule: collection.createRule,
updateRule: collection.updateRule,
deleteRule: collection.deleteRule,
fields: (collection.fields || []).filter((field) => field && field.name !== 'id').map((field) => normalizeFieldPayload(field)),
indexes: collection.indexes || [],
};
}
async function applyRules() {
const collections = await pb.collections.getFullList({ sort: 'name' });
const changed = [];
for (const collection of collections) {
if (isSystemCollection(collection)) continue;
const hasSoftDelete = (collection.fields || []).some((field) => field && field.name === 'is_delete');
if (!hasSoftDelete) continue;
const nextListRule = mergeRuleWithSoftDelete(collection.listRule);
const nextViewRule = mergeRuleWithSoftDelete(collection.viewRule);
const listChanged = nextListRule !== (collection.listRule || '');
const viewChanged = nextViewRule !== (collection.viewRule || '');
if (!listChanged && !viewChanged) {
changed.push({ name: collection.name, action: 'skipped', listRule: nextListRule, viewRule: nextViewRule });
continue;
}
await pb.collections.update(collection.id, buildCollectionPayload(collection));
changed.push({ name: collection.name, action: 'updated', listRule: nextListRule, viewRule: nextViewRule });
}
return changed;
}
async function verifyRules() {
const collections = await pb.collections.getFullList({ sort: 'name' });
const failed = [];
for (const collection of collections) {
if (isSystemCollection(collection)) continue;
const hasSoftDelete = (collection.fields || []).some((field) => field && field.name === 'is_delete');
if (!hasSoftDelete) continue;
const listRule = String(collection.listRule || '');
const viewRule = String(collection.viewRule || '');
if (!listRule.includes(SOFT_DELETE_RULE) || !viewRule.includes(SOFT_DELETE_RULE)) {
failed.push({ name: collection.name, listRule, viewRule });
}
}
if (failed.length) {
throw new Error(`以下集合未正确应用软删除规则: ${JSON.stringify(failed, null, 2)}`);
}
}
async function main() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const changed = await applyRules();
console.log('📝 规则更新结果:');
console.log(JSON.stringify(changed, null, 2));
await verifyRules();
console.log('✅ 校验通过:所有带 is_delete 的业务集合已默认过滤 is_delete = 0。');
console.log('🎉 软删除默认查询规则迁移完成!');
} catch (error) {
console.error('❌ 软删除默认查询规则迁移失败:', error.response?.data || error.message || error);
process.exitCode = 1;
}
}
main();

View File

@@ -15,6 +15,7 @@ const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE
const OWNER_AUTH_RULE = '@request.auth.id != ""'; const OWNER_AUTH_RULE = '@request.auth.id != ""';
const CART_OWNER_MATCH_RULE = 'cart_owner = @request.auth.openid'; const CART_OWNER_MATCH_RULE = 'cart_owner = @request.auth.openid';
const ORDER_OWNER_MATCH_RULE = 'order_owner = @request.auth.openid'; const ORDER_OWNER_MATCH_RULE = 'order_owner = @request.auth.openid';
const SOFT_DELETE_RULE = 'is_delete = 0';
if (!AUTH_TOKEN) { if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。'); console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。');
@@ -27,13 +28,13 @@ const collections = [
{ {
name: 'tbl_cart', name: 'tbl_cart',
type: 'base', type: 'base',
listRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`, listRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
viewRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`, viewRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
createRule: `${OWNER_AUTH_RULE} && @request.body.cart_owner = @request.auth.openid`, createRule: `${OWNER_AUTH_RULE} && @request.body.cart_owner = @request.auth.openid`,
updateRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`, updateRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
deleteRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`, deleteRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
fields: [ fields: [
{ name: 'cart_id', type: 'text', required: true }, { name: 'cart_id', type: 'text', required: true, autogeneratePattern: 'CART-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'cart_number', type: 'text', required: true }, { name: 'cart_number', type: 'text', required: true },
{ name: 'cart_create', type: 'autodate', onCreate: true, onUpdate: false }, { name: 'cart_create', type: 'autodate', onCreate: true, onUpdate: false },
{ name: 'cart_owner', type: 'text', required: true }, { name: 'cart_owner', type: 'text', required: true },
@@ -42,6 +43,7 @@ const collections = [
{ name: 'cart_status', type: 'text', required: true }, { name: 'cart_status', type: 'text', required: true },
{ name: 'cart_at_price', type: 'number', required: true }, { name: 'cart_at_price', type: 'number', required: true },
{ name: 'cart_remark', type: 'text' }, { name: 'cart_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_cart_cart_id ON tbl_cart (cart_id)', 'CREATE UNIQUE INDEX idx_tbl_cart_cart_id ON tbl_cart (cart_id)',
@@ -57,13 +59,13 @@ const collections = [
{ {
name: 'tbl_order', name: 'tbl_order',
type: 'base', type: 'base',
listRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`, listRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
viewRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`, viewRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
createRule: `${OWNER_AUTH_RULE} && @request.body.order_owner = @request.auth.openid`, createRule: `${OWNER_AUTH_RULE} && @request.body.order_owner = @request.auth.openid`,
updateRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`, updateRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
deleteRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`, deleteRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
fields: [ fields: [
{ name: 'order_id', type: 'text', required: true }, { name: 'order_id', type: 'text', required: true, autogeneratePattern: 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}' },
{ name: 'order_number', type: 'text', required: true }, { name: 'order_number', type: 'text', required: true },
{ name: 'order_create', type: 'autodate', onCreate: true, onUpdate: false }, { name: 'order_create', type: 'autodate', onCreate: true, onUpdate: false },
{ name: 'order_owner', type: 'text', required: true }, { name: 'order_owner', type: 'text', required: true },
@@ -73,6 +75,7 @@ const collections = [
{ name: 'order_snap', type: 'json', required: true }, { name: 'order_snap', type: 'json', required: true },
{ name: 'order_amount', type: 'number', required: true }, { name: 'order_amount', type: 'number', required: true },
{ name: 'order_remark', type: 'text' }, { name: 'order_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_order_order_id ON tbl_order (order_id)', 'CREATE UNIQUE INDEX idx_tbl_order_order_id ON tbl_order (order_id)',

View File

@@ -12,6 +12,7 @@ try {
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, ''); 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 || ''; const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
const SOFT_DELETE_RULE = 'is_delete = 0';
if (!AUTH_TOKEN) { if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行字典建表。'); console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行字典建表。');
@@ -23,8 +24,8 @@ const pb = new PocketBase(PB_URL);
const collectionData = { const collectionData = {
name: 'tbl_system_dict', name: 'tbl_system_dict',
type: 'base', type: 'base',
listRule: '', listRule: SOFT_DELETE_RULE,
viewRule: '', viewRule: SOFT_DELETE_RULE,
createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
@@ -38,6 +39,7 @@ const collectionData = {
{ name: 'dict_word_sort_order', type: 'text' }, { name: 'dict_word_sort_order', type: 'text' },
{ name: 'dict_word_parent_id', type: 'text' }, { name: 'dict_word_parent_id', type: 'text' },
{ name: 'dict_word_remark', type: 'text' }, { name: 'dict_word_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_system_dict_id ON tbl_system_dict (system_dict_id)', 'CREATE UNIQUE INDEX idx_system_dict_id ON tbl_system_dict (system_dict_id)',

View File

@@ -12,6 +12,7 @@ try {
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, ''); 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 || ''; const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
const SOFT_DELETE_RULE = 'is_delete = 0';
if (!AUTH_TOKEN) { if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。'); console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。');
@@ -24,8 +25,8 @@ const collections = [
{ {
name: 'tbl_attachments', name: 'tbl_attachments',
type: 'base', type: 'base',
listRule: '', listRule: SOFT_DELETE_RULE,
viewRule: '', viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'attachments_id', type: 'text', required: true }, { name: 'attachments_id', type: 'text', required: true },
{ name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 4294967296, mimeTypes: [] }, { name: 'attachments_link', type: 'file', maxSelect: 0, maxSize: 4294967296, mimeTypes: [] },
@@ -37,6 +38,7 @@ const collections = [
{ name: 'attachments_ocr', type: 'text' }, { name: 'attachments_ocr', type: 'text' },
{ name: 'attachments_status', type: 'text' }, { name: 'attachments_status', type: 'text' },
{ name: 'attachments_remark', type: 'text' }, { name: 'attachments_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_attachments_attachments_id ON tbl_attachments (attachments_id)', 'CREATE UNIQUE INDEX idx_tbl_attachments_attachments_id ON tbl_attachments (attachments_id)',
@@ -47,8 +49,8 @@ const collections = [
{ {
name: 'tbl_document', name: 'tbl_document',
type: 'base', type: 'base',
listRule: '', listRule: SOFT_DELETE_RULE,
viewRule: '', viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'document_id', type: 'text', required: true }, { name: 'document_id', type: 'text', required: true },
{ name: 'document_create', type: 'autodate', onCreate: true, onUpdate: false }, { name: 'document_create', type: 'autodate', onCreate: true, onUpdate: false },
@@ -77,6 +79,7 @@ const collections = [
{ name: 'document_application_scenarios', type: 'text' }, { name: 'document_application_scenarios', type: 'text' },
{ name: 'document_hotel_type', type: 'text' }, { name: 'document_hotel_type', type: 'text' },
{ name: 'document_remark', type: 'text' }, { name: 'document_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)', 'CREATE UNIQUE INDEX idx_tbl_document_document_id ON tbl_document (document_id)',
@@ -99,6 +102,7 @@ const collections = [
{ name: 'doh_user_id', type: 'text' }, { name: 'doh_user_id', type: 'text' },
{ name: 'doh_current_count', type: 'number' }, { name: 'doh_current_count', type: 'number' },
{ name: 'doh_remark', type: 'text' }, { name: 'doh_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_document_operation_history_doh_id ON tbl_document_operation_history (doh_id)', 'CREATE UNIQUE INDEX idx_tbl_document_operation_history_doh_id ON tbl_document_operation_history (doh_id)',

View File

@@ -0,0 +1,198 @@
import { createRequire } from 'module';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import PocketBase from 'pocketbase';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let runtimeConfig = {};
try {
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
} catch (_error) {
runtimeConfig = {};
}
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const result = {};
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) continue;
const index = line.indexOf('=');
if (index === -1) continue;
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
result[key] = value;
}
return result;
}
const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env'));
const PB_URL = String(
process.env.PB_URL
|| backendEnv.POCKETBASE_API_URL
|| runtimeConfig.POCKETBASE_API_URL
|| 'http://127.0.0.1:8090'
).replace(/\/+$/, '');
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行 cart/order 业务 ID 自动生成迁移。');
process.exit(1);
}
const pb = new PocketBase(PB_URL);
const TARGETS = [
{
collectionName: 'tbl_cart',
fieldName: 'cart_id',
autogeneratePattern: 'CART-[0-9]{13}-[A-Za-z0-9]{6}',
},
{
collectionName: 'tbl_order',
fieldName: 'order_id',
autogeneratePattern: 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}',
},
];
function normalizeFieldPayload(field, overrides) {
const payload = Object.assign({}, field, overrides || {});
if (payload.type === 'number') {
if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) payload.onlyInt = !!payload.onlyInt;
}
if (payload.type === 'autodate') {
payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true;
payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false;
}
if (payload.type === 'file') {
payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0;
payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0;
payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null;
}
return payload;
}
function buildCollectionPayload(collection, fields) {
return {
name: collection.name,
type: collection.type,
listRule: collection.listRule,
viewRule: collection.viewRule,
createRule: collection.createRule,
updateRule: collection.updateRule,
deleteRule: collection.deleteRule,
fields,
indexes: collection.indexes || [],
};
}
async function ensureAutoGenerateField(target) {
const collection = await pb.collections.getOne(target.collectionName);
const fields = Array.isArray(collection.fields) ? collection.fields : [];
const existingField = fields.find((field) => field && field.name === target.fieldName);
if (!existingField) {
throw new Error(`${target.collectionName}.${target.fieldName} 不存在`);
}
const currentPattern = String(existingField.autogeneratePattern || '');
if (existingField.type === 'text' && currentPattern === target.autogeneratePattern) {
return {
collectionName: target.collectionName,
fieldName: target.fieldName,
action: 'skipped',
autogeneratePattern: currentPattern,
};
}
const nextFields = fields
.filter((field) => field && field.name !== 'id')
.map((field) => {
if (field.name !== target.fieldName) {
return normalizeFieldPayload(field);
}
return normalizeFieldPayload(field, {
type: 'text',
required: true,
autogeneratePattern: target.autogeneratePattern,
});
});
await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields));
return {
collectionName: target.collectionName,
fieldName: target.fieldName,
action: 'updated',
autogeneratePattern: target.autogeneratePattern,
};
}
async function verifyTargets() {
const result = [];
for (const target of TARGETS) {
const collection = await pb.collections.getOne(target.collectionName);
const field = (collection.fields || []).find((item) => item && item.name === target.fieldName);
const pattern = String(field && field.autogeneratePattern || '');
const ok = !!field && field.type === 'text' && pattern === target.autogeneratePattern;
result.push({
collectionName: target.collectionName,
fieldName: target.fieldName,
type: field ? field.type : '',
required: !!(field && field.required),
autogeneratePattern: pattern,
ok,
});
}
const failed = result.filter((item) => !item.ok);
if (failed.length) {
throw new Error(`以下字段未正确启用自动生成: ${JSON.stringify(failed, null, 2)}`);
}
return result;
}
async function main() {
try {
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
pb.authStore.save(AUTH_TOKEN, null);
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
const changed = [];
for (const target of TARGETS) {
changed.push(await ensureAutoGenerateField(target));
}
console.log('📝 业务 ID 自动生成处理结果:');
console.log(JSON.stringify(changed, null, 2));
const verified = await verifyTargets();
console.log('📝 校验结果:');
console.log(JSON.stringify(verified, null, 2));
console.log('🎉 cart_id / order_id 自动生成规则已就绪。');
} catch (error) {
console.error('❌ cart/order 业务 ID 自动生成迁移失败:', error.response?.data || error.message || error);
process.exitCode = 1;
}
}
main();

View File

@@ -8,13 +8,14 @@ const ADMIN_PASSWORD = 'Momo123456';
// ========================================== // ==========================================
const pb = new PocketBase(PB_URL); const pb = new PocketBase(PB_URL);
const SOFT_DELETE_RULE = 'is_delete = 0';
const collections = [ const collections = [
{ {
name: 'tbl_system_dict', name: 'tbl_system_dict',
type: 'base', type: 'base',
listRule: '', listRule: SOFT_DELETE_RULE,
viewRule: '', viewRule: SOFT_DELETE_RULE,
createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
@@ -26,7 +27,8 @@ const collections = [
{ name: 'dict_word_is_enabled', type: 'bool' }, { name: 'dict_word_is_enabled', type: 'bool' },
{ name: 'dict_word_sort_order', type: 'text' }, { name: 'dict_word_sort_order', type: 'text' },
{ name: 'dict_word_parent_id', type: 'text' }, { name: 'dict_word_parent_id', type: 'text' },
{ name: 'dict_word_remark', type: 'text' } { name: 'dict_word_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_system_dict_id ON tbl_system_dict (system_dict_id)', 'CREATE UNIQUE INDEX idx_system_dict_id ON tbl_system_dict (system_dict_id)',
@@ -56,7 +58,8 @@ const collections = [
{ name: 'company_status', type: 'text' }, { name: 'company_status', type: 'text' },
{ name: 'company_level', type: 'text' }, { name: 'company_level', type: 'text' },
{ name: 'company_owner_openid', type: 'text' }, { name: 'company_owner_openid', type: 'text' },
{ name: 'company_remark', type: 'text' } { name: 'company_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_company_id ON tbl_company (company_id)', 'CREATE UNIQUE INDEX idx_company_id ON tbl_company (company_id)',
@@ -71,7 +74,8 @@ const collections = [
{ name: 'usergroups_id', type: 'text', required: true }, { name: 'usergroups_id', type: 'text', required: true },
{ name: 'usergroups_name', type: 'text' }, { name: 'usergroups_name', type: 'text' },
{ name: 'usergroups_level', type: 'number' }, { name: 'usergroups_level', type: 'number' },
{ name: 'usergroups_remark', type: 'text' } { name: 'usergroups_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_usergroups_id ON tbl_user_groups (usergroups_id)' 'CREATE UNIQUE INDEX idx_usergroups_id ON tbl_user_groups (usergroups_id)'
@@ -97,7 +101,8 @@ const collections = [
{ name: 'users_id_pic_b', type: 'text' }, { name: 'users_id_pic_b', type: 'text' },
{ name: 'users_title_picture', type: 'text' }, { name: 'users_title_picture', type: 'text' },
{ name: 'users_picture', type: 'text' }, { name: 'users_picture', type: 'text' },
{ name: 'usergroups_id', type: 'text' } { name: 'usergroups_id', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_users_id ON tbl_users (users_id)', 'CREATE UNIQUE INDEX idx_users_id ON tbl_users (users_id)',

View File

@@ -32,13 +32,14 @@ const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD || backendEnv.PB_ADMIN_PASS
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || backendEnv.POCKETBASE_AUTH_TOKEN || ''; const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || backendEnv.POCKETBASE_AUTH_TOKEN || '';
const pb = new PocketBase(PB_URL); const pb = new PocketBase(PB_URL);
const SOFT_DELETE_RULE = 'is_delete = 0';
const collections = [ const collections = [
{ {
name: 'tbl_auth_users', name: 'tbl_auth_users',
type: 'auth', type: 'auth',
listRule: '@request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB"', listRule: '@request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB" && is_delete = 0',
viewRule: '@request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB"', viewRule: '@request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB" && is_delete = 0',
createRule: '', createRule: '',
updateRule: '', updateRule: '',
deleteRule: '@request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB"', deleteRule: '@request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB"',
@@ -66,6 +67,7 @@ const collections = [
{ name: 'users_id_pic_b', type: 'text' }, { name: 'users_id_pic_b', type: 'text' },
{ name: 'users_title_picture', type: 'text' }, { name: 'users_title_picture', type: 'text' },
{ name: 'users_picture', type: 'text' }, { name: 'users_picture', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
{ name: 'usergroups_id', type: 'text' } { name: 'usergroups_id', type: 'text' }
], ],
indexes: [ indexes: [
@@ -86,11 +88,14 @@ const collections = [
{ {
name: 'tbl_auth_resources', name: 'tbl_auth_resources',
type: 'base', type: 'base',
listRule: SOFT_DELETE_RULE,
viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'res_id', type: 'text', required: true }, { name: 'res_id', type: 'text', required: true },
{ name: 'table_name', type: 'text', required: true }, { name: 'table_name', type: 'text', required: true },
{ name: 'column_name', type: 'text' }, { name: 'column_name', type: 'text' },
{ name: 'res_type', type: 'text', required: true } { name: 'res_type', type: 'text', required: true },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_auth_resources_res_id ON tbl_auth_resources (res_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_resources_res_id ON tbl_auth_resources (res_id)',
@@ -102,12 +107,15 @@ const collections = [
{ {
name: 'tbl_auth_roles', name: 'tbl_auth_roles',
type: 'base', type: 'base',
listRule: SOFT_DELETE_RULE,
viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'role_id', type: 'text', required: true }, { name: 'role_id', type: 'text', required: true },
{ name: 'role_name', type: 'text', required: true }, { name: 'role_name', type: 'text', required: true },
{ name: 'role_code', type: 'text' }, { name: 'role_code', type: 'text' },
{ name: 'role_status', type: 'number' }, { name: 'role_status', type: 'number' },
{ name: 'role_remark', type: 'text' } { name: 'role_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_id ON tbl_auth_roles (role_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_roles_role_id ON tbl_auth_roles (role_id)',
@@ -118,12 +126,15 @@ const collections = [
{ {
name: 'tbl_auth_role_perms', name: 'tbl_auth_role_perms',
type: 'base', type: 'base',
listRule: SOFT_DELETE_RULE,
viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'role_perm_id', type: 'text', required: true }, { name: 'role_perm_id', type: 'text', required: true },
{ name: 'role_id', type: 'text', required: true }, { name: 'role_id', type: 'text', required: true },
{ name: 'res_id', type: 'text', required: true }, { name: 'res_id', type: 'text', required: true },
{ name: 'access_level', type: 'number', required: true }, { name: 'access_level', type: 'number', required: true },
{ name: 'priority', type: 'number' } { name: 'priority', type: 'number' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_auth_role_perms_role_perm_id ON tbl_auth_role_perms (role_perm_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_role_perms_role_perm_id ON tbl_auth_role_perms (role_perm_id)',
@@ -135,12 +146,15 @@ const collections = [
{ {
name: 'tbl_auth_user_overrides', name: 'tbl_auth_user_overrides',
type: 'base', type: 'base',
listRule: SOFT_DELETE_RULE,
viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'override_id', type: 'text', required: true }, { name: 'override_id', type: 'text', required: true },
{ name: 'users_convers_id', type: 'text', required: true }, { name: 'users_convers_id', type: 'text', required: true },
{ name: 'res_id', type: 'text', required: true }, { name: 'res_id', type: 'text', required: true },
{ name: 'access_level', type: 'number', required: true }, { name: 'access_level', type: 'number', required: true },
{ name: 'priority', type: 'number' } { name: 'priority', type: 'number' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_override_id ON tbl_auth_user_overrides (override_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_user_overrides_override_id ON tbl_auth_user_overrides (override_id)',
@@ -152,12 +166,15 @@ const collections = [
{ {
name: 'tbl_auth_row_scopes', name: 'tbl_auth_row_scopes',
type: 'base', type: 'base',
listRule: SOFT_DELETE_RULE,
viewRule: SOFT_DELETE_RULE,
fields: [ fields: [
{ name: 'scope_id', type: 'text', required: true }, { name: 'scope_id', type: 'text', required: true },
{ name: 'target_type', type: 'text', required: true }, { name: 'target_type', type: 'text', required: true },
{ name: 'target_id', type: 'text', required: true }, { name: 'target_id', type: 'text', required: true },
{ name: 'table_name', type: 'text', required: true }, { name: 'table_name', type: 'text', required: true },
{ name: 'filter_sql', type: 'editor', required: true } { name: 'filter_sql', type: 'editor', required: true },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_auth_row_scopes_scope_id ON tbl_auth_row_scopes (scope_id)', 'CREATE UNIQUE INDEX idx_tbl_auth_row_scopes_scope_id ON tbl_auth_row_scopes (scope_id)',

View File

@@ -12,6 +12,7 @@ try {
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, ''); 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 || ''; const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
const SOFT_DELETE_RULE = 'is_delete = 0';
if (!AUTH_TOKEN) { if (!AUTH_TOKEN) {
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。'); console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN无法执行建表。');
@@ -25,8 +26,8 @@ const collections = [
name: 'tbl_product_list', name: 'tbl_product_list',
type: 'base', type: 'base',
// Empty rules in PocketBase mean public read access. // Empty rules in PocketBase mean public read access.
listRule: '', listRule: SOFT_DELETE_RULE,
viewRule: '', viewRule: SOFT_DELETE_RULE,
createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', createRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', updateRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")', deleteRule: '(@request.auth.users_idtype = "ManagePlatform" || @request.auth.usergroups_id = "ROLE-1774666070666-9dDrTB")',
@@ -50,6 +51,7 @@ const collections = [
{ name: 'prod_list_basic_price', type: 'number' }, { name: 'prod_list_basic_price', type: 'number' },
{ name: 'prod_list_vip_price', type: 'json' }, { name: 'prod_list_vip_price', type: 'json' },
{ name: 'prod_list_remark', type: 'text' }, { name: 'prod_list_remark', type: 'text' },
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
], ],
indexes: [ indexes: [
'CREATE UNIQUE INDEX idx_tbl_product_list_prod_list_id ON tbl_product_list (prod_list_id)', 'CREATE UNIQUE INDEX idx_tbl_product_list_prod_list_id ON tbl_product_list (prod_list_id)',
@@ -108,7 +110,9 @@ function buildCollectionPayload(collectionData, existingCollection) {
} }
const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field])); const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field]));
const fields = (existingCollection.fields || []).map((existingField) => { const fields = (existingCollection.fields || []).filter((existingField) => {
return existingField && existingField.name !== 'id';
}).map((existingField) => {
const targetField = targetFieldMap.get(existingField.name); const targetField = targetFieldMap.get(existingField.name);
if (!targetField) { if (!targetField) {
return existingField; return existingField;