Compare commits
8 Commits
eb4765cd10
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 614c0147e5 | |||
| cd0373be3c | |||
| ab9f075d61 | |||
| 8ea23ef3d1 | |||
| cafd69ea2c | |||
| 91fcdcd65a | |||
| c8a1f72bde | |||
| 94811b52e2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/back-end/node_modules
|
/back-end/node_modules
|
||||||
/.tmp-upload-probe
|
/.tmp-upload-probe
|
||||||
|
/.tmp-openapi-validate/node_modules
|
||||||
|
|||||||
9
.tmp-openapi-validate/package.json
Normal file
9
.tmp-openapi-validate/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
| `users_phone` | `手机号 | string` |
|
| `users_phone` | `手机号 | string` |
|
||||||
| `users_phone_masked` | `脱敏手机号 | string` |
|
| `users_phone_masked` | `脱敏手机号 | string` |
|
||||||
| `users_level` | `用户等级 | string` |
|
| `users_level` | `用户等级 | string` |
|
||||||
|
| `users_level_name` | `用户等级名称 | string` |
|
||||||
| `users_tag` | `用户标签 | string` |
|
| `users_tag` | `用户标签 | string` |
|
||||||
| `users_picture` | `用户头像附件ID | string` |
|
| `users_picture` | `用户头像附件ID | string` |
|
||||||
| `users_picture_url` | `用户头像文件流链接 | string` |
|
| `users_picture_url` | `用户头像文件流链接 | string` |
|
||||||
|
|||||||
@@ -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 字段清单里单独声明。
|
||||||
|
|||||||
@@ -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`,资源列表/详情默认不返回已软删除数据。
|
||||||
|
|
||||||
## 索引
|
## 索引
|
||||||
|
|
||||||
|
|||||||
@@ -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`,角色权限映射默认不返回已软删除记录。
|
||||||
|
|||||||
@@ -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`,角色列表/详情默认不返回已软删除数据。
|
||||||
|
|||||||
@@ -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`,行级范围列表/详情默认隐藏已软删除规则。
|
||||||
|
|||||||
@@ -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`,用户覆盖权限列表/详情默认隐藏已软删除记录。
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
| `users_idtype` | `text` | 否 | 身份来源类型或证件类型 |
|
| `users_idtype` | `text` | 否 | 身份来源类型或证件类型 |
|
||||||
| `users_id_number` | `text` | 否 | 证件号 |
|
| `users_id_number` | `text` | 否 | 证件号 |
|
||||||
| `users_phone` | `text` | 否 | 手机号 |
|
| `users_phone` | `text` | 否 | 手机号 |
|
||||||
| `users_level` | `text` | 否 | 用户等级文本 |
|
| `users_level` | `text` | 否 | 用户等级枚举值,保存“数据-会员等级”字典 enum |
|
||||||
| `users_type` | `text` | 否 | 用户类型 |
|
| `users_type` | `text` | 否 | 用户类型 |
|
||||||
| `company_id` | `text` | 否 | 所属公司业务 ID |
|
| `company_id` | `text` | 否 | 所属公司业务 ID |
|
||||||
| `users_parent_id` | `text` | 否 | 上级用户业务 ID |
|
| `users_parent_id` | `text` | 否 | 上级用户业务 ID |
|
||||||
@@ -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,5 +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` 默认保持为空;已有用户后续登录 / 更新流程也不会自动改写该字段。
|
||||||
- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。
|
- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。
|
||||||
|
|||||||
55
docs/pb_tbl_cart.md
Normal file
55
docs/pb_tbl_cart.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# pb_tbl_cart
|
||||||
|
|
||||||
|
> 来源:购物车表需求草图、`script/pocketbase.cart-order.js`
|
||||||
|
> 类型:`base`
|
||||||
|
> 读写规则:微信端原生访问建议仅允许记录所有者访问;当前脚本已按 `cart_owner = 当前 token 对应 openid` 配置原生 collection 规则,管理后台建议通过 hooks / API 聚合查询
|
||||||
|
|
||||||
|
## 表用途
|
||||||
|
|
||||||
|
用于存储购物车商品项明细。
|
||||||
|
|
||||||
|
当前结构按“一个购物车商品项一条记录”设计:
|
||||||
|
|
||||||
|
- `cart_id` 是单条购物车项业务 ID
|
||||||
|
- `cart_number` 是购物车名称 / 分组号,同一购物车下的多条商品项可共用同一个 `cart_number`
|
||||||
|
- `cart_owner` 用于标识购物车所属用户
|
||||||
|
|
||||||
|
## 字段清单
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `id` | `text` | 是 | PocketBase 记录主键 |
|
||||||
|
| `cart_id` | `text` | 是 | 购物车项业务 ID,唯一标识 |
|
||||||
|
| `cart_number` | `text` | 是 | 购物车名称 / 分组号,默认可按“用户名+年月日时分秒”生成 |
|
||||||
|
| `cart_create` | `autodate` | 否 | 购物车项创建时间,由数据库自动生成 |
|
||||||
|
| `cart_owner` | `text` | 是 | 生成者 openid,约定保存 `tbl_auth_users.openid` |
|
||||||
|
| `cart_product_id` | `text` | 是 | 产品 ID,建议保存 `tbl_product_list.prod_list_id` |
|
||||||
|
| `cart_product_quantity` | `number` | 是 | 产品数量,建议业务侧约束为正整数 |
|
||||||
|
| `cart_status` | `text` | 是 | 购物车状态,建议值:`有效` / `无效` |
|
||||||
|
| `cart_at_price` | `number` | 是 | 加入购物车时的价格,用于后续降价提醒或对比 |
|
||||||
|
| `cart_remark` | `text` | 否 | 备注 |
|
||||||
|
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
|
||||||
|
|
||||||
|
## 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 说明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_cart_cart_id` | `UNIQUE INDEX` | 保证 `cart_id` 唯一 |
|
||||||
|
| `idx_tbl_cart_cart_number` | `INDEX` | 加速按购物车名称 / 分组号查询 |
|
||||||
|
| `idx_tbl_cart_cart_owner` | `INDEX` | 加速按所属用户查询 |
|
||||||
|
| `idx_tbl_cart_cart_product_id` | `INDEX` | 加速按产品查询 |
|
||||||
|
| `idx_tbl_cart_cart_status` | `INDEX` | 加速按购物车状态过滤 |
|
||||||
|
| `idx_tbl_cart_cart_create` | `INDEX` | 加速按购物车项创建时间倒序查询 |
|
||||||
|
| `idx_tbl_cart_owner_number` | `INDEX` | 加速同一用户下按购物车分组查询 |
|
||||||
|
| `idx_tbl_cart_owner_status` | `INDEX` | 加速查询某用户的有效购物车项 |
|
||||||
|
|
||||||
|
## 补充约定
|
||||||
|
|
||||||
|
- `cart_owner`、`cart_product_id` 当前按文本字段保存业务 ID,不直接建立 relation,便于兼容现有 hooks 业务模型。
|
||||||
|
- `cart_owner` 统一保存 `tbl_auth_users.openid`,便于直接使用微信登录返回 token 做原生访问控制。
|
||||||
|
- `is_delete` 用于软删除控制,购物车项删除时建议优先标记为 `1`。
|
||||||
|
- 集合默认查询规则已内置 `is_delete = 0`,常规列表/详情不会返回已软删除数据。
|
||||||
|
- `cart_product_quantity`、`cart_at_price` 使用 `number`,数量正整数与价格精度建议在 hooks / API 层统一校验。
|
||||||
|
- 当购物车被清空时,建议业务侧将历史记录 `cart_status` 置为 `无效`,而不是直接覆盖有效记录。
|
||||||
|
- `cart_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。
|
||||||
|
- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。
|
||||||
@@ -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 字段清单里单独声明。
|
||||||
|
|||||||
@@ -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 字段清单里单独声明。
|
||||||
|
|||||||
@@ -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 字段清单里单独声明。
|
||||||
|
|||||||
57
docs/pb_tbl_order.md
Normal file
57
docs/pb_tbl_order.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# pb_tbl_order
|
||||||
|
|
||||||
|
> 来源:订单表需求草图、`script/pocketbase.cart-order.js`
|
||||||
|
> 类型:`base`
|
||||||
|
> 读写规则:微信端原生访问建议仅允许记录所有者访问;当前脚本已按 `order_owner = 当前 token 对应 openid` 配置原生 collection 规则,管理后台建议通过 hooks / API 聚合查询
|
||||||
|
|
||||||
|
## 表用途
|
||||||
|
|
||||||
|
用于存储订单主记录与订单快照。
|
||||||
|
|
||||||
|
当前结构按“一个订单一条记录”设计:
|
||||||
|
|
||||||
|
- `order_id` 是订单业务 ID
|
||||||
|
- `order_number` 是订单编号,建议按“用户名+年月日时分秒”或统一编号规则生成
|
||||||
|
- `order_snap` 使用 JSON 完整保存下单时的商品、数量、价格、折扣等快照,避免后续商品数据变化影响历史订单
|
||||||
|
|
||||||
|
## 字段清单
|
||||||
|
|
||||||
|
| 字段名 | 类型 | 必填 | 说明 |
|
||||||
|
| :--- | :--- | :---: | :--- |
|
||||||
|
| `id` | `text` | 是 | PocketBase 记录主键 |
|
||||||
|
| `order_id` | `text` | 是 | 订单业务 ID,唯一标识 |
|
||||||
|
| `order_number` | `text` | 是 | 订单编号,建议业务侧自动生成并保证可追踪 |
|
||||||
|
| `order_create` | `autodate` | 否 | 订单创建时间,由数据库自动生成 |
|
||||||
|
| `order_owner` | `text` | 是 | 生成者 openid,约定保存 `tbl_auth_users.openid` |
|
||||||
|
| `order_source` | `text` | 是 | 订单来源,建议值:`购物车` / `方案清单` |
|
||||||
|
| `order_status` | `text` | 是 | 订单状态,建议值:`订单已生成` / `订单已确定` / `订单已交付` / `订单已验收` / `订单已结束` |
|
||||||
|
| `order_source_id` | `text` | 是 | 订单来源关联 ID,如购物车 ID 或方案清单 ID |
|
||||||
|
| `order_snap` | `json` | 是 | 订单快照,完整保存订单明细信息 |
|
||||||
|
| `order_amount` | `number` | 是 | 订单总金额 |
|
||||||
|
| `order_remark` | `text` | 否 | 订单备注 |
|
||||||
|
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
|
||||||
|
|
||||||
|
## 索引
|
||||||
|
|
||||||
|
| 索引名 | 类型 | 说明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `idx_tbl_order_order_id` | `UNIQUE INDEX` | 保证 `order_id` 唯一 |
|
||||||
|
| `idx_tbl_order_order_number` | `UNIQUE INDEX` | 保证 `order_number` 唯一 |
|
||||||
|
| `idx_tbl_order_order_owner` | `INDEX` | 加速按下单用户查询 |
|
||||||
|
| `idx_tbl_order_order_source` | `INDEX` | 加速按订单来源过滤 |
|
||||||
|
| `idx_tbl_order_order_status` | `INDEX` | 加速按订单状态过滤 |
|
||||||
|
| `idx_tbl_order_order_source_id` | `INDEX` | 加速按来源关联 ID 查询 |
|
||||||
|
| `idx_tbl_order_order_create` | `INDEX` | 加速按订单创建时间倒序查询 |
|
||||||
|
| `idx_tbl_order_owner_status` | `INDEX` | 加速查询某用户在不同状态下的订单 |
|
||||||
|
|
||||||
|
## 补充约定
|
||||||
|
|
||||||
|
- `order_snap` 建议至少包含商品信息、数量、下单时价格、折扣、优惠、实际成交金额等字段。
|
||||||
|
- 当订单进入 `订单已确定` 及之后状态时,建议业务侧锁定关键字段,不再允许修改订单核心数据。
|
||||||
|
- `order_owner`、`order_source_id` 当前按文本字段保存业务 ID,不直接建立 relation,便于兼容现有 hooks 业务模型。
|
||||||
|
- `order_owner` 统一保存 `tbl_auth_users.openid`,便于直接使用微信登录返回 token 做原生访问控制。
|
||||||
|
- `is_delete` 用于软删除控制,订单删除/归档时建议优先标记为 `1`。
|
||||||
|
- 集合默认查询规则已内置 `is_delete = 0`,常规列表/详情不会返回已软删除数据。
|
||||||
|
- `order_amount` 使用 `number`,货币精度策略建议后续统一为“分”或固定小数位。
|
||||||
|
- `order_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。
|
||||||
|
- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
| `prod_list_id` | `text` | 是 | 产品列表业务 ID,唯一标识 |
|
| `prod_list_id` | `text` | 是 | 产品列表业务 ID,唯一标识 |
|
||||||
| `prod_list_name` | `text` | 是 | 产品名称 |
|
| `prod_list_name` | `text` | 是 | 产品名称 |
|
||||||
| `prod_list_modelnumber` | `text` | 否 | 产品型号 |
|
| `prod_list_modelnumber` | `text` | 否 | 产品型号 |
|
||||||
| `prod_list_icon` | `text` | 否 | 产品图标,保存 `tbl_attachments.attachments_id` |
|
| `prod_list_icon` | `text` | 否 | 产品图标,保存 `tbl_attachments.attachments_id`,多图时以 `|` 分隔 |
|
||||||
| `prod_list_description` | `text` | 否 | 产品说明(editor 内容,建议保存 Markdown 或已净化 HTML) |
|
| `prod_list_description` | `text` | 否 | 产品说明(editor 内容,建议保存 Markdown 或已净化 HTML) |
|
||||||
| `prod_list_feature` | `text` | 否 | 产品特色 |
|
| `prod_list_feature` | `text` | 否 | 产品特色 |
|
||||||
| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 数组,格式为 `[ { "name": "属性名", "value": "属性值" } ]`) |
|
| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 数组,格式为 `[ { "sort": 1, "name": "属性名", "value": "属性值" } ]`) |
|
||||||
| `prod_list_plantype` | `text` | 否 | 产品方案 |
|
| `prod_list_plantype` | `text` | 否 | 产品方案 |
|
||||||
| `prod_list_category` | `text` | 是 | 产品分类(必填,单选) |
|
| `prod_list_category` | `text` | 是 | 产品分类(必填,单选) |
|
||||||
| `prod_list_sort` | `number` | 否 | 排序值(同分类内按升序) |
|
| `prod_list_sort` | `number` | 否 | 排序值(同分类内按升序) |
|
||||||
@@ -29,7 +29,9 @@
|
|||||||
| `prod_list_tags` | `text` | 否 | 产品标签(辅助检索,以 `|` 聚合) |
|
| `prod_list_tags` | `text` | 否 | 产品标签(辅助检索,以 `|` 聚合) |
|
||||||
| `prod_list_status` | `text` | 否 | 产品状态(有效 / 过期 / 主推等) |
|
| `prod_list_status` | `text` | 否 | 产品状态(有效 / 过期 / 主推等) |
|
||||||
| `prod_list_basic_price` | `number` | 否 | 基础价格 |
|
| `prod_list_basic_price` | `number` | 否 | 基础价格 |
|
||||||
|
| `prod_list_vip_price` | `json` | 否 | 会员价数组,格式为 `[{"viplevel":"会员等级枚举值","price":1999}]` |
|
||||||
| `prod_list_remark` | `text` | 否 | 备注 |
|
| `prod_list_remark` | `text` | 否 | 备注 |
|
||||||
|
| `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` |
|
||||||
|
|
||||||
## 索引
|
## 索引
|
||||||
|
|
||||||
@@ -47,9 +49,14 @@
|
|||||||
|
|
||||||
## 补充约定
|
## 补充约定
|
||||||
|
|
||||||
- `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` 字段,写入时应直接提交数组结构:`[{"name":"属性名","value":"属性值"}]`。
|
- `prod_list_parameters` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"sort":1,"name":"属性名","value":"属性值"}]`。
|
||||||
|
- `prod_list_parameters.sort` 用于稳定参数展示顺序,约定为正整数;前端未填写时可按当前录入/导入顺序自动补齐。
|
||||||
|
- `prod_list_vip_price` 使用 PocketBase `json` 字段,写入时应直接提交数组结构:`[{"viplevel":"VIP1","price":1999}]`。
|
||||||
|
- `prod_list_vip_price.viplevel` 必须保存“数据-会员等级”字典中的枚举值,`price` 保存对应会员等级价格。
|
||||||
- `prod_list_description` 作为 editor 内容字段,建议统一为 Markdown 或净化后的 HTML;若允许 HTML,需在写入前做 XSS 白名单过滤。
|
- `prod_list_description` 作为 editor 内容字段,建议统一为 Markdown 或净化后的 HTML;若允许 HTML,需在写入前做 XSS 白名单过滤。
|
||||||
- 前端渲染 `prod_list_description` 时应保持和存储格式一致(Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。
|
- 前端渲染 `prod_list_description` 时应保持和存储格式一致(Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。
|
||||||
- `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。
|
- `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。
|
||||||
|
|||||||
@@ -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 字段清单里单独声明。
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/detail.js`)
|
|||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/create.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/create.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/update.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/update.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/delete.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/product/delete.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart/list.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart/detail.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart/create.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart/update.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart/delete.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/list.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/detail.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/create.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/update.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/order/delete.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js`)
|
||||||
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/cart-order/manage-user-update.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document-history/list.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/document-history/list.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js`)
|
||||||
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/role-save.js`)
|
require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/role-save.js`)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ require(`${__hooks}/bai_web_pb_hooks/pages/document-manage.js`)
|
|||||||
require(`${__hooks}/bai_web_pb_hooks/pages/product-manage.js`)
|
require(`${__hooks}/bai_web_pb_hooks/pages/product-manage.js`)
|
||||||
require(`${__hooks}/bai_web_pb_hooks/pages/dictionary-manage.js`)
|
require(`${__hooks}/bai_web_pb_hooks/pages/dictionary-manage.js`)
|
||||||
require(`${__hooks}/bai_web_pb_hooks/pages/sdk-permission-manage.js`)
|
require(`${__hooks}/bai_web_pb_hooks/pages/sdk-permission-manage.js`)
|
||||||
|
require(`${__hooks}/bai_web_pb_hooks/pages/cart-order-manage.js`)
|
||||||
require(`${__hooks}/bai_chat_alm_hooks/bai-ai-manage-main.pb.js`)
|
require(`${__hooks}/bai_chat_alm_hooks/bai-ai-manage-main.pb.js`)
|
||||||
require(`${__hooks}/bai_chat_alm_hooks/bai-chat.pb.js`)
|
require(`${__hooks}/bai_chat_alm_hooks/bai-chat.pb.js`)
|
||||||
require(`${__hooks}/bai_chat_alm_hooks/bai-sql-lab.pb.js`)
|
require(`${__hooks}/bai_chat_alm_hooks/bai-sql-lab.pb.js`)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/cart-order/manage-user-update', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
guards.requireManagePlatformUser(e)
|
||||||
|
const payload = guards.validateCartOrderManageUserUpdateBody(e)
|
||||||
|
const data = cartOrderService.updateManageUser(payload)
|
||||||
|
|
||||||
|
return success(e, '更新用户信息成功', {
|
||||||
|
user: data,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('更新用户信息失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
routerAdd('POST', '/api/cart-order/manage-users', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
guards.requireManagePlatformUser(e)
|
||||||
|
const payload = guards.validateCartOrderManageListBody(e)
|
||||||
|
const data = cartOrderService.listManageUsersCartOrders(payload)
|
||||||
|
|
||||||
|
return success(e, '查询用户信息、购物车与订单成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('查询用户信息、购物车与订单失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/cart/create', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateCartMutationBody(e, false)
|
||||||
|
const data = cartOrderService.createCart(authState, payload)
|
||||||
|
|
||||||
|
return success(e, '创建购物车记录成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('创建购物车记录失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/cart/delete', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateCartDeleteBody(e)
|
||||||
|
const data = cartOrderService.deleteCart(authState.openid, payload.cart_id)
|
||||||
|
|
||||||
|
return success(e, '删除购物车记录成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('删除购物车记录失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
23
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js
Normal file
23
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
routerAdd('POST', '/api/cart/detail', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
const payload = guards.validateCartDetailBody(e)
|
||||||
|
const data = cartOrderService.getCartDetail(authState.openid, payload.cart_id)
|
||||||
|
|
||||||
|
return success(e, '查询购物车详情成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('查询购物车详情失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/cart/list', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
const payload = guards.validateCartListBody(e)
|
||||||
|
const data = cartOrderService.listCarts(authState.openid, payload)
|
||||||
|
|
||||||
|
return success(e, '查询购物车列表成功', {
|
||||||
|
items: data,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('查询购物车列表失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/cart/update', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateCartMutationBody(e, true)
|
||||||
|
const data = cartOrderService.updateCart(authState.openid, payload)
|
||||||
|
|
||||||
|
return success(e, '更新购物车记录成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('更新购物车记录失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/order/create', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateOrderMutationBody(e, false)
|
||||||
|
const data = cartOrderService.createOrder(authState, payload)
|
||||||
|
|
||||||
|
return success(e, '创建订单成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('创建订单失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/order/delete', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateOrderDeleteBody(e)
|
||||||
|
const data = cartOrderService.deleteOrder(authState.openid, payload.order_id)
|
||||||
|
|
||||||
|
return success(e, '删除订单成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('删除订单失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
23
pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js
Normal file
23
pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
routerAdd('POST', '/api/order/detail', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
const payload = guards.validateOrderDetailBody(e)
|
||||||
|
const data = cartOrderService.getOrderDetail(authState.openid, payload.order_id)
|
||||||
|
|
||||||
|
return success(e, '查询订单详情成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('查询订单详情失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/order/list', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
const payload = guards.validateOrderListBody(e)
|
||||||
|
const data = cartOrderService.listOrders(authState.openid, payload)
|
||||||
|
|
||||||
|
return success(e, '查询订单列表成功', {
|
||||||
|
items: data,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('查询订单列表失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routerAdd('POST', '/api/order/update', function (e) {
|
||||||
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
|
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
guards.requireJson(e)
|
||||||
|
const authState = guards.requireAuthUser(e)
|
||||||
|
guards.duplicateGuard(e)
|
||||||
|
|
||||||
|
const payload = guards.validateOrderMutationBody(e, true)
|
||||||
|
const data = cartOrderService.updateOrder(authState.openid, payload)
|
||||||
|
|
||||||
|
return success(e, '更新订单成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
logger.error('更新订单失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
routerAdd('POST', '/api/sdk-permission/context', function (e) {
|
routerAdd('POST', '/api/sdk-permission/context', function (e) {
|
||||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||||
const permissionService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js`)
|
const permissionService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js`)
|
||||||
const { success } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||||
|
|
||||||
|
try {
|
||||||
guards.requireJson(e)
|
guards.requireJson(e)
|
||||||
guards.requireManagePlatformUser(e)
|
guards.requireManagePlatformUser(e)
|
||||||
|
|
||||||
@@ -10,6 +12,17 @@ routerAdd('POST', '/api/sdk-permission/context', function (e) {
|
|||||||
const data = permissionService.getManagementContext(payload.keyword)
|
const data = permissionService.getManagementContext(payload.keyword)
|
||||||
|
|
||||||
return success(e, '查询权限管理上下文成功', data)
|
return success(e, '查询权限管理上下文成功', data)
|
||||||
|
} catch (err) {
|
||||||
|
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||||
|
|
||||||
|
logger.error('查询权限管理上下文失败', {
|
||||||
|
status: status,
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
data: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
|
||||||
|
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
APP_BASE_URL=https://bai-api.blv-oa.com
|
APP_BASE_URL=https://bai-api.blv-oa.com
|
||||||
POCKETBASE_API_URL=https://bai-api.blv-oa.com/pb/
|
POCKETBASE_API_URL=http://127.0.0.1:8090
|
||||||
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo
|
POCKETBASE_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo
|
||||||
|
|
||||||
#正式服
|
#正式服
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module.exports = {
|
|||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
APP_VERSION: '0.1.21',
|
APP_VERSION: '0.1.21',
|
||||||
APP_BASE_URL: 'https://bai-api.blv-oa.com',
|
APP_BASE_URL: 'https://bai-api.blv-oa.com',
|
||||||
POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb',
|
POCKETBASE_API_URL: 'http://127.0.0.1:8090',
|
||||||
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
|
POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo',
|
||||||
WECHAT_APPID: 'wx3bd7a7b19679da7a',
|
WECHAT_APPID: 'wx3bd7a7b19679da7a',
|
||||||
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
|
WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1',
|
||||||
|
|||||||
@@ -107,14 +107,32 @@ function validateSystemRefreshBody(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requireManagePlatformUser(e) {
|
function requireManagePlatformUser(e) {
|
||||||
const authUser = requireAuthUser(e)
|
const authHeader = e.request.header.get('Authorization') || ''
|
||||||
const idType = authUser.authRecord.getString('users_idtype')
|
const parts = authHeader.split(' ')
|
||||||
|
|
||||||
|
if (parts.length !== 2 || parts[0] !== 'Bearer' || !parts[1]) {
|
||||||
|
throw createAppError(401, '请求头缺少 Authorization')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.auth) {
|
||||||
|
throw createAppError(401, '认证令牌无效或已过期')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.auth.collection().name !== 'tbl_auth_users') {
|
||||||
|
throw createAppError(401, '认证用户集合不正确')
|
||||||
|
}
|
||||||
|
|
||||||
|
const authRecord = e.auth
|
||||||
|
const idType = authRecord.getString('users_idtype')
|
||||||
|
|
||||||
if (idType !== 'ManagePlatform') {
|
if (idType !== 'ManagePlatform') {
|
||||||
throw createAppError(403, '仅平台管理用户可访问')
|
throw createAppError(403, '仅平台管理用户可访问')
|
||||||
}
|
}
|
||||||
|
|
||||||
return authUser
|
return {
|
||||||
|
openid: authRecord.getString('openid') || '',
|
||||||
|
authRecord: authRecord,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateDictionaryListBody(e) {
|
function validateDictionaryListBody(e) {
|
||||||
@@ -355,16 +373,31 @@ function normalizeProductParameters(value) {
|
|||||||
const result = []
|
const result = []
|
||||||
const indexByName = {}
|
const indexByName = {}
|
||||||
|
|
||||||
function upsert(nameValue, rawValue) {
|
function normalizeParameterSort(rawSort, fallbackSort) {
|
||||||
|
if (rawSort === '' || rawSort === null || typeof rawSort === 'undefined') {
|
||||||
|
return fallbackSort
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(rawSort)
|
||||||
|
if (!Number.isFinite(num) || num <= 0 || Math.floor(num) !== num) {
|
||||||
|
throw createAppError(400, 'prod_list_parameters.sort 必须为正整数')
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsert(nameValue, rawValue, rawSort, fallbackSort) {
|
||||||
const name = String(nameValue || '').trim()
|
const name = String(nameValue || '').trim()
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
|
const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
|
||||||
|
const normalizedSort = normalizeParameterSort(rawSort, fallbackSort)
|
||||||
const existingIndex = indexByName[name]
|
const existingIndex = indexByName[name]
|
||||||
if (typeof existingIndex === 'number') {
|
if (typeof existingIndex === 'number') {
|
||||||
result[existingIndex].value = normalizedValue
|
result[existingIndex].value = normalizedValue
|
||||||
|
result[existingIndex].sort = normalizedSort
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +405,7 @@ function normalizeProductParameters(value) {
|
|||||||
result.push({
|
result.push({
|
||||||
name: name,
|
name: name,
|
||||||
value: normalizedValue,
|
value: normalizedValue,
|
||||||
|
sort: normalizedSort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +415,7 @@ function normalizeProductParameters(value) {
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
upsert(item.name || item.key, item.value)
|
upsert(item.name || item.key, item.value, item.sort, result.length + 1)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -393,7 +427,58 @@ function normalizeProductParameters(value) {
|
|||||||
const keys = Object.keys(value)
|
const keys = Object.keys(value)
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i += 1) {
|
for (let i = 0; i < keys.length; i += 1) {
|
||||||
upsert(keys[i], value[keys[i]])
|
upsert(keys[i], value[keys[i]], '', result.length + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeProductVipPrice(value) {
|
||||||
|
if (value === null || typeof value === 'undefined' || value === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = value
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
try {
|
||||||
|
source = JSON.parse(source)
|
||||||
|
} catch (_error) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 必须为合法 JSON 数组')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(source)) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 必须为数组')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = []
|
||||||
|
const levelMap = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < source.length; i += 1) {
|
||||||
|
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
|
||||||
|
if (!item) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const viplevel = String(item.viplevel || '').trim()
|
||||||
|
if (!viplevel) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 viplevel 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = Number(item.price)
|
||||||
|
if (!Number.isFinite(price)) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 price 必须为数字')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (levelMap[viplevel]) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 中 viplevel 不允许重复:' + viplevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
levelMap[viplevel] = true
|
||||||
|
result.push({
|
||||||
|
viplevel: viplevel,
|
||||||
|
price: price,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -436,13 +521,15 @@ 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 || '',
|
||||||
prod_list_icon: payload.prod_list_icon || '',
|
prod_list_icon: normalizeAttachmentIdList(payload.prod_list_icon, 'prod_list_icon').join('|'),
|
||||||
prod_list_description: payload.prod_list_description || '',
|
prod_list_description: payload.prod_list_description || '',
|
||||||
prod_list_feature: payload.prod_list_feature || '',
|
prod_list_feature: payload.prod_list_feature || '',
|
||||||
prod_list_parameters: normalizeProductParameters(payload.prod_list_parameters),
|
prod_list_parameters: normalizeProductParameters(payload.prod_list_parameters),
|
||||||
|
prod_list_function: normalizeProductParameters(payload.prod_list_function),
|
||||||
prod_list_plantype: payload.prod_list_plantype || '',
|
prod_list_plantype: payload.prod_list_plantype || '',
|
||||||
prod_list_category: payload.prod_list_category || '',
|
prod_list_category: payload.prod_list_category || '',
|
||||||
prod_list_sort: typeof payload.prod_list_sort === 'undefined' ? 0 : payload.prod_list_sort,
|
prod_list_sort: typeof payload.prod_list_sort === 'undefined' ? 0 : payload.prod_list_sort,
|
||||||
@@ -452,6 +539,7 @@ function validateProductMutationBody(e, isUpdate) {
|
|||||||
prod_list_tags: payload.prod_list_tags || '',
|
prod_list_tags: payload.prod_list_tags || '',
|
||||||
prod_list_status: payload.prod_list_status || '',
|
prod_list_status: payload.prod_list_status || '',
|
||||||
prod_list_basic_price: typeof payload.prod_list_basic_price === 'undefined' ? '' : payload.prod_list_basic_price,
|
prod_list_basic_price: typeof payload.prod_list_basic_price === 'undefined' ? '' : payload.prod_list_basic_price,
|
||||||
|
prod_list_vip_price: normalizeProductVipPrice(payload.prod_list_vip_price),
|
||||||
prod_list_remark: payload.prod_list_remark || '',
|
prod_list_remark: payload.prod_list_remark || '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,6 +548,157 @@ function validateProductDeleteBody(e) {
|
|||||||
return validateProductDetailBody(e)
|
return validateProductDetailBody(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCartListBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword: payload.keyword || '',
|
||||||
|
cart_status: payload.cart_status || '',
|
||||||
|
cart_number: payload.cart_number || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCartDetailBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
if (!payload.cart_id) {
|
||||||
|
throw createAppError(400, 'cart_id 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cart_id: String(payload.cart_id || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCartMutationBody(e, isUpdate) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
if (isUpdate && !payload.cart_id) {
|
||||||
|
throw createAppError(400, 'cart_id 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUpdate) {
|
||||||
|
if (!payload.cart_product_id) {
|
||||||
|
throw createAppError(400, 'cart_product_id 为必填项')
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_product_quantity === 'undefined') {
|
||||||
|
throw createAppError(400, 'cart_product_quantity 为必填项')
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_at_price === 'undefined') {
|
||||||
|
throw createAppError(400, 'cart_at_price 为必填项')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cart_id: payload.cart_id || '',
|
||||||
|
cart_number: Object.prototype.hasOwnProperty.call(payload, 'cart_number') ? payload.cart_number : undefined,
|
||||||
|
cart_owner: Object.prototype.hasOwnProperty.call(payload, 'cart_owner') ? payload.cart_owner : undefined,
|
||||||
|
cart_product_id: Object.prototype.hasOwnProperty.call(payload, 'cart_product_id') ? payload.cart_product_id : undefined,
|
||||||
|
cart_product_quantity: Object.prototype.hasOwnProperty.call(payload, 'cart_product_quantity') ? payload.cart_product_quantity : undefined,
|
||||||
|
cart_status: Object.prototype.hasOwnProperty.call(payload, 'cart_status') ? payload.cart_status : undefined,
|
||||||
|
cart_at_price: Object.prototype.hasOwnProperty.call(payload, 'cart_at_price') ? payload.cart_at_price : undefined,
|
||||||
|
cart_remark: Object.prototype.hasOwnProperty.call(payload, 'cart_remark') ? payload.cart_remark : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCartDeleteBody(e) {
|
||||||
|
return validateCartDetailBody(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOrderListBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyword: payload.keyword || '',
|
||||||
|
order_status: payload.order_status || '',
|
||||||
|
order_source: payload.order_source || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOrderDetailBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
if (!payload.order_id) {
|
||||||
|
throw createAppError(400, 'order_id 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
order_id: String(payload.order_id || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOrderMutationBody(e, isUpdate) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
|
||||||
|
if (isUpdate && !payload.order_id) {
|
||||||
|
throw createAppError(400, 'order_id 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUpdate) {
|
||||||
|
if (!payload.order_source) {
|
||||||
|
throw createAppError(400, 'order_source 为必填项')
|
||||||
|
}
|
||||||
|
if (!payload.order_source_id) {
|
||||||
|
throw createAppError(400, 'order_source_id 为必填项')
|
||||||
|
}
|
||||||
|
if (typeof payload.order_snap === 'undefined') {
|
||||||
|
throw createAppError(400, 'order_snap 为必填项')
|
||||||
|
}
|
||||||
|
if (typeof payload.order_amount === 'undefined') {
|
||||||
|
throw createAppError(400, 'order_amount 为必填项')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
order_id: payload.order_id || '',
|
||||||
|
order_number: Object.prototype.hasOwnProperty.call(payload, 'order_number') ? payload.order_number : undefined,
|
||||||
|
order_owner: Object.prototype.hasOwnProperty.call(payload, 'order_owner') ? payload.order_owner : undefined,
|
||||||
|
order_source: Object.prototype.hasOwnProperty.call(payload, 'order_source') ? payload.order_source : undefined,
|
||||||
|
order_status: Object.prototype.hasOwnProperty.call(payload, 'order_status') ? payload.order_status : undefined,
|
||||||
|
order_source_id: Object.prototype.hasOwnProperty.call(payload, 'order_source_id') ? payload.order_source_id : undefined,
|
||||||
|
order_snap: Object.prototype.hasOwnProperty.call(payload, 'order_snap') ? payload.order_snap : undefined,
|
||||||
|
order_amount: Object.prototype.hasOwnProperty.call(payload, 'order_amount') ? payload.order_amount : undefined,
|
||||||
|
order_remark: Object.prototype.hasOwnProperty.call(payload, 'order_remark') ? payload.order_remark : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOrderDeleteBody(e) {
|
||||||
|
return validateOrderDetailBody(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCartOrderManageListBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
return {
|
||||||
|
keyword: payload.keyword || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCartOrderManageUserUpdateBody(e) {
|
||||||
|
const payload = parseBody(e)
|
||||||
|
if (!payload.openid) {
|
||||||
|
throw createAppError(400, 'openid 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openid: String(payload.openid || '').trim(),
|
||||||
|
users_name: Object.prototype.hasOwnProperty.call(payload, 'users_name') ? payload.users_name : undefined,
|
||||||
|
users_phone: Object.prototype.hasOwnProperty.call(payload, 'users_phone') ? payload.users_phone : undefined,
|
||||||
|
users_id_number: Object.prototype.hasOwnProperty.call(payload, 'users_id_number') ? payload.users_id_number : undefined,
|
||||||
|
users_level: Object.prototype.hasOwnProperty.call(payload, 'users_level') ? payload.users_level : undefined,
|
||||||
|
users_type: Object.prototype.hasOwnProperty.call(payload, 'users_type') ? payload.users_type : undefined,
|
||||||
|
users_tag: Object.prototype.hasOwnProperty.call(payload, 'users_tag') ? payload.users_tag : undefined,
|
||||||
|
users_status: Object.prototype.hasOwnProperty.call(payload, 'users_status') ? payload.users_status : undefined,
|
||||||
|
users_rank_level: Object.prototype.hasOwnProperty.call(payload, 'users_rank_level') ? payload.users_rank_level : undefined,
|
||||||
|
users_auth_type: Object.prototype.hasOwnProperty.call(payload, 'users_auth_type') ? payload.users_auth_type : undefined,
|
||||||
|
company_id: Object.prototype.hasOwnProperty.call(payload, 'company_id') ? payload.company_id : undefined,
|
||||||
|
users_parent_id: Object.prototype.hasOwnProperty.call(payload, 'users_parent_id') ? payload.users_parent_id : undefined,
|
||||||
|
users_promo_code: Object.prototype.hasOwnProperty.call(payload, 'users_promo_code') ? payload.users_promo_code : undefined,
|
||||||
|
usergroups_id: Object.prototype.hasOwnProperty.call(payload, 'usergroups_id') ? payload.usergroups_id : undefined,
|
||||||
|
users_picture: Object.prototype.hasOwnProperty.call(payload, 'users_picture') ? payload.users_picture : undefined,
|
||||||
|
users_id_pic_a: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_a') ? payload.users_id_pic_a : undefined,
|
||||||
|
users_id_pic_b: Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_b') ? payload.users_id_pic_b : undefined,
|
||||||
|
users_title_picture: Object.prototype.hasOwnProperty.call(payload, 'users_title_picture') ? payload.users_title_picture : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function validateDocumentHistoryListBody(e) {
|
function validateDocumentHistoryListBody(e) {
|
||||||
const payload = parseBody(e)
|
const payload = parseBody(e)
|
||||||
|
|
||||||
@@ -635,6 +874,16 @@ module.exports = {
|
|||||||
validateProductDetailBody,
|
validateProductDetailBody,
|
||||||
validateProductMutationBody,
|
validateProductMutationBody,
|
||||||
validateProductDeleteBody,
|
validateProductDeleteBody,
|
||||||
|
validateCartListBody,
|
||||||
|
validateCartDetailBody,
|
||||||
|
validateCartMutationBody,
|
||||||
|
validateCartDeleteBody,
|
||||||
|
validateOrderListBody,
|
||||||
|
validateOrderDetailBody,
|
||||||
|
validateOrderMutationBody,
|
||||||
|
validateOrderDeleteBody,
|
||||||
|
validateCartOrderManageListBody,
|
||||||
|
validateCartOrderManageUserUpdateBody,
|
||||||
validateDocumentHistoryListBody,
|
validateDocumentHistoryListBody,
|
||||||
validateSdkPermissionContextBody,
|
validateSdkPermissionContextBody,
|
||||||
validateSdkPermissionRoleBody,
|
validateSdkPermissionRoleBody,
|
||||||
|
|||||||
@@ -0,0 +1,796 @@
|
|||||||
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
|
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||||
|
const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`)
|
||||||
|
const userService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/userService.js`)
|
||||||
|
|
||||||
|
function buildBusinessId(prefix) {
|
||||||
|
return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonResponse(response, actionName) {
|
||||||
|
if (!response) {
|
||||||
|
throw createAppError(500, actionName + ' 失败:PocketBase 响应为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.json && typeof response.json === 'object') {
|
||||||
|
return response.json
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof response.body === 'string' && response.body) {
|
||||||
|
return JSON.parse(response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.body && typeof response.body === 'object') {
|
||||||
|
return response.body
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof response.data === 'string' && response.data) {
|
||||||
|
return JSON.parse(response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data && typeof response.data === 'object') {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPocketBaseApiBaseUrl() {
|
||||||
|
const base = String(env.pocketbaseApiUrl || '').replace(/\/+$/, '')
|
||||||
|
if (!base) {
|
||||||
|
throw createAppError(500, '缺少 POCKETBASE_API_URL 配置')
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPocketBaseAuthToken() {
|
||||||
|
const token = String(env.pocketbaseAuthToken || '').trim()
|
||||||
|
if (!token) {
|
||||||
|
throw createAppError(500, '缺少 POCKETBASE_AUTH_TOKEN 配置')
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPocketBase(method, path, body, actionName) {
|
||||||
|
const response = $http.send({
|
||||||
|
url: getPocketBaseApiBaseUrl() + path,
|
||||||
|
method: method,
|
||||||
|
headers: body ? {
|
||||||
|
Authorization: 'Bearer ' + getPocketBaseAuthToken(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
} : {
|
||||||
|
Authorization: 'Bearer ' + getPocketBaseAuthToken(),
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : '',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response || response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
throw createAppError(500, actionName + ' 失败', {
|
||||||
|
statusCode: response ? response.statusCode : 0,
|
||||||
|
body: response ? String(response.body || '') : '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJsonResponse(response, actionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value || '').replace(/^\s+|\s+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateNumberSegment(date) {
|
||||||
|
const current = date || new Date()
|
||||||
|
const yyyy = String(current.getFullYear())
|
||||||
|
const mm = String(current.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(current.getDate()).padStart(2, '0')
|
||||||
|
const hh = String(current.getHours()).padStart(2, '0')
|
||||||
|
const mi = String(current.getMinutes()).padStart(2, '0')
|
||||||
|
const ss = String(current.getSeconds()).padStart(2, '0')
|
||||||
|
return yyyy + mm + dd + hh + mi + ss
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDisplayNumber(prefix, authRecord, openid) {
|
||||||
|
const baseName = normalizeText(authRecord && typeof authRecord.getString === 'function' ? authRecord.getString('users_name') : '') || normalizeText(openid) || prefix
|
||||||
|
return baseName + '-' + formatDateNumberSegment(new Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNumberValue(value, fieldName) {
|
||||||
|
if (value === '' || value === null || typeof value === 'undefined') {
|
||||||
|
throw createAppError(400, fieldName + ' 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须为数字')
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePositiveIntegerValue(value, fieldName) {
|
||||||
|
const num = normalizeNumberValue(value, fieldName)
|
||||||
|
if (num <= 0 || Math.floor(num) !== num) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须为正整数')
|
||||||
|
}
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptionalNumberValue(value, fieldName) {
|
||||||
|
if (value === '' || value === null || typeof value === 'undefined') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(value)
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须为数字')
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCartStatus(value) {
|
||||||
|
return normalizeText(value) || '有效'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOrderStatus(value) {
|
||||||
|
return normalizeText(value) || '订单已生成'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOrderSource(value) {
|
||||||
|
return normalizeText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeJsonField(value, fieldName) {
|
||||||
|
if (value === null || typeof value === 'undefined' || value === '') {
|
||||||
|
throw createAppError(400, fieldName + ' 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
} catch (_err) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须可序列化为 JSON')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
} catch (_err) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须可序列化为 JSON')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (_err) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须是 JSON 对象、数组或合法 JSON 字符串')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createAppError(400, fieldName + ' 必须是 JSON 对象、数组或合法 JSON 字符串')
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonFieldForOutput(value) {
|
||||||
|
if (value === null || typeof value === 'undefined' || value === '') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
} catch (_err) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (_err) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProductRecordByBusinessId(productId) {
|
||||||
|
const records = $app.findRecordsByFilter('tbl_product_list', 'prod_list_id = {:productId}', '', 1, 0, {
|
||||||
|
productId: productId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return records.length ? records[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProductInfo(record) {
|
||||||
|
if (!record) {
|
||||||
|
return {
|
||||||
|
prod_list_id: '',
|
||||||
|
prod_list_name: '',
|
||||||
|
prod_list_modelnumber: '',
|
||||||
|
prod_list_basic_price: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prod_list_id: record.getString('prod_list_id'),
|
||||||
|
prod_list_name: record.getString('prod_list_name'),
|
||||||
|
prod_list_modelnumber: record.getString('prod_list_modelnumber'),
|
||||||
|
prod_list_basic_price: record.get('prod_list_basic_price'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureProductExists(productId) {
|
||||||
|
const targetId = normalizeText(productId)
|
||||||
|
if (!targetId) {
|
||||||
|
throw createAppError(400, 'cart_product_id 为必填项')
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = findProductRecordByBusinessId(targetId)
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(400, 'cart_product_id 对应产品不存在:' + targetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCartRecordByBusinessId(cartId) {
|
||||||
|
const records = $app.findRecordsByFilter('tbl_cart', 'cart_id = {:cartId}', '', 1, 0, {
|
||||||
|
cartId: cartId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return records.length ? records[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOrderRecordByBusinessId(orderId) {
|
||||||
|
const records = $app.findRecordsByFilter('tbl_order', 'order_id = {:orderId}', '', 1, 0, {
|
||||||
|
orderId: orderId,
|
||||||
|
})
|
||||||
|
|
||||||
|
return records.length ? records[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRecordOwner(record, fieldName, authOpenid, resourceLabel) {
|
||||||
|
const owner = normalizeText(record && typeof record.getString === 'function' ? record.getString(fieldName) : '')
|
||||||
|
if (!owner || owner !== normalizeText(authOpenid)) {
|
||||||
|
throw createAppError(403, '无权访问该' + resourceLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportCartRecord(record, productRecord) {
|
||||||
|
const productInfo = buildProductInfo(productRecord || findProductRecordByBusinessId(record.getString('cart_product_id')))
|
||||||
|
|
||||||
|
return {
|
||||||
|
pb_id: record.id,
|
||||||
|
cart_id: record.getString('cart_id'),
|
||||||
|
cart_number: record.getString('cart_number'),
|
||||||
|
cart_create: String(record.get('cart_create') || ''),
|
||||||
|
cart_owner: record.getString('cart_owner'),
|
||||||
|
cart_product_id: record.getString('cart_product_id'),
|
||||||
|
cart_product_quantity: Number(record.get('cart_product_quantity') || 0),
|
||||||
|
cart_status: record.getString('cart_status'),
|
||||||
|
cart_at_price: Number(record.get('cart_at_price') || 0),
|
||||||
|
cart_remark: record.getString('cart_remark'),
|
||||||
|
product_name: productInfo.prod_list_name,
|
||||||
|
product_modelnumber: productInfo.prod_list_modelnumber,
|
||||||
|
product_basic_price: productInfo.prod_list_basic_price,
|
||||||
|
created: String(record.created || ''),
|
||||||
|
updated: String(record.updated || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportOrderRecord(record) {
|
||||||
|
return {
|
||||||
|
pb_id: record.id,
|
||||||
|
order_id: record.getString('order_id'),
|
||||||
|
order_number: record.getString('order_number'),
|
||||||
|
order_create: String(record.get('order_create') || ''),
|
||||||
|
order_owner: record.getString('order_owner'),
|
||||||
|
order_source: record.getString('order_source'),
|
||||||
|
order_status: record.getString('order_status'),
|
||||||
|
order_source_id: record.getString('order_source_id'),
|
||||||
|
order_snap: parseJsonFieldForOutput(record.get('order_snap')),
|
||||||
|
order_amount: Number(record.get('order_amount') || 0),
|
||||||
|
order_remark: record.getString('order_remark'),
|
||||||
|
created: String(record.created || ''),
|
||||||
|
updated: String(record.updated || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAdminCartRecord(record) {
|
||||||
|
const productId = record && typeof record.getString === 'function'
|
||||||
|
? record.getString('cart_product_id')
|
||||||
|
: String(record && record.cart_product_id || '')
|
||||||
|
const productInfo = buildProductInfo(findProductRecordByBusinessId(productId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
pb_id: String(record && record.id || ''),
|
||||||
|
cart_id: record && typeof record.getString === 'function' ? record.getString('cart_id') : String(record && record.cart_id || ''),
|
||||||
|
cart_number: record && typeof record.getString === 'function' ? record.getString('cart_number') : String(record && record.cart_number || ''),
|
||||||
|
cart_create: record && typeof record.get === 'function' ? String(record.get('cart_create') || '') : String(record && record.cart_create || ''),
|
||||||
|
cart_owner: record && typeof record.getString === 'function' ? record.getString('cart_owner') : String(record && record.cart_owner || ''),
|
||||||
|
cart_product_id: productId,
|
||||||
|
cart_product_quantity: record && typeof record.get === 'function' ? Number(record.get('cart_product_quantity') || 0) : Number(record && record.cart_product_quantity || 0),
|
||||||
|
cart_status: record && typeof record.getString === 'function' ? record.getString('cart_status') : String(record && record.cart_status || ''),
|
||||||
|
cart_at_price: record && typeof record.get === 'function' ? Number(record.get('cart_at_price') || 0) : Number(record && record.cart_at_price || 0),
|
||||||
|
cart_remark: record && typeof record.getString === 'function' ? record.getString('cart_remark') : String(record && record.cart_remark || ''),
|
||||||
|
product_name: productInfo.prod_list_name,
|
||||||
|
product_modelnumber: productInfo.prod_list_modelnumber,
|
||||||
|
product_basic_price: productInfo.prod_list_basic_price,
|
||||||
|
created: String(record && record.created || ''),
|
||||||
|
updated: String(record && record.updated || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAdminOrderRecord(record) {
|
||||||
|
return {
|
||||||
|
pb_id: String(record && record.id || ''),
|
||||||
|
order_id: record && typeof record.getString === 'function' ? record.getString('order_id') : String(record && record.order_id || ''),
|
||||||
|
order_number: record && typeof record.getString === 'function' ? record.getString('order_number') : String(record && record.order_number || ''),
|
||||||
|
order_create: record && typeof record.get === 'function' ? String(record.get('order_create') || '') : String(record && record.order_create || ''),
|
||||||
|
order_owner: record && typeof record.getString === 'function' ? record.getString('order_owner') : String(record && record.order_owner || ''),
|
||||||
|
order_source: record && typeof record.getString === 'function' ? record.getString('order_source') : String(record && record.order_source || ''),
|
||||||
|
order_status: record && typeof record.getString === 'function' ? record.getString('order_status') : String(record && record.order_status || ''),
|
||||||
|
order_source_id: record && typeof record.getString === 'function' ? record.getString('order_source_id') : String(record && record.order_source_id || ''),
|
||||||
|
order_snap: record && typeof record.get === 'function' ? parseJsonFieldForOutput(record.get('order_snap')) : parseJsonFieldForOutput(record && record.order_snap),
|
||||||
|
order_amount: record && typeof record.get === 'function' ? Number(record.get('order_amount') || 0) : Number(record && record.order_amount || 0),
|
||||||
|
order_remark: record && typeof record.getString === 'function' ? record.getString('order_remark') : String(record && record.order_remark || ''),
|
||||||
|
created: String(record && record.created || ''),
|
||||||
|
updated: String(record && record.updated || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAdminManageUser(userRecord, groupedCarts, groupedOrders) {
|
||||||
|
const openid = userRecord && typeof userRecord.getString === 'function'
|
||||||
|
? userRecord.getString('openid')
|
||||||
|
: String(userRecord && userRecord.openid || '')
|
||||||
|
const carts = groupedCarts[openid] || []
|
||||||
|
const orders = groupedOrders[openid] || []
|
||||||
|
let cartTotalQuantity = 0
|
||||||
|
let orderTotalAmount = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < carts.length; i += 1) {
|
||||||
|
cartTotalQuantity += Number(carts[i].cart_product_quantity || 0)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < orders.length; i += 1) {
|
||||||
|
orderTotalAmount += Number(orders[i].order_amount || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pb_id: String(userRecord && userRecord.id || ''),
|
||||||
|
openid: openid,
|
||||||
|
users_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id') : String(userRecord && userRecord.users_id || ''),
|
||||||
|
users_name: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_name') : String(userRecord && userRecord.users_name || ''),
|
||||||
|
users_phone: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_phone') : String(userRecord && userRecord.users_phone || ''),
|
||||||
|
users_level: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_level') : String(userRecord && userRecord.users_level || ''),
|
||||||
|
users_level_name: userService.resolveUserLevelName(userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_level') : String(userRecord && userRecord.users_level || '')),
|
||||||
|
users_type: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_type') : String(userRecord && userRecord.users_type || ''),
|
||||||
|
users_idtype: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_idtype') : String(userRecord && userRecord.users_idtype || ''),
|
||||||
|
users_id_number: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id_number') : String(userRecord && userRecord.users_id_number || ''),
|
||||||
|
users_status: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_status') : String(userRecord && userRecord.users_status || ''),
|
||||||
|
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
|
||||||
|
: Number(userRecord.get('users_rank_level')),
|
||||||
|
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
|
||||||
|
: Number(userRecord.get('users_auth_type')),
|
||||||
|
users_tag: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_tag') : String(userRecord && userRecord.users_tag || ''),
|
||||||
|
company_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('company_id') : String(userRecord && userRecord.company_id || ''),
|
||||||
|
users_parent_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_parent_id') : String(userRecord && userRecord.users_parent_id || ''),
|
||||||
|
users_promo_code: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_promo_code') : String(userRecord && userRecord.users_promo_code || ''),
|
||||||
|
usergroups_id: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('usergroups_id') : String(userRecord && userRecord.usergroups_id || ''),
|
||||||
|
users_picture: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_picture') : String(userRecord && userRecord.users_picture || ''),
|
||||||
|
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: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_id_pic_b') : String(userRecord && userRecord.users_id_pic_b || ''),
|
||||||
|
users_title_picture: userRecord && typeof userRecord.getString === 'function' ? userRecord.getString('users_title_picture') : String(userRecord && userRecord.users_title_picture || ''),
|
||||||
|
cart_count: carts.length,
|
||||||
|
cart_total_quantity: cartTotalQuantity,
|
||||||
|
order_count: orders.length,
|
||||||
|
order_total_amount: orderTotalAmount,
|
||||||
|
carts: carts,
|
||||||
|
orders: orders,
|
||||||
|
created: String(userRecord && userRecord.created || ''),
|
||||||
|
updated: String(userRecord && userRecord.updated || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listCarts(authOpenid, payload) {
|
||||||
|
const records = $app.findRecordsByFilter('tbl_cart', 'cart_owner = {:owner}', '-cart_create', 500, 0, {
|
||||||
|
owner: authOpenid,
|
||||||
|
})
|
||||||
|
const keyword = normalizeText(payload.keyword).toLowerCase()
|
||||||
|
const cartStatus = normalizeText(payload.cart_status)
|
||||||
|
const cartNumber = normalizeText(payload.cart_number)
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (let i = 0; i < records.length; i += 1) {
|
||||||
|
const item = exportCartRecord(records[i])
|
||||||
|
const matchedKeyword = !keyword
|
||||||
|
|| item.cart_id.toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| item.cart_number.toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| item.cart_product_id.toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| normalizeText(item.product_name).toLowerCase().indexOf(keyword) !== -1
|
||||||
|
const matchedStatus = !cartStatus || item.cart_status === cartStatus
|
||||||
|
const matchedNumber = !cartNumber || item.cart_number === cartNumber
|
||||||
|
|
||||||
|
if (matchedKeyword && matchedStatus && matchedNumber) {
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCartDetail(authOpenid, cartId) {
|
||||||
|
const record = findCartRecordByBusinessId(normalizeText(cartId))
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(404, '未找到对应购物车记录')
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRecordOwner(record, 'cart_owner', authOpenid, '购物车记录')
|
||||||
|
return exportCartRecord(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCart(authState, payload) {
|
||||||
|
const productRecord = ensureProductExists(payload.cart_product_id)
|
||||||
|
const collection = $app.findCollectionByNameOrId('tbl_cart')
|
||||||
|
const record = new Record(collection)
|
||||||
|
|
||||||
|
record.set('cart_id', buildBusinessId('CART'))
|
||||||
|
record.set('cart_number', normalizeText(payload.cart_number) || buildDisplayNumber('CART', authState.authRecord, authState.openid))
|
||||||
|
record.set('cart_owner', authState.openid)
|
||||||
|
record.set('cart_product_id', productRecord.getString('prod_list_id'))
|
||||||
|
record.set('cart_product_quantity', normalizePositiveIntegerValue(payload.cart_product_quantity, 'cart_product_quantity'))
|
||||||
|
record.set('cart_status', normalizeCartStatus(payload.cart_status))
|
||||||
|
record.set('cart_at_price', normalizeNumberValue(payload.cart_at_price, 'cart_at_price'))
|
||||||
|
record.set('cart_remark', normalizeText(payload.cart_remark))
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app.save(record)
|
||||||
|
} catch (err) {
|
||||||
|
throw createAppError(400, '创建购物车记录失败', {
|
||||||
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
|
originalData: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('购物车记录创建成功', {
|
||||||
|
cart_id: record.getString('cart_id'),
|
||||||
|
cart_owner: authState.openid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return exportCartRecord(record, productRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCart(authOpenid, payload) {
|
||||||
|
const record = findCartRecordByBusinessId(normalizeText(payload.cart_id))
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(404, '未找到待修改的购物车记录')
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRecordOwner(record, 'cart_owner', authOpenid, '购物车记录')
|
||||||
|
|
||||||
|
let productRecord = null
|
||||||
|
if (typeof payload.cart_number !== 'undefined') {
|
||||||
|
record.set('cart_number', normalizeText(payload.cart_number) || record.getString('cart_number'))
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_product_id !== 'undefined') {
|
||||||
|
productRecord = ensureProductExists(payload.cart_product_id)
|
||||||
|
record.set('cart_product_id', productRecord.getString('prod_list_id'))
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_product_quantity !== 'undefined') {
|
||||||
|
record.set('cart_product_quantity', normalizePositiveIntegerValue(payload.cart_product_quantity, 'cart_product_quantity'))
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_status !== 'undefined') {
|
||||||
|
record.set('cart_status', normalizeCartStatus(payload.cart_status))
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_at_price !== 'undefined') {
|
||||||
|
record.set('cart_at_price', normalizeNumberValue(payload.cart_at_price, 'cart_at_price'))
|
||||||
|
}
|
||||||
|
if (typeof payload.cart_remark !== 'undefined') {
|
||||||
|
record.set('cart_remark', normalizeText(payload.cart_remark))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app.save(record)
|
||||||
|
} catch (err) {
|
||||||
|
throw createAppError(400, '更新购物车记录失败', {
|
||||||
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
|
originalData: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('购物车记录更新成功', {
|
||||||
|
cart_id: record.getString('cart_id'),
|
||||||
|
cart_owner: authOpenid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return exportCartRecord(record, productRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteCart(authOpenid, cartId) {
|
||||||
|
const record = findCartRecordByBusinessId(normalizeText(cartId))
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(404, '未找到待删除的购物车记录')
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRecordOwner(record, 'cart_owner', authOpenid, '购物车记录')
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app.delete(record)
|
||||||
|
} catch (err) {
|
||||||
|
throw createAppError(400, '删除购物车记录失败', {
|
||||||
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
|
originalData: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('购物车记录删除成功', {
|
||||||
|
cart_id: record.getString('cart_id'),
|
||||||
|
cart_owner: authOpenid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
cart_id: normalizeText(cartId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listOrders(authOpenid, payload) {
|
||||||
|
const records = $app.findRecordsByFilter('tbl_order', 'order_owner = {:owner}', '-order_create', 500, 0, {
|
||||||
|
owner: authOpenid,
|
||||||
|
})
|
||||||
|
const keyword = normalizeText(payload.keyword).toLowerCase()
|
||||||
|
const orderStatus = normalizeText(payload.order_status)
|
||||||
|
const orderSource = normalizeText(payload.order_source)
|
||||||
|
const result = []
|
||||||
|
|
||||||
|
for (let i = 0; i < records.length; i += 1) {
|
||||||
|
const item = exportOrderRecord(records[i])
|
||||||
|
const matchedKeyword = !keyword
|
||||||
|
|| item.order_id.toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| item.order_number.toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| item.order_source_id.toLowerCase().indexOf(keyword) !== -1
|
||||||
|
const matchedStatus = !orderStatus || item.order_status === orderStatus
|
||||||
|
const matchedSource = !orderSource || item.order_source === orderSource
|
||||||
|
|
||||||
|
if (matchedKeyword && matchedStatus && matchedSource) {
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderDetail(authOpenid, orderId) {
|
||||||
|
const record = findOrderRecordByBusinessId(normalizeText(orderId))
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(404, '未找到对应订单记录')
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRecordOwner(record, 'order_owner', authOpenid, '订单记录')
|
||||||
|
return exportOrderRecord(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOrder(authState, payload) {
|
||||||
|
const collection = $app.findCollectionByNameOrId('tbl_order')
|
||||||
|
const record = new Record(collection)
|
||||||
|
|
||||||
|
record.set('order_id', buildBusinessId('ORDER'))
|
||||||
|
record.set('order_number', normalizeText(payload.order_number) || buildDisplayNumber('ORDER', authState.authRecord, authState.openid))
|
||||||
|
record.set('order_owner', authState.openid)
|
||||||
|
record.set('order_source', normalizeOrderSource(payload.order_source))
|
||||||
|
record.set('order_status', normalizeOrderStatus(payload.order_status))
|
||||||
|
record.set('order_source_id', normalizeText(payload.order_source_id))
|
||||||
|
record.set('order_snap', normalizeJsonField(payload.order_snap, 'order_snap'))
|
||||||
|
record.set('order_amount', normalizeNumberValue(payload.order_amount, 'order_amount'))
|
||||||
|
record.set('order_remark', normalizeText(payload.order_remark))
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app.save(record)
|
||||||
|
} catch (err) {
|
||||||
|
throw createAppError(400, '创建订单失败', {
|
||||||
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
|
originalData: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('订单创建成功', {
|
||||||
|
order_id: record.getString('order_id'),
|
||||||
|
order_owner: authState.openid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return exportOrderRecord(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOrder(authOpenid, payload) {
|
||||||
|
const record = findOrderRecordByBusinessId(normalizeText(payload.order_id))
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(404, '未找到待修改的订单')
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRecordOwner(record, 'order_owner', authOpenid, '订单记录')
|
||||||
|
|
||||||
|
if (typeof payload.order_number !== 'undefined') {
|
||||||
|
record.set('order_number', normalizeText(payload.order_number) || record.getString('order_number'))
|
||||||
|
}
|
||||||
|
if (typeof payload.order_source !== 'undefined') {
|
||||||
|
const nextSource = normalizeOrderSource(payload.order_source)
|
||||||
|
if (!nextSource) {
|
||||||
|
throw createAppError(400, 'order_source 为必填项')
|
||||||
|
}
|
||||||
|
record.set('order_source', nextSource)
|
||||||
|
}
|
||||||
|
if (typeof payload.order_status !== 'undefined') {
|
||||||
|
record.set('order_status', normalizeOrderStatus(payload.order_status))
|
||||||
|
}
|
||||||
|
if (typeof payload.order_source_id !== 'undefined') {
|
||||||
|
const nextSourceId = normalizeText(payload.order_source_id)
|
||||||
|
if (!nextSourceId) {
|
||||||
|
throw createAppError(400, 'order_source_id 为必填项')
|
||||||
|
}
|
||||||
|
record.set('order_source_id', nextSourceId)
|
||||||
|
}
|
||||||
|
if (typeof payload.order_snap !== 'undefined') {
|
||||||
|
record.set('order_snap', normalizeJsonField(payload.order_snap, 'order_snap'))
|
||||||
|
}
|
||||||
|
if (typeof payload.order_amount !== 'undefined') {
|
||||||
|
record.set('order_amount', normalizeNumberValue(payload.order_amount, 'order_amount'))
|
||||||
|
}
|
||||||
|
if (typeof payload.order_remark !== 'undefined') {
|
||||||
|
record.set('order_remark', normalizeText(payload.order_remark))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app.save(record)
|
||||||
|
} catch (err) {
|
||||||
|
throw createAppError(400, '更新订单失败', {
|
||||||
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
|
originalData: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('订单更新成功', {
|
||||||
|
order_id: record.getString('order_id'),
|
||||||
|
order_owner: authOpenid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return exportOrderRecord(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOrder(authOpenid, orderId) {
|
||||||
|
const record = findOrderRecordByBusinessId(normalizeText(orderId))
|
||||||
|
if (!record) {
|
||||||
|
throw createAppError(404, '未找到待删除的订单')
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRecordOwner(record, 'order_owner', authOpenid, '订单记录')
|
||||||
|
|
||||||
|
try {
|
||||||
|
$app.delete(record)
|
||||||
|
} catch (err) {
|
||||||
|
throw createAppError(400, '删除订单失败', {
|
||||||
|
originalMessage: (err && err.message) || '未知错误',
|
||||||
|
originalData: (err && err.data) || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('订单删除成功', {
|
||||||
|
order_id: record.getString('order_id'),
|
||||||
|
order_owner: authOpenid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
order_id: normalizeText(orderId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportManageUser(userRecord, groupedCarts, groupedOrders) {
|
||||||
|
const openid = userRecord.getString('openid')
|
||||||
|
const carts = groupedCarts[openid] || []
|
||||||
|
const orders = groupedOrders[openid] || []
|
||||||
|
let cartTotalQuantity = 0
|
||||||
|
let orderTotalAmount = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < carts.length; i += 1) {
|
||||||
|
cartTotalQuantity += Number(carts[i].cart_product_quantity || 0)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < orders.length; i += 1) {
|
||||||
|
orderTotalAmount += Number(orders[i].order_amount || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pb_id: userRecord.id,
|
||||||
|
openid: openid,
|
||||||
|
users_id: userRecord.getString('users_id'),
|
||||||
|
users_name: userRecord.getString('users_name'),
|
||||||
|
users_phone: userRecord.getString('users_phone'),
|
||||||
|
users_type: userRecord.getString('users_type'),
|
||||||
|
users_idtype: userRecord.getString('users_idtype'),
|
||||||
|
company_id: userRecord.getString('company_id'),
|
||||||
|
cart_count: carts.length,
|
||||||
|
cart_total_quantity: cartTotalQuantity,
|
||||||
|
order_count: orders.length,
|
||||||
|
order_total_amount: orderTotalAmount,
|
||||||
|
carts: carts,
|
||||||
|
orders: orders,
|
||||||
|
created: String(userRecord.created || ''),
|
||||||
|
updated: String(userRecord.updated || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listManageUsersCartOrders(payload) {
|
||||||
|
const keyword = normalizeText(payload.keyword).toLowerCase()
|
||||||
|
const userRecords = $app.findRecordsByFilter('tbl_auth_users', '', '-users_id', 500, 0)
|
||||||
|
const cartRecords = $app.findRecordsByFilter('tbl_cart', '', '-cart_create', 1000, 0)
|
||||||
|
const orderRecords = $app.findRecordsByFilter('tbl_order', '', '-order_create', 1000, 0)
|
||||||
|
const groupedCarts = {}
|
||||||
|
const groupedOrders = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < cartRecords.length; i += 1) {
|
||||||
|
const owner = cartRecords[i] && typeof cartRecords[i].getString === 'function'
|
||||||
|
? cartRecords[i].getString('cart_owner')
|
||||||
|
: String(cartRecords[i] && cartRecords[i].cart_owner || '')
|
||||||
|
if (!groupedCarts[owner]) {
|
||||||
|
groupedCarts[owner] = []
|
||||||
|
}
|
||||||
|
groupedCarts[owner].push(exportAdminCartRecord(cartRecords[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < orderRecords.length; i += 1) {
|
||||||
|
const owner = orderRecords[i] && typeof orderRecords[i].getString === 'function'
|
||||||
|
? orderRecords[i].getString('order_owner')
|
||||||
|
: String(orderRecords[i] && orderRecords[i].order_owner || '')
|
||||||
|
if (!groupedOrders[owner]) {
|
||||||
|
groupedOrders[owner] = []
|
||||||
|
}
|
||||||
|
groupedOrders[owner].push(exportAdminOrderRecord(orderRecords[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < userRecords.length; i += 1) {
|
||||||
|
const item = exportAdminManageUser(userRecords[i], groupedCarts, groupedOrders)
|
||||||
|
const matchedKeyword = !keyword
|
||||||
|
|| normalizeText(item.openid).toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| normalizeText(item.users_id).toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| normalizeText(item.users_name).toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|| normalizeText(item.users_phone).toLowerCase().indexOf(keyword) !== -1
|
||||||
|
|
||||||
|
if (matchedKeyword) {
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: result,
|
||||||
|
user_level_options: userService.getUserLevelOptions(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateManageUser(payload) {
|
||||||
|
return userService.updateManagedUserProfile(payload).user
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listCarts,
|
||||||
|
getCartDetail,
|
||||||
|
createCart,
|
||||||
|
updateCart,
|
||||||
|
deleteCart,
|
||||||
|
listOrders,
|
||||||
|
getOrderDetail,
|
||||||
|
createOrder,
|
||||||
|
updateOrder,
|
||||||
|
deleteOrder,
|
||||||
|
listManageUsersCartOrders,
|
||||||
|
updateManageUser,
|
||||||
|
}
|
||||||
@@ -99,6 +99,17 @@ function exportDictionaryRecord(record) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortDictionaryItems(items) {
|
||||||
|
return (Array.isArray(items) ? items.slice() : []).sort(function (a, b) {
|
||||||
|
const sortDiff = Number(a.sortOrder || 0) - Number(b.sortOrder || 0)
|
||||||
|
if (sortDiff !== 0) {
|
||||||
|
return sortDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(a.enum || '').localeCompare(String(b.enum || ''))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function findDictionaryByName(dictName) {
|
function findDictionaryByName(dictName) {
|
||||||
const records = $app.findRecordsByFilter('tbl_system_dict', 'dict_name = {:dictName}', '', 1, 0, {
|
const records = $app.findRecordsByFilter('tbl_system_dict', 'dict_name = {:dictName}', '', 1, 0, {
|
||||||
dictName: dictName,
|
dictName: dictName,
|
||||||
@@ -157,6 +168,15 @@ function getDictionaryByName(dictName) {
|
|||||||
return exportDictionaryRecord(record)
|
return exportDictionaryRecord(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDictionaryItemsByName(dictName) {
|
||||||
|
const dictionary = getDictionaryByName(dictName)
|
||||||
|
if (!dictionary.dict_word_is_enabled) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortDictionaryItems(dictionary.items)
|
||||||
|
}
|
||||||
|
|
||||||
function createDictionary(payload) {
|
function createDictionary(payload) {
|
||||||
ensureDictionaryNameUnique(payload.dict_name)
|
ensureDictionaryNameUnique(payload.dict_name)
|
||||||
|
|
||||||
@@ -192,9 +212,9 @@ function updateDictionary(payload) {
|
|||||||
throw createAppError(404, '未找到待修改的字典')
|
throw createAppError(404, '未找到待修改的字典')
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureDictionaryNameUnique(payload.dict_name, record.id)
|
const immutableName = record.getString('dict_name')
|
||||||
|
|
||||||
record.set('dict_name', payload.dict_name)
|
record.set('dict_name', immutableName)
|
||||||
record.set('dict_word_is_enabled', payload.dict_word_is_enabled)
|
record.set('dict_word_is_enabled', payload.dict_word_is_enabled)
|
||||||
record.set('dict_word_parent_id', payload.dict_word_parent_id)
|
record.set('dict_word_parent_id', payload.dict_word_parent_id)
|
||||||
record.set('dict_word_remark', payload.dict_word_remark)
|
record.set('dict_word_remark', payload.dict_word_remark)
|
||||||
@@ -210,7 +230,7 @@ function updateDictionary(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info('字典修改成功', {
|
logger.info('字典修改成功', {
|
||||||
dict_name: payload.dict_name,
|
dict_name: immutableName,
|
||||||
original_dict_name: payload.original_dict_name,
|
original_dict_name: payload.original_dict_name,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -245,6 +265,7 @@ function deleteDictionary(dictName) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
listDictionaries,
|
listDictionaries,
|
||||||
getDictionaryByName,
|
getDictionaryByName,
|
||||||
|
getDictionaryItemsByName,
|
||||||
createDictionary,
|
createDictionary,
|
||||||
updateDictionary,
|
updateDictionary,
|
||||||
deleteDictionary,
|
deleteDictionary,
|
||||||
|
|||||||
@@ -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,24 +467,31 @@ function deleteAttachment(attachmentId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listDocuments(payload) {
|
function listDocuments(payload) {
|
||||||
const allRecords = $app.findRecordsByFilter('tbl_document', '', '', 500, 0)
|
const allRecords = fetchAllRecordsByFilter('tbl_document', '', '')
|
||||||
const keyword = String(payload.keyword || '').toLowerCase()
|
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 || '')
|
||||||
const result = []
|
const result = []
|
||||||
|
|
||||||
for (let i = 0; i < allRecords.length; i += 1) {
|
for (let i = 0; i < allRecords.length; i += 1) {
|
||||||
const item = exportDocumentRecord(allRecords[i])
|
const item = exportDocumentRecord(allRecords[i])
|
||||||
const matchedKeyword = !keyword
|
const matchedTitleKeyword = !titleKeyword
|
||||||
|| item.document_id.toLowerCase().indexOf(keyword) !== -1
|
|| item.document_title.toLowerCase().indexOf(titleKeyword) !== -1
|
||||||
|| item.document_title.toLowerCase().indexOf(keyword) !== -1
|
|
||||||
|| item.document_subtitle.toLowerCase().indexOf(keyword) !== -1
|
|
||||||
|| item.document_summary.toLowerCase().indexOf(keyword) !== -1
|
|
||||||
|| item.document_keywords.toLowerCase().indexOf(keyword) !== -1
|
|
||||||
const matchedStatus = !status || item.document_status === status
|
const matchedStatus = !status || item.document_status === status
|
||||||
const matchedType = !type || item.document_type === type
|
const matchedType = !type || String(item.document_type || '')
|
||||||
|
.split('|')
|
||||||
|
.map(function (token) { return String(token || '').trim() })
|
||||||
|
.some(function (token) {
|
||||||
|
if (!token) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (token === type) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return token.indexOf(type + '@') === 0
|
||||||
|
})
|
||||||
|
|
||||||
if (matchedKeyword && matchedStatus && matchedType) {
|
if (matchedTitleKeyword && matchedStatus && matchedType) {
|
||||||
result.push(item)
|
result.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,65 @@
|
|||||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||||
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||||
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
||||||
|
const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`)
|
||||||
|
|
||||||
|
const USER_LEVEL_DICT_NAME = '数据-会员等级'
|
||||||
|
|
||||||
function buildBusinessId(prefix) {
|
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
|
||||||
@@ -37,6 +87,16 @@ function normalizeSortValue(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizePipeValues(value) {
|
function normalizePipeValues(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map(function (item) {
|
||||||
|
return normalizeText(item)
|
||||||
|
})
|
||||||
|
.filter(function (item) {
|
||||||
|
return !!item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.split('|')
|
.split('|')
|
||||||
.map(function (item) {
|
.map(function (item) {
|
||||||
@@ -70,6 +130,64 @@ function normalizeRequiredCategory(value) {
|
|||||||
return values[0]
|
return values[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePositiveSort(rawSort, fieldName, fallbackSort) {
|
||||||
|
if (rawSort === '' || rawSort === null || typeof rawSort === 'undefined') {
|
||||||
|
return fallbackSort
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(rawSort)
|
||||||
|
if (!Number.isFinite(num) || num <= 0 || Math.floor(num) !== num) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须为正整数')
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePositiveSortForOutput(rawSort, fallbackSort) {
|
||||||
|
if (rawSort === '' || rawSort === null || typeof rawSort === 'undefined') {
|
||||||
|
return fallbackSort
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(rawSort)
|
||||||
|
if (!Number.isFinite(num) || num <= 0 || Math.floor(num) !== num) {
|
||||||
|
return fallbackSort
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortParameterRows(rows) {
|
||||||
|
return rows
|
||||||
|
.slice()
|
||||||
|
.sort(function (a, b) {
|
||||||
|
const sortDiff = Number(a.sort || 0) - Number(b.sort || 0)
|
||||||
|
if (sortDiff !== 0) {
|
||||||
|
return sortDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(a.__inputIndex || 0) - Number(b.__inputIndex || 0)
|
||||||
|
})
|
||||||
|
.map(function (item) {
|
||||||
|
return {
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
sort: Number(item.sort || 0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeObjectKeys(source) {
|
||||||
|
if (!source || typeof source !== 'object') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Object.keys(source)
|
||||||
|
} catch (_error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildCategoryRankMap(records) {
|
function buildCategoryRankMap(records) {
|
||||||
const grouped = {}
|
const grouped = {}
|
||||||
for (let i = 0; i < records.length; i += 1) {
|
for (let i = 0; i < records.length; i += 1) {
|
||||||
@@ -118,16 +236,18 @@ function normalizeParameters(value) {
|
|||||||
const result = []
|
const result = []
|
||||||
const indexByName = {}
|
const indexByName = {}
|
||||||
|
|
||||||
function upsert(nameValue, rawValue) {
|
function upsert(nameValue, rawValue, rawSort, fallbackSort, inputIndex) {
|
||||||
const name = normalizeText(nameValue)
|
const name = normalizeText(nameValue)
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
|
const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
|
||||||
|
const normalizedSort = normalizePositiveSort(rawSort, 'prod_list_parameters.sort', fallbackSort)
|
||||||
const existingIndex = indexByName[name]
|
const existingIndex = indexByName[name]
|
||||||
if (typeof existingIndex === 'number') {
|
if (typeof existingIndex === 'number') {
|
||||||
result[existingIndex].value = normalizedValue
|
result[existingIndex].value = normalizedValue
|
||||||
|
result[existingIndex].sort = normalizedSort
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +255,8 @@ function normalizeParameters(value) {
|
|||||||
result.push({
|
result.push({
|
||||||
name: name,
|
name: name,
|
||||||
value: normalizedValue,
|
value: normalizedValue,
|
||||||
|
sort: normalizedSort,
|
||||||
|
__inputIndex: inputIndex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,21 +266,21 @@ function normalizeParameters(value) {
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
upsert(item.name || item.key, item.value)
|
upsert(item.name || item.key, item.value, item.sort, i + 1, i + 1)
|
||||||
}
|
}
|
||||||
return result
|
return sortParameterRows(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value !== 'object') {
|
if (typeof value !== 'object') {
|
||||||
throw createAppError(400, 'prod_list_parameters 必须是数组或对象')
|
throw createAppError(400, 'prod_list_parameters 必须是数组或对象')
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(value)
|
const keys = safeObjectKeys(value)
|
||||||
for (let i = 0; i < keys.length; i += 1) {
|
for (let i = 0; i < keys.length; i += 1) {
|
||||||
upsert(keys[i], value[keys[i]])
|
upsert(keys[i], value[keys[i]], '', i + 1, i + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return sortParameterRows(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeParametersForOutput(value) {
|
function normalizeParametersForOutput(value) {
|
||||||
@@ -166,10 +288,37 @@ function normalizeParametersForOutput(value) {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pushOrUpdate(targetRows, indexByName, nameValue, rawValue, rawSort, fallbackSort, inputIndex) {
|
||||||
|
const name = normalizeText(nameValue)
|
||||||
|
if (!name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue)
|
||||||
|
const normalizedSort = normalizePositiveSortForOutput(rawSort, fallbackSort)
|
||||||
|
const existingIndex = indexByName[name]
|
||||||
|
if (typeof existingIndex === 'number') {
|
||||||
|
targetRows[existingIndex].value = normalizedValue
|
||||||
|
targetRows[existingIndex].sort = normalizedSort
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
indexByName[name] = targetRows.length
|
||||||
|
targetRows.push({
|
||||||
|
name: name,
|
||||||
|
value: normalizedValue,
|
||||||
|
sort: normalizedSort,
|
||||||
|
__inputIndex: inputIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let source = value
|
let source = value
|
||||||
if (typeof source === 'string') {
|
if (typeof source === 'string') {
|
||||||
try {
|
try {
|
||||||
source = JSON.parse(source)
|
source = JSON.parse(source)
|
||||||
|
if (source === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
const raw = normalizeText(source)
|
const raw = normalizeText(source)
|
||||||
if (raw.indexOf('map[') === 0 && raw.endsWith(']')) {
|
if (raw.indexOf('map[') === 0 && raw.endsWith(']')) {
|
||||||
@@ -191,16 +340,9 @@ function normalizeParametersForOutput(value) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const val = pair.slice(separatorIndex + 1)
|
const val = pair.slice(separatorIndex + 1)
|
||||||
const normalizedValue = val === null || typeof val === 'undefined' ? '' : String(val)
|
pushOrUpdate(result, indexByName, key, val, '', i + 1, i + 1)
|
||||||
const existingIndex = indexByName[key]
|
|
||||||
if (typeof existingIndex === 'number') {
|
|
||||||
result[existingIndex].value = normalizedValue
|
|
||||||
} else {
|
|
||||||
indexByName[key] = result.length
|
|
||||||
result.push({ name: key, value: normalizedValue })
|
|
||||||
}
|
}
|
||||||
}
|
return sortParameterRows(result)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -214,47 +356,165 @@ function normalizeParametersForOutput(value) {
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const name = normalizeText(item.name || item.key)
|
pushOrUpdate(mapped, indexByName, item.name || item.key, item.value, item.sort, i + 1, i + 1)
|
||||||
if (!name) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
const normalizedValue = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value)
|
return sortParameterRows(mapped)
|
||||||
const existingIndex = indexByName[name]
|
|
||||||
if (typeof existingIndex === 'number') {
|
|
||||||
mapped[existingIndex].value = normalizedValue
|
|
||||||
} else {
|
|
||||||
indexByName[name] = mapped.length
|
|
||||||
mapped.push({ name: name, value: normalizedValue })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mapped
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof source !== 'object') {
|
if (typeof source !== 'object') {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
// Some PocketBase/Goja map-like values are not directly enumerable; roundtrip to plain object.
|
// Some PocketBase/Goja map-like values are not directly enumerable; roundtrip to plain object.
|
||||||
try {
|
try {
|
||||||
source = JSON.parse(JSON.stringify(source))
|
source = JSON.parse(JSON.stringify(source))
|
||||||
} catch (_error) {}
|
} catch (_error) {}
|
||||||
|
|
||||||
|
if (source === null || typeof source !== 'object') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const result = []
|
const result = []
|
||||||
const indexByName = {}
|
const indexByName = {}
|
||||||
const keys = Object.keys(source)
|
const keys = safeObjectKeys(source)
|
||||||
for (let i = 0; i < keys.length; i += 1) {
|
for (let i = 0; i < keys.length; i += 1) {
|
||||||
const name = normalizeText(keys[i])
|
pushOrUpdate(result, indexByName, keys[i], source[keys[i]], '', i + 1, i + 1)
|
||||||
if (!name) {
|
}
|
||||||
|
|
||||||
|
return sortParameterRows(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVipLevelValueMap() {
|
||||||
|
let items = []
|
||||||
|
try {
|
||||||
|
items = dictionaryService.getDictionaryItemsByName(USER_LEVEL_DICT_NAME)
|
||||||
|
} catch (_error) {
|
||||||
|
items = []
|
||||||
|
}
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
const key = normalizeText(items[i].enum)
|
||||||
|
if (!key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const current = source[keys[i]]
|
result[key] = {
|
||||||
const normalizedValue = current === null || typeof current === 'undefined' ? '' : String(current)
|
value: key,
|
||||||
const existingIndex = indexByName[name]
|
label: String(items[i].description || ''),
|
||||||
if (typeof existingIndex === 'number') {
|
}
|
||||||
result[existingIndex].value = normalizedValue
|
}
|
||||||
} else {
|
|
||||||
indexByName[name] = result.length
|
return result
|
||||||
result.push({ name: name, value: normalizedValue })
|
}
|
||||||
|
|
||||||
|
function normalizeVipPriceRows(value) {
|
||||||
|
if (value === null || typeof value === 'undefined' || value === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = value
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
try {
|
||||||
|
source = JSON.parse(source)
|
||||||
|
} catch (_error) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 必须为合法 JSON 数组')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(source)) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 必须为数组')
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelMap = getVipLevelValueMap()
|
||||||
|
const result = []
|
||||||
|
const duplicatedLevelMap = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < source.length; i += 1) {
|
||||||
|
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
|
||||||
|
if (!item) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const viplevel = normalizeText(item.viplevel)
|
||||||
|
if (!viplevel) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 viplevel 为必填项')
|
||||||
|
}
|
||||||
|
if (!levelMap[viplevel]) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 viplevel 不在“数据-会员等级”字典中:' + viplevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = Number(item.price)
|
||||||
|
if (!Number.isFinite(price)) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 第 ' + (i + 1) + ' 项 price 必须为数字')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicatedLevelMap[viplevel]) {
|
||||||
|
throw createAppError(400, 'prod_list_vip_price 中 viplevel 不允许重复:' + viplevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicatedLevelMap[viplevel] = true
|
||||||
|
result.push({
|
||||||
|
viplevel: viplevel,
|
||||||
|
price: price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVipPriceRowsForOutput(value) {
|
||||||
|
if (value === null || typeof value === 'undefined' || value === '') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = value
|
||||||
|
if (typeof source === 'string') {
|
||||||
|
try {
|
||||||
|
source = JSON.parse(source)
|
||||||
|
} catch (_error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(source)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelMap = getVipLevelValueMap()
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < source.length; i += 1) {
|
||||||
|
const item = source[i] && typeof source[i] === 'object' ? source[i] : null
|
||||||
|
if (!item) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const viplevel = normalizeText(item.viplevel)
|
||||||
|
const price = Number(item.price)
|
||||||
|
if (!viplevel || !Number.isFinite(price)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
viplevel: viplevel,
|
||||||
|
viplevel_name: levelMap[viplevel] ? levelMap[viplevel].label : '',
|
||||||
|
price: price,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAttachmentIdList(value) {
|
||||||
|
const result = []
|
||||||
|
const items = normalizePipeValues(value)
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
if (result.indexOf(items[i]) === -1) {
|
||||||
|
result.push(items[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,15 +522,17 @@ function normalizeParametersForOutput(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureAttachmentExists(attachmentId, fieldName) {
|
function ensureAttachmentExists(attachmentId, fieldName) {
|
||||||
const value = normalizeText(attachmentId)
|
const values = normalizeAttachmentIdList(attachmentId)
|
||||||
if (!value) {
|
if (!values.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i += 1) {
|
||||||
try {
|
try {
|
||||||
documentService.getAttachmentDetail(value)
|
documentService.getAttachmentDetail(values[i])
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
throw createAppError(400, fieldName + ' 对应附件不存在:' + value)
|
throw createAppError(400, fieldName + ' 对应附件不存在:' + values[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,34 +545,51 @@ function findProductRecordByBusinessId(productId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportProductRecord(record, extra) {
|
function exportProductRecord(record, extra) {
|
||||||
const iconId = record.getString('prod_list_icon')
|
const iconIds = normalizeAttachmentIdList(record.getString('prod_list_icon'))
|
||||||
let iconAttachment = null
|
const iconAttachments = []
|
||||||
|
const iconUrls = []
|
||||||
|
|
||||||
if (iconId) {
|
for (let i = 0; i < iconIds.length; i += 1) {
|
||||||
try {
|
try {
|
||||||
iconAttachment = documentService.getAttachmentDetail(iconId)
|
const attachment = documentService.getAttachmentDetail(iconIds[i])
|
||||||
|
iconAttachments.push(attachment)
|
||||||
|
iconUrls.push(attachment.attachments_url || '')
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
iconAttachment = null
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const firstIconAttachment = iconAttachments.length ? iconAttachments[0] : null
|
||||||
|
|
||||||
const parametersText = normalizeText(record.getString('prod_list_parameters'))
|
const parametersText = normalizeText(record.getString('prod_list_parameters'))
|
||||||
const parametersFromText = parametersText ? normalizeParametersForOutput(parametersText) : {}
|
const parametersFromText = parametersText ? normalizeParametersForOutput(parametersText) : []
|
||||||
const parametersRaw = record.get('prod_list_parameters')
|
const parametersRaw = record.get('prod_list_parameters')
|
||||||
const parametersFromRaw = normalizeParametersForOutput(parametersRaw)
|
const parametersFromRaw = normalizeParametersForOutput(parametersRaw)
|
||||||
const parameters = parametersFromText.length ? parametersFromText : parametersFromRaw
|
const parameters = parametersFromText.length ? parametersFromText : parametersFromRaw
|
||||||
|
const functionText = normalizeText(record.getString('prod_list_function'))
|
||||||
|
const functionFromText = functionText ? normalizeParametersForOutput(functionText) : []
|
||||||
|
const functionRaw = record.get('prod_list_function')
|
||||||
|
const functionFromRaw = normalizeParametersForOutput(functionRaw)
|
||||||
|
const functions = functionFromText.length ? functionFromText : functionFromRaw
|
||||||
|
const vipPriceText = normalizeText(record.getString('prod_list_vip_price'))
|
||||||
|
const vipPriceFromText = vipPriceText ? normalizeVipPriceRowsForOutput(vipPriceText) : []
|
||||||
|
const vipPriceFromRaw = normalizeVipPriceRowsForOutput(record.get('prod_list_vip_price'))
|
||||||
|
const vipPrice = vipPriceFromText.length ? vipPriceFromText : vipPriceFromRaw
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pb_id: record.id,
|
pb_id: record.id,
|
||||||
prod_list_id: record.getString('prod_list_id'),
|
prod_list_id: record.getString('prod_list_id'),
|
||||||
prod_list_name: record.getString('prod_list_name'),
|
prod_list_name: record.getString('prod_list_name'),
|
||||||
prod_list_modelnumber: record.getString('prod_list_modelnumber'),
|
prod_list_modelnumber: record.getString('prod_list_modelnumber'),
|
||||||
prod_list_icon: iconId,
|
prod_list_icon: iconIds.join('|'),
|
||||||
prod_list_icon_attachment: iconAttachment,
|
prod_list_icon_ids: iconIds,
|
||||||
prod_list_icon_url: iconAttachment ? iconAttachment.attachments_url : '',
|
prod_list_icon_attachments: iconAttachments,
|
||||||
|
prod_list_icon_urls: iconUrls,
|
||||||
|
prod_list_icon_attachment: firstIconAttachment,
|
||||||
|
prod_list_icon_url: firstIconAttachment ? firstIconAttachment.attachments_url : '',
|
||||||
prod_list_description: record.getString('prod_list_description'),
|
prod_list_description: record.getString('prod_list_description'),
|
||||||
prod_list_feature: record.getString('prod_list_feature'),
|
prod_list_feature: record.getString('prod_list_feature'),
|
||||||
prod_list_parameters: parameters,
|
prod_list_parameters: parameters,
|
||||||
|
prod_list_function: functions,
|
||||||
prod_list_plantype: record.getString('prod_list_plantype'),
|
prod_list_plantype: record.getString('prod_list_plantype'),
|
||||||
prod_list_category: record.getString('prod_list_category'),
|
prod_list_category: record.getString('prod_list_category'),
|
||||||
prod_list_sort: Number(record.get('prod_list_sort') || 0),
|
prod_list_sort: Number(record.get('prod_list_sort') || 0),
|
||||||
@@ -320,6 +599,7 @@ function exportProductRecord(record, extra) {
|
|||||||
prod_list_power_supply: record.getString('prod_list_power_supply'),
|
prod_list_power_supply: record.getString('prod_list_power_supply'),
|
||||||
prod_list_tags: record.getString('prod_list_tags'),
|
prod_list_tags: record.getString('prod_list_tags'),
|
||||||
prod_list_basic_price: record.get('prod_list_basic_price'),
|
prod_list_basic_price: record.get('prod_list_basic_price'),
|
||||||
|
prod_list_vip_price: vipPrice,
|
||||||
prod_list_status: record.getString('prod_list_status'),
|
prod_list_status: record.getString('prod_list_status'),
|
||||||
prod_list_remark: record.getString('prod_list_remark'),
|
prod_list_remark: record.getString('prod_list_remark'),
|
||||||
created: String(record.created || ''),
|
created: String(record.created || ''),
|
||||||
@@ -328,7 +608,8 @@ function exportProductRecord(record, extra) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listProducts(payload) {
|
function listProducts(payload) {
|
||||||
const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0)
|
try {
|
||||||
|
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)
|
||||||
@@ -336,7 +617,15 @@ function listProducts(payload) {
|
|||||||
|
|
||||||
const allItems = []
|
const allItems = []
|
||||||
for (let i = 0; i < allRecords.length; i += 1) {
|
for (let i = 0; i < allRecords.length; i += 1) {
|
||||||
|
try {
|
||||||
allItems.push(exportProductRecord(allRecords[i]))
|
allItems.push(exportProductRecord(allRecords[i]))
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('产品记录导出失败,已跳过', {
|
||||||
|
pb_id: allRecords[i] && allRecords[i].id ? allRecords[i].id : '',
|
||||||
|
prod_list_id: allRecords[i] ? allRecords[i].getString('prod_list_id') : '',
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rankMap = buildCategoryRankMap(allItems)
|
const rankMap = buildCategoryRankMap(allItems)
|
||||||
@@ -364,6 +653,12 @@ function listProducts(payload) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('产品列表构建失败,返回空列表兜底', {
|
||||||
|
errMsg: (err && err.message) || '未知错误',
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductDetail(productId) {
|
function getProductDetail(productId) {
|
||||||
@@ -372,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]))
|
||||||
@@ -396,13 +691,15 @@ 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))
|
||||||
record.set('prod_list_icon', normalizeText(payload.prod_list_icon))
|
record.set('prod_list_icon', joinUniquePipeValues(payload.prod_list_icon))
|
||||||
record.set('prod_list_description', normalizeText(payload.prod_list_description))
|
record.set('prod_list_description', normalizeText(payload.prod_list_description))
|
||||||
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
|
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
|
||||||
record.set('prod_list_parameters', normalizeParameters(payload.prod_list_parameters))
|
record.set('prod_list_parameters', normalizeParameters(payload.prod_list_parameters))
|
||||||
|
record.set('prod_list_function', normalizeParameters(payload.prod_list_function))
|
||||||
record.set('prod_list_plantype', normalizeText(payload.prod_list_plantype))
|
record.set('prod_list_plantype', normalizeText(payload.prod_list_plantype))
|
||||||
record.set('prod_list_category', normalizeRequiredCategory(payload.prod_list_category))
|
record.set('prod_list_category', normalizeRequiredCategory(payload.prod_list_category))
|
||||||
record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort))
|
record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort))
|
||||||
@@ -412,6 +709,7 @@ function createProduct(_userOpenid, payload) {
|
|||||||
record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags))
|
record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags))
|
||||||
record.set('prod_list_status', normalizeText(payload.prod_list_status))
|
record.set('prod_list_status', normalizeText(payload.prod_list_status))
|
||||||
record.set('prod_list_basic_price', normalizeOptionalNumberValue(payload.prod_list_basic_price, 'prod_list_basic_price'))
|
record.set('prod_list_basic_price', normalizeOptionalNumberValue(payload.prod_list_basic_price, 'prod_list_basic_price'))
|
||||||
|
record.set('prod_list_vip_price', normalizeVipPriceRows(payload.prod_list_vip_price))
|
||||||
record.set('prod_list_remark', normalizeText(payload.prod_list_remark))
|
record.set('prod_list_remark', normalizeText(payload.prod_list_remark))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -448,10 +746,11 @@ function updateProduct(_userOpenid, payload) {
|
|||||||
|
|
||||||
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))
|
||||||
record.set('prod_list_icon', normalizeText(payload.prod_list_icon))
|
record.set('prod_list_icon', joinUniquePipeValues(payload.prod_list_icon))
|
||||||
record.set('prod_list_description', normalizeText(payload.prod_list_description))
|
record.set('prod_list_description', normalizeText(payload.prod_list_description))
|
||||||
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
|
record.set('prod_list_feature', normalizeText(payload.prod_list_feature))
|
||||||
record.set('prod_list_parameters', normalizeParameters(payload.prod_list_parameters))
|
record.set('prod_list_parameters', normalizeParameters(payload.prod_list_parameters))
|
||||||
|
record.set('prod_list_function', normalizeParameters(payload.prod_list_function))
|
||||||
record.set('prod_list_plantype', normalizeText(payload.prod_list_plantype))
|
record.set('prod_list_plantype', normalizeText(payload.prod_list_plantype))
|
||||||
record.set('prod_list_category', normalizeRequiredCategory(payload.prod_list_category))
|
record.set('prod_list_category', normalizeRequiredCategory(payload.prod_list_category))
|
||||||
record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort))
|
record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort))
|
||||||
@@ -461,6 +760,7 @@ function updateProduct(_userOpenid, payload) {
|
|||||||
record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags))
|
record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags))
|
||||||
record.set('prod_list_status', normalizeText(payload.prod_list_status))
|
record.set('prod_list_status', normalizeText(payload.prod_list_status))
|
||||||
record.set('prod_list_basic_price', normalizeOptionalNumberValue(payload.prod_list_basic_price, 'prod_list_basic_price'))
|
record.set('prod_list_basic_price', normalizeOptionalNumberValue(payload.prod_list_basic_price, 'prod_list_basic_price'))
|
||||||
|
record.set('prod_list_vip_price', normalizeVipPriceRows(payload.prod_list_vip_price))
|
||||||
record.set('prod_list_remark', normalizeText(payload.prod_list_remark))
|
record.set('prod_list_remark', normalizeText(payload.prod_list_remark))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function uniqueList(values) {
|
|||||||
|
|
||||||
function listRoles() {
|
function listRoles() {
|
||||||
const records = $app.findRecordsByFilter(ROLE_COLLECTION, '', 'role_name', 500, 0)
|
const records = $app.findRecordsByFilter(ROLE_COLLECTION, '', 'role_name', 500, 0)
|
||||||
|
|
||||||
return records.map(function (record) {
|
return records.map(function (record) {
|
||||||
return {
|
return {
|
||||||
pb_id: record.id,
|
pb_id: record.id,
|
||||||
@@ -353,7 +354,7 @@ function saveCollectionRules(payload) {
|
|||||||
|
|
||||||
function listUsers(keyword, roleMap) {
|
function listUsers(keyword, roleMap) {
|
||||||
const search = normalizeText(keyword).toLowerCase()
|
const search = normalizeText(keyword).toLowerCase()
|
||||||
const records = $app.findRecordsByFilter(USER_COLLECTION, '', '', 500, 0)
|
const records = $app.findRecordsByFilter(USER_COLLECTION, '', '-users_id', 500, 0)
|
||||||
const items = records.map(function (record) {
|
const items = records.map(function (record) {
|
||||||
const usergroupsId = record.getString('usergroups_id')
|
const usergroupsId = record.getString('usergroups_id')
|
||||||
const role = roleMap && roleMap[usergroupsId] ? roleMap[usergroupsId] : null
|
const role = roleMap && roleMap[usergroupsId] ? roleMap[usergroupsId] : null
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.
|
|||||||
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`)
|
||||||
const wechatService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/wechatService.js`)
|
const wechatService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/wechatService.js`)
|
||||||
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
const documentService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/documentService.js`)
|
||||||
|
const dictionaryService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js`)
|
||||||
const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`)
|
const env = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/config/env.js`)
|
||||||
|
|
||||||
const GUEST_USER_TYPE = '游客'
|
const GUEST_USER_TYPE = '游客'
|
||||||
const REGISTERED_USER_TYPE = '注册用户'
|
const REGISTERED_USER_TYPE = '注册用户'
|
||||||
const WECHAT_ID_TYPE = 'WeChat'
|
const WECHAT_ID_TYPE = 'WeChat'
|
||||||
const MANAGE_PLATFORM_ID_TYPE = 'ManagePlatform'
|
const MANAGE_PLATFORM_ID_TYPE = 'ManagePlatform'
|
||||||
|
const USER_LEVEL_DICT_NAME = '数据-会员等级'
|
||||||
const mutationLocks = {}
|
const mutationLocks = {}
|
||||||
|
|
||||||
function buildUserId() {
|
function buildUserId() {
|
||||||
@@ -42,6 +44,74 @@ function isAllProfileFieldsEmpty(record) {
|
|||||||
return !record.getString('users_name') && !record.getString('users_phone') && !record.getString('users_picture')
|
return !record.getString('users_name') && !record.getString('users_phone') && !record.getString('users_picture')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value || '').replace(/^\s+|\s+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptionalNumber(value, fieldName) {
|
||||||
|
const raw = normalizeText(value)
|
||||||
|
if (!raw) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = Number(raw)
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
throw createAppError(400, fieldName + ' 必须为数字')
|
||||||
|
}
|
||||||
|
|
||||||
|
return num
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserLevelItems() {
|
||||||
|
try {
|
||||||
|
return dictionaryService.getDictionaryItemsByName(USER_LEVEL_DICT_NAME)
|
||||||
|
} catch (_error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserLevelOptions() {
|
||||||
|
return getUserLevelItems().map(function (item) {
|
||||||
|
return {
|
||||||
|
value: String(item.enum || ''),
|
||||||
|
label: String(item.description || ''),
|
||||||
|
sort: Number(item.sortOrder || 0),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUserLevelName(usersLevel) {
|
||||||
|
const level = normalizeText(usersLevel)
|
||||||
|
if (!level) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = getUserLevelItems()
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
if (normalizeText(items[i].enum) === level) {
|
||||||
|
return String(items[i].description || '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidUserLevel(usersLevel) {
|
||||||
|
const level = normalizeText(usersLevel)
|
||||||
|
if (!level) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = getUserLevelItems()
|
||||||
|
for (let i = 0; i < items.length; i += 1) {
|
||||||
|
if (normalizeText(items[i].enum) === level) {
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createAppError(400, 'users_level 不在“数据-会员等级”字典中')
|
||||||
|
}
|
||||||
|
|
||||||
function withUserLock(lockKey, handler) {
|
function withUserLock(lockKey, handler) {
|
||||||
if (mutationLocks[lockKey]) {
|
if (mutationLocks[lockKey]) {
|
||||||
throw createAppError(429, '请求过于频繁,请稍后重试')
|
throw createAppError(429, '请求过于频繁,请稍后重试')
|
||||||
@@ -275,6 +345,7 @@ function enrichUser(userRecord) {
|
|||||||
users_phone: userRecord.getString('users_phone'),
|
users_phone: userRecord.getString('users_phone'),
|
||||||
users_phone_masked: maskPhone(userRecord.getString('users_phone')),
|
users_phone_masked: maskPhone(userRecord.getString('users_phone')),
|
||||||
users_level: userRecord.getString('users_level'),
|
users_level: userRecord.getString('users_level'),
|
||||||
|
users_level_name: resolveUserLevelName(userRecord.getString('users_level')),
|
||||||
users_tag: userRecord.getString('users_tag'),
|
users_tag: userRecord.getString('users_tag'),
|
||||||
users_picture: userPicture.id,
|
users_picture: userPicture.id,
|
||||||
users_picture_url: userPicture.url,
|
users_picture_url: userPicture.url,
|
||||||
@@ -438,7 +509,6 @@ function registerPlatformUser(payload) {
|
|||||||
record.set('users_name', payload.users_name)
|
record.set('users_name', payload.users_name)
|
||||||
record.set('users_phone', payload.users_phone)
|
record.set('users_phone', payload.users_phone)
|
||||||
record.set('users_id_number', payload.users_id_number || '')
|
record.set('users_id_number', payload.users_id_number || '')
|
||||||
record.set('users_level', payload.users_level || '')
|
|
||||||
record.set('users_type', payload.users_type || GUEST_USER_TYPE)
|
record.set('users_type', payload.users_type || GUEST_USER_TYPE)
|
||||||
record.set('users_tag', payload.users_tag || '')
|
record.set('users_tag', payload.users_tag || '')
|
||||||
record.set('company_id', payload.company_id || '')
|
record.set('company_id', payload.company_id || '')
|
||||||
@@ -637,6 +707,99 @@ function updateWechatUserProfile(usersWxOpenid, payload) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateManagedUserProfile(payload) {
|
||||||
|
return withUserLock('manage-user-update:' + payload.openid, function () {
|
||||||
|
const currentUser = findUserByOpenid(payload.openid)
|
||||||
|
if (!currentUser) {
|
||||||
|
throw createAppError(404, '未找到待编辑的用户')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPhone = Object.prototype.hasOwnProperty.call(payload, 'users_phone')
|
||||||
|
? normalizeText(payload.users_phone)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (typeof nextPhone !== 'undefined' && nextPhone && nextPhone !== currentUser.getString('users_phone')) {
|
||||||
|
const samePhoneUsers = $app.findRecordsByFilter('tbl_auth_users', 'users_phone = {:phone}', '', 10, 0, {
|
||||||
|
phone: nextPhone,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < samePhoneUsers.length; i += 1) {
|
||||||
|
if (samePhoneUsers[i].id !== currentUser.id) {
|
||||||
|
throw createAppError(400, '手机号已被注册')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_name')) {
|
||||||
|
currentUser.set('users_name', normalizeText(payload.users_name))
|
||||||
|
}
|
||||||
|
if (typeof nextPhone !== 'undefined') {
|
||||||
|
currentUser.set('users_phone', nextPhone)
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_id_number')) {
|
||||||
|
currentUser.set('users_id_number', normalizeText(payload.users_id_number))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_level')) {
|
||||||
|
currentUser.set('users_level', ensureValidUserLevel(payload.users_level))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_type')) {
|
||||||
|
currentUser.set('users_type', normalizeText(payload.users_type))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_tag')) {
|
||||||
|
currentUser.set('users_tag', normalizeText(payload.users_tag))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'company_id')) {
|
||||||
|
currentUser.set('company_id', normalizeText(payload.company_id))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_parent_id')) {
|
||||||
|
currentUser.set('users_parent_id', normalizeText(payload.users_parent_id))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_promo_code')) {
|
||||||
|
currentUser.set('users_promo_code', normalizeText(payload.users_promo_code))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'usergroups_id')) {
|
||||||
|
currentUser.set('usergroups_id', normalizeText(payload.usergroups_id))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_status')) {
|
||||||
|
currentUser.set('users_status', normalizeText(payload.users_status))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_rank_level')) {
|
||||||
|
currentUser.set('users_rank_level', normalizeOptionalNumber(payload.users_rank_level, 'users_rank_level'))
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_auth_type')) {
|
||||||
|
currentUser.set('users_auth_type', normalizeOptionalNumber(payload.users_auth_type, 'users_auth_type'))
|
||||||
|
}
|
||||||
|
|
||||||
|
applyUserAttachmentFields(currentUser, payload)
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_picture') && !normalizeText(payload.users_picture)) {
|
||||||
|
currentUser.set('users_picture', '')
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_a') && !normalizeText(payload.users_id_pic_a)) {
|
||||||
|
currentUser.set('users_id_pic_a', '')
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_id_pic_b') && !normalizeText(payload.users_id_pic_b)) {
|
||||||
|
currentUser.set('users_id_pic_b', '')
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, 'users_title_picture') && !normalizeText(payload.users_title_picture)) {
|
||||||
|
currentUser.set('users_title_picture', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAuthUserRecord(currentUser)
|
||||||
|
|
||||||
|
const user = enrichUser(currentUser)
|
||||||
|
|
||||||
|
logger.info('管理端更新用户资料成功', {
|
||||||
|
users_id: user.users_id,
|
||||||
|
openid: user.openid,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 'update_success',
|
||||||
|
user: user,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function refreshAuthToken(openid) {
|
function refreshAuthToken(openid) {
|
||||||
const userRecord = findUserByOpenid(openid)
|
const userRecord = findUserByOpenid(openid)
|
||||||
if (!userRecord) {
|
if (!userRecord) {
|
||||||
@@ -690,8 +853,11 @@ module.exports = {
|
|||||||
authenticatePlatformUser,
|
authenticatePlatformUser,
|
||||||
ensureAuthToken,
|
ensureAuthToken,
|
||||||
updateWechatUserProfile,
|
updateWechatUserProfile,
|
||||||
|
updateManagedUserProfile,
|
||||||
refreshAuthToken,
|
refreshAuthToken,
|
||||||
issueAuthToken,
|
issueAuthToken,
|
||||||
registerPlatformUser,
|
registerPlatformUser,
|
||||||
|
resolveUserLevelName,
|
||||||
|
getUserLevelOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ function successWithToken(e, msg, data, token, code) {
|
|||||||
|
|
||||||
function fail(e, msg, data, code) {
|
function fail(e, msg, data, code) {
|
||||||
const meta = applyHttpMeta(e, code || 400, msg || '操作失败')
|
const meta = applyHttpMeta(e, code || 400, msg || '操作失败')
|
||||||
return e.json(meta.statusCode, normalizePayloadData(data))
|
const payload = normalizePayloadData(data)
|
||||||
|
payload.statusCode = meta.statusCode
|
||||||
|
payload.errMsg = meta.errMsg
|
||||||
|
payload.message = meta.errMsg
|
||||||
|
return e.json(meta.statusCode, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ routerAdd("POST", "/pb-api-v1/chat/send", (c) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"message": userMsg,
|
"message": userMsg,
|
||||||
"mode": "chat",
|
"mode": "chat", // chat: 对话模式,query: 问答模式, agent: 代理模式
|
||||||
"sessionId": "8888"
|
"sessionId": "8888"
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
Alpine.data('weChat', () => ({
|
Alpine.data('weChat', () => ({
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'ai', content: '您好!我是您的专属助理小慧。有什么可以帮到您?' }
|
{ role: 'ai', content: '您好!我是您的专属助理小宝。有什么可以帮到您?' }
|
||||||
],
|
],
|
||||||
inputMsg: '',
|
inputMsg: '',
|
||||||
isTyping: false,
|
isTyping: false,
|
||||||
|
|||||||
9
pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js
Normal file
9
pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
routerAdd('GET', '/manage/cart-order-manage', function (e) {
|
||||||
|
const html = $template.loadFiles(
|
||||||
|
__hooks + '/bai_web_pb_hooks/views/cart-order-manage.html',
|
||||||
|
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||||
|
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||||
|
).render({})
|
||||||
|
|
||||||
|
return e.html(200, html)
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,94 +1,9 @@
|
|||||||
routerAdd('GET', '/manage', function (e) {
|
routerAdd('GET', '/manage', function (e) {
|
||||||
const html = `<!DOCTYPE html>
|
const html = $template.loadFiles(
|
||||||
<html lang="zh-CN">
|
__hooks + '/bai_web_pb_hooks/views/index.html',
|
||||||
<head>
|
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||||
<meta charset="UTF-8" />
|
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
).render({})
|
||||||
<title>管理主页</title>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var token = localStorage.getItem('pb_manage_token') || ''
|
|
||||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
|
||||||
if (!token || !isLoggedIn) {
|
|
||||||
window.location.replace('/pb/manage/login')
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
|
||||||
.wrap { max-width: 1440px; margin: 0 auto; padding: 24px 14px; }
|
|
||||||
.hero { background: #ffffff; border-radius: 20px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 22px; border: 1px solid #e5e7eb; }
|
|
||||||
h1 { margin: 0 0 14px; font-size: 30px; }
|
|
||||||
.module + .module { margin-top: 18px; }
|
|
||||||
.module-title { margin: 0 0 10px; font-size: 22px; color: #0f172a; }
|
|
||||||
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
|
|
||||||
.card { background: #f8fafc; border: 1px solid #dbe3f0; border-radius: 16px; padding: 16px; text-align: center; }
|
|
||||||
.card h2 { margin: 0 0 8px; font-size: 19px; }
|
|
||||||
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px; border-radius: 12px; text-decoration: none; background: #2563eb; color: #fff; font-weight: 600; margin-top: 12px; }
|
|
||||||
.actions { margin-top: 14px; display: flex; justify-content: flex-start; }
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
.grid { grid-template-columns: 1fr; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="wrap">
|
|
||||||
<section class="hero">
|
|
||||||
<h1>管理主页</h1>
|
|
||||||
<section class="module">
|
|
||||||
<h2 class="module-title">平台管理</h2>
|
|
||||||
<div class="grid">
|
|
||||||
<article class="card">
|
|
||||||
<h2>字典管理</h2>
|
|
||||||
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>文档管理</h2>
|
|
||||||
<a class="btn" href="/pb/manage/document-manage">进入文档管理</a>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>产品管理</h2>
|
|
||||||
<a class="btn" href="/pb/manage/product-manage">进入产品管理</a>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>SDK 权限管理</h2>
|
|
||||||
<a class="btn" href="/pb/manage/sdk-permission-manage">进入权限管理</a>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="module">
|
|
||||||
<h2 class="module-title">AI 管理</h2>
|
|
||||||
<div class="grid">
|
|
||||||
<article class="card">
|
|
||||||
<h2>AI 审计管理</h2>
|
|
||||||
<a class="btn" href="/pb/bai-ai-manage">进入审计管理</a>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>AI 聊天测试</h2>
|
|
||||||
<a class="btn" href="/pb/bai-chat">进入聊天测试</a>
|
|
||||||
</article>
|
|
||||||
<article class="card">
|
|
||||||
<h2>SQL 实验室</h2>
|
|
||||||
<a class="btn" href="/pb/bai-ai-sql-lab">进入 SQL 实验室</a>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div class="actions">
|
|
||||||
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
|
||||||
localStorage.removeItem('pb_manage_token')
|
|
||||||
localStorage.removeItem('pb_manage_logged_in')
|
|
||||||
localStorage.removeItem('pb_manage_login_account')
|
|
||||||
localStorage.removeItem('pb_manage_login_time')
|
|
||||||
window.location.replace('/pb/manage/login')
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
return e.html(200, html)
|
return e.html(200, html)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,157 +1,9 @@
|
|||||||
function renderLoginPage(e) {
|
function renderLoginPage(e) {
|
||||||
const html = `<!DOCTYPE html>
|
const html = $template.loadFiles(
|
||||||
<html lang="zh-CN">
|
__hooks + '/bai_web_pb_hooks/views/login.html',
|
||||||
<head>
|
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||||
<meta charset="UTF-8" />
|
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
).render({})
|
||||||
<title>登录</title>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var token = localStorage.getItem('pb_manage_token') || ''
|
|
||||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
|
||||||
if (token && isLoggedIn) {
|
|
||||||
window.location.replace('/pb/manage')
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
.wrap {
|
|
||||||
max-width: 1440px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 34px 14px;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
max-width: 420px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #dbe3f0;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 22px;
|
|
||||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
h1 { margin-top: 0; }
|
|
||||||
p { line-height: 1.8; color: #4b5563; margin-bottom: 20px; }
|
|
||||||
.field { margin-bottom: 14px; }
|
|
||||||
.field label { display: block; margin-bottom: 8px; color: #334155; font-weight: 600; }
|
|
||||||
.field input {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 11px 14px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
background: #2563eb;
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.status { margin-top: 12px; min-height: 22px; font-size: 14px; }
|
|
||||||
.status.error { color: #b91c1c; }
|
|
||||||
.status.success { color: #15803d; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="wrap">
|
|
||||||
<section class="card">
|
|
||||||
<h1>后台登录</h1>
|
|
||||||
<p>请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。</p>
|
|
||||||
<form id="loginForm">
|
|
||||||
<div class="field">
|
|
||||||
<label for="account">登录账号</label>
|
|
||||||
<input id="account" name="account" placeholder="邮箱或手机号" required />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="password">密码</label>
|
|
||||||
<input id="password" name="password" type="password" placeholder="请输入密码" required />
|
|
||||||
</div>
|
|
||||||
<button class="btn" id="submitBtn" type="submit">登录</button>
|
|
||||||
</form>
|
|
||||||
<div id="status" class="status"></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var API_BASE = '/pb/api'
|
|
||||||
var form = document.getElementById('loginForm')
|
|
||||||
var accountInput = document.getElementById('account')
|
|
||||||
var passwordInput = document.getElementById('password')
|
|
||||||
var statusEl = document.getElementById('status')
|
|
||||||
var submitBtn = document.getElementById('submitBtn')
|
|
||||||
|
|
||||||
function setStatus(message, type) {
|
|
||||||
statusEl.textContent = message || ''
|
|
||||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
form.addEventListener('submit', async function (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
var loginAccount = accountInput.value.trim()
|
|
||||||
var password = passwordInput.value
|
|
||||||
if (!loginAccount || !password) {
|
|
||||||
setStatus('请填写账号和密码', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitBtn.disabled = true
|
|
||||||
submitBtn.textContent = '登录中...'
|
|
||||||
setStatus('正在登录,请稍候...', '')
|
|
||||||
|
|
||||||
try {
|
|
||||||
var response = await fetch(API_BASE + '/platform/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
login_account: loginAccount,
|
|
||||||
password: password,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
var result = await response.json()
|
|
||||||
if (!response.ok || !result || result.code >= 400) {
|
|
||||||
throw new Error((result && result.msg) || '登录失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = result.token || (result.data && result.data.token) || ''
|
|
||||||
if (!token) {
|
|
||||||
throw new Error('登录成功但未获取到 token')
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('pb_manage_token', token)
|
|
||||||
localStorage.setItem('pb_manage_logged_in', '1')
|
|
||||||
localStorage.setItem('pb_manage_login_account', loginAccount)
|
|
||||||
localStorage.setItem('pb_manage_login_time', new Date().toISOString())
|
|
||||||
|
|
||||||
setStatus('登录成功,正在跳转...', 'success')
|
|
||||||
window.location.replace('/pb/manage')
|
|
||||||
} catch (error) {
|
|
||||||
localStorage.removeItem('pb_manage_token')
|
|
||||||
localStorage.removeItem('pb_manage_logged_in')
|
|
||||||
setStatus(error.message || '登录失败', 'error')
|
|
||||||
} finally {
|
|
||||||
submitBtn.disabled = false
|
|
||||||
submitBtn.textContent = '登录'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
return e.html(200, html)
|
return e.html(200, html)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,745 +1,9 @@
|
|||||||
routerAdd('GET', '/manage/sdk-permission-manage', function (e) {
|
routerAdd('GET', '/manage/sdk-permission-manage', function (e) {
|
||||||
const html = `<!DOCTYPE html>
|
const html = $template.loadFiles(
|
||||||
<html lang="zh-CN">
|
__hooks + '/bai_web_pb_hooks/views/sdk-permission-manage.html',
|
||||||
<head>
|
__hooks + '/bai_web_pb_hooks/shared/theme-head.html',
|
||||||
<meta charset="UTF-8" />
|
__hooks + '/bai_web_pb_hooks/shared/theme-body.html'
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
).render({})
|
||||||
<title>SDK 权限管理</title>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var token = localStorage.getItem('pb_manage_token') || ''
|
|
||||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
|
||||||
if (!token || !isLoggedIn) {
|
|
||||||
window.location.replace('/pb/manage/login')
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef6ff 0%, #f8fafc 100%); color: #0f172a; }
|
|
||||||
.wrap { max-width: 1440px; margin: 0 auto; padding: 24px 14px 40px; }
|
|
||||||
.panel { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 20px; padding: 18px; }
|
|
||||||
.panel + .panel { margin-top: 14px; }
|
|
||||||
h1, h2, h3 { margin-top: 0; }
|
|
||||||
p { color: #475569; line-height: 1.7; }
|
|
||||||
.actions, .toolbar, .row-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
|
||||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 700; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
|
||||||
.btn-primary { background: #2563eb; color: #fff; }
|
|
||||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
|
||||||
.btn-danger { background: #dc2626; color: #fff; }
|
|
||||||
.btn-warning { background: #f59e0b; color: #fff; }
|
|
||||||
.btn-success { background: #16a34a; color: #fff; }
|
|
||||||
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
|
||||||
.status.success { color: #15803d; }
|
|
||||||
.status.error { color: #b91c1c; }
|
|
||||||
.note { padding: 14px 16px; border-radius: 16px; background: #eff6ff; color: #1d4ed8; font-size: 14px; line-height: 1.7; }
|
|
||||||
table { width: 100%; border-collapse: collapse; }
|
|
||||||
thead { background: #eff6ff; }
|
|
||||||
th, td { padding: 14px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
|
||||||
th { font-size: 13px; color: #475569; }
|
|
||||||
tr:hover td { background: #f8fafc; }
|
|
||||||
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 9px 11px; font-size: 14px; background: #fff; }
|
|
||||||
textarea { min-height: 80px; resize: vertical; }
|
|
||||||
.empty { text-align: center; padding: 24px 16px; color: #64748b; }
|
|
||||||
.muted { color: #64748b; font-size: 12px; }
|
|
||||||
.grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
|
|
||||||
.full { grid-column: 1 / -1; }
|
|
||||||
.rule-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
|
|
||||||
.rule-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 12px; background: #f8fbff; }
|
|
||||||
.rule-card h4 { margin: 0 0 10px; font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
|
||||||
.rule-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; color: #475569; font-size: 12px; }
|
|
||||||
.rule-meta input[type="checkbox"] { width: auto; margin: 0; }
|
|
||||||
.rule-toggle { display: inline-flex; align-items: center; gap: 6px; color: #0f172a; font-size: 13px; font-weight: 600; }
|
|
||||||
.rule-toggle input[type="checkbox"] { width: auto; margin: 0; }
|
|
||||||
.split { display: grid; grid-template-columns: 1.15fr 1fr; gap: 14px; }
|
|
||||||
.table-wrap { overflow: auto; }
|
|
||||||
.collection-table { table-layout: fixed; }
|
|
||||||
.collection-col { width: 264px; }
|
|
||||||
.rule-col { width: calc(100% - 264px); }
|
|
||||||
.collection-meta { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
|
|
||||||
.loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(4px); z-index: 9998; }
|
|
||||||
.loading-mask.show { display: flex; }
|
|
||||||
.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: sdkSpin 0.9s linear infinite; }
|
|
||||||
.loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
|
|
||||||
@keyframes sdkSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.split, .rule-grid, .grid { grid-template-columns: 1fr; }
|
|
||||||
.collection-col, .rule-col { width: auto; }
|
|
||||||
table, thead, tbody, th, td, tr { display: block; }
|
|
||||||
thead { display: none; }
|
|
||||||
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
|
||||||
td { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="wrap">
|
|
||||||
<section class="panel">
|
|
||||||
<h1>SDK 权限管理</h1>
|
|
||||||
<p>这里管理的是 <code>tbl_auth_users</code> 用户通过 PocketBase SDK 直连数据库时的业务权限。<strong>ManagePlatform</strong> 会被视为你的业务管理员,但不会自动变成 PocketBase 原生 <code>_superusers</code>。</p>
|
|
||||||
<div class="actions">
|
|
||||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
|
||||||
<button class="btn btn-light" id="refreshBtn" type="button">刷新数据</button>
|
|
||||||
<button class="btn btn-success" id="syncManageBtn" type="button">同步 ManagePlatform 全权限</button>
|
|
||||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
|
||||||
</div>
|
|
||||||
<div class="status" id="status"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="note" id="noteBox">加载中...</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2>角色管理</h2>
|
|
||||||
<div class="grid">
|
|
||||||
<input id="newRoleName" placeholder="角色名称" />
|
|
||||||
<input id="newRoleCode" placeholder="角色编码,可为空" />
|
|
||||||
<input id="newRoleStatus" type="number" placeholder="状态,默认1" value="1" />
|
|
||||||
<button class="btn btn-primary" id="createRoleBtn" type="button">新增角色</button>
|
|
||||||
<div class="muted">角色 ID 由系统自动生成,页面不显示。</div>
|
|
||||||
<textarea id="newRoleRemark" class="full" placeholder="备注"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap" style="margin-top:16px;">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>名称</th>
|
|
||||||
<th>编码</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>备注</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="roleTableBody">
|
|
||||||
<tr><td colspan="5" class="empty">暂无角色。</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2>用户授权</h2>
|
|
||||||
<div class="toolbar">
|
|
||||||
<input id="userKeywordInput" placeholder="按姓名、手机号、openid、角色搜索" />
|
|
||||||
<button class="btn btn-light" id="searchUserBtn" type="button">查询用户</button>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap" style="margin-top:16px;">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>用户</th>
|
|
||||||
<th>身份类型</th>
|
|
||||||
<th>当前角色</th>
|
|
||||||
<th>授权</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="userTableBody">
|
|
||||||
<tr><td colspan="5" class="empty">暂无用户。</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2>Collection 直连权限</h2>
|
|
||||||
<div class="toolbar">
|
|
||||||
<select id="permissionRoleSelect"></select>
|
|
||||||
<div class="muted">这里是按“当前配置角色”逐表分配 CRUD 权限。下面依次对应“列表、详情、新增、修改、删除”五种权限,勾选后会立即保存。公开访问或自定义规则的操作不会显示授权勾选框。</div>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap" style="margin-top:16px;">
|
|
||||||
<table class="collection-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="collection-col">集合</th>
|
|
||||||
<th class="rule-col">当前角色权限</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="collectionTableBody">
|
|
||||||
<tr><td colspan="2" class="empty">暂无集合。</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<div class="loading-mask" id="loadingMask">
|
|
||||||
<div class="loading-card">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<div class="loading-text" id="loadingText">处理中,请稍候...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API_BASE = '/pb/api/sdk-permission'
|
|
||||||
const tokenKey = 'pb_manage_token'
|
|
||||||
const statusEl = document.getElementById('status')
|
|
||||||
const noteBox = document.getElementById('noteBox')
|
|
||||||
const roleTableBody = document.getElementById('roleTableBody')
|
|
||||||
const userTableBody = document.getElementById('userTableBody')
|
|
||||||
const collectionTableBody = document.getElementById('collectionTableBody')
|
|
||||||
const permissionRoleSelect = document.getElementById('permissionRoleSelect')
|
|
||||||
const loadingMask = document.getElementById('loadingMask')
|
|
||||||
const loadingText = document.getElementById('loadingText')
|
|
||||||
const loadingState = { count: 0 }
|
|
||||||
const state = {
|
|
||||||
roles: [],
|
|
||||||
users: [],
|
|
||||||
collections: [],
|
|
||||||
userKeyword: '',
|
|
||||||
selectedPermissionRoleId: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(message, type) {
|
|
||||||
statusEl.textContent = message || ''
|
|
||||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoading(message) {
|
|
||||||
loadingState.count += 1
|
|
||||||
loadingText.textContent = message || '处理中,请稍候...'
|
|
||||||
loadingMask.classList.add('show')
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideLoading() {
|
|
||||||
loadingState.count = Math.max(0, loadingState.count - 1)
|
|
||||||
if (!loadingState.count) {
|
|
||||||
loadingMask.classList.remove('show')
|
|
||||||
loadingText.textContent = '处理中,请稍候...'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getToken() {
|
|
||||||
return localStorage.getItem(tokenKey) || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value) {
|
|
||||||
return String(value || '')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoleById(roleId) {
|
|
||||||
const target = String(roleId || '')
|
|
||||||
return state.roles.find(function (role) {
|
|
||||||
return role.role_id === target
|
|
||||||
}) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoleName(roleId) {
|
|
||||||
const role = getRoleById(roleId)
|
|
||||||
return role ? role.role_name : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncSelectedPermissionRole() {
|
|
||||||
const exists = state.roles.some(function (role) {
|
|
||||||
return role.role_id === state.selectedPermissionRoleId
|
|
||||||
})
|
|
||||||
if (!exists) {
|
|
||||||
state.selectedPermissionRoleId = state.roles.length ? state.roles[0].role_id : ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeErrMsg(value) {
|
|
||||||
if (!value) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(value)
|
|
||||||
} catch (_err) {
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapApiResponse(data, res) {
|
|
||||||
const safe = data && typeof data === 'object' ? data : {}
|
|
||||||
const headerCode = Number(res && res.headers ? (res.headers.get('X-Status-Code') || 0) : 0)
|
|
||||||
const headerErrMsg = decodeErrMsg(res && res.headers ? res.headers.get('X-Err-Msg') : '')
|
|
||||||
const code = Number(safe.statusCode || safe.code || headerCode || 0)
|
|
||||||
const errMsg = safe.errMsg || safe.msg || headerErrMsg || ''
|
|
||||||
const hasLegacyEnvelope = Object.prototype.hasOwnProperty.call(safe, 'data')
|
|
||||||
&& (Object.prototype.hasOwnProperty.call(safe, 'code')
|
|
||||||
|| Object.prototype.hasOwnProperty.call(safe, 'statusCode')
|
|
||||||
|| Object.prototype.hasOwnProperty.call(safe, 'msg')
|
|
||||||
|| Object.prototype.hasOwnProperty.call(safe, 'errMsg'))
|
|
||||||
const payload = hasLegacyEnvelope ? (safe.data || {}) : safe
|
|
||||||
return { code: code, errMsg: errMsg, payload: payload }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestJson(path, payload) {
|
|
||||||
const token = getToken()
|
|
||||||
if (!token) {
|
|
||||||
localStorage.removeItem('pb_manage_logged_in')
|
|
||||||
window.location.replace('/pb/manage/login')
|
|
||||||
throw new Error('登录状态已失效,请重新登录')
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(API_BASE + path, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: 'Bearer ' + token,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload || {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
const unwrapped = unwrapApiResponse(data, res)
|
|
||||||
if (!res.ok || !data || unwrapped.code >= 400) {
|
|
||||||
if (res.status === 401 || res.status === 403 || unwrapped.code === 401 || unwrapped.code === 403) {
|
|
||||||
localStorage.removeItem('pb_manage_token')
|
|
||||||
localStorage.removeItem('pb_manage_logged_in')
|
|
||||||
localStorage.removeItem('pb_manage_login_account')
|
|
||||||
localStorage.removeItem('pb_manage_login_time')
|
|
||||||
window.location.replace('/pb/manage/login')
|
|
||||||
}
|
|
||||||
throw new Error(unwrapped.errMsg || '请求失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
return unwrapped.payload
|
|
||||||
}
|
|
||||||
|
|
||||||
function roleOptionsHtml(selectedRoleId) {
|
|
||||||
const current = String(selectedRoleId || '')
|
|
||||||
return ['<option value="">未分配</option>'].concat(state.roles.map(function (role) {
|
|
||||||
return '<option value="' + escapeHtml(role.role_id) + '"' + (current === role.role_id ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
|
||||||
})).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRoles() {
|
|
||||||
if (!state.roles.length) {
|
|
||||||
roleTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无角色。</td></tr>'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
roleTableBody.innerHTML = state.roles.map(function (role) {
|
|
||||||
return '<tr data-role-id="' + escapeHtml(role.role_id) + '">'
|
|
||||||
+ '<td><input data-role-field="role_name" value="' + escapeHtml(role.role_name) + '" /></td>'
|
|
||||||
+ '<td><input data-role-field="role_code" value="' + escapeHtml(role.role_code) + '" /></td>'
|
|
||||||
+ '<td><input data-role-field="role_status" type="number" value="' + escapeHtml(role.role_status) + '" /></td>'
|
|
||||||
+ '<td><textarea data-role-field="role_remark">' + escapeHtml(role.role_remark) + '</textarea></td>'
|
|
||||||
+ '<td><div class="row-actions"><button class="btn btn-light" type="button" onclick="window.__saveRoleRow(\\'' + encodeURIComponent(role.role_id) + '\\')">保存</button><button class="btn btn-danger" type="button" onclick="window.__deleteRoleRow(\\'' + encodeURIComponent(role.role_id) + '\\')">删除</button></div></td>'
|
|
||||||
+ '</tr>'
|
|
||||||
}).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderUsers() {
|
|
||||||
if (!state.users.length) {
|
|
||||||
userTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无匹配用户。</td></tr>'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userTableBody.innerHTML = state.users.map(function (user) {
|
|
||||||
const name = user.users_name || '未命名用户'
|
|
||||||
const phone = user.users_phone || '无手机号'
|
|
||||||
const roleName = user.role_name || getRoleName(user.usergroups_id) || '未分配'
|
|
||||||
return '<tr data-user-id="' + escapeHtml(user.pb_id) + '">'
|
|
||||||
+ '<td><div><strong>' + escapeHtml(name) + '</strong></div><div class="muted">' + escapeHtml(phone) + '</div><div class="muted">' + escapeHtml(user.openid) + '</div></td>'
|
|
||||||
+ '<td><div>' + escapeHtml(user.users_idtype || '') + '</div><div class="muted">' + escapeHtml(user.users_type || '') + '</div></td>'
|
|
||||||
+ '<td>' + escapeHtml(roleName) + '</td>'
|
|
||||||
+ '<td><select data-user-role-select="1">' + roleOptionsHtml(user.usergroups_id) + '</select></td>'
|
|
||||||
+ '<td><button class="btn btn-light" type="button" onclick="window.__saveUserRole(\\'' + escapeHtml(user.pb_id) + '\\')">保存角色</button></td>'
|
|
||||||
+ '</tr>'
|
|
||||||
}).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPermissionRoleOptions() {
|
|
||||||
if (!state.roles.length) {
|
|
||||||
permissionRoleSelect.innerHTML = '<option value="">暂无角色</option>'
|
|
||||||
permissionRoleSelect.disabled = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
permissionRoleSelect.disabled = false
|
|
||||||
permissionRoleSelect.innerHTML = state.roles.map(function (role) {
|
|
||||||
return '<option value="' + escapeHtml(role.role_id) + '"' + (role.role_id === state.selectedPermissionRoleId ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
|
||||||
}).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOperationLabel(operation) {
|
|
||||||
const map = {
|
|
||||||
list: '列表',
|
|
||||||
view: '详情',
|
|
||||||
create: '新增',
|
|
||||||
update: '修改',
|
|
||||||
delete: '删除',
|
|
||||||
}
|
|
||||||
return map[operation] || operation
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRuleSummary(config, selectedRoleId) {
|
|
||||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
|
||||||
const items = []
|
|
||||||
if (current.mode === 'public') items.push('公开可访问')
|
|
||||||
if (current.mode === 'authenticated') items.push('登录用户可访问')
|
|
||||||
if (current.includeManagePlatform) items.push('含 ManagePlatform')
|
|
||||||
if (current.mode === 'custom') items.push('自定义规则')
|
|
||||||
if (Array.isArray(current.roles) && current.roles.length) {
|
|
||||||
items.push('已分配角色数:' + current.roles.length)
|
|
||||||
}
|
|
||||||
return items.length ? items.join(',') : '当前无额外说明'
|
|
||||||
}
|
|
||||||
|
|
||||||
function canControlRule(config) {
|
|
||||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
|
||||||
return !!state.selectedPermissionRoleId && current.mode !== 'custom' && current.mode !== 'public'
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCollectionFullyChecked(collection) {
|
|
||||||
if (!collection || !collection.parsedRules) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const operations = ['list', 'view', 'create', 'update', 'delete']
|
|
||||||
let controllableCount = 0
|
|
||||||
let checkedCount = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < operations.length; i += 1) {
|
|
||||||
const config = collection.parsedRules[operations[i]]
|
|
||||||
if (!canControlRule(config)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
controllableCount += 1
|
|
||||||
if (config && Array.isArray(config.roles) && config.roles.indexOf(state.selectedPermissionRoleId) !== -1) {
|
|
||||||
checkedCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return controllableCount > 0 && controllableCount === checkedCount
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCollectionControllableCount(collection) {
|
|
||||||
if (!collection || !collection.parsedRules) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const operations = ['list', 'view', 'create', 'update', 'delete']
|
|
||||||
let controllableCount = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < operations.length; i += 1) {
|
|
||||||
if (canControlRule(collection.parsedRules[operations[i]])) {
|
|
||||||
controllableCount += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return controllableCount
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRuleCard(collectionName, operation, config) {
|
|
||||||
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
|
||||||
const selectedRoleId = state.selectedPermissionRoleId
|
|
||||||
const checked = selectedRoleId && Array.isArray(current.roles) && current.roles.indexOf(selectedRoleId) !== -1
|
|
||||||
const canControl = canControlRule(current)
|
|
||||||
const summary = current.mode === 'public'
|
|
||||||
? '可公开访问'
|
|
||||||
: getRuleSummary(current, selectedRoleId)
|
|
||||||
return '<div class="rule-card" data-collection="' + escapeHtml(collectionName) + '" data-op="' + operation + '">'
|
|
||||||
+ '<h4>' + (canControl ? '<label class="rule-toggle"><input type="checkbox" data-rule-field="allowSelectedRole"' + (checked ? ' checked' : '') + ' /></label>' : '') + '<span>' + getOperationLabel(operation) + '</span></h4>'
|
|
||||||
+ '<div class="muted" style="margin-top:8px;">' + escapeHtml(summary) + '</div>'
|
|
||||||
+ (current.mode === 'custom' ? '<div class="muted" style="margin-top:8px;">当前操作使用 custom 规则,禁止修改</div>' : '')
|
|
||||||
+ '</div>'
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCollections() {
|
|
||||||
if (!state.collections.length) {
|
|
||||||
collectionTableBody.innerHTML = '<tr><td colspan="2" class="empty">暂无可管理集合。</td></tr>'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionTableBody.innerHTML = state.collections.map(function (collection) {
|
|
||||||
const allChecked = isCollectionFullyChecked(collection)
|
|
||||||
const controllableCount = getCollectionControllableCount(collection)
|
|
||||||
return '<tr data-collection-row="' + escapeHtml(collection.name) + '">'
|
|
||||||
+ '<td class="collection-col"><div class="collection-meta"><div><strong>' + escapeHtml(collection.name) + '</strong></div><div class="muted">' + escapeHtml(collection.type) + '</div><label class="rule-toggle"><input type="checkbox" data-rule-field="toggleCollection"' + (allChecked ? ' checked' : '') + (state.selectedPermissionRoleId && controllableCount > 0 ? '' : ' disabled') + ' /><span>全选</span></label></div></td>'
|
|
||||||
+ '<td><div class="rule-grid">'
|
|
||||||
+ renderRuleCard(collection.name, 'list', collection.parsedRules.list)
|
|
||||||
+ renderRuleCard(collection.name, 'view', collection.parsedRules.view)
|
|
||||||
+ renderRuleCard(collection.name, 'create', collection.parsedRules.create)
|
|
||||||
+ renderRuleCard(collection.name, 'update', collection.parsedRules.update)
|
|
||||||
+ renderRuleCard(collection.name, 'delete', collection.parsedRules.delete)
|
|
||||||
+ '</div></td>'
|
|
||||||
+ '</tr>'
|
|
||||||
}).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadContext() {
|
|
||||||
showLoading('正在加载权限管理数据...')
|
|
||||||
setStatus('正在加载权限管理数据...', '')
|
|
||||||
try {
|
|
||||||
const data = await requestJson('/context', { keyword: state.userKeyword })
|
|
||||||
state.roles = Array.isArray(data.roles) ? data.roles : []
|
|
||||||
state.users = Array.isArray(data.users) ? data.users : []
|
|
||||||
state.collections = Array.isArray(data.collections) ? data.collections : []
|
|
||||||
syncSelectedPermissionRole()
|
|
||||||
noteBox.textContent = data.note || '权限管理说明已加载。'
|
|
||||||
renderRoles()
|
|
||||||
renderUsers()
|
|
||||||
renderPermissionRoleOptions()
|
|
||||||
renderCollections()
|
|
||||||
setStatus('权限管理数据已刷新。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '加载权限管理数据失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoleRowPayload(roleId) {
|
|
||||||
const targetId = decodeURIComponent(roleId)
|
|
||||||
const row = roleTableBody.querySelector('[data-role-id="' + targetId.replace(/"/g, '\\"') + '"]')
|
|
||||||
if (!row) {
|
|
||||||
throw new Error('未找到对应角色行')
|
|
||||||
}
|
|
||||||
|
|
||||||
const find = function (fieldName) {
|
|
||||||
const input = row.querySelector('[data-role-field="' + fieldName + '"]')
|
|
||||||
return input ? input.value : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
original_role_id: targetId,
|
|
||||||
role_name: find('role_name'),
|
|
||||||
role_code: find('role_code'),
|
|
||||||
role_status: find('role_status'),
|
|
||||||
role_remark: find('role_remark'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRuleBoxConfig(collectionName, operation) {
|
|
||||||
const collection = state.collections.find(function (item) {
|
|
||||||
return item.name === collectionName
|
|
||||||
})
|
|
||||||
const current = collection && collection.parsedRules ? collection.parsedRules[operation] : null
|
|
||||||
const base = current || {
|
|
||||||
mode: 'locked',
|
|
||||||
includeManagePlatform: false,
|
|
||||||
roles: [],
|
|
||||||
rawExpression: '',
|
|
||||||
}
|
|
||||||
if (base.mode === 'custom') {
|
|
||||||
return {
|
|
||||||
mode: 'custom',
|
|
||||||
includeManagePlatform: !!base.includeManagePlatform,
|
|
||||||
roles: Array.isArray(base.roles) ? base.roles.slice() : [],
|
|
||||||
rawExpression: base.rawExpression || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const box = collectionTableBody.querySelector('.rule-card[data-collection="' + collectionName.replace(/"/g, '\\"') + '"][data-op="' + operation + '"]')
|
|
||||||
const allowEl = box ? box.querySelector('[data-rule-field="allowSelectedRole"]') : null
|
|
||||||
const roles = Array.isArray(base.roles) ? base.roles.slice() : []
|
|
||||||
const selectedRoleId = state.selectedPermissionRoleId
|
|
||||||
const nextRoles = roles.filter(function (roleId) {
|
|
||||||
return roleId !== selectedRoleId
|
|
||||||
})
|
|
||||||
|
|
||||||
if (selectedRoleId && allowEl && allowEl.checked && nextRoles.indexOf(selectedRoleId) === -1) {
|
|
||||||
nextRoles.push(selectedRoleId)
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextMode = base.mode
|
|
||||||
if (nextMode === 'locked' && nextRoles.length) {
|
|
||||||
nextMode = 'roleBased'
|
|
||||||
} else if (nextMode === 'roleBased' && !nextRoles.length && !base.includeManagePlatform) {
|
|
||||||
nextMode = 'locked'
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mode: nextMode,
|
|
||||||
includeManagePlatform: !!base.includeManagePlatform,
|
|
||||||
roles: nextRoles,
|
|
||||||
rawExpression: base.rawExpression || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createRole() {
|
|
||||||
const payload = {
|
|
||||||
role_name: document.getElementById('newRoleName').value.trim(),
|
|
||||||
role_code: document.getElementById('newRoleCode').value.trim(),
|
|
||||||
role_status: document.getElementById('newRoleStatus').value.trim() || '1',
|
|
||||||
role_remark: document.getElementById('newRoleRemark').value.trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('正在新增角色...')
|
|
||||||
try {
|
|
||||||
await requestJson('/role-save', payload)
|
|
||||||
document.getElementById('newRoleName').value = ''
|
|
||||||
document.getElementById('newRoleCode').value = ''
|
|
||||||
document.getElementById('newRoleStatus').value = '1'
|
|
||||||
document.getElementById('newRoleRemark').value = ''
|
|
||||||
await loadContext()
|
|
||||||
setStatus('角色新增成功。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '新增角色失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveRoleRow(roleId) {
|
|
||||||
showLoading('正在保存角色...')
|
|
||||||
try {
|
|
||||||
await requestJson('/role-save', getRoleRowPayload(roleId))
|
|
||||||
await loadContext()
|
|
||||||
setStatus('角色保存成功。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '保存角色失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteRoleRow(roleId) {
|
|
||||||
const targetId = decodeURIComponent(roleId)
|
|
||||||
if (!window.confirm('确认删除角色「' + targetId + '」吗?这会清空绑定该角色的用户,并从已解析的集合规则中移除它。')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('正在删除角色...')
|
|
||||||
try {
|
|
||||||
await requestJson('/role-delete', { role_id: targetId })
|
|
||||||
await loadContext()
|
|
||||||
setStatus('角色删除成功。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '删除角色失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveUserRole(pbId) {
|
|
||||||
const row = userTableBody.querySelector('[data-user-id="' + pbId.replace(/"/g, '\\"') + '"]')
|
|
||||||
const select = row ? row.querySelector('[data-user-role-select="1"]') : null
|
|
||||||
const payload = {
|
|
||||||
pb_id: pbId,
|
|
||||||
usergroups_id: select ? select.value : '',
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('正在保存用户角色...')
|
|
||||||
try {
|
|
||||||
await requestJson('/user-role-update', payload)
|
|
||||||
await loadContext()
|
|
||||||
setStatus('用户角色已更新。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '更新用户角色失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveCollectionRules(collectionName) {
|
|
||||||
const targetName = decodeURIComponent(collectionName)
|
|
||||||
if (!state.selectedPermissionRoleId) {
|
|
||||||
setStatus('请先选择一个要配置权限的角色。', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const payload = {
|
|
||||||
collection_name: targetName,
|
|
||||||
rules: {
|
|
||||||
list: getRuleBoxConfig(targetName, 'list'),
|
|
||||||
view: getRuleBoxConfig(targetName, 'view'),
|
|
||||||
create: getRuleBoxConfig(targetName, 'create'),
|
|
||||||
update: getRuleBoxConfig(targetName, 'update'),
|
|
||||||
delete: getRuleBoxConfig(targetName, 'delete'),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('正在同步集合权限...')
|
|
||||||
try {
|
|
||||||
await requestJson('/collection-save', payload)
|
|
||||||
await loadContext()
|
|
||||||
setStatus('已保存角色「' + (getRoleName(state.selectedPermissionRoleId) || '未命名角色') + '」在集合「' + targetName + '」上的权限。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '保存集合权限失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncManagePlatform() {
|
|
||||||
if (!window.confirm('确认将 ManagePlatform 同步为所有业务集合的全权限吗?这不会创建 _superusers,但会为业务表开放全部 CRUD。')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading('正在同步 ManagePlatform 全权限...')
|
|
||||||
try {
|
|
||||||
const data = await requestJson('/manageplatform-sync', {})
|
|
||||||
await loadContext()
|
|
||||||
setStatus('已同步 ManagePlatform 全权限,共处理 ' + String((data && data.count) || 0) + ' 个集合。', 'success')
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(err.message || '同步 ManagePlatform 全权限失败', 'error')
|
|
||||||
} finally {
|
|
||||||
hideLoading()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.__saveRoleRow = saveRoleRow
|
|
||||||
window.__deleteRoleRow = deleteRoleRow
|
|
||||||
window.__saveUserRole = saveUserRole
|
|
||||||
window.__saveCollectionRules = saveCollectionRules
|
|
||||||
|
|
||||||
document.getElementById('createRoleBtn').addEventListener('click', createRole)
|
|
||||||
document.getElementById('refreshBtn').addEventListener('click', loadContext)
|
|
||||||
document.getElementById('syncManageBtn').addEventListener('click', syncManagePlatform)
|
|
||||||
permissionRoleSelect.addEventListener('change', function () {
|
|
||||||
state.selectedPermissionRoleId = permissionRoleSelect.value || ''
|
|
||||||
renderPermissionRoleOptions()
|
|
||||||
renderCollections()
|
|
||||||
})
|
|
||||||
collectionTableBody.addEventListener('change', function (event) {
|
|
||||||
const target = event.target
|
|
||||||
if (!target) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const field = target.getAttribute('data-rule-field')
|
|
||||||
if (field === 'allowSelectedRole') {
|
|
||||||
const box = target.closest('.rule-card')
|
|
||||||
if (!box) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const collectionName = box.getAttribute('data-collection') || ''
|
|
||||||
saveCollectionRules(collectionName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field === 'toggleCollection') {
|
|
||||||
const row = target.closest('[data-collection-row]')
|
|
||||||
if (!row) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const checkboxes = row.querySelectorAll('[data-rule-field="allowSelectedRole"]')
|
|
||||||
for (let i = 0; i < checkboxes.length; i += 1) {
|
|
||||||
if (!checkboxes[i].disabled) {
|
|
||||||
checkboxes[i].checked = !!target.checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const collectionName = row.getAttribute('data-collection-row') || ''
|
|
||||||
saveCollectionRules(collectionName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
document.getElementById('searchUserBtn').addEventListener('click', function () {
|
|
||||||
state.userKeyword = document.getElementById('userKeywordInput').value.trim()
|
|
||||||
loadContext()
|
|
||||||
})
|
|
||||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
|
||||||
localStorage.removeItem('pb_manage_token')
|
|
||||||
localStorage.removeItem('pb_manage_logged_in')
|
|
||||||
localStorage.removeItem('pb_manage_login_account')
|
|
||||||
localStorage.removeItem('pb_manage_login_time')
|
|
||||||
window.location.replace('/pb/manage/login')
|
|
||||||
})
|
|
||||||
|
|
||||||
loadContext()
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
return e.html(200, html)
|
return e.html(200, html)
|
||||||
})
|
})
|
||||||
|
|||||||
32
pocket-base/bai_web_pb_hooks/shared/theme-body.html
Normal file
32
pocket-base/bai_web_pb_hooks/shared/theme-body.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{{ define "theme_body" }}
|
||||||
|
<button id="themeToggleBtn" class="theme-toggle" type="button" aria-label="切换深色或浅色模式">深色模式</button>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var THEME_KEY = 'pb_manage_theme'
|
||||||
|
var root = document.documentElement
|
||||||
|
var toggleBtn = document.getElementById('themeToggleBtn')
|
||||||
|
if (!toggleBtn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTheme() {
|
||||||
|
var theme = root.getAttribute('data-theme')
|
||||||
|
return theme === 'dark' ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
root.setAttribute('data-theme', theme)
|
||||||
|
root.style.colorScheme = theme
|
||||||
|
localStorage.setItem(THEME_KEY, theme)
|
||||||
|
toggleBtn.textContent = theme === 'dark' ? '浅色模式' : '深色模式'
|
||||||
|
toggleBtn.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false')
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBtn.addEventListener('click', function () {
|
||||||
|
applyTheme(getTheme() === 'dark' ? 'light' : 'dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
applyTheme(getTheme())
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
||||||
186
pocket-base/bai_web_pb_hooks/shared/theme-head.html
Normal file
186
pocket-base/bai_web_pb_hooks/shared/theme-head.html
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
{{ define "theme_head" }}
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var THEME_KEY = 'pb_manage_theme'
|
||||||
|
var storedTheme = localStorage.getItem(THEME_KEY)
|
||||||
|
var fallbackTheme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
var theme = storedTheme === 'dark' || storedTheme === 'light' ? storedTheme : fallbackTheme
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
document.documentElement.style.colorScheme = theme
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
html { color-scheme: light; }
|
||||||
|
html[data-theme="dark"] { color-scheme: dark; }
|
||||||
|
.theme-toggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
z-index: 10000;
|
||||||
|
min-width: 112px;
|
||||||
|
min-height: 42px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.32);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
color: #0f172a;
|
||||||
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.theme-toggle:hover { transform: translateY(-1px); }
|
||||||
|
.theme-toggle:active { transform: translateY(0); }
|
||||||
|
html[data-theme="dark"] body {
|
||||||
|
background: #000 !important;
|
||||||
|
color: #e5e7eb !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .hero,
|
||||||
|
html[data-theme="dark"] .card,
|
||||||
|
html[data-theme="dark"] .panel,
|
||||||
|
html[data-theme="dark"] .topbar,
|
||||||
|
html[data-theme="dark"] .modal-card,
|
||||||
|
html[data-theme="dark"] .loading-card,
|
||||||
|
html[data-theme="dark"] .note,
|
||||||
|
html[data-theme="dark"] .option-box,
|
||||||
|
html[data-theme="dark"] .option-item,
|
||||||
|
html[data-theme="dark"] .param-table-wrap,
|
||||||
|
html[data-theme="dark"] .item-table,
|
||||||
|
html[data-theme="dark"] .table-wrap,
|
||||||
|
html[data-theme="dark"] .rule-card,
|
||||||
|
html[data-theme="dark"] .summary-card,
|
||||||
|
html[data-theme="dark"] .user-card,
|
||||||
|
html[data-theme="dark"] .image-tile,
|
||||||
|
html[data-theme="dark"] .image-tile-thumb,
|
||||||
|
html[data-theme="dark"] .thumb,
|
||||||
|
html[data-theme="dark"] .thumb-card {
|
||||||
|
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.35) !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] h1,
|
||||||
|
html[data-theme="dark"] h2,
|
||||||
|
html[data-theme="dark"] h3,
|
||||||
|
html[data-theme="dark"] label,
|
||||||
|
html[data-theme="dark"] th,
|
||||||
|
html[data-theme="dark"] strong,
|
||||||
|
html[data-theme="dark"] .module-title,
|
||||||
|
html[data-theme="dark"] .modal-title,
|
||||||
|
html[data-theme="dark"] .loading-text {
|
||||||
|
color: #f8fafc !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] p,
|
||||||
|
html[data-theme="dark"] .muted,
|
||||||
|
html[data-theme="dark"] .hint,
|
||||||
|
html[data-theme="dark"] .thumb-caption,
|
||||||
|
html[data-theme="dark"] .thumb-meta,
|
||||||
|
html[data-theme="dark"] .image-tile-order,
|
||||||
|
html[data-theme="dark"] .item-field-label,
|
||||||
|
html[data-theme="dark"] .drop-tip,
|
||||||
|
html[data-theme="dark"] .empty,
|
||||||
|
html[data-theme="dark"] td::before {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] input,
|
||||||
|
html[data-theme="dark"] textarea,
|
||||||
|
html[data-theme="dark"] select {
|
||||||
|
background: rgba(0, 0, 0, 0.78) !important;
|
||||||
|
border-color: rgba(148, 163, 184, 0.28) !important;
|
||||||
|
color: #f8fafc !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] input::placeholder,
|
||||||
|
html[data-theme="dark"] textarea::placeholder {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] table thead,
|
||||||
|
html[data-theme="dark"] thead {
|
||||||
|
background: rgba(30, 41, 59, 0.92) !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] th,
|
||||||
|
html[data-theme="dark"] td,
|
||||||
|
html[data-theme="dark"] tr,
|
||||||
|
html[data-theme="dark"] .item-table,
|
||||||
|
html[data-theme="dark"] .param-table-wrap {
|
||||||
|
border-color: rgba(148, 163, 184, 0.16) !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] tr:hover td {
|
||||||
|
background: rgba(30, 41, 59, 0.68) !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .btn-light,
|
||||||
|
html[data-theme="dark"] .btn-secondary,
|
||||||
|
html[data-theme="dark"] .btn:not(.btn-primary):not(.btn-danger):not(.btn-warning) {
|
||||||
|
background: rgba(30, 41, 59, 0.9) !important;
|
||||||
|
border-color: rgba(148, 163, 184, 0.25) !important;
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .btn-primary {
|
||||||
|
background: #3b82f6 !important;
|
||||||
|
color: #eff6ff !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .btn-danger {
|
||||||
|
background: #dc2626 !important;
|
||||||
|
color: #fee2e2 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .btn-warning {
|
||||||
|
background: #d97706 !important;
|
||||||
|
color: #fffbeb !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .badge-on {
|
||||||
|
background: rgba(34, 197, 94, 0.18) !important;
|
||||||
|
color: #86efac !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .badge-off {
|
||||||
|
background: rgba(239, 68, 68, 0.16) !important;
|
||||||
|
color: #fca5a5 !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .selection-tag {
|
||||||
|
background: rgba(37, 99, 235, 0.18) !important;
|
||||||
|
color: #bfdbfe !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .stat-tag {
|
||||||
|
background: rgba(37, 99, 235, 0.18) !important;
|
||||||
|
color: #bfdbfe !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .user-card.active {
|
||||||
|
background: rgba(10, 10, 10, 0.96) !important;
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.45) !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .note {
|
||||||
|
color: #bfdbfe !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .rule-meta,
|
||||||
|
html[data-theme="dark"] .rule-toggle,
|
||||||
|
html[data-theme="dark"] .user-name,
|
||||||
|
html[data-theme="dark"] .detail-title,
|
||||||
|
html[data-theme="dark"] .section-title,
|
||||||
|
html[data-theme="dark"] .summary-value {
|
||||||
|
color: #f8fafc !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .modal,
|
||||||
|
html[data-theme="dark"] .modal-mask,
|
||||||
|
html[data-theme="dark"] .loading-mask,
|
||||||
|
html[data-theme="dark"] .image-viewer {
|
||||||
|
background: rgba(2, 6, 23, 0.7) !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .loading-spinner {
|
||||||
|
border-color: rgba(59, 130, 246, 0.16) !important;
|
||||||
|
border-top-color: #60a5fa !important;
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .theme-toggle {
|
||||||
|
background: rgba(15, 23, 42, 0.86);
|
||||||
|
color: #f8fafc;
|
||||||
|
border-color: rgba(148, 163, 184, 0.26);
|
||||||
|
box-shadow: 0 18px 40px rgba(2, 6, 23, 0.42);
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.theme-toggle {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ end }}
|
||||||
631
pocket-base/bai_web_pb_hooks/views/cart-order-manage.html
Normal file
631
pocket-base/bai_web_pb_hooks/views/cart-order-manage.html
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>用户信息及订单管理</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var token = localStorage.getItem('pb_manage_token') || ''
|
||||||
|
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||||
|
if (!token || !isLoggedIn) {
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%); color: #1f2937; }
|
||||||
|
.container { max-width: 1520px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||||
|
.panel { background: rgba(255,255,255,0.97); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 20px; padding: 18px; }
|
||||||
|
.panel + .panel { margin-top: 14px; }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 10px; }
|
||||||
|
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
|
.btn-primary { background: #2563eb; color: #fff; }
|
||||||
|
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||||
|
.btn-danger { background: #dc2626; color: #fff; }
|
||||||
|
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||||
|
.status.success { color: #15803d; }
|
||||||
|
.status.error { color: #b91c1c; }
|
||||||
|
input, select, textarea { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||||
|
textarea { min-height: 90px; resize: vertical; }
|
||||||
|
.layout { display: grid; grid-template-columns: minmax(340px, 420px) minmax(0, 1fr); gap: 14px; align-items: start; }
|
||||||
|
.user-list { display: grid; gap: 10px; max-height: 72vh; overflow: auto; }
|
||||||
|
.user-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; cursor: pointer; }
|
||||||
|
.user-card.active { border-color: #2563eb; background: #eff6ff; box-shadow: inset 0 0 0 1px #bfdbfe; }
|
||||||
|
.user-name { font-size: 18px; font-weight: 700; color: #0f172a; }
|
||||||
|
.user-meta { margin-top: 6px; color: #64748b; font-size: 13px; word-break: break-all; }
|
||||||
|
.user-stats { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.stat-tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 12px; font-weight: 700; }
|
||||||
|
.detail-header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
|
||||||
|
.detail-title { margin: 0; font-size: 22px; }
|
||||||
|
.detail-meta { color: #64748b; font-size: 13px; line-height: 1.7; }
|
||||||
|
.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||||
|
.summary-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; }
|
||||||
|
.summary-label { color: #64748b; font-size: 13px; }
|
||||||
|
.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; }
|
||||||
|
.field-block { display: grid; gap: 6px; }
|
||||||
|
.field-label { font-size: 13px; color: #64748b; }
|
||||||
|
.attachment-panel { border: 1px solid #dbe3f0; border-radius: 14px; padding: 12px; background: #f8fbff; }
|
||||||
|
.attachment-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
||||||
|
.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-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; }
|
||||||
|
.section + .section { margin-top: 14px; }
|
||||||
|
.section-title { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
|
||||||
|
.table-wrap { overflow: auto; border: 1px solid #dbe3f0; border-radius: 16px; background: #fff; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
thead { background: #eff6ff; }
|
||||||
|
th, td { padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||||
|
.muted { color: #64748b; font-size: 12px; }
|
||||||
|
.empty { text-align: center; padding: 24px; color: #64748b; }
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.profile-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.toolbar { grid-template-columns: 1fr; }
|
||||||
|
.summary-grid { grid-template-columns: 1fr; }
|
||||||
|
table, thead, tbody, th, td, tr { display: block; }
|
||||||
|
thead { display: none; }
|
||||||
|
tr { border-bottom: 1px solid #e5e7eb; }
|
||||||
|
td { display: flex; justify-content: space-between; gap: 10px; }
|
||||||
|
td::before { content: attr(data-label); font-weight: 700; color: #475569; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ template "theme_head" . }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<section class="panel">
|
||||||
|
<h1 style="margin-top:0;">用户信息及订单管理</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||||
|
<button class="btn btn-light" id="reloadBtn" type="button">刷新</button>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="toolbar">
|
||||||
|
<input id="keywordInput" placeholder="按用户名 / openid / 手机号 / users_id 搜索" />
|
||||||
|
<button class="btn btn-primary" id="searchBtn" type="button">搜索</button>
|
||||||
|
<button class="btn btn-light" id="resetBtn" type="button">重置</button>
|
||||||
|
<button class="btn btn-light" id="refreshBtn" type="button">重新加载</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="layout">
|
||||||
|
<section class="panel">
|
||||||
|
<h2 style="margin-top:0;">用户列表</h2>
|
||||||
|
<div id="userList" class="user-list"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div id="detailWrap"></div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = '/pb/api'
|
||||||
|
const tokenKey = 'pb_manage_token'
|
||||||
|
const state = {
|
||||||
|
users: [],
|
||||||
|
selectedOpenid: '',
|
||||||
|
userLevelOptions: [],
|
||||||
|
attachmentDetails: {
|
||||||
|
userUsersPicture: null,
|
||||||
|
userUsersIdPicA: null,
|
||||||
|
userUsersIdPicB: null,
|
||||||
|
userUsersTitlePicture: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusEl = document.getElementById('status')
|
||||||
|
const keywordInput = document.getElementById('keywordInput')
|
||||||
|
const userListEl = document.getElementById('userList')
|
||||||
|
const detailWrapEl = document.getElementById('detailWrap')
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(message, type) {
|
||||||
|
statusEl.textContent = message || ''
|
||||||
|
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem(tokenKey) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestJson(path, payload) {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) {
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
throw new Error('登录状态已失效,请重新登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(API_BASE + path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload || {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error((data && (data.errMsg || data.message)) || '请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedUser() {
|
||||||
|
return state.users.find(function (item) {
|
||||||
|
return normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||||
|
}) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
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 renderAttachmentUploader(fieldId, label, value) {
|
||||||
|
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="field-block full">'
|
||||||
|
+ '<label class="field-label" for="' + fieldId + '">' + escapeHtml(label) + '</label>'
|
||||||
|
+ '<div class="attachment-panel">'
|
||||||
|
+ '<input id="' + fieldId + '" value="' + escapeHtml(value || '') + '" />'
|
||||||
|
+ '<input class="attachment-hidden-input" id="' + fieldId + 'File" type="file" accept="image/*" data-upload-field="' + escapeHtml(fieldId) + '" />'
|
||||||
|
+ '<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">'
|
||||||
|
+ '<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>'
|
||||||
|
+ '</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserList() {
|
||||||
|
if (!state.users.length) {
|
||||||
|
userListEl.innerHTML = '<div class="empty">暂无匹配用户。</div>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userListEl.innerHTML = state.users.map(function (item) {
|
||||||
|
const isActive = normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||||
|
return '<div class="user-card' + (isActive ? ' active' : '') + '" data-openid="' + escapeHtml(item.openid) + '">'
|
||||||
|
+ '<div class="user-name">' + escapeHtml(item.users_name || item.users_id || item.openid) + '</div>'
|
||||||
|
+ '<div class="user-meta">openid:' + escapeHtml(item.openid || '-') + '</div>'
|
||||||
|
+ '<div class="user-meta">手机号:' + escapeHtml(item.users_phone || '-') + '</div>'
|
||||||
|
+ '<div class="user-meta">users_id:' + escapeHtml(item.users_id || '-') + '</div>'
|
||||||
|
+ '<div class="user-meta">会员等级:' + escapeHtml(item.users_level_name || item.users_level || '-') + '</div>'
|
||||||
|
+ '<div class="user-stats">'
|
||||||
|
+ '<span class="stat-tag">购物车 ' + escapeHtml(item.cart_count || 0) + '</span>'
|
||||||
|
+ '<span class="stat-tag">购物数量 ' + escapeHtml(item.cart_total_quantity || 0) + '</span>'
|
||||||
|
+ '<span class="stat-tag">订单 ' + escapeHtml(item.order_count || 0) + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>'
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCartTable(items) {
|
||||||
|
if (!items.length) {
|
||||||
|
return '<div class="empty">当前用户暂无购物车记录。</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="table-wrap"><table><thead><tr>'
|
||||||
|
+ '<th>商品名称</th><th>型号</th><th>数量</th><th>单价</th><th>状态</th><th>加入时间</th><th>购物车名</th>'
|
||||||
|
+ '</tr></thead><tbody>'
|
||||||
|
+ items.map(function (item) {
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td data-label="商品名称"><div>' + escapeHtml(item.product_name || item.cart_product_id || '-') + '</div><div class="muted">' + escapeHtml(item.cart_product_id || '-') + '</div></td>'
|
||||||
|
+ '<td data-label="型号">' + escapeHtml(item.product_modelnumber || '-') + '</td>'
|
||||||
|
+ '<td data-label="数量">' + escapeHtml(item.cart_product_quantity || 0) + '</td>'
|
||||||
|
+ '<td data-label="单价">¥' + escapeHtml(item.cart_at_price || 0) + '</td>'
|
||||||
|
+ '<td data-label="状态">' + escapeHtml(item.cart_status || '-') + '</td>'
|
||||||
|
+ '<td data-label="加入时间">' + escapeHtml(item.cart_create || item.created || '-') + '</td>'
|
||||||
|
+ '<td data-label="购物车名">' + escapeHtml(item.cart_number || '-') + '</td>'
|
||||||
|
+ '</tr>'
|
||||||
|
}).join('')
|
||||||
|
+ '</tbody></table></div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserLevelOptions(selectedValue) {
|
||||||
|
const current = normalizeText(selectedValue)
|
||||||
|
let html = '<option value="">未设置</option>'
|
||||||
|
|
||||||
|
for (let i = 0; i < state.userLevelOptions.length; i += 1) {
|
||||||
|
const item = state.userLevelOptions[i]
|
||||||
|
const value = normalizeText(item.value)
|
||||||
|
html += '<option value="' + escapeHtml(value) + '"' + (value === current ? ' selected' : '') + '>'
|
||||||
|
+ escapeHtml(item.label || item.value || '')
|
||||||
|
+ '</option>'
|
||||||
|
}
|
||||||
|
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserProfileForm(user) {
|
||||||
|
return '<div class="section"><h3 class="section-title">用户信息维护</h3>'
|
||||||
|
+ '<div class="profile-grid">'
|
||||||
|
+ '<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>'
|
||||||
|
+ '<div class="field-block"><label class="field-label" for="userUsersLevel">会员等级</label><select id="userUsersLevel">' + renderUserLevelOptions(user.users_level) + '</select></div>'
|
||||||
|
+ '<div class="field-block"><label class="field-label" for="userUsersType">用户类型</label><input id="userUsersType" value="' + escapeHtml(user.users_type || '') + '" /></div>'
|
||||||
|
+ '<div class="field-block"><label class="field-label" for="userUsersStatus">用户状态</label><input id="userUsersStatus" value="' + escapeHtml(user.users_status || '') + '" /></div>'
|
||||||
|
+ '<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>'
|
||||||
|
+ '<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>'
|
||||||
|
+ renderAttachmentUploader('userUsersPicture', '头像附件ID', user.users_picture || '')
|
||||||
|
+ renderAttachmentUploader('userUsersIdPicA', '证件正面附件ID', user.users_id_pic_a || '')
|
||||||
|
+ renderAttachmentUploader('userUsersIdPicB', '证件反面附件ID', user.users_id_pic_b || '')
|
||||||
|
+ renderAttachmentUploader('userUsersTitlePicture', '资质附件ID', user.users_title_picture || '')
|
||||||
|
+ '<div class="detail-actions"><button class="btn btn-primary" id="saveUserBtn" type="button">保存用户信息</button></div>'
|
||||||
|
+ '</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOrderTable(items) {
|
||||||
|
if (!items.length) {
|
||||||
|
return '<div class="empty">当前用户暂无订单记录。</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="table-wrap"><table><thead><tr>'
|
||||||
|
+ '<th>订单编号</th><th>来源</th><th>来源ID</th><th>金额</th><th>状态</th><th>下单时间</th>'
|
||||||
|
+ '</tr></thead><tbody>'
|
||||||
|
+ items.map(function (item) {
|
||||||
|
return '<tr>'
|
||||||
|
+ '<td data-label="订单编号"><div>' + escapeHtml(item.order_number || item.order_id || '-') + '</div><div class="muted">' + escapeHtml(item.order_id || '-') + '</div></td>'
|
||||||
|
+ '<td data-label="来源">' + escapeHtml(item.order_source || '-') + '</td>'
|
||||||
|
+ '<td data-label="来源ID">' + escapeHtml(item.order_source_id || '-') + '</td>'
|
||||||
|
+ '<td data-label="金额">¥' + escapeHtml(item.order_amount || 0) + '</td>'
|
||||||
|
+ '<td data-label="状态">' + escapeHtml(item.order_status || '-') + '</td>'
|
||||||
|
+ '<td data-label="下单时间">' + escapeHtml(item.order_create || item.created || '-') + '</td>'
|
||||||
|
+ '</tr>'
|
||||||
|
}).join('')
|
||||||
|
+ '</tbody></table></div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDetail() {
|
||||||
|
const user = getSelectedUser()
|
||||||
|
if (!user) {
|
||||||
|
detailWrapEl.innerHTML = '<div class="empty">请选择左侧用户查看信息维护、购物车与订单详情。</div>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detailWrapEl.innerHTML = '<div class="detail-header">'
|
||||||
|
+ '<div>'
|
||||||
|
+ '<h2 class="detail-title">' + escapeHtml(user.users_name || user.users_id || user.openid) + '</h2>'
|
||||||
|
+ '<div class="detail-meta">openid:' + escapeHtml(user.openid || '-') + '</div>'
|
||||||
|
+ '<div class="detail-meta">users_id:' + escapeHtml(user.users_id || '-') + '</div>'
|
||||||
|
+ '<div class="detail-meta">users_idtype:' + escapeHtml(user.users_idtype || '-') + '</div>'
|
||||||
|
+ '<div class="detail-meta">手机号:' + escapeHtml(user.users_phone || '-') + '</div>'
|
||||||
|
+ '<div class="detail-meta">会员等级:' + escapeHtml(user.users_level_name || user.users_level || '-') + '</div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ renderUserProfileForm(user)
|
||||||
|
+ '<div class="summary-grid">'
|
||||||
|
+ '<div class="summary-card"><div class="summary-label">购物车记录数</div><div class="summary-value">' + escapeHtml(user.cart_count || 0) + '</div></div>'
|
||||||
|
+ '<div class="summary-card"><div class="summary-label">购物车商品总数</div><div class="summary-value">' + escapeHtml(user.cart_total_quantity || 0) + '</div></div>'
|
||||||
|
+ '<div class="summary-card"><div class="summary-label">订单数</div><div class="summary-value">' + escapeHtml(user.order_count || 0) + '</div></div>'
|
||||||
|
+ '<div class="summary-card"><div class="summary-label">订单总金额</div><div class="summary-value">¥' + escapeHtml(user.order_total_amount || 0) + '</div></div>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="section"><h3 class="section-title">当前购物车详情</h3>' + renderCartTable(Array.isArray(user.carts) ? user.carts : []) + '</div>'
|
||||||
|
+ '<div class="section"><h3 class="section-title">订单记录</h3>' + renderOrderTable(Array.isArray(user.orders) ? user.orders : []) + '</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
setStatus('正在加载用户信息、购物车与订单数据...', '')
|
||||||
|
try {
|
||||||
|
const data = await requestJson('/cart-order/manage-users', {
|
||||||
|
keyword: normalizeText(keywordInput.value),
|
||||||
|
})
|
||||||
|
state.users = Array.isArray(data.items) ? data.items : []
|
||||||
|
state.userLevelOptions = Array.isArray(data.user_level_options) ? data.user_level_options : []
|
||||||
|
if (!state.users.length) {
|
||||||
|
state.selectedOpenid = ''
|
||||||
|
} else if (!getSelectedUser()) {
|
||||||
|
state.selectedOpenid = normalizeText(state.users[0].openid)
|
||||||
|
}
|
||||||
|
renderUserList()
|
||||||
|
renderDetail()
|
||||||
|
refreshSelectedAttachmentDetails()
|
||||||
|
setStatus('加载完成,共 ' + state.users.length + ' 位用户。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
renderUserList()
|
||||||
|
renderDetail()
|
||||||
|
setStatus(err.message || '加载失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSelectedUser() {
|
||||||
|
const user = getSelectedUser()
|
||||||
|
if (!user) {
|
||||||
|
setStatus('请先选择用户。', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await requestJson('/cart-order/manage-user-update', {
|
||||||
|
openid: user.openid,
|
||||||
|
users_name: document.getElementById('userUsersName').value,
|
||||||
|
users_phone: document.getElementById('userUsersPhone').value,
|
||||||
|
users_level: document.getElementById('userUsersLevel').value,
|
||||||
|
users_type: document.getElementById('userUsersType').value,
|
||||||
|
users_status: document.getElementById('userUsersStatus').value,
|
||||||
|
users_rank_level: document.getElementById('userUsersRankLevel').value,
|
||||||
|
users_auth_type: document.getElementById('userUsersAuthType').value,
|
||||||
|
company_id: document.getElementById('userCompanyId').value,
|
||||||
|
users_tag: document.getElementById('userUsersTag').value,
|
||||||
|
users_parent_id: document.getElementById('userUsersParentId').value,
|
||||||
|
users_promo_code: document.getElementById('userUsersPromoCode').value,
|
||||||
|
usergroups_id: document.getElementById('userUsergroupsId').value,
|
||||||
|
users_id_number: document.getElementById('userUsersIdNumber').value,
|
||||||
|
users_picture: document.getElementById('userUsersPicture').value,
|
||||||
|
users_id_pic_a: document.getElementById('userUsersIdPicA').value,
|
||||||
|
users_id_pic_b: document.getElementById('userUsersIdPicB').value,
|
||||||
|
users_title_picture: document.getElementById('userUsersTitlePicture').value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedUser = data && data.user ? data.user : null
|
||||||
|
if (updatedUser) {
|
||||||
|
const index = state.users.findIndex(function (item) {
|
||||||
|
return normalizeText(item.openid) === normalizeText(updatedUser.openid)
|
||||||
|
})
|
||||||
|
if (index !== -1) {
|
||||||
|
state.users[index] = Object.assign({}, state.users[index], updatedUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderUserList()
|
||||||
|
renderDetail()
|
||||||
|
setStatus('用户信息保存成功。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '保存用户信息失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAttachmentUpload(fieldId, file) {
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = document.getElementById(fieldId)
|
||||||
|
if (!input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelMap = {
|
||||||
|
userUsersPicture: '头像',
|
||||||
|
userUsersIdPicA: '证件正面',
|
||||||
|
userUsersIdPicB: '证件反面',
|
||||||
|
userUsersTitlePicture: '资质附件',
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setStatus('正在上传' + (labelMap[fieldId] || '附件') + '...', '')
|
||||||
|
const uploaded = await uploadAttachment(file, fieldId)
|
||||||
|
input.value = uploaded.attachments_id || ''
|
||||||
|
state.attachmentDetails[fieldId] = uploaded
|
||||||
|
renderDetail()
|
||||||
|
setStatus((labelMap[fieldId] || '附件') + '上传成功,请点击“保存用户信息”完成写入。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '上传附件失败', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userListEl.addEventListener('click', function (event) {
|
||||||
|
const target = event.target && event.target.closest ? event.target.closest('[data-openid]') : null
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.selectedOpenid = normalizeText(target.getAttribute('data-openid'))
|
||||||
|
renderUserList()
|
||||||
|
renderDetail()
|
||||||
|
refreshSelectedAttachmentDetails()
|
||||||
|
})
|
||||||
|
|
||||||
|
detailWrapEl.addEventListener('click', function (event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target && target.id === 'saveUserBtn') {
|
||||||
|
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) {
|
||||||
|
const input = document.getElementById(clearAttachment)
|
||||||
|
const fileInput = document.getElementById(clearAttachment + 'File')
|
||||||
|
if (input) {
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = ''
|
||||||
|
}
|
||||||
|
state.attachmentDetails[clearAttachment] = null
|
||||||
|
renderDetail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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('reloadBtn').addEventListener('click', loadUsers)
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', loadUsers)
|
||||||
|
document.getElementById('resetBtn').addEventListener('click', function () {
|
||||||
|
keywordInput.value = ''
|
||||||
|
loadUsers()
|
||||||
|
})
|
||||||
|
|
||||||
|
loadUsers()
|
||||||
|
</script>
|
||||||
|
{{ template "theme_body" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1197
pocket-base/bai_web_pb_hooks/views/dictionary-manage.html
Normal file
1197
pocket-base/bai_web_pb_hooks/views/dictionary-manage.html
Normal file
File diff suppressed because it is too large
Load Diff
1736
pocket-base/bai_web_pb_hooks/views/document-manage.html
Normal file
1736
pocket-base/bai_web_pb_hooks/views/document-manage.html
Normal file
File diff suppressed because it is too large
Load Diff
96
pocket-base/bai_web_pb_hooks/views/index.html
Normal file
96
pocket-base/bai_web_pb_hooks/views/index.html
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>管理主页</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var token = localStorage.getItem('pb_manage_token') || ''
|
||||||
|
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||||
|
if (!token || !isLoggedIn) {
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f3f6fb 0%, #eef2ff 100%); color: #1f2937; }
|
||||||
|
.wrap { max-width: 1440px; margin: 0 auto; padding: 24px 14px; }
|
||||||
|
.hero { background: #ffffff; border-radius: 20px; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); padding: 22px; border: 1px solid #e5e7eb; }
|
||||||
|
h1 { margin: 0 0 14px; font-size: 30px; }
|
||||||
|
.module + .module { margin-top: 18px; }
|
||||||
|
.module-title { margin: 0 0 10px; font-size: 22px; color: #0f172a; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
|
||||||
|
.card { background: #f8fafc; border: 1px solid #dbe3f0; border-radius: 16px; padding: 16px; text-align: center; }
|
||||||
|
.card h2 { margin: 0 0 8px; font-size: 19px; }
|
||||||
|
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px; border-radius: 12px; text-decoration: none; background: #2563eb; color: #fff; font-weight: 600; margin-top: 12px; }
|
||||||
|
.actions { margin-top: 14px; display: flex; justify-content: flex-start; }
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ template "theme_head" . }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<section class="hero">
|
||||||
|
<h1>管理主页</h1>
|
||||||
|
<section class="module">
|
||||||
|
<h2 class="module-title">平台管理</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<article class="card">
|
||||||
|
<h2>字典管理</h2>
|
||||||
|
<a class="btn" href="/pb/manage/dictionary-manage">进入字典管理</a>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h2>文档管理</h2>
|
||||||
|
<a class="btn" href="/pb/manage/document-manage">进入文档管理</a>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h2>产品管理</h2>
|
||||||
|
<a class="btn" href="/pb/manage/product-manage">进入产品管理</a>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h2>SDK 权限管理</h2>
|
||||||
|
<a class="btn" href="/pb/manage/sdk-permission-manage">进入权限管理</a>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h2>购物车与订单</h2>
|
||||||
|
<a class="btn" href="/pb/manage/cart-order-manage">进入用户信息及订单管理</a>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="module">
|
||||||
|
<h2 class="module-title">AI 管理</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<article class="card">
|
||||||
|
<h2>AI 审计管理</h2>
|
||||||
|
<a class="btn" href="/pb/bai-ai-manage">进入审计管理</a>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h2>AI 聊天测试</h2>
|
||||||
|
<a class="btn" href="/pb/bai-chat">进入聊天测试</a>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h2>SQL 实验室</h2>
|
||||||
|
<a class="btn" href="/pb/bai-ai-sql-lab">进入 SQL 实验室</a>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="logoutBtn" class="btn" type="button" style="background:#dc2626;">退出登录</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||||
|
localStorage.removeItem('pb_manage_token')
|
||||||
|
localStorage.removeItem('pb_manage_logged_in')
|
||||||
|
localStorage.removeItem('pb_manage_login_account')
|
||||||
|
localStorage.removeItem('pb_manage_login_time')
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{{ template "theme_body" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
155
pocket-base/bai_web_pb_hooks/views/login.html
Normal file
155
pocket-base/bai_web_pb_hooks/views/login.html
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>登录</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var token = localStorage.getItem('pb_manage_token') || ''
|
||||||
|
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||||
|
if (token && isLoggedIn) {
|
||||||
|
window.location.replace('/pb/manage')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background: linear-gradient(180deg, #eff6ff 0%, #f8fafc 100%);
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1440px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 34px 14px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #dbe3f0;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 22px;
|
||||||
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
h1 { margin-top: 0; }
|
||||||
|
p { line-height: 1.8; color: #4b5563; margin-bottom: 20px; }
|
||||||
|
.field { margin-bottom: 14px; }
|
||||||
|
.field label { display: block; margin-bottom: 8px; color: #334155; font-weight: 600; }
|
||||||
|
.field input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.status { margin-top: 12px; min-height: 22px; font-size: 14px; }
|
||||||
|
.status.error { color: #b91c1c; }
|
||||||
|
.status.success { color: #15803d; }
|
||||||
|
</style>
|
||||||
|
{{ template "theme_head" . }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<section class="card">
|
||||||
|
<h1>后台登录</h1>
|
||||||
|
<p>请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。</p>
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="field">
|
||||||
|
<label for="account">登录账号</label>
|
||||||
|
<input id="account" name="account" placeholder="邮箱或手机号" required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input id="password" name="password" type="password" placeholder="请输入密码" required />
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="submitBtn" type="submit">登录</button>
|
||||||
|
</form>
|
||||||
|
<div id="status" class="status"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var API_BASE = '/pb/api'
|
||||||
|
var form = document.getElementById('loginForm')
|
||||||
|
var accountInput = document.getElementById('account')
|
||||||
|
var passwordInput = document.getElementById('password')
|
||||||
|
var statusEl = document.getElementById('status')
|
||||||
|
var submitBtn = document.getElementById('submitBtn')
|
||||||
|
|
||||||
|
function setStatus(message, type) {
|
||||||
|
statusEl.textContent = message || ''
|
||||||
|
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', async function (event) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
var loginAccount = accountInput.value.trim()
|
||||||
|
var password = passwordInput.value
|
||||||
|
if (!loginAccount || !password) {
|
||||||
|
setStatus('请填写账号和密码', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn.disabled = true
|
||||||
|
submitBtn.textContent = '登录中...'
|
||||||
|
setStatus('正在登录,请稍候...', '')
|
||||||
|
|
||||||
|
try {
|
||||||
|
var response = await fetch(API_BASE + '/platform/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
login_account: loginAccount,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
var result = await response.json()
|
||||||
|
if (!response.ok || !result || result.code >= 400) {
|
||||||
|
throw new Error((result && result.msg) || '登录失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = result.token || (result.data && result.data.token) || ''
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('登录成功但未获取到 token')
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('pb_manage_token', token)
|
||||||
|
localStorage.setItem('pb_manage_logged_in', '1')
|
||||||
|
localStorage.setItem('pb_manage_login_account', loginAccount)
|
||||||
|
localStorage.setItem('pb_manage_login_time', new Date().toISOString())
|
||||||
|
|
||||||
|
setStatus('登录成功,正在跳转...', 'success')
|
||||||
|
window.location.replace('/pb/manage')
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('pb_manage_token')
|
||||||
|
localStorage.removeItem('pb_manage_logged_in')
|
||||||
|
setStatus(error.message || '登录失败', 'error')
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false
|
||||||
|
submitBtn.textContent = '登录'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
{{ template "theme_body" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2521
pocket-base/bai_web_pb_hooks/views/product-manage.html
Normal file
2521
pocket-base/bai_web_pb_hooks/views/product-manage.html
Normal file
File diff suppressed because it is too large
Load Diff
735
pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html
Normal file
735
pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>SDK 权限管理</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var token = localStorage.getItem('pb_manage_token') || ''
|
||||||
|
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||||
|
if (!token || !isLoggedIn) {
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef6ff 0%, #f8fafc 100%); color: #0f172a; }
|
||||||
|
.wrap { max-width: 1440px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||||
|
.panel { background: rgba(255,255,255,0.96); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 20px; padding: 18px; }
|
||||||
|
.panel + .panel { margin-top: 14px; }
|
||||||
|
h1, h2, h3 { margin-top: 0; }
|
||||||
|
p { color: #475569; line-height: 1.7; }
|
||||||
|
.actions, .toolbar, .row-actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
|
||||||
|
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 700; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
|
.btn-primary { background: #2563eb; color: #fff; }
|
||||||
|
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||||
|
.btn-danger { background: #dc2626; color: #fff; }
|
||||||
|
.btn-warning { background: #f59e0b; color: #fff; }
|
||||||
|
.btn-success { background: #16a34a; color: #fff; }
|
||||||
|
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||||
|
.status.success { color: #15803d; }
|
||||||
|
.status.error { color: #b91c1c; }
|
||||||
|
.note { padding: 14px 16px; border-radius: 16px; background: #eff6ff; color: #1d4ed8; font-size: 14px; line-height: 1.7; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
thead { background: #eff6ff; }
|
||||||
|
th, td { padding: 14px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||||
|
th { font-size: 13px; color: #475569; }
|
||||||
|
tr:hover td { background: #f8fafc; }
|
||||||
|
input, textarea, select { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 9px 11px; font-size: 14px; background: #fff; }
|
||||||
|
textarea { min-height: 80px; resize: vertical; }
|
||||||
|
.empty { text-align: center; padding: 24px 16px; color: #64748b; }
|
||||||
|
.muted { color: #64748b; font-size: 12px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
|
||||||
|
.full { grid-column: 1 / -1; }
|
||||||
|
.rule-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
|
||||||
|
.rule-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 12px; background: #f8fbff; }
|
||||||
|
.rule-card h4 { margin: 0 0 10px; font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
||||||
|
.rule-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; color: #475569; font-size: 12px; }
|
||||||
|
.rule-meta input[type="checkbox"] { width: auto; margin: 0; }
|
||||||
|
.rule-toggle { display: inline-flex; align-items: center; gap: 6px; color: #0f172a; font-size: 13px; font-weight: 600; }
|
||||||
|
.rule-toggle input[type="checkbox"] { width: auto; margin: 0; }
|
||||||
|
.split { display: grid; grid-template-columns: 1.15fr 1fr; gap: 14px; }
|
||||||
|
.table-wrap { overflow: auto; }
|
||||||
|
.collection-table { table-layout: fixed; }
|
||||||
|
.collection-col { width: 264px; }
|
||||||
|
.rule-col { width: calc(100% - 264px); }
|
||||||
|
.collection-meta { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
|
||||||
|
.loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(4px); z-index: 9998; }
|
||||||
|
.loading-mask.show { display: flex; }
|
||||||
|
.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: sdkSpin 0.9s linear infinite; }
|
||||||
|
.loading-text { color: #0f172a; font-size: 15px; font-weight: 700; }
|
||||||
|
@keyframes sdkSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.split, .rule-grid, .grid { grid-template-columns: 1fr; }
|
||||||
|
.collection-col, .rule-col { width: auto; }
|
||||||
|
table, thead, tbody, th, td, tr { display: block; }
|
||||||
|
thead { display: none; }
|
||||||
|
tr { margin-bottom: 14px; border: 1px solid #e5e7eb; border-radius: 16px; overflow: hidden; }
|
||||||
|
td { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ template "theme_head" . }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<section class="panel">
|
||||||
|
<h1>SDK 权限管理</h1>
|
||||||
|
<p>这里管理的是 <code>tbl_auth_users</code> 用户通过 PocketBase SDK 直连数据库时的业务权限。<strong>ManagePlatform</strong> 会被视为你的业务管理员,但不会自动变成 PocketBase 原生 <code>_superusers</code>。</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||||
|
<button class="btn btn-light" id="refreshBtn" type="button">刷新数据</button>
|
||||||
|
<button class="btn btn-success" id="syncManageBtn" type="button">同步 ManagePlatform 全权限</button>
|
||||||
|
</div>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="note" id="noteBox">加载中...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>角色管理</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<input id="newRoleName" placeholder="角色名称" />
|
||||||
|
<input id="newRoleCode" placeholder="角色编码,可为空" />
|
||||||
|
<input id="newRoleStatus" type="number" placeholder="状态,默认1" value="1" />
|
||||||
|
<button class="btn btn-primary" id="createRoleBtn" type="button">新增角色</button>
|
||||||
|
<div class="muted">角色 ID 由系统自动生成,页面不显示。</div>
|
||||||
|
<textarea id="newRoleRemark" class="full" placeholder="备注"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap" style="margin-top:16px;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>编码</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>备注</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="roleTableBody">
|
||||||
|
<tr><td colspan="5" class="empty">暂无角色。</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>用户授权</h2>
|
||||||
|
<div class="toolbar">
|
||||||
|
<input id="userKeywordInput" placeholder="按姓名、手机号、openid、角色搜索" />
|
||||||
|
<button class="btn btn-light" id="searchUserBtn" type="button">查询用户</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap" style="margin-top:16px;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>用户</th>
|
||||||
|
<th>身份类型</th>
|
||||||
|
<th>当前角色</th>
|
||||||
|
<th>授权</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="userTableBody">
|
||||||
|
<tr><td colspan="5" class="empty">暂无用户。</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Collection 直连权限</h2>
|
||||||
|
<div class="toolbar">
|
||||||
|
<select id="permissionRoleSelect"></select>
|
||||||
|
<div class="muted">这里是按“当前配置角色”逐表分配 CRUD 权限。下面依次对应“列表、详情、新增、修改、删除”五种权限,勾选后会立即保存。公开访问或自定义规则的操作不会显示授权勾选框。</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-wrap" style="margin-top:16px;">
|
||||||
|
<table class="collection-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="collection-col">集合</th>
|
||||||
|
<th class="rule-col">当前角色权限</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="collectionTableBody">
|
||||||
|
<tr><td colspan="2" class="empty">暂无集合。</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="loading-mask" id="loadingMask">
|
||||||
|
<div class="loading-card">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text" id="loadingText">处理中,请稍候...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = '/pb/api/sdk-permission'
|
||||||
|
const tokenKey = 'pb_manage_token'
|
||||||
|
const statusEl = document.getElementById('status')
|
||||||
|
const noteBox = document.getElementById('noteBox')
|
||||||
|
const roleTableBody = document.getElementById('roleTableBody')
|
||||||
|
const userTableBody = document.getElementById('userTableBody')
|
||||||
|
const collectionTableBody = document.getElementById('collectionTableBody')
|
||||||
|
const permissionRoleSelect = document.getElementById('permissionRoleSelect')
|
||||||
|
const loadingMask = document.getElementById('loadingMask')
|
||||||
|
const loadingText = document.getElementById('loadingText')
|
||||||
|
const loadingState = { count: 0 }
|
||||||
|
const state = {
|
||||||
|
roles: [],
|
||||||
|
users: [],
|
||||||
|
collections: [],
|
||||||
|
userKeyword: '',
|
||||||
|
selectedPermissionRoleId: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(message, type) {
|
||||||
|
statusEl.textContent = message || ''
|
||||||
|
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading(message) {
|
||||||
|
loadingState.count += 1
|
||||||
|
loadingText.textContent = message || '处理中,请稍候...'
|
||||||
|
loadingMask.classList.add('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
loadingState.count = Math.max(0, loadingState.count - 1)
|
||||||
|
if (!loadingState.count) {
|
||||||
|
loadingMask.classList.remove('show')
|
||||||
|
loadingText.textContent = '处理中,请稍候...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return localStorage.getItem(tokenKey) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleById(roleId) {
|
||||||
|
const target = String(roleId || '')
|
||||||
|
return state.roles.find(function (role) {
|
||||||
|
return role.role_id === target
|
||||||
|
}) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleName(roleId) {
|
||||||
|
const role = getRoleById(roleId)
|
||||||
|
return role ? role.role_name : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSelectedPermissionRole() {
|
||||||
|
const exists = state.roles.some(function (role) {
|
||||||
|
return role.role_id === state.selectedPermissionRoleId
|
||||||
|
})
|
||||||
|
if (!exists) {
|
||||||
|
state.selectedPermissionRoleId = state.roles.length ? state.roles[0].role_id : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeErrMsg(value) {
|
||||||
|
if (!value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value)
|
||||||
|
} catch (_err) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapApiResponse(data, res) {
|
||||||
|
const safe = data && typeof data === 'object' ? data : {}
|
||||||
|
const headerCode = Number(res && res.headers ? (res.headers.get('X-Status-Code') || 0) : 0)
|
||||||
|
const headerErrMsg = decodeErrMsg(res && res.headers ? res.headers.get('X-Err-Msg') : '')
|
||||||
|
const code = Number(safe.statusCode || safe.code || headerCode || 0)
|
||||||
|
const errMsg = safe.errMsg || safe.msg || headerErrMsg || ''
|
||||||
|
const hasLegacyEnvelope = Object.prototype.hasOwnProperty.call(safe, 'data')
|
||||||
|
&& (Object.prototype.hasOwnProperty.call(safe, 'code')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(safe, 'statusCode')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(safe, 'msg')
|
||||||
|
|| Object.prototype.hasOwnProperty.call(safe, 'errMsg'))
|
||||||
|
const payload = hasLegacyEnvelope ? (safe.data || {}) : safe
|
||||||
|
return { code: code, errMsg: errMsg, payload: payload }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestJson(path, payload) {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) {
|
||||||
|
localStorage.removeItem('pb_manage_logged_in')
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
throw new Error('登录状态已失效,请重新登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(API_BASE + path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: 'Bearer ' + token,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload || {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
const unwrapped = unwrapApiResponse(data, res)
|
||||||
|
if (!res.ok || !data || unwrapped.code >= 400) {
|
||||||
|
if (res.status === 401 || res.status === 403 || unwrapped.code === 401 || unwrapped.code === 403) {
|
||||||
|
localStorage.removeItem('pb_manage_token')
|
||||||
|
localStorage.removeItem('pb_manage_logged_in')
|
||||||
|
localStorage.removeItem('pb_manage_login_account')
|
||||||
|
localStorage.removeItem('pb_manage_login_time')
|
||||||
|
window.location.replace('/pb/manage/login')
|
||||||
|
}
|
||||||
|
throw new Error(unwrapped.errMsg || '请求失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return unwrapped.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
function roleOptionsHtml(selectedRoleId) {
|
||||||
|
const current = String(selectedRoleId || '')
|
||||||
|
return ['<option value="">未分配</option>'].concat(state.roles.map(function (role) {
|
||||||
|
return '<option value="' + escapeHtml(role.role_id) + '"' + (current === role.role_id ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
||||||
|
})).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRoles() {
|
||||||
|
if (!state.roles.length) {
|
||||||
|
roleTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无角色。</td></tr>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roleTableBody.innerHTML = state.roles.map(function (role) {
|
||||||
|
return '<tr data-role-id="' + escapeHtml(role.role_id) + '">'
|
||||||
|
+ '<td><input data-role-field="role_name" value="' + escapeHtml(role.role_name) + '" /></td>'
|
||||||
|
+ '<td><input data-role-field="role_code" value="' + escapeHtml(role.role_code) + '" /></td>'
|
||||||
|
+ '<td><input data-role-field="role_status" type="number" value="' + escapeHtml(role.role_status) + '" /></td>'
|
||||||
|
+ '<td><textarea data-role-field="role_remark">' + escapeHtml(role.role_remark) + '</textarea></td>'
|
||||||
|
+ '<td><div class="row-actions"><button class="btn btn-light" type="button" onclick="window.__saveRoleRow(\'' + encodeURIComponent(role.role_id) + '\')">保存</button><button class="btn btn-danger" type="button" onclick="window.__deleteRoleRow(\'' + encodeURIComponent(role.role_id) + '\')">删除</button></div></td>'
|
||||||
|
+ '</tr>'
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUsers() {
|
||||||
|
if (!state.users.length) {
|
||||||
|
userTableBody.innerHTML = '<tr><td colspan="5" class="empty">暂无匹配用户。</td></tr>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userTableBody.innerHTML = state.users.map(function (user) {
|
||||||
|
const name = user.users_name || '未命名用户'
|
||||||
|
const phone = user.users_phone || '无手机号'
|
||||||
|
const roleName = user.role_name || getRoleName(user.usergroups_id) || '未分配'
|
||||||
|
return '<tr data-user-id="' + escapeHtml(user.pb_id) + '">'
|
||||||
|
+ '<td><div><strong>' + escapeHtml(name) + '</strong></div><div class="muted">' + escapeHtml(phone) + '</div><div class="muted">' + escapeHtml(user.openid) + '</div></td>'
|
||||||
|
+ '<td><div>' + escapeHtml(user.users_idtype || '') + '</div><div class="muted">' + escapeHtml(user.users_type || '') + '</div></td>'
|
||||||
|
+ '<td>' + escapeHtml(roleName) + '</td>'
|
||||||
|
+ '<td><select data-user-role-select="1">' + roleOptionsHtml(user.usergroups_id) + '</select></td>'
|
||||||
|
+ '<td><button class="btn btn-light" type="button" onclick="window.__saveUserRole(\'' + escapeHtml(user.pb_id) + '\')">保存角色</button></td>'
|
||||||
|
+ '</tr>'
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPermissionRoleOptions() {
|
||||||
|
if (!state.roles.length) {
|
||||||
|
permissionRoleSelect.innerHTML = '<option value="">暂无角色</option>'
|
||||||
|
permissionRoleSelect.disabled = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionRoleSelect.disabled = false
|
||||||
|
permissionRoleSelect.innerHTML = state.roles.map(function (role) {
|
||||||
|
return '<option value="' + escapeHtml(role.role_id) + '"' + (role.role_id === state.selectedPermissionRoleId ? ' selected' : '') + '>' + escapeHtml(role.role_name) + '</option>'
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOperationLabel(operation) {
|
||||||
|
const map = {
|
||||||
|
list: '列表',
|
||||||
|
view: '详情',
|
||||||
|
create: '新增',
|
||||||
|
update: '修改',
|
||||||
|
delete: '删除',
|
||||||
|
}
|
||||||
|
return map[operation] || operation
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuleSummary(config, selectedRoleId) {
|
||||||
|
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||||
|
const items = []
|
||||||
|
if (current.mode === 'public') items.push('公开可访问')
|
||||||
|
if (current.mode === 'authenticated') items.push('登录用户可访问')
|
||||||
|
if (current.includeManagePlatform) items.push('含 ManagePlatform')
|
||||||
|
if (current.mode === 'custom') items.push('自定义规则')
|
||||||
|
if (Array.isArray(current.roles) && current.roles.length) {
|
||||||
|
items.push('已分配角色数:' + current.roles.length)
|
||||||
|
}
|
||||||
|
return items.length ? items.join(',') : '当前无额外说明'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canControlRule(config) {
|
||||||
|
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||||
|
return !!state.selectedPermissionRoleId && current.mode !== 'custom' && current.mode !== 'public'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCollectionFullyChecked(collection) {
|
||||||
|
if (!collection || !collection.parsedRules) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const operations = ['list', 'view', 'create', 'update', 'delete']
|
||||||
|
let controllableCount = 0
|
||||||
|
let checkedCount = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < operations.length; i += 1) {
|
||||||
|
const config = collection.parsedRules[operations[i]]
|
||||||
|
if (!canControlRule(config)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
controllableCount += 1
|
||||||
|
if (config && Array.isArray(config.roles) && config.roles.indexOf(state.selectedPermissionRoleId) !== -1) {
|
||||||
|
checkedCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllableCount > 0 && controllableCount === checkedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCollectionControllableCount(collection) {
|
||||||
|
if (!collection || !collection.parsedRules) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const operations = ['list', 'view', 'create', 'update', 'delete']
|
||||||
|
let controllableCount = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < operations.length; i += 1) {
|
||||||
|
if (canControlRule(collection.parsedRules[operations[i]])) {
|
||||||
|
controllableCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return controllableCount
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRuleCard(collectionName, operation, config) {
|
||||||
|
const current = config || { mode: 'locked', includeManagePlatform: false, roles: [], rawExpression: '' }
|
||||||
|
const selectedRoleId = state.selectedPermissionRoleId
|
||||||
|
const checked = selectedRoleId && Array.isArray(current.roles) && current.roles.indexOf(selectedRoleId) !== -1
|
||||||
|
const canControl = canControlRule(current)
|
||||||
|
const summary = current.mode === 'public'
|
||||||
|
? '可公开访问'
|
||||||
|
: getRuleSummary(current, selectedRoleId)
|
||||||
|
return '<div class="rule-card" data-collection="' + escapeHtml(collectionName) + '" data-op="' + operation + '">'
|
||||||
|
+ '<h4>' + (canControl ? '<label class="rule-toggle"><input type="checkbox" data-rule-field="allowSelectedRole"' + (checked ? ' checked' : '') + ' /></label>' : '') + '<span>' + getOperationLabel(operation) + '</span></h4>'
|
||||||
|
+ '<div class="muted" style="margin-top:8px;">' + escapeHtml(summary) + '</div>'
|
||||||
|
+ (current.mode === 'custom' ? '<div class="muted" style="margin-top:8px;">当前操作使用 custom 规则,禁止修改</div>' : '')
|
||||||
|
+ '</div>'
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCollections() {
|
||||||
|
if (!state.collections.length) {
|
||||||
|
collectionTableBody.innerHTML = '<tr><td colspan="2" class="empty">暂无可管理集合。</td></tr>'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionTableBody.innerHTML = state.collections.map(function (collection) {
|
||||||
|
const allChecked = isCollectionFullyChecked(collection)
|
||||||
|
const controllableCount = getCollectionControllableCount(collection)
|
||||||
|
return '<tr data-collection-row="' + escapeHtml(collection.name) + '">'
|
||||||
|
+ '<td class="collection-col"><div class="collection-meta"><div><strong>' + escapeHtml(collection.name) + '</strong></div><div class="muted">' + escapeHtml(collection.type) + '</div><label class="rule-toggle"><input type="checkbox" data-rule-field="toggleCollection"' + (allChecked ? ' checked' : '') + (state.selectedPermissionRoleId && controllableCount > 0 ? '' : ' disabled') + ' /><span>全选</span></label></div></td>'
|
||||||
|
+ '<td><div class="rule-grid">'
|
||||||
|
+ renderRuleCard(collection.name, 'list', collection.parsedRules.list)
|
||||||
|
+ renderRuleCard(collection.name, 'view', collection.parsedRules.view)
|
||||||
|
+ renderRuleCard(collection.name, 'create', collection.parsedRules.create)
|
||||||
|
+ renderRuleCard(collection.name, 'update', collection.parsedRules.update)
|
||||||
|
+ renderRuleCard(collection.name, 'delete', collection.parsedRules.delete)
|
||||||
|
+ '</div></td>'
|
||||||
|
+ '</tr>'
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadContext() {
|
||||||
|
showLoading('正在加载权限管理数据...')
|
||||||
|
setStatus('正在加载权限管理数据...', '')
|
||||||
|
try {
|
||||||
|
const data = await requestJson('/context', { keyword: state.userKeyword })
|
||||||
|
state.roles = Array.isArray(data.roles) ? data.roles : []
|
||||||
|
state.users = Array.isArray(data.users) ? data.users : []
|
||||||
|
state.collections = Array.isArray(data.collections) ? data.collections : []
|
||||||
|
syncSelectedPermissionRole()
|
||||||
|
noteBox.textContent = data.note || '权限管理说明已加载。'
|
||||||
|
renderRoles()
|
||||||
|
renderUsers()
|
||||||
|
renderPermissionRoleOptions()
|
||||||
|
renderCollections()
|
||||||
|
setStatus('权限管理数据已刷新。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '加载权限管理数据失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleRowPayload(roleId) {
|
||||||
|
const targetId = decodeURIComponent(roleId)
|
||||||
|
const row = roleTableBody.querySelector('[data-role-id="' + targetId.replace(/"/g, '\\"') + '"]')
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('未找到对应角色行')
|
||||||
|
}
|
||||||
|
|
||||||
|
const find = function (fieldName) {
|
||||||
|
const input = row.querySelector('[data-role-field="' + fieldName + '"]')
|
||||||
|
return input ? input.value : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
original_role_id: targetId,
|
||||||
|
role_name: find('role_name'),
|
||||||
|
role_code: find('role_code'),
|
||||||
|
role_status: find('role_status'),
|
||||||
|
role_remark: find('role_remark'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuleBoxConfig(collectionName, operation) {
|
||||||
|
const collection = state.collections.find(function (item) {
|
||||||
|
return item.name === collectionName
|
||||||
|
})
|
||||||
|
const current = collection && collection.parsedRules ? collection.parsedRules[operation] : null
|
||||||
|
const base = current || {
|
||||||
|
mode: 'locked',
|
||||||
|
includeManagePlatform: false,
|
||||||
|
roles: [],
|
||||||
|
rawExpression: '',
|
||||||
|
}
|
||||||
|
if (base.mode === 'custom') {
|
||||||
|
return {
|
||||||
|
mode: 'custom',
|
||||||
|
includeManagePlatform: !!base.includeManagePlatform,
|
||||||
|
roles: Array.isArray(base.roles) ? base.roles.slice() : [],
|
||||||
|
rawExpression: base.rawExpression || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = collectionTableBody.querySelector('.rule-card[data-collection="' + collectionName.replace(/"/g, '\\"') + '"][data-op="' + operation + '"]')
|
||||||
|
const allowEl = box ? box.querySelector('[data-rule-field="allowSelectedRole"]') : null
|
||||||
|
const roles = Array.isArray(base.roles) ? base.roles.slice() : []
|
||||||
|
const selectedRoleId = state.selectedPermissionRoleId
|
||||||
|
const nextRoles = roles.filter(function (roleId) {
|
||||||
|
return roleId !== selectedRoleId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (selectedRoleId && allowEl && allowEl.checked && nextRoles.indexOf(selectedRoleId) === -1) {
|
||||||
|
nextRoles.push(selectedRoleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextMode = base.mode
|
||||||
|
if (nextMode === 'locked' && nextRoles.length) {
|
||||||
|
nextMode = 'roleBased'
|
||||||
|
} else if (nextMode === 'roleBased' && !nextRoles.length && !base.includeManagePlatform) {
|
||||||
|
nextMode = 'locked'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: nextMode,
|
||||||
|
includeManagePlatform: !!base.includeManagePlatform,
|
||||||
|
roles: nextRoles,
|
||||||
|
rawExpression: base.rawExpression || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRole() {
|
||||||
|
const payload = {
|
||||||
|
role_name: document.getElementById('newRoleName').value.trim(),
|
||||||
|
role_code: document.getElementById('newRoleCode').value.trim(),
|
||||||
|
role_status: document.getElementById('newRoleStatus').value.trim() || '1',
|
||||||
|
role_remark: document.getElementById('newRoleRemark').value.trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在新增角色...')
|
||||||
|
try {
|
||||||
|
await requestJson('/role-save', payload)
|
||||||
|
document.getElementById('newRoleName').value = ''
|
||||||
|
document.getElementById('newRoleCode').value = ''
|
||||||
|
document.getElementById('newRoleStatus').value = '1'
|
||||||
|
document.getElementById('newRoleRemark').value = ''
|
||||||
|
await loadContext()
|
||||||
|
setStatus('角色新增成功。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '新增角色失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRoleRow(roleId) {
|
||||||
|
showLoading('正在保存角色...')
|
||||||
|
try {
|
||||||
|
await requestJson('/role-save', getRoleRowPayload(roleId))
|
||||||
|
await loadContext()
|
||||||
|
setStatus('角色保存成功。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '保存角色失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRoleRow(roleId) {
|
||||||
|
const targetId = decodeURIComponent(roleId)
|
||||||
|
if (!window.confirm('确认删除角色「' + targetId + '」吗?这会清空绑定该角色的用户,并从已解析的集合规则中移除它。')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在删除角色...')
|
||||||
|
try {
|
||||||
|
await requestJson('/role-delete', { role_id: targetId })
|
||||||
|
await loadContext()
|
||||||
|
setStatus('角色删除成功。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '删除角色失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveUserRole(pbId) {
|
||||||
|
const row = userTableBody.querySelector('[data-user-id="' + pbId.replace(/"/g, '\\"') + '"]')
|
||||||
|
const select = row ? row.querySelector('[data-user-role-select="1"]') : null
|
||||||
|
const payload = {
|
||||||
|
pb_id: pbId,
|
||||||
|
usergroups_id: select ? select.value : '',
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在保存用户角色...')
|
||||||
|
try {
|
||||||
|
await requestJson('/user-role-update', payload)
|
||||||
|
await loadContext()
|
||||||
|
setStatus('用户角色已更新。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '更新用户角色失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCollectionRules(collectionName) {
|
||||||
|
const targetName = decodeURIComponent(collectionName)
|
||||||
|
if (!state.selectedPermissionRoleId) {
|
||||||
|
setStatus('请先选择一个要配置权限的角色。', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
collection_name: targetName,
|
||||||
|
rules: {
|
||||||
|
list: getRuleBoxConfig(targetName, 'list'),
|
||||||
|
view: getRuleBoxConfig(targetName, 'view'),
|
||||||
|
create: getRuleBoxConfig(targetName, 'create'),
|
||||||
|
update: getRuleBoxConfig(targetName, 'update'),
|
||||||
|
delete: getRuleBoxConfig(targetName, 'delete'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在同步集合权限...')
|
||||||
|
try {
|
||||||
|
await requestJson('/collection-save', payload)
|
||||||
|
await loadContext()
|
||||||
|
setStatus('已保存角色「' + (getRoleName(state.selectedPermissionRoleId) || '未命名角色') + '」在集合「' + targetName + '」上的权限。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '保存集合权限失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncManagePlatform() {
|
||||||
|
if (!window.confirm('确认将 ManagePlatform 同步为所有业务集合的全权限吗?这不会创建 _superusers,但会为业务表开放全部 CRUD。')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在同步 ManagePlatform 全权限...')
|
||||||
|
try {
|
||||||
|
const data = await requestJson('/manageplatform-sync', {})
|
||||||
|
await loadContext()
|
||||||
|
setStatus('已同步 ManagePlatform 全权限,共处理 ' + String((data && data.count) || 0) + ' 个集合。', 'success')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus(err.message || '同步 ManagePlatform 全权限失败', 'error')
|
||||||
|
} finally {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__saveRoleRow = saveRoleRow
|
||||||
|
window.__deleteRoleRow = deleteRoleRow
|
||||||
|
window.__saveUserRole = saveUserRole
|
||||||
|
window.__saveCollectionRules = saveCollectionRules
|
||||||
|
|
||||||
|
document.getElementById('createRoleBtn').addEventListener('click', createRole)
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', loadContext)
|
||||||
|
document.getElementById('syncManageBtn').addEventListener('click', syncManagePlatform)
|
||||||
|
permissionRoleSelect.addEventListener('change', function () {
|
||||||
|
state.selectedPermissionRoleId = permissionRoleSelect.value || ''
|
||||||
|
renderPermissionRoleOptions()
|
||||||
|
renderCollections()
|
||||||
|
})
|
||||||
|
collectionTableBody.addEventListener('change', function (event) {
|
||||||
|
const target = event.target
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = target.getAttribute('data-rule-field')
|
||||||
|
if (field === 'allowSelectedRole') {
|
||||||
|
const box = target.closest('.rule-card')
|
||||||
|
if (!box) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const collectionName = box.getAttribute('data-collection') || ''
|
||||||
|
saveCollectionRules(collectionName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'toggleCollection') {
|
||||||
|
const row = target.closest('[data-collection-row]')
|
||||||
|
if (!row) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const checkboxes = row.querySelectorAll('[data-rule-field="allowSelectedRole"]')
|
||||||
|
for (let i = 0; i < checkboxes.length; i += 1) {
|
||||||
|
if (!checkboxes[i].disabled) {
|
||||||
|
checkboxes[i].checked = !!target.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const collectionName = row.getAttribute('data-collection-row') || ''
|
||||||
|
saveCollectionRules(collectionName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
document.getElementById('searchUserBtn').addEventListener('click', function () {
|
||||||
|
state.userKeyword = document.getElementById('userKeywordInput').value.trim()
|
||||||
|
loadContext()
|
||||||
|
})
|
||||||
|
|
||||||
|
loadContext()
|
||||||
|
</script>
|
||||||
|
{{ template "theme_body" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -229,6 +229,9 @@ components:
|
|||||||
users_level:
|
users_level:
|
||||||
type: string
|
type: string
|
||||||
description: "用户等级"
|
description: "用户等级"
|
||||||
|
users_level_name:
|
||||||
|
type: string
|
||||||
|
description: "用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析"
|
||||||
users_tag:
|
users_tag:
|
||||||
type: string
|
type: string
|
||||||
description: "用户标签"
|
description: "用户标签"
|
||||||
@@ -293,6 +296,7 @@ components:
|
|||||||
users_phone: 手机号 | string
|
users_phone: 手机号 | string
|
||||||
users_phone_masked: 手机号脱敏值 | string
|
users_phone_masked: 手机号脱敏值 | string
|
||||||
users_level: 用户等级 | string
|
users_level: 用户等级 | string
|
||||||
|
users_level_name: 用户等级名称 | string
|
||||||
users_tag: 用户标签 | string
|
users_tag: 用户标签 | string
|
||||||
users_picture: 用户头像附件ID | string
|
users_picture: 用户头像附件ID | string
|
||||||
users_picture_url: 用户头像文件流链接 | string
|
users_picture_url: 用户头像文件流链接 | string
|
||||||
@@ -1230,6 +1234,7 @@ paths:
|
|||||||
创建平台用户 auth record。
|
创建平台用户 auth record。
|
||||||
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
||||||
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
||||||
|
首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||||
注册成功后直接返回 PocketBase 原生 auth token。
|
注册成功后直接返回 PocketBase 原生 auth token。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -1260,6 +1265,7 @@ paths:
|
|||||||
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
||||||
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
||||||
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||||
|
返回体中的 `user.users_level_name` 为服务端按“数据-会员等级”字典实时解析后的等级名称。
|
||||||
登录成功后直接返回 PocketBase 原生 auth token。
|
登录成功后直接返回 PocketBase 原生 auth token。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -1803,6 +1809,3 @@ paths:
|
|||||||
description: "非 ManagePlatform 用户无权访问"
|
description: "非 ManagePlatform 用户无权访问"
|
||||||
'415':
|
'415':
|
||||||
description: "请求体必须为 application/json"
|
description: "请求体必须为 application/json"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
135
pocket-base/spec/openapi-wx/paths/attachments.yaml
Normal file
135
pocket-base/spec/openapi-wx/paths/attachments.yaml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
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'
|
||||||
|
examples:
|
||||||
|
byAttachmentsId:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
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
|
||||||
|
byAttachmentsIds:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
collectionName: 集合名称 | string
|
||||||
|
attachments_id: ATT-1774599142438-8n1UcU
|
||||||
|
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
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
collectionName: 集合名称 | string
|
||||||
|
attachments_id: ATT-1774599143999-7pQkLm
|
||||||
|
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'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
335
pocket-base/spec/openapi-wx/paths/cart-hooks.yaml
Normal file
335
pocket-base/spec/openapi-wx/paths/cart-hooks.yaml
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
cartList:
|
||||||
|
post:
|
||||||
|
operationId: postCartList
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 按索引字段模糊查询购物车列表
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 查询当前登录用户的购物车列表。
|
||||||
|
|
||||||
|
查询规则:
|
||||||
|
- 仅允许查询当前 `Authorization` 对应用户自己的购物车记录
|
||||||
|
- 支持通过 `keyword` 对多个索引相关字段做统一模糊搜索,当前实现覆盖:
|
||||||
|
- `cart_id`
|
||||||
|
- `cart_number`
|
||||||
|
- `cart_product_id`
|
||||||
|
- `product_name`
|
||||||
|
- 支持按 `cart_status` 精确过滤
|
||||||
|
- 支持按 `cart_number` 精确过滤
|
||||||
|
|
||||||
|
目标软删除契约:
|
||||||
|
- 目标行为应仅返回 `is_delete = 0` 的记录
|
||||||
|
- 当前仓库实现尚未显式追加 `is_delete = 0` 过滤,请以实际后端代码为准
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartListRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartListResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
cartDetail:
|
||||||
|
post:
|
||||||
|
operationId: postCartDetail
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 按 cart_id 精确查询购物车详情
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 按 `cart_id` 查询单条购物车记录。
|
||||||
|
|
||||||
|
查询规则:
|
||||||
|
- 仅允许访问当前 `Authorization` 对应用户自己的购物车记录
|
||||||
|
- 查询键为业务 ID `cart_id`,不是 PocketBase 原生 `recordId`
|
||||||
|
|
||||||
|
目标软删除契约:
|
||||||
|
- 目标行为应仅允许查询 `is_delete = 0` 的记录
|
||||||
|
- 当前仓库实现尚未显式追加 `is_delete = 0` 过滤,请以实际后端代码为准
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartDetailRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权访问该购物车记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 未找到对应购物车记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
cartCreate:
|
||||||
|
post:
|
||||||
|
operationId: postCartCreate
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 新增购物车记录
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 新增一条购物车记录。
|
||||||
|
|
||||||
|
创建规则:
|
||||||
|
- 服务端自动根据当前 token 写入 `cart_owner`
|
||||||
|
- 服务端自动生成 `cart_id`
|
||||||
|
- 若未传 `cart_number`,服务端会自动生成展示编号
|
||||||
|
- `cart_product_id`、`cart_product_quantity`、`cart_at_price` 为必填
|
||||||
|
|
||||||
|
目标软删除契约:
|
||||||
|
- 新建记录应默认为 `is_delete = 0`
|
||||||
|
- 当前仓库导出响应中尚未显式返回 `is_delete` 字段
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartCreateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 创建成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、产品不存在或创建失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
cartUpdate:
|
||||||
|
post:
|
||||||
|
operationId: postCartUpdate
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 修改购物车记录
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 按 `cart_id` 更新购物车记录。
|
||||||
|
|
||||||
|
更新规则:
|
||||||
|
- 仅允许修改当前 `Authorization` 对应用户自己的购物车记录
|
||||||
|
- `cart_id` 为必填,用于精确定位目标记录
|
||||||
|
- 其余字段均为可选增量更新
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartUpdateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 更新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、产品不存在或更新失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权访问该购物车记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 未找到待修改的购物车记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
cartDelete:
|
||||||
|
post:
|
||||||
|
operationId: postCartDelete
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 软删除购物车记录
|
||||||
|
description: |
|
||||||
|
目标契约:按 `cart_id` 软删除购物车记录,将 `is_delete` 标记为 `1`,而不是物理删除。
|
||||||
|
|
||||||
|
当前仓库实现差异:
|
||||||
|
- 当前 `cartOrderService.deleteCart()` 仍直接调用 `$app.delete(record)` 做物理删除
|
||||||
|
- 因此当前后端实现与本软删除契约不一致
|
||||||
|
- 若要严格按本文档执行,需先同步调整后端服务实现,并在列表/详情接口中补充 `is_delete = 0` 过滤
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartDeleteRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 删除成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartDeleteResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或删除失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权访问该购物车记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 未找到待删除的购物车记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
250
pocket-base/spec/openapi-wx/paths/cart-native.yaml
Normal file
250
pocket-base/spec/openapi-wx/paths/cart-native.yaml
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
cartRecords:
|
||||||
|
get:
|
||||||
|
operationId: getPocketBaseCartRecords
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 查询当前用户购物车列表或按业务 ID 精确查询
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records list 接口查询 `tbl_cart`。
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `listRule = @request.auth.id != "" && cart_owner = @request.auth.openid && is_delete = 0`
|
||||||
|
- 因此调用方只能读到 `cart_owner` 等于自己 `openid` 且未软删除的记录
|
||||||
|
|
||||||
|
标准调用方式:
|
||||||
|
1. 查询当前登录用户全部购物车:
|
||||||
|
- 不传 `filter`
|
||||||
|
- 可选传 `sort=-cart_create`
|
||||||
|
2. 按业务 ID 精确查单条:
|
||||||
|
- `filter=cart_id="CART-..."`
|
||||||
|
- `perPage=1`
|
||||||
|
- `page=1`
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 这是 PocketBase 原生返回结构,不是 hooks 统一包装
|
||||||
|
- 即使不传 `filter`,返回结果也会继续受 `listRule` 限制
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: filter
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: |
|
||||||
|
PocketBase 标准过滤表达式。
|
||||||
|
|
||||||
|
- 查当前用户全部购物车时:不传
|
||||||
|
- 按 `cart_id` 精确查单条时:`cart_id="CART-1770000000000-abc123"`
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: cart_id="CART-1770000000000-abc123"
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
default: 1
|
||||||
|
- name: perPage
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
default: 20
|
||||||
|
- name: sort
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: PocketBase 原生排序表达式,推荐 `-cart_create`
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: -cart_create
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartListResponse'
|
||||||
|
'400':
|
||||||
|
description: 查询参数错误或不满足 listRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
post:
|
||||||
|
operationId: postPocketBaseCartRecord
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 创建购物车记录
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records create 接口向 `tbl_cart` 新增记录。
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `createRule = @request.auth.id != "" && @request.body.cart_owner = @request.auth.openid`
|
||||||
|
- 因此客户端创建时必须显式提交 `cart_owner`,并且值必须等于当前 token 对应的 `openid`
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
- 不能依赖服务端自动补 owner
|
||||||
|
- 不能省略 `cart_owner`
|
||||||
|
- 不满足规则时 PocketBase 会直接返回 `400`
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartCreateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 创建成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、违反字段约束或不满足 createRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
|
||||||
|
cartRecordById:
|
||||||
|
patch:
|
||||||
|
operationId: patchPocketBaseCartRecordByRecordId
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 更新购物车记录
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records update 接口更新 `tbl_cart`。
|
||||||
|
|
||||||
|
标准调用流程:
|
||||||
|
1. 先通过 `GET /pb/api/collections/tbl_cart/records?filter=cart_id="..."&perPage=1&page=1` 找到原生 `recordId`
|
||||||
|
2. 再调用当前 `PATCH /pb/api/collections/tbl_cart/records/{recordId}`
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `updateRule = @request.auth.id != "" && cart_owner = @request.auth.openid`
|
||||||
|
- 调用方只能修改自己的购物车记录
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: recordId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: l2r3nq7rqhuob0h
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartUpdateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 更新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或违反字段约束
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'404':
|
||||||
|
description: 记录不存在或不满足 updateRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
delete:
|
||||||
|
operationId: deletePocketBaseCartRecordByRecordId
|
||||||
|
tags:
|
||||||
|
- 购物车
|
||||||
|
summary: 删除购物车记录
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records delete 接口删除 `tbl_cart`。
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `deleteRule = @request.auth.id != "" && cart_owner = @request.auth.openid`
|
||||||
|
- 调用方只能删除自己的购物车记录
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: recordId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: l2r3nq7rqhuob0h
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 删除成功
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'404':
|
||||||
|
description: 记录不存在或不满足 deleteRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
290
pocket-base/spec/openapi-wx/paths/company.yaml
Normal file
290
pocket-base/spec/openapi-wx/paths/company.yaml
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
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'
|
||||||
|
examples:
|
||||||
|
default:
|
||||||
|
value:
|
||||||
|
company_name: 微信侧测试企业
|
||||||
|
company_type: 渠道商
|
||||||
|
company_entity: 张三
|
||||||
|
company_usci: '91310000123456789A'
|
||||||
|
company_nationality: 中国
|
||||||
|
company_nationality_code: CN
|
||||||
|
company_province: 上海
|
||||||
|
company_province_code: '310000'
|
||||||
|
company_city: 上海
|
||||||
|
company_city_code: '310100'
|
||||||
|
company_district: 浦东新区
|
||||||
|
company_district_code: '310115'
|
||||||
|
company_postalcode: '200000'
|
||||||
|
company_add: 上海市浦东新区XX路1号
|
||||||
|
company_status: 有效
|
||||||
|
company_level: A
|
||||||
|
company_owner_openid: wx-openid-owner-001
|
||||||
|
company_remark: 小程序公开创建示例
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 创建成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或违反当前集合约束
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
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'
|
||||||
|
examples:
|
||||||
|
byCompanyId:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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
|
||||||
|
listAll:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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
|
||||||
|
notFoundByCompanyId:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items: []
|
||||||
|
'400':
|
||||||
|
description: 查询参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
|
||||||
|
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'
|
||||||
|
examples:
|
||||||
|
default:
|
||||||
|
value:
|
||||||
|
company_name: 微信侧测试企业(更新)
|
||||||
|
company_status: 有效
|
||||||
|
company_level: S
|
||||||
|
company_owner_openid: wx-openid-owner-001
|
||||||
|
company_district: 徐汇区
|
||||||
|
company_district_code: '310104'
|
||||||
|
company_remark: 通过 company_id 先定位再修改
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 更新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCompanyRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或违反集合约束
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 当前调用方没有 update 权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'404':
|
||||||
|
description: 记录不存在
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
222
pocket-base/spec/openapi-wx/paths/documents.yaml
Normal file
222
pocket-base/spec/openapi-wx/paths/documents.yaml
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
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'
|
||||||
|
examples:
|
||||||
|
listAll:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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: 正文内容 | 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: 0
|
||||||
|
document_download_count: 0
|
||||||
|
document_favorite_count: 0
|
||||||
|
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
|
||||||
|
filterByTypeToken:
|
||||||
|
value:
|
||||||
|
page: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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: 正文内容 | 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: 0
|
||||||
|
document_download_count: 0
|
||||||
|
document_favorite_count: 0
|
||||||
|
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
|
||||||
|
- id: ofy47wp9mmm0aub
|
||||||
|
collectionId: pbc_3636602973
|
||||||
|
collectionName: tbl_document
|
||||||
|
created: '2026-03-28 07:20:00.000Z'
|
||||||
|
updated: '2026-03-28 07:20:00.000Z'
|
||||||
|
document_id: DOC-1774680568340-TeUSQn
|
||||||
|
document_create: '2026-03-28 08:22:48.000Z'
|
||||||
|
document_effect_date: ''
|
||||||
|
document_expiry_date: ''
|
||||||
|
document_title: 易从碳达人节能系统,为酒店每天每间房省二元,以智能推动酒店ESG双碳落地!上海酒店用品展我们在E7A01等您!!
|
||||||
|
document_type: DICT-1774599144591-hAEFQj@UT1
|
||||||
|
document_subtitle: ''
|
||||||
|
document_summary: ''
|
||||||
|
document_content: ''
|
||||||
|
document_image: ATT-1774680568287-zuhJWN
|
||||||
|
document_video: ''
|
||||||
|
document_file: ''
|
||||||
|
document_status: 有效
|
||||||
|
document_owner: su13106859882
|
||||||
|
document_relation_model: ''
|
||||||
|
document_keywords: ''
|
||||||
|
document_share_count: 0
|
||||||
|
document_download_count: 0
|
||||||
|
document_favorite_count: 0
|
||||||
|
document_embedding_status: ''
|
||||||
|
document_embedding_error: ''
|
||||||
|
document_embedding_lasttime: ''
|
||||||
|
document_vector_version: ''
|
||||||
|
document_product_categories: ''
|
||||||
|
document_application_scenarios: ''
|
||||||
|
document_hotel_type: ''
|
||||||
|
document_remark: ''
|
||||||
|
'400':
|
||||||
|
description: 查询参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
334
pocket-base/spec/openapi-wx/paths/order-hooks.yaml
Normal file
334
pocket-base/spec/openapi-wx/paths/order-hooks.yaml
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
orderList:
|
||||||
|
post:
|
||||||
|
operationId: postOrderList
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 按索引字段模糊查询订单列表
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 查询当前登录用户的订单列表。
|
||||||
|
|
||||||
|
查询规则:
|
||||||
|
- 仅允许查询当前 `Authorization` 对应用户自己的订单记录
|
||||||
|
- 支持通过 `keyword` 对多个索引相关字段做统一模糊搜索,当前实现覆盖:
|
||||||
|
- `order_id`
|
||||||
|
- `order_number`
|
||||||
|
- `order_source_id`
|
||||||
|
- 支持按 `order_status` 精确过滤
|
||||||
|
- 支持按 `order_source` 精确过滤
|
||||||
|
|
||||||
|
目标软删除契约:
|
||||||
|
- 目标行为应仅返回 `is_delete = 0` 的记录
|
||||||
|
- 当前仓库实现尚未显式追加 `is_delete = 0` 过滤,请以实际后端代码为准
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: false
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderListRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderListResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
orderDetail:
|
||||||
|
post:
|
||||||
|
operationId: postOrderDetail
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 按 order_id 精确查询订单详情
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 按 `order_id` 查询单条订单记录。
|
||||||
|
|
||||||
|
查询规则:
|
||||||
|
- 仅允许访问当前 `Authorization` 对应用户自己的订单记录
|
||||||
|
- 查询键为业务 ID `order_id`,不是 PocketBase 原生 `recordId`
|
||||||
|
|
||||||
|
目标软删除契约:
|
||||||
|
- 目标行为应仅允许查询 `is_delete = 0` 的记录
|
||||||
|
- 当前仓库实现尚未显式追加 `is_delete = 0` 过滤,请以实际后端代码为准
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderDetailRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权访问该订单记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 未找到对应订单记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
orderCreate:
|
||||||
|
post:
|
||||||
|
operationId: postOrderCreate
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 新增订单记录
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 新增一条订单记录。
|
||||||
|
|
||||||
|
创建规则:
|
||||||
|
- 服务端自动根据当前 token 写入 `order_owner`
|
||||||
|
- 服务端自动生成 `order_id`
|
||||||
|
- 若未传 `order_number`,服务端会自动生成展示编号
|
||||||
|
- `order_source`、`order_source_id`、`order_snap`、`order_amount` 为必填
|
||||||
|
|
||||||
|
目标软删除契约:
|
||||||
|
- 新建记录应默认为 `is_delete = 0`
|
||||||
|
- 当前仓库导出响应中尚未显式返回 `is_delete` 字段
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderCreateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 创建成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或创建失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
orderUpdate:
|
||||||
|
post:
|
||||||
|
operationId: postOrderUpdate
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 修改订单记录
|
||||||
|
description: |
|
||||||
|
调用自定义 hooks API 按 `order_id` 更新订单记录。
|
||||||
|
|
||||||
|
更新规则:
|
||||||
|
- 仅允许修改当前 `Authorization` 对应用户自己的订单记录
|
||||||
|
- `order_id` 为必填,用于精确定位目标记录
|
||||||
|
- 其余字段均为可选增量更新
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderUpdateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 更新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或更新失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权访问该订单记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 未找到待修改的订单记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
orderDelete:
|
||||||
|
post:
|
||||||
|
operationId: postOrderDelete
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 软删除订单记录
|
||||||
|
description: |
|
||||||
|
目标契约:按 `order_id` 软删除订单记录,将 `is_delete` 标记为 `1`,而不是物理删除。
|
||||||
|
|
||||||
|
当前仓库实现差异:
|
||||||
|
- 当前 `cartOrderService.deleteOrder()` 仍直接调用 `$app.delete(record)` 做物理删除
|
||||||
|
- 因此当前后端实现与本软删除契约不一致
|
||||||
|
- 若要严格按本文档执行,需先同步调整后端服务实现,并在列表/详情接口中补充 `is_delete = 0` 过滤
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderDeleteRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 删除成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderDeleteResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或删除失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权访问该订单记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 未找到待删除的订单记录
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体必须为 application/json
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 重复请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
241
pocket-base/spec/openapi-wx/paths/order-native.yaml
Normal file
241
pocket-base/spec/openapi-wx/paths/order-native.yaml
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
orderRecords:
|
||||||
|
get:
|
||||||
|
operationId: getPocketBaseOrderRecords
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 查询当前用户订单列表或按业务 ID 精确查询
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records list 接口查询 `tbl_order`。
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `listRule = @request.auth.id != "" && order_owner = @request.auth.openid && is_delete = 0`
|
||||||
|
- 因此调用方只能读到 `order_owner` 等于自己 `openid` 且未软删除的记录
|
||||||
|
|
||||||
|
标准调用方式:
|
||||||
|
1. 查询当前登录用户全部订单:
|
||||||
|
- 不传 `filter`
|
||||||
|
- 可选传 `sort=-order_create`
|
||||||
|
2. 按业务 ID 精确查单条:
|
||||||
|
- `filter=order_id="ORDER-..."`
|
||||||
|
- `perPage=1`
|
||||||
|
- `page=1`
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: filter
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: order_id="ORDER-1770000000000-abc123"
|
||||||
|
- name: page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
default: 1
|
||||||
|
- name: perPage
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
default: 20
|
||||||
|
- name: sort
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: PocketBase 原生排序表达式,推荐 `-order_create`
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: -order_create
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderListResponse'
|
||||||
|
'400':
|
||||||
|
description: 查询参数错误或不满足 listRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
post:
|
||||||
|
operationId: postPocketBaseOrderRecord
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 创建订单记录
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records create 接口向 `tbl_order` 新增记录。
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `createRule = @request.auth.id != "" && @request.body.order_owner = @request.auth.openid`
|
||||||
|
- 因此客户端创建时必须显式提交 `order_owner`,并且值必须等于当前 token 对应的 `openid`
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
- 不能依赖服务端自动补 owner
|
||||||
|
- 不能省略 `order_owner`
|
||||||
|
- 不满足规则时 PocketBase 会直接返回 `400`
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderCreateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 创建成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、违反字段约束或不满足 createRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
|
||||||
|
orderRecordById:
|
||||||
|
patch:
|
||||||
|
operationId: patchPocketBaseOrderRecordByRecordId
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 更新订单记录
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records update 接口更新 `tbl_order`。
|
||||||
|
|
||||||
|
标准调用流程:
|
||||||
|
1. 先通过 `GET /pb/api/collections/tbl_order/records?filter=order_id="..."&perPage=1&page=1` 找到原生 `recordId`
|
||||||
|
2. 再调用当前 `PATCH /pb/api/collections/tbl_order/records/{recordId}`
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `updateRule = @request.auth.id != "" && order_owner = @request.auth.openid`
|
||||||
|
- 调用方只能修改自己的订单记录
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: recordId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: l2r3nq7rqhuob0h
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderUpdateRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 更新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderRecord'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或违反字段约束
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'404':
|
||||||
|
description: 记录不存在或不满足 updateRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
delete:
|
||||||
|
operationId: deletePocketBaseOrderRecordByRecordId
|
||||||
|
tags:
|
||||||
|
- 订单
|
||||||
|
summary: 删除订单记录
|
||||||
|
description: |
|
||||||
|
使用 PocketBase 原生 records delete 接口删除 `tbl_order`。
|
||||||
|
|
||||||
|
当前线上权限规则:
|
||||||
|
- `deleteRule = @request.auth.id != "" && order_owner = @request.auth.openid`
|
||||||
|
- 调用方只能删除自己的订单记录
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
parameters:
|
||||||
|
- name: recordId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: l2r3nq7rqhuob0h
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: 删除成功
|
||||||
|
'401':
|
||||||
|
description: token 缺失、无效或已过期
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'404':
|
||||||
|
description: 记录不存在或不满足 deleteRule
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
119
pocket-base/spec/openapi-wx/paths/products.yaml
Normal file
119
pocket-base/spec/openapi-wx/paths/products.yaml
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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'
|
||||||
|
examples:
|
||||||
|
byCategoryAscSort:
|
||||||
|
value:
|
||||||
|
page: <页码>|<integer>
|
||||||
|
perPage: <每页条数>|<integer>
|
||||||
|
totalItems: <总记录数>|<integer>
|
||||||
|
totalPages: <总页数>|<integer>
|
||||||
|
items:
|
||||||
|
- id: <PocketBase记录主键>|<string>
|
||||||
|
collectionId: <集合ID>|<string>
|
||||||
|
collectionName: <集合名称>|<string>
|
||||||
|
created: <记录创建时间>|<string>
|
||||||
|
updated: <记录更新时间>|<string>
|
||||||
|
prod_list_id: <产品列表业务ID>|<string>
|
||||||
|
prod_list_name: <产品名称>|<string>
|
||||||
|
prod_list_modelnumber: <产品型号>|<string>
|
||||||
|
prod_list_icon: <产品图标附件ID>|<string>
|
||||||
|
prod_list_description: <产品说明>|<string>
|
||||||
|
prod_list_feature: <产品特色>|<string>
|
||||||
|
prod_list_parameters:
|
||||||
|
- name: <属性名>|<string>
|
||||||
|
value: <属性值>|<string>
|
||||||
|
prod_list_plantype: <产品方案>|<string>
|
||||||
|
prod_list_category: <产品分类>|<string>
|
||||||
|
prod_list_sort: 10
|
||||||
|
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: 1999
|
||||||
|
prod_list_vip_price:
|
||||||
|
- viplevel: VIP1
|
||||||
|
price: 1899
|
||||||
|
prod_list_remark: <备注>|<string>
|
||||||
|
'400':
|
||||||
|
description: 查询参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'403':
|
||||||
|
description: 集合规则被锁定或服务端权限设置异常
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseNativeError'
|
||||||
89
pocket-base/spec/openapi-wx/paths/system.yaml
Normal file
89
pocket-base/spec/openapi-wx/paths/system.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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'
|
||||||
|
'400':
|
||||||
|
description: 请求参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
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'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 刷新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/RefreshTokenResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或微信 code 换取失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 无效,且未提供有效的 `users_wx_code`
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 当前用户不存在
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体不是 JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
117
pocket-base/spec/openapi-wx/paths/wechat-auth.yaml
Normal file
117
pocket-base/spec/openapi-wx/paths/wechat-auth.yaml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
wechatLogin:
|
||||||
|
post:
|
||||||
|
security: []
|
||||||
|
operationId: postWechatLogin
|
||||||
|
tags:
|
||||||
|
- 微信认证
|
||||||
|
summary: 微信登录或首次注册
|
||||||
|
description: |
|
||||||
|
使用微信 `users_wx_code` 换取微信 openid。
|
||||||
|
若 `tbl_auth_users` 中不存在对应用户,则自动创建新 auth 用户并返回 token。
|
||||||
|
首次注册时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/WechatLoginRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 登录或注册成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/AuthSuccessResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误或保存用户失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: 认证失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体不是 JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
|
||||||
|
wechatProfile:
|
||||||
|
post:
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
operationId: postWechatProfile
|
||||||
|
tags:
|
||||||
|
- 微信认证
|
||||||
|
summary: 更新微信用户资料
|
||||||
|
description: |
|
||||||
|
基于当前 `Authorization` 对应的 auth 用户按“非空字段增量更新”资料。
|
||||||
|
|
||||||
|
更新规则:
|
||||||
|
- 所有字段都不是必填
|
||||||
|
- 如果传了 `users_phone_code`,服务端优先调用微信接口换取真实手机号并写入 `users_phone`
|
||||||
|
- 如果没传 `users_phone_code`,但传了 `users_phone`,则直接将该手机号写入数据库
|
||||||
|
- 如果上传了 `users_picture`、`users_id_pic_a`、`users_id_pic_b`、`users_title_picture`,会按附件 ID 进行关联校验并更新
|
||||||
|
- 若当前用户类型为 `游客`,且本次未显式传 `users_type`,服务端会自动升级为 `注册用户`
|
||||||
|
- 如果某个字段未传或传空,则不会清空数据库中的已有值
|
||||||
|
- 只有请求体里非空的字段才会更新到数据库
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/WechatProfileRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 更新成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/WechatProfileResponse'
|
||||||
|
'400':
|
||||||
|
description: 参数错误、手机号已被占用、附件 ID 无效、微信手机号换取失败或资料更新失败
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: token 无效或缺少 openid
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'415':
|
||||||
|
description: 请求体不是 JSON
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'429':
|
||||||
|
description: 请求过于频繁
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 服务端错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/ErrorResponse'
|
||||||
125
pocket-base/spec/openapi-wx/schemas/attachments.yaml
Normal file
125
pocket-base/spec/openapi-wx/schemas/attachments.yaml
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
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
|
||||||
185
pocket-base/spec/openapi-wx/schemas/cart-hooks.yaml
Normal file
185
pocket-base/spec/openapi-wx/schemas/cart-hooks.yaml
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
CartRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/HookRecordBase'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
description: 购物车业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
description: 购物车名称或分组号
|
||||||
|
example: wx-user-20260403153000
|
||||||
|
cart_create:
|
||||||
|
type: string
|
||||||
|
description: 购物车项创建时间,由数据库自动生成
|
||||||
|
example: 2026-04-03 15:30:00.000Z
|
||||||
|
cart_owner:
|
||||||
|
type: string
|
||||||
|
description: 购物车所有者 openid
|
||||||
|
example: wx-openid-user-001
|
||||||
|
cart_product_id:
|
||||||
|
type: string
|
||||||
|
description: 产品业务 ID
|
||||||
|
example: PROD-1770000000000-abcd12
|
||||||
|
cart_product_quantity:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 产品数量
|
||||||
|
example: 2
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
description: 购物车状态
|
||||||
|
example: 有效
|
||||||
|
cart_at_price:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 加入购物车时价格
|
||||||
|
example: 1999
|
||||||
|
cart_remark:
|
||||||
|
type: string
|
||||||
|
description: 备注
|
||||||
|
example: 小程序加入购物车示例
|
||||||
|
is_delete:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 软删除标记;目标契约字段,当前 hooks 响应可能尚未显式透出
|
||||||
|
example: 0
|
||||||
|
product_name:
|
||||||
|
type: string
|
||||||
|
description: 产品名称(服务端联动补充)
|
||||||
|
example: BAI 智能主机
|
||||||
|
product_modelnumber:
|
||||||
|
type: string
|
||||||
|
description: 产品型号(服务端联动补充)
|
||||||
|
example: BAI-HOST-01
|
||||||
|
product_basic_price:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
- 'null'
|
||||||
|
description: 产品基础价格(服务端联动补充)
|
||||||
|
example: 1999
|
||||||
|
|
||||||
|
CartListRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
keyword:
|
||||||
|
type: string
|
||||||
|
description: 对 `cart_id`、`cart_number`、`cart_product_id` 等索引相关字段的统一模糊搜索关键字
|
||||||
|
example: CART-1770
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
description: 购物车状态精确过滤
|
||||||
|
example: 有效
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
description: 购物车名称或分组号精确过滤
|
||||||
|
example: wx-user-20260403153000
|
||||||
|
|
||||||
|
CartDetailRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cart_id
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
description: 购物车业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
|
||||||
|
CartCreateRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cart_product_id
|
||||||
|
- cart_product_quantity
|
||||||
|
- cart_at_price
|
||||||
|
properties:
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
description: 可选;未传时服务端自动生成
|
||||||
|
cart_product_id:
|
||||||
|
type: string
|
||||||
|
description: 产品业务 ID
|
||||||
|
example: PROD-1770000000000-abcd12
|
||||||
|
cart_product_quantity:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 产品数量,需为正整数
|
||||||
|
example: 2
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
description: 可选;未传时默认 `有效`
|
||||||
|
example: 有效
|
||||||
|
cart_at_price:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 加入购物车时价格
|
||||||
|
example: 1999
|
||||||
|
cart_remark:
|
||||||
|
type: string
|
||||||
|
description: 备注
|
||||||
|
example: 小程序加入购物车示例
|
||||||
|
|
||||||
|
CartUpdateRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cart_id
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
description: 购物车业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
cart_product_id:
|
||||||
|
type: string
|
||||||
|
cart_product_quantity:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
cart_at_price:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
cart_remark:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
CartDeleteRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cart_id
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
description: 购物车业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
|
||||||
|
CartListResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CartRecord'
|
||||||
|
|
||||||
|
CartDeleteResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
description: 购物车业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
is_delete:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 目标软删除标记值;当前实现可能仍返回物理删除结果
|
||||||
|
example: 1
|
||||||
134
pocket-base/spec/openapi-wx/schemas/cart-native.yaml
Normal file
134
pocket-base/spec/openapi-wx/schemas/cart-native.yaml
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
PocketBaseCartFields:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
description: 购物车业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
description: 购物车名称或分组号
|
||||||
|
example: wx-user-20260403153000
|
||||||
|
cart_create:
|
||||||
|
type: string
|
||||||
|
description: 购物车项创建时间,由数据库自动生成
|
||||||
|
example: 2026-04-03 15:30:00.000Z
|
||||||
|
cart_owner:
|
||||||
|
type: string
|
||||||
|
description: 购物车所有者 openid
|
||||||
|
example: wx-openid-user-001
|
||||||
|
cart_product_id:
|
||||||
|
type: string
|
||||||
|
description: 产品业务 ID
|
||||||
|
example: PROD-1770000000000-abcd12
|
||||||
|
cart_product_quantity:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 产品数量
|
||||||
|
example: 2
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
description: 购物车状态
|
||||||
|
example: 有效
|
||||||
|
cart_at_price:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
description: 加入购物车时价格
|
||||||
|
example: 1999
|
||||||
|
cart_remark:
|
||||||
|
type: string
|
||||||
|
description: 备注
|
||||||
|
example: 小程序加入购物车示例
|
||||||
|
|
||||||
|
PocketBaseCartRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase'
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartFields'
|
||||||
|
|
||||||
|
PocketBaseCartCreateRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cart_id
|
||||||
|
- cart_number
|
||||||
|
- cart_owner
|
||||||
|
- cart_product_id
|
||||||
|
- cart_product_quantity
|
||||||
|
- cart_status
|
||||||
|
- cart_at_price
|
||||||
|
properties:
|
||||||
|
cart_id:
|
||||||
|
type: string
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
cart_owner:
|
||||||
|
type: string
|
||||||
|
description: 必须显式提交,且值必须等于当前 token 对应 openid
|
||||||
|
cart_product_id:
|
||||||
|
type: string
|
||||||
|
cart_product_quantity:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
cart_at_price:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
cart_remark:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
PocketBaseCartUpdateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cart_number:
|
||||||
|
type: string
|
||||||
|
cart_owner:
|
||||||
|
type: string
|
||||||
|
description: 若提交,必须仍等于当前 token 对应 openid
|
||||||
|
cart_product_id:
|
||||||
|
type: string
|
||||||
|
cart_product_quantity:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
cart_status:
|
||||||
|
type: string
|
||||||
|
cart_at_price:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
cart_remark:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
PocketBaseCartListResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- page
|
||||||
|
- perPage
|
||||||
|
- totalItems
|
||||||
|
- totalPages
|
||||||
|
- items
|
||||||
|
properties:
|
||||||
|
page:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
perPage:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
totalItems:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
totalPages:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCartRecord'
|
||||||
125
pocket-base/spec/openapi-wx/schemas/common.yaml
Normal file
125
pocket-base/spec/openapi-wx/schemas/common.yaml
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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:
|
||||||
|
任意业务字段: 业务响应数据 | object
|
||||||
|
|
||||||
|
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:
|
||||||
|
任意错误字段: 错误附加信息 | object
|
||||||
|
|
||||||
|
HookRecordBase:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pb_id:
|
||||||
|
type: string
|
||||||
|
description: PocketBase 记录主键 id
|
||||||
|
example: l2r3nq7rqhuob0h
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
description: PocketBase 系统创建时间
|
||||||
|
example: 2026-04-03 15:30:00.000Z
|
||||||
|
updated:
|
||||||
|
type: string
|
||||||
|
description: PocketBase 系统更新时间
|
||||||
|
example: 2026-04-03 15:35:00.000Z
|
||||||
|
|
||||||
|
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: PocketBase原生错误信息 | string
|
||||||
|
data:
|
||||||
|
任意错误字段: 原生错误附加信息 | object
|
||||||
|
|
||||||
|
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: 集合ID | string
|
||||||
|
collectionName: 集合名称 | string
|
||||||
|
created: 记录创建时间 | string
|
||||||
|
updated: 记录更新时间 | string
|
||||||
412
pocket-base/spec/openapi-wx/schemas/company.yaml
Normal file
412
pocket-base/spec/openapi-wx/schemas/company.yaml
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
PocketBaseCompanyRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase'
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseCompanyFields'
|
||||||
|
example:
|
||||||
|
id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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
|
||||||
221
pocket-base/spec/openapi-wx/schemas/documents.yaml
Normal file
221
pocket-base/spec/openapi-wx/schemas/documents.yaml
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
PocketBaseDocumentRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase'
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseDocumentFields'
|
||||||
|
example:
|
||||||
|
id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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: 正文内容 | 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: 0
|
||||||
|
document_download_count: 0
|
||||||
|
document_favorite_count: 0
|
||||||
|
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: 页码 | integer
|
||||||
|
perPage: 每页条数 | integer
|
||||||
|
totalItems: 总记录数 | integer
|
||||||
|
totalPages: 总页数 | integer
|
||||||
|
items:
|
||||||
|
- id: PocketBase记录主键 | string
|
||||||
|
collectionId: 集合ID | string
|
||||||
|
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: 正文内容 | 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: 0
|
||||||
|
document_download_count: 0
|
||||||
|
document_favorite_count: 0
|
||||||
|
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
|
||||||
191
pocket-base/spec/openapi-wx/schemas/order-hooks.yaml
Normal file
191
pocket-base/spec/openapi-wx/schemas/order-hooks.yaml
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
OrderRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/HookRecordBase'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
description: 订单业务 ID
|
||||||
|
example: ORDER-1770000000000-abc123
|
||||||
|
order_number:
|
||||||
|
type: string
|
||||||
|
description: 订单编号
|
||||||
|
example: wx-user-20260403153500
|
||||||
|
order_create:
|
||||||
|
type: string
|
||||||
|
description: 订单创建时间,由数据库自动生成
|
||||||
|
example: 2026-04-03 15:35:00.000Z
|
||||||
|
order_owner:
|
||||||
|
type: string
|
||||||
|
description: 订单所有者 openid
|
||||||
|
example: wx-openid-user-001
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
description: 订单来源
|
||||||
|
example: 购物车
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
description: 订单状态
|
||||||
|
example: 订单已生成
|
||||||
|
order_source_id:
|
||||||
|
type: string
|
||||||
|
description: 来源关联业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
order_snap:
|
||||||
|
description: 订单快照 JSON
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
order_amount:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 订单金额
|
||||||
|
example: 3998
|
||||||
|
order_remark:
|
||||||
|
type: string
|
||||||
|
description: 备注
|
||||||
|
example: 小程序订单示例
|
||||||
|
is_delete:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 软删除标记;目标契约字段,当前 hooks 响应可能尚未显式透出
|
||||||
|
example: 0
|
||||||
|
|
||||||
|
OrderListRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
keyword:
|
||||||
|
type: string
|
||||||
|
description: 对 `order_id`、`order_number`、`order_source_id` 等索引相关字段的统一模糊搜索关键字
|
||||||
|
example: ORDER-1770
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
description: 订单状态精确过滤
|
||||||
|
example: 订单已生成
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
description: 订单来源精确过滤
|
||||||
|
example: 购物车
|
||||||
|
|
||||||
|
OrderDetailRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
description: 订单业务 ID
|
||||||
|
example: ORDER-1770000000000-abc123
|
||||||
|
|
||||||
|
OrderCreateRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- order_source
|
||||||
|
- order_source_id
|
||||||
|
- order_snap
|
||||||
|
- order_amount
|
||||||
|
properties:
|
||||||
|
order_number:
|
||||||
|
type: string
|
||||||
|
description: 可选;未传时服务端自动生成
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
description: 订单来源
|
||||||
|
example: 购物车
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
description: 可选;未传时默认 `订单已生成`
|
||||||
|
example: 订单已生成
|
||||||
|
order_source_id:
|
||||||
|
type: string
|
||||||
|
description: 来源关联业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
order_snap:
|
||||||
|
description: 订单快照 JSON
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
order_amount:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 订单金额
|
||||||
|
example: 3998
|
||||||
|
order_remark:
|
||||||
|
type: string
|
||||||
|
description: 备注
|
||||||
|
example: 小程序订单示例
|
||||||
|
|
||||||
|
OrderUpdateRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
description: 订单业务 ID
|
||||||
|
example: ORDER-1770000000000-abc123
|
||||||
|
order_number:
|
||||||
|
type: string
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
order_source_id:
|
||||||
|
type: string
|
||||||
|
order_snap:
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
order_amount:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
order_remark:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
OrderDeleteRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
description: 订单业务 ID
|
||||||
|
example: ORDER-1770000000000-abc123
|
||||||
|
|
||||||
|
OrderListResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/OrderRecord'
|
||||||
|
|
||||||
|
OrderDeleteResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
description: 订单业务 ID
|
||||||
|
example: ORDER-1770000000000-abc123
|
||||||
|
is_delete:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- number
|
||||||
|
description: 目标软删除标记值;当前实现可能仍返回物理删除结果
|
||||||
|
example: 1
|
||||||
154
pocket-base/spec/openapi-wx/schemas/order-native.yaml
Normal file
154
pocket-base/spec/openapi-wx/schemas/order-native.yaml
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
PocketBaseOrderFields:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
description: 订单业务 ID
|
||||||
|
example: ORDER-1770000000000-abc123
|
||||||
|
order_number:
|
||||||
|
type: string
|
||||||
|
description: 订单编号
|
||||||
|
example: wx-user-20260403153500
|
||||||
|
order_create:
|
||||||
|
type: string
|
||||||
|
description: 订单创建时间,由数据库自动生成
|
||||||
|
example: 2026-04-03 15:35:00.000Z
|
||||||
|
order_owner:
|
||||||
|
type: string
|
||||||
|
description: 订单所有者 openid
|
||||||
|
example: wx-openid-user-001
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
description: 订单来源
|
||||||
|
example: 购物车
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
description: 订单状态
|
||||||
|
example: 订单已生成
|
||||||
|
order_source_id:
|
||||||
|
type: string
|
||||||
|
description: 来源关联业务 ID
|
||||||
|
example: CART-1770000000000-abc123
|
||||||
|
order_snap:
|
||||||
|
description: 订单快照 JSON
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
order_amount:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
description: 订单金额
|
||||||
|
example: 3998
|
||||||
|
order_remark:
|
||||||
|
type: string
|
||||||
|
description: 备注
|
||||||
|
example: 小程序订单示例
|
||||||
|
|
||||||
|
PocketBaseOrderRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase'
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderFields'
|
||||||
|
|
||||||
|
PocketBaseOrderCreateRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- order_id
|
||||||
|
- order_number
|
||||||
|
- order_owner
|
||||||
|
- order_source
|
||||||
|
- order_status
|
||||||
|
- order_source_id
|
||||||
|
- order_snap
|
||||||
|
- order_amount
|
||||||
|
properties:
|
||||||
|
order_id:
|
||||||
|
type: string
|
||||||
|
order_number:
|
||||||
|
type: string
|
||||||
|
order_owner:
|
||||||
|
type: string
|
||||||
|
description: 必须显式提交,且值必须等于当前 token 对应 openid
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
order_source_id:
|
||||||
|
type: string
|
||||||
|
order_snap:
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
order_amount:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
order_remark:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
PocketBaseOrderUpdateRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
order_number:
|
||||||
|
type: string
|
||||||
|
order_owner:
|
||||||
|
type: string
|
||||||
|
description: 若提交,必须仍等于当前 token 对应 openid
|
||||||
|
order_source:
|
||||||
|
type: string
|
||||||
|
order_status:
|
||||||
|
type: string
|
||||||
|
order_source_id:
|
||||||
|
type: string
|
||||||
|
order_snap:
|
||||||
|
oneOf:
|
||||||
|
- type: object
|
||||||
|
additionalProperties: true
|
||||||
|
- type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
|
order_amount:
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
order_remark:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
PocketBaseOrderListResponse:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- page
|
||||||
|
- perPage
|
||||||
|
- totalItems
|
||||||
|
- totalPages
|
||||||
|
- items
|
||||||
|
properties:
|
||||||
|
page:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
perPage:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
totalItems:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
totalPages:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseOrderRecord'
|
||||||
203
pocket-base/spec/openapi-wx/schemas/products.yaml
Normal file
203
pocket-base/spec/openapi-wx/schemas/products.yaml
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
PocketBaseProductListRecord:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseRecordBase'
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/PocketBaseProductListFields'
|
||||||
|
example:
|
||||||
|
id: <PocketBase记录主键>|<string>
|
||||||
|
collectionId: <集合ID>|<string>
|
||||||
|
collectionName: <集合名称>|<string>
|
||||||
|
created: <记录创建时间>|<string>
|
||||||
|
updated: <记录更新时间>|<string>
|
||||||
|
prod_list_id: <产品列表业务ID>|<string>
|
||||||
|
prod_list_name: <产品名称>|<string>
|
||||||
|
prod_list_modelnumber: <产品型号>|<string>
|
||||||
|
prod_list_icon: <产品图标附件ID>|<string>
|
||||||
|
prod_list_description: <产品说明>|<string>
|
||||||
|
prod_list_feature: <产品特色>|<string>
|
||||||
|
prod_list_parameters:
|
||||||
|
- name: <属性名>|<string>
|
||||||
|
value: <属性值>|<string>
|
||||||
|
prod_list_plantype: <产品方案>|<string>
|
||||||
|
prod_list_category: <产品分类>|<string>
|
||||||
|
prod_list_sort: 10
|
||||||
|
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: 1999
|
||||||
|
prod_list_vip_price:
|
||||||
|
- viplevel: VIP1
|
||||||
|
price: 1899
|
||||||
|
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: <页码>|<integer>
|
||||||
|
perPage: <每页条数>|<integer>
|
||||||
|
totalItems: <总记录数>|<integer>
|
||||||
|
totalPages: <总页数>|<integer>
|
||||||
|
items:
|
||||||
|
- id: <PocketBase记录主键>|<string>
|
||||||
|
collectionId: <集合ID>|<string>
|
||||||
|
collectionName: <集合名称>|<string>
|
||||||
|
created: <记录创建时间>|<string>
|
||||||
|
updated: <记录更新时间>|<string>
|
||||||
|
prod_list_id: <产品列表业务ID>|<string>
|
||||||
|
prod_list_name: <产品名称>|<string>
|
||||||
|
prod_list_modelnumber: <产品型号>|<string>
|
||||||
|
prod_list_icon: <产品图标附件ID>|<string>
|
||||||
|
prod_list_description: <产品说明>|<string>
|
||||||
|
prod_list_feature: <产品特色>|<string>
|
||||||
|
prod_list_parameters:
|
||||||
|
- name: <属性名>|<string>
|
||||||
|
value: <属性值>|<string>
|
||||||
|
prod_list_plantype: <产品方案>|<string>
|
||||||
|
prod_list_category: <产品分类>|<string>
|
||||||
|
prod_list_sort: 10
|
||||||
|
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: 1999
|
||||||
|
prod_list_vip_price:
|
||||||
|
- viplevel: VIP1
|
||||||
|
price: 1899
|
||||||
|
prod_list_remark: <备注>|<string>
|
||||||
55
pocket-base/spec/openapi-wx/schemas/system.yaml
Normal file
55
pocket-base/spec/openapi-wx/schemas/system.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
SystemRefreshTokenRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
users_wx_code:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
description: |
|
||||||
|
可选。
|
||||||
|
当前 token 失效时,可通过该 code 重新签发 token。
|
||||||
|
example: 0a1b2c3d4e5f6g
|
||||||
|
|
||||||
|
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: {}
|
||||||
|
token: 新签发的PocketBase原生auth token | string
|
||||||
|
|
||||||
|
UsersCountData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
total_users:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
example: 用户总数 | 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: 用户总数 | integer
|
||||||
389
pocket-base/spec/openapi-wx/schemas/wechat-auth.yaml
Normal file
389
pocket-base/spec/openapi-wx/schemas/wechat-auth.yaml
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
UserInfo:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pb_id:
|
||||||
|
description: "PocketBase 记录主键 id"
|
||||||
|
type: string
|
||||||
|
users_convers_id:
|
||||||
|
description: "会话侧用户 ID"
|
||||||
|
type: string
|
||||||
|
users_id:
|
||||||
|
description: "用户业务 ID"
|
||||||
|
type: string
|
||||||
|
users_idtype:
|
||||||
|
description: "身份来源类型或证件类型"
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
enum:
|
||||||
|
- WeChat
|
||||||
|
- ManagePlatform
|
||||||
|
- type: string
|
||||||
|
users_id_number:
|
||||||
|
description: "证件号"
|
||||||
|
type: string
|
||||||
|
users_type:
|
||||||
|
description: "用户类型"
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
enum:
|
||||||
|
- 游客
|
||||||
|
- 注册用户
|
||||||
|
- type: string
|
||||||
|
users_name:
|
||||||
|
description: "用户姓名 / 昵称"
|
||||||
|
type: string
|
||||||
|
users_status:
|
||||||
|
description: "用户状态"
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- number
|
||||||
|
users_rank_level:
|
||||||
|
description: "用户星级数值"
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
users_auth_type:
|
||||||
|
description: "账户类型"
|
||||||
|
type:
|
||||||
|
- number
|
||||||
|
- integer
|
||||||
|
- string
|
||||||
|
users_phone:
|
||||||
|
description: "手机号"
|
||||||
|
type: string
|
||||||
|
users_phone_masked:
|
||||||
|
type: string
|
||||||
|
users_level:
|
||||||
|
type: string
|
||||||
|
description: 用户等级
|
||||||
|
users_level_name:
|
||||||
|
type: string
|
||||||
|
description: 用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析
|
||||||
|
users_tag:
|
||||||
|
type: string
|
||||||
|
description: 用户标签
|
||||||
|
users_picture:
|
||||||
|
type: string
|
||||||
|
description: 用户头像附件的 `attachments_id`
|
||||||
|
users_picture_url:
|
||||||
|
type: string
|
||||||
|
description: 根据 `users_picture -> tbl_attachments` 自动解析出的头像文件流链接
|
||||||
|
users_id_pic_a:
|
||||||
|
type: string
|
||||||
|
description: 证件正面附件的 `attachments_id`
|
||||||
|
users_id_pic_a_url:
|
||||||
|
type: string
|
||||||
|
description: 根据 `users_id_pic_a -> tbl_attachments` 自动解析出的文件流链接
|
||||||
|
users_id_pic_b:
|
||||||
|
type: string
|
||||||
|
description: 证件反面附件的 `attachments_id`
|
||||||
|
users_id_pic_b_url:
|
||||||
|
type: string
|
||||||
|
description: 根据 `users_id_pic_b -> tbl_attachments` 自动解析出的文件流链接
|
||||||
|
users_title_picture:
|
||||||
|
type: string
|
||||||
|
description: 资质附件的 `attachments_id`
|
||||||
|
users_title_picture_url:
|
||||||
|
type: string
|
||||||
|
description: 根据 `users_title_picture -> tbl_attachments` 自动解析出的文件流链接
|
||||||
|
openid:
|
||||||
|
type: string
|
||||||
|
description: 全平台统一身份标识
|
||||||
|
company_id:
|
||||||
|
type: string
|
||||||
|
description: 公司业务 id,存储 `tbl_company.company_id`
|
||||||
|
users_parent_id:
|
||||||
|
type: string
|
||||||
|
description: 上级用户业务 id
|
||||||
|
users_promo_code:
|
||||||
|
type: string
|
||||||
|
description: 推广码
|
||||||
|
usergroups_id:
|
||||||
|
type: string
|
||||||
|
description: 用户组业务 id
|
||||||
|
company:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/CompanyInfo'
|
||||||
|
created:
|
||||||
|
type: string
|
||||||
|
description: 用户创建时间
|
||||||
|
updated:
|
||||||
|
type: string
|
||||||
|
description: 用户更新时间
|
||||||
|
example:
|
||||||
|
pb_id: PocketBase记录主键id | string
|
||||||
|
users_convers_id: 会话侧用户ID | string
|
||||||
|
users_id: 用户业务ID | string
|
||||||
|
users_idtype: 用户身份来源类型 | string
|
||||||
|
users_id_number: 证件号 | string
|
||||||
|
users_type: 用户类型 | string
|
||||||
|
users_name: 用户姓名或昵称 | string
|
||||||
|
users_status: 用户状态 | string
|
||||||
|
users_rank_level: 用户星级数值 | number
|
||||||
|
users_auth_type: 账户类型 | number
|
||||||
|
users_phone: 手机号 | string
|
||||||
|
users_phone_masked: 手机号脱敏值 | string
|
||||||
|
users_level: 用户等级 | string
|
||||||
|
users_level_name: 用户等级名称 | string
|
||||||
|
users_tag: 用户标签 | string
|
||||||
|
users_picture: 用户头像附件ID | string
|
||||||
|
users_picture_url: 用户头像文件流链接 | string
|
||||||
|
users_id_pic_a: 证件正面附件ID | string
|
||||||
|
users_id_pic_a_url: 证件正面文件流链接 | string
|
||||||
|
users_id_pic_b: 证件反面附件ID | string
|
||||||
|
users_id_pic_b_url: 证件反面文件流链接 | string
|
||||||
|
users_title_picture: 资质附件ID | string
|
||||||
|
users_title_picture_url: 资质附件文件流链接 | string
|
||||||
|
openid: 全平台统一身份标识 | string
|
||||||
|
company_id: 公司业务id | string
|
||||||
|
users_parent_id: 上级用户业务id | string
|
||||||
|
users_promo_code: 推广码 | string
|
||||||
|
usergroups_id: 用户组业务id | string
|
||||||
|
company:
|
||||||
|
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
|
||||||
|
created: 用户创建时间 | string
|
||||||
|
updated: 用户更新时间 | string
|
||||||
|
|
||||||
|
WechatLoginRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- users_wx_code
|
||||||
|
properties:
|
||||||
|
users_wx_code:
|
||||||
|
type: string
|
||||||
|
description: 微信小程序登录临时凭证 code
|
||||||
|
example: 0a1b2c3d4e5f6g
|
||||||
|
|
||||||
|
WechatProfileRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
users_name:
|
||||||
|
type: string
|
||||||
|
description: "用户姓名 / 昵称"
|
||||||
|
example: 张三
|
||||||
|
users_phone_code:
|
||||||
|
type: string
|
||||||
|
description: 可选。若传入,服务端优先通过微信接口换取真实手机号并写入数据库
|
||||||
|
example: 2b7d9f2e3c4a5b6d7e8f
|
||||||
|
users_phone:
|
||||||
|
type: string
|
||||||
|
description: 可选。未传 `users_phone_code` 时,可直接写入手机号
|
||||||
|
example: '13800138000'
|
||||||
|
users_type:
|
||||||
|
type: string
|
||||||
|
description: 可选。用户类型;仅在传入非空值时更新
|
||||||
|
example: 服务商
|
||||||
|
company_id:
|
||||||
|
type: string
|
||||||
|
description: 可选。公司业务 id;仅在传入非空值时更新
|
||||||
|
example: WX-COMPANY-10001
|
||||||
|
users_tag:
|
||||||
|
type: string
|
||||||
|
description: 可选。用户标签;非空时才更新
|
||||||
|
example: 核心客户
|
||||||
|
users_picture:
|
||||||
|
type: string
|
||||||
|
description: 可选。用户头像附件的 `attachments_id`
|
||||||
|
example: ATT-1743123456789-abc123
|
||||||
|
users_id_pic_a:
|
||||||
|
type: string
|
||||||
|
description: 可选。证件正面附件的 `attachments_id`
|
||||||
|
users_id_pic_b:
|
||||||
|
type: string
|
||||||
|
description: 可选。证件反面附件的 `attachments_id`
|
||||||
|
users_title_picture:
|
||||||
|
type: string
|
||||||
|
description: 可选。资质附件的 `attachments_id`
|
||||||
|
|
||||||
|
AuthSuccessData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
enum:
|
||||||
|
- register_success
|
||||||
|
- login_success
|
||||||
|
- type: string
|
||||||
|
is_info_complete:
|
||||||
|
type:
|
||||||
|
- boolean
|
||||||
|
- string
|
||||||
|
user:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/UserInfo'
|
||||||
|
|
||||||
|
AuthSuccessResponse:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/ApiResponseBase'
|
||||||
|
- type: object
|
||||||
|
required:
|
||||||
|
- token
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
description: "业务响应数据"
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/AuthSuccessData'
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: PocketBase 原生 auth token
|
||||||
|
example:
|
||||||
|
statusCode: 业务状态码 | integer
|
||||||
|
errMsg: 业务提示信息 | string
|
||||||
|
data:
|
||||||
|
status: 登录或注册状态 | string
|
||||||
|
is_info_complete: 资料是否完整 | boolean
|
||||||
|
user:
|
||||||
|
pb_id: PocketBase记录主键id | string
|
||||||
|
users_convers_id: 会话侧用户ID | string
|
||||||
|
users_id: 用户业务ID | string
|
||||||
|
users_idtype: 用户身份来源类型 | string
|
||||||
|
users_id_number: 证件号 | string
|
||||||
|
users_type: 用户类型 | string
|
||||||
|
users_name: 用户姓名或昵称 | string
|
||||||
|
users_status: 用户状态 | string
|
||||||
|
users_rank_level: 用户星级数值 | number
|
||||||
|
users_auth_type: 账户类型 | number
|
||||||
|
users_phone: 手机号 | string
|
||||||
|
users_phone_masked: 手机号脱敏值 | string
|
||||||
|
users_level: 用户等级 | string
|
||||||
|
users_level_name: 用户等级名称 | string
|
||||||
|
users_tag: 用户标签 | string
|
||||||
|
users_picture: 用户头像附件ID | string
|
||||||
|
users_picture_url: 用户头像文件流链接 | string
|
||||||
|
users_id_pic_a: 证件正面附件ID | string
|
||||||
|
users_id_pic_a_url: 证件正面文件流链接 | string
|
||||||
|
users_id_pic_b: 证件反面附件ID | string
|
||||||
|
users_id_pic_b_url: 证件反面文件流链接 | string
|
||||||
|
users_title_picture: 资质附件ID | string
|
||||||
|
users_title_picture_url: 资质附件文件流链接 | string
|
||||||
|
openid: 全平台统一身份标识 | string
|
||||||
|
company_id: 公司业务id | string
|
||||||
|
users_parent_id: 上级用户业务id | string
|
||||||
|
users_promo_code: 推广码 | string
|
||||||
|
usergroups_id: 用户组业务id | string
|
||||||
|
company:
|
||||||
|
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
|
||||||
|
created: 用户创建时间 | string
|
||||||
|
updated: 用户更新时间 | string
|
||||||
|
token: PocketBase原生认证token | string
|
||||||
|
|
||||||
|
WechatProfileResponseData:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
enum:
|
||||||
|
- update_success
|
||||||
|
- type: string
|
||||||
|
user:
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/UserInfo'
|
||||||
|
|
||||||
|
WechatProfileResponse:
|
||||||
|
allOf:
|
||||||
|
- $ref: '../../openapi-wx.yaml#/components/schemas/ApiResponseBase'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
description: "业务响应数据"
|
||||||
|
$ref: '../../openapi-wx.yaml#/components/schemas/WechatProfileResponseData'
|
||||||
|
example:
|
||||||
|
statusCode: 业务状态码 | integer
|
||||||
|
errMsg: 业务提示信息 | string
|
||||||
|
data:
|
||||||
|
status: 资料更新状态 | string
|
||||||
|
user:
|
||||||
|
pb_id: PocketBase记录主键id | string
|
||||||
|
users_convers_id: 会话侧用户ID | string
|
||||||
|
users_id: 用户业务ID | string
|
||||||
|
users_idtype: 用户身份来源类型 | string
|
||||||
|
users_id_number: 证件号 | string
|
||||||
|
users_type: 用户类型 | string
|
||||||
|
users_name: 用户姓名或昵称 | string
|
||||||
|
users_status: 用户状态 | string
|
||||||
|
users_rank_level: 用户星级数值 | number
|
||||||
|
users_auth_type: 账户类型 | number
|
||||||
|
users_phone: 手机号 | string
|
||||||
|
users_phone_masked: 手机号脱敏值 | string
|
||||||
|
users_level: 用户等级 | string
|
||||||
|
users_level_name: 用户等级名称 | string
|
||||||
|
users_tag: 用户标签 | string
|
||||||
|
users_picture: 用户头像附件ID | string
|
||||||
|
users_picture_url: 用户头像文件流链接 | string
|
||||||
|
users_id_pic_a: 证件正面附件ID | string
|
||||||
|
users_id_pic_a_url: 证件正面文件流链接 | string
|
||||||
|
users_id_pic_b: 证件反面附件ID | string
|
||||||
|
users_id_pic_b_url: 证件反面文件流链接 | string
|
||||||
|
users_title_picture: 资质附件ID | string
|
||||||
|
users_title_picture_url: 资质附件文件流链接 | string
|
||||||
|
openid: 全平台统一身份标识 | string
|
||||||
|
company_id: 公司业务id | string
|
||||||
|
users_parent_id: 上级用户业务id | string
|
||||||
|
users_promo_code: 推广码 | string
|
||||||
|
usergroups_id: 用户组业务id | string
|
||||||
|
company:
|
||||||
|
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
|
||||||
|
created: 用户创建时间 | string
|
||||||
|
updated: 用户更新时间 | string
|
||||||
@@ -205,6 +205,9 @@ components:
|
|||||||
users_level:
|
users_level:
|
||||||
type: string
|
type: string
|
||||||
description: 用户等级
|
description: 用户等级
|
||||||
|
users_level_name:
|
||||||
|
type: string
|
||||||
|
description: 用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析
|
||||||
users_tag:
|
users_tag:
|
||||||
type: string
|
type: string
|
||||||
description: 用户标签
|
description: 用户标签
|
||||||
@@ -269,6 +272,7 @@ components:
|
|||||||
users_phone: 手机号 | string
|
users_phone: 手机号 | string
|
||||||
users_phone_masked: 手机号脱敏值 | string
|
users_phone_masked: 手机号脱敏值 | string
|
||||||
users_level: 用户等级 | string
|
users_level: 用户等级 | string
|
||||||
|
users_level_name: 用户等级名称 | string
|
||||||
users_tag: 用户标签 | string
|
users_tag: 用户标签 | string
|
||||||
users_picture: 用户头像附件ID | string
|
users_picture: 用户头像附件ID | string
|
||||||
users_picture_url: 用户头像文件流链接 | string
|
users_picture_url: 用户头像文件流链接 | string
|
||||||
@@ -1249,6 +1253,7 @@ paths:
|
|||||||
使用微信 code 换取微信侧 openid,并写入统一身份字段 `tbl_auth_users.openid`。
|
使用微信 code 换取微信侧 openid,并写入统一身份字段 `tbl_auth_users.openid`。
|
||||||
若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record,随后返回 PocketBase 原生 auth token。
|
若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record,随后返回 PocketBase 原生 auth token。
|
||||||
首次注册创建时会写入 `users_idtype = WeChat`。
|
首次注册创建时会写入 `users_idtype = WeChat`。
|
||||||
|
首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||||
返回的 `token` 可直接用于 PocketBase SDK 与当前 hooks 接口调用。
|
返回的 `token` 可直接用于 PocketBase SDK 与当前 hooks 接口调用。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -1281,6 +1286,7 @@ paths:
|
|||||||
创建平台用户 auth record。
|
创建平台用户 auth record。
|
||||||
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。
|
||||||
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。
|
||||||
|
首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。
|
||||||
注册成功后直接返回 PocketBase 原生 auth token。
|
注册成功后直接返回 PocketBase 原生 auth token。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -1311,6 +1317,7 @@ paths:
|
|||||||
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。
|
||||||
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。
|
||||||
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。
|
||||||
|
返回体中的 `user.users_level_name` 为服务端按“数据-会员等级”字典实时解析后的等级名称。
|
||||||
登录成功后直接返回 PocketBase 原生 auth token。
|
登录成功后直接返回 PocketBase 原生 auth token。
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -1882,5 +1889,3 @@ paths:
|
|||||||
description: 非 ManagePlatform 用户无权访问
|
description: 非 ManagePlatform 用户无权访问
|
||||||
'415':
|
'415':
|
||||||
description: 请求体必须为 application/json
|
description: 请求体必须为 application/json
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
155
script/add-product-function-field.js
Normal file
155
script/add-product-function-field.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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 pbUrl = String(
|
||||||
|
process.env.PB_URL
|
||||||
|
|| backendEnv.POCKETBASE_API_URL
|
||||||
|
|| runtimeConfig.POCKETBASE_API_URL
|
||||||
|
|| 'http://127.0.0.1:8090'
|
||||||
|
).replace(/\/+$/, '');
|
||||||
|
const authToken = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
|
||||||
|
|
||||||
|
if (!authToken) {
|
||||||
|
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行字段迁移。');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFieldPayload(field, targetSpec) {
|
||||||
|
const payload = {
|
||||||
|
name: targetSpec && targetSpec.name ? targetSpec.name : field.name,
|
||||||
|
type: targetSpec && targetSpec.type ? targetSpec.type : field.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (field && field.id) {
|
||||||
|
payload.id = field.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof field.required !== 'undefined') {
|
||||||
|
payload.required = !!field.required;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.type === 'autodate') {
|
||||||
|
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
|
||||||
|
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const pb = new PocketBase(pbUrl);
|
||||||
|
pb.authStore.save(authToken, null);
|
||||||
|
|
||||||
|
console.log(`🔄 开始处理字段迁移,PocketBase: ${pbUrl}`);
|
||||||
|
|
||||||
|
const collections = await pb.collections.getFullList({ sort: '-created' });
|
||||||
|
const collection = collections.find((item) => item.name === 'tbl_product_list');
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
throw new Error('未找到集合 tbl_product_list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFieldName = 'prod_list_function';
|
||||||
|
const targetFieldType = 'json';
|
||||||
|
const existingField = (collection.fields || []).find((field) => field.name === targetFieldName);
|
||||||
|
|
||||||
|
const hasBrokenCustomIdField = (collection.fields || []).some((field) => field && field.name === 'id');
|
||||||
|
|
||||||
|
if (existingField && existingField.type === targetFieldType && !hasBrokenCustomIdField) {
|
||||||
|
console.log('✅ 字段已存在且类型正确,无需变更。');
|
||||||
|
console.log('✅ 校验完成: tbl_product_list.prod_list_function (json)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextFields = [];
|
||||||
|
let patched = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < (collection.fields || []).length; i += 1) {
|
||||||
|
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) {
|
||||||
|
nextFields.push(normalizeFieldPayload(field, { name: targetFieldName, type: targetFieldType }));
|
||||||
|
patched = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFields.push(normalizeFieldPayload(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patched) {
|
||||||
|
nextFields.push({
|
||||||
|
name: targetFieldName,
|
||||||
|
type: targetFieldType,
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await pb.collections.update(collection.id, {
|
||||||
|
name: collection.name,
|
||||||
|
type: collection.type,
|
||||||
|
listRule: collection.listRule,
|
||||||
|
viewRule: collection.viewRule,
|
||||||
|
createRule: collection.createRule,
|
||||||
|
updateRule: collection.updateRule,
|
||||||
|
deleteRule: collection.deleteRule,
|
||||||
|
fields: nextFields,
|
||||||
|
indexes: collection.indexes || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await pb.collections.getOne(collection.id);
|
||||||
|
const verifiedField = (updated.fields || []).find((field) => field.name === targetFieldName);
|
||||||
|
|
||||||
|
if (!verifiedField || verifiedField.type !== targetFieldType) {
|
||||||
|
throw new Error('字段写入后校验失败:prod_list_function 未成功写入 json 类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 字段迁移成功: tbl_product_list.prod_list_function (json)');
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error('❌ 迁移失败:', {
|
||||||
|
status: error && error.status,
|
||||||
|
message: error && error.message,
|
||||||
|
response: error && error.response,
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
| users_id_number | text | 证件号 |
|
| users_id_number | text | 证件号 |
|
||||||
| users_phone | text | 用户电话号码 |
|
| users_phone | text | 用户电话号码 |
|
||||||
| users_wx_openid | text | 微信号 |
|
| users_wx_openid | text | 微信号 |
|
||||||
| users_level | text | 用户等级 |
|
| users_level | text | 用户等级枚举值(新用户默认空) |
|
||||||
| users_type | text | 用户类型 |
|
| users_type | text | 用户类型 |
|
||||||
| users_tag | text | 用户标签 |
|
| users_tag | text | 用户标签 |
|
||||||
| users_status | text | 用户状态 |
|
| users_status | text | 用户状态 |
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
"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",
|
||||||
"test:company-native-api": "node test-tbl-company-native-api.js",
|
"test:company-native-api": "node test-tbl-company-native-api.js",
|
||||||
"test:company-owner-sync": "node test-company-owner-sync.js"
|
"test:company-owner-sync": "node test-company-owner-sync.js"
|
||||||
},
|
},
|
||||||
|
|||||||
233
script/pocketbase.add-is-delete-field.js
Normal file
233
script/pocketbase.add-is-delete-field.js
Normal 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();
|
||||||
176
script/pocketbase.apply-soft-delete-rules.js
Normal file
176
script/pocketbase.apply-soft-delete-rules.js
Normal 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();
|
||||||
285
script/pocketbase.cart-order.js
Normal file
285
script/pocketbase.cart-order.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import { createRequire } from 'module';
|
||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
let runtimeConfig = {};
|
||||||
|
try {
|
||||||
|
runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js');
|
||||||
|
} catch (_error) {
|
||||||
|
runtimeConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PB_URL = (process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, '');
|
||||||
|
const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || '';
|
||||||
|
const OWNER_AUTH_RULE = '@request.auth.id != ""';
|
||||||
|
const CART_OWNER_MATCH_RULE = 'cart_owner = @request.auth.openid';
|
||||||
|
const ORDER_OWNER_MATCH_RULE = 'order_owner = @request.auth.openid';
|
||||||
|
const SOFT_DELETE_RULE = 'is_delete = 0';
|
||||||
|
|
||||||
|
if (!AUTH_TOKEN) {
|
||||||
|
console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行建表。');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pb = new PocketBase(PB_URL);
|
||||||
|
|
||||||
|
const collections = [
|
||||||
|
{
|
||||||
|
name: 'tbl_cart',
|
||||||
|
type: 'base',
|
||||||
|
listRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
|
||||||
|
viewRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
|
||||||
|
createRule: `${OWNER_AUTH_RULE} && @request.body.cart_owner = @request.auth.openid`,
|
||||||
|
updateRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
|
||||||
|
deleteRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_RULE}`,
|
||||||
|
fields: [
|
||||||
|
{ 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_create', type: 'autodate', onCreate: true, onUpdate: false },
|
||||||
|
{ name: 'cart_owner', type: 'text', required: true },
|
||||||
|
{ name: 'cart_product_id', type: 'text', required: true },
|
||||||
|
{ name: 'cart_product_quantity', type: 'number', required: true },
|
||||||
|
{ name: 'cart_status', type: 'text', required: true },
|
||||||
|
{ name: 'cart_at_price', type: 'number', required: true },
|
||||||
|
{ name: 'cart_remark', type: 'text' },
|
||||||
|
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE UNIQUE INDEX idx_tbl_cart_cart_id ON tbl_cart (cart_id)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_cart_number ON tbl_cart (cart_number)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_cart_owner ON tbl_cart (cart_owner)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_cart_product_id ON tbl_cart (cart_product_id)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_cart_status ON tbl_cart (cart_status)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_cart_create ON tbl_cart (cart_create)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_owner_number ON tbl_cart (cart_owner, cart_number)',
|
||||||
|
'CREATE INDEX idx_tbl_cart_owner_status ON tbl_cart (cart_owner, cart_status)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tbl_order',
|
||||||
|
type: 'base',
|
||||||
|
listRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
|
||||||
|
viewRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE} && ${SOFT_DELETE_RULE}`,
|
||||||
|
createRule: `${OWNER_AUTH_RULE} && @request.body.order_owner = @request.auth.openid`,
|
||||||
|
updateRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
|
||||||
|
deleteRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`,
|
||||||
|
fields: [
|
||||||
|
{ 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_create', type: 'autodate', onCreate: true, onUpdate: false },
|
||||||
|
{ name: 'order_owner', type: 'text', required: true },
|
||||||
|
{ name: 'order_source', type: 'text', required: true },
|
||||||
|
{ name: 'order_status', type: 'text', required: true },
|
||||||
|
{ name: 'order_source_id', type: 'text', required: true },
|
||||||
|
{ name: 'order_snap', type: 'json', required: true },
|
||||||
|
{ name: 'order_amount', type: 'number', required: true },
|
||||||
|
{ name: 'order_remark', type: 'text' },
|
||||||
|
{ name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true },
|
||||||
|
],
|
||||||
|
indexes: [
|
||||||
|
'CREATE UNIQUE INDEX idx_tbl_order_order_id ON tbl_order (order_id)',
|
||||||
|
'CREATE UNIQUE INDEX idx_tbl_order_order_number ON tbl_order (order_number)',
|
||||||
|
'CREATE INDEX idx_tbl_order_order_owner ON tbl_order (order_owner)',
|
||||||
|
'CREATE INDEX idx_tbl_order_order_source ON tbl_order (order_source)',
|
||||||
|
'CREATE INDEX idx_tbl_order_order_status ON tbl_order (order_status)',
|
||||||
|
'CREATE INDEX idx_tbl_order_order_source_id ON tbl_order (order_source_id)',
|
||||||
|
'CREATE INDEX idx_tbl_order_order_create ON tbl_order (order_create)',
|
||||||
|
'CREATE INDEX idx_tbl_order_owner_status ON tbl_order (order_owner, order_status)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeFieldPayload(field, existingField) {
|
||||||
|
const payload = existingField
|
||||||
|
? Object.assign({}, existingField)
|
||||||
|
: {
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingField && existingField.id) {
|
||||||
|
payload.id = existingField.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.name = field.name;
|
||||||
|
payload.type = field.type;
|
||||||
|
|
||||||
|
if (typeof field.required !== 'undefined') {
|
||||||
|
payload.required = field.required;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'autodate') {
|
||||||
|
payload.onCreate = typeof field.onCreate === 'boolean' ? field.onCreate : true;
|
||||||
|
payload.onUpdate = typeof field.onUpdate === 'boolean' ? field.onUpdate : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCollectionPayload(collectionData, existingCollection) {
|
||||||
|
if (!existingCollection) {
|
||||||
|
return {
|
||||||
|
name: collectionData.name,
|
||||||
|
type: collectionData.type,
|
||||||
|
listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : null,
|
||||||
|
viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : null,
|
||||||
|
createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : null,
|
||||||
|
updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : null,
|
||||||
|
deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : null,
|
||||||
|
fields: collectionData.fields.map((field) => normalizeFieldPayload(field, null)),
|
||||||
|
indexes: collectionData.indexes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFieldMap = new Map(collectionData.fields.map((field) => [field.name, field]));
|
||||||
|
const fields = (existingCollection.fields || []).map((existingField) => {
|
||||||
|
const targetField = targetFieldMap.get(existingField.name);
|
||||||
|
if (!targetField) {
|
||||||
|
return existingField;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFieldMap.delete(existingField.name);
|
||||||
|
return normalizeFieldPayload(targetField, existingField);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const field of targetFieldMap.values()) {
|
||||||
|
fields.push(normalizeFieldPayload(field, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: collectionData.name,
|
||||||
|
type: collectionData.type,
|
||||||
|
listRule: Object.prototype.hasOwnProperty.call(collectionData, 'listRule') ? collectionData.listRule : existingCollection.listRule,
|
||||||
|
viewRule: Object.prototype.hasOwnProperty.call(collectionData, 'viewRule') ? collectionData.viewRule : existingCollection.viewRule,
|
||||||
|
createRule: Object.prototype.hasOwnProperty.call(collectionData, 'createRule') ? collectionData.createRule : existingCollection.createRule,
|
||||||
|
updateRule: Object.prototype.hasOwnProperty.call(collectionData, 'updateRule') ? collectionData.updateRule : existingCollection.updateRule,
|
||||||
|
deleteRule: Object.prototype.hasOwnProperty.call(collectionData, 'deleteRule') ? collectionData.deleteRule : existingCollection.deleteRule,
|
||||||
|
fields: fields,
|
||||||
|
indexes: collectionData.indexes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFieldList(fields) {
|
||||||
|
return (fields || []).map((field) => ({
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
required: !!field.required,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrUpdateCollection(collectionData) {
|
||||||
|
console.log(`🔄 正在处理表: ${collectionData.name} ...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const list = await pb.collections.getFullList({
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
const existing = list.find((item) => item.name === collectionData.name);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await pb.collections.update(existing.id, buildCollectionPayload(collectionData, existing));
|
||||||
|
console.log(`♻️ ${collectionData.name} 已存在,已按最新结构更新。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pb.collections.create(buildCollectionPayload(collectionData, null));
|
||||||
|
console.log(`✅ ${collectionData.name} 创建完成。`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 处理集合 ${collectionData.name} 失败:`, {
|
||||||
|
status: error.status,
|
||||||
|
message: error.message,
|
||||||
|
response: error.response,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCollectionByName(collectionName) {
|
||||||
|
const list = await pb.collections.getFullList({
|
||||||
|
sort: '-created',
|
||||||
|
});
|
||||||
|
return list.find((item) => item.name === collectionName) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyCollections(targetCollections) {
|
||||||
|
console.log('\n🔍 开始校验购物车与订单表结构及索引...');
|
||||||
|
|
||||||
|
for (const target of targetCollections) {
|
||||||
|
const remote = await getCollectionByName(target.name);
|
||||||
|
if (!remote) {
|
||||||
|
throw new Error(`${target.name} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteFields = normalizeFieldList(remote.fields);
|
||||||
|
const targetFields = normalizeFieldList(target.fields);
|
||||||
|
const remoteFieldMap = new Map(remoteFields.map((field) => [field.name, field.type]));
|
||||||
|
const remoteRequiredMap = new Map(remoteFields.map((field) => [field.name, field.required]));
|
||||||
|
const missingFields = [];
|
||||||
|
const mismatchedTypes = [];
|
||||||
|
const mismatchedRequired = [];
|
||||||
|
|
||||||
|
for (const field of targetFields) {
|
||||||
|
if (!remoteFieldMap.has(field.name)) {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteFieldMap.get(field.name) !== field.type) {
|
||||||
|
mismatchedTypes.push(`${field.name}:${remoteFieldMap.get(field.name)}!=${field.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteRequiredMap.get(field.name) !== !!field.required) {
|
||||||
|
mismatchedRequired.push(`${field.name}:${remoteRequiredMap.get(field.name)}!=${!!field.required}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteIndexes = new Set(remote.indexes || []);
|
||||||
|
const missingIndexes = target.indexes.filter((indexSql) => !remoteIndexes.has(indexSql));
|
||||||
|
|
||||||
|
if (remote.type !== target.type) {
|
||||||
|
throw new Error(`${target.name} 类型不匹配,期望 ${target.type},实际 ${remote.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missingFields.length && !mismatchedTypes.length && !mismatchedRequired.length && !missingIndexes.length) {
|
||||||
|
console.log(`✅ ${target.name} 校验通过。`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`❌ ${target.name} 校验失败:`);
|
||||||
|
if (missingFields.length) {
|
||||||
|
console.log(` - 缺失字段: ${missingFields.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (mismatchedTypes.length) {
|
||||||
|
console.log(` - 字段类型不匹配: ${mismatchedTypes.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (mismatchedRequired.length) {
|
||||||
|
console.log(` - 字段必填属性不匹配: ${mismatchedRequired.join(', ')}`);
|
||||||
|
}
|
||||||
|
if (missingIndexes.length) {
|
||||||
|
console.log(` - 缺失索引: ${missingIndexes.join(' | ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`${target.name} 结构与预期不一致`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 正在连接 PocketBase: ${PB_URL}`);
|
||||||
|
pb.authStore.save(AUTH_TOKEN, null);
|
||||||
|
console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。');
|
||||||
|
|
||||||
|
for (const collectionData of collections) {
|
||||||
|
await createOrUpdateCollection(collectionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await verifyCollections(collections);
|
||||||
|
console.log('\n🎉 购物车与订单表结构初始化并校验完成!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 初始化失败:', error.response?.data || error.message);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
198
script/pocketbase.ensure-cart-order-autogen-id.js
Normal file
198
script/pocketbase.ensure-cart-order-autogen-id.js
Normal 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();
|
||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
|||||||
@@ -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")',
|
||||||
@@ -38,6 +39,7 @@ const collections = [
|
|||||||
{ name: 'prod_list_description', type: 'text' },
|
{ name: 'prod_list_description', type: 'text' },
|
||||||
{ name: 'prod_list_feature', type: 'text' },
|
{ name: 'prod_list_feature', type: 'text' },
|
||||||
{ name: 'prod_list_parameters', type: 'json' },
|
{ name: 'prod_list_parameters', type: 'json' },
|
||||||
|
{ name: 'prod_list_function', type: 'json' },
|
||||||
{ name: 'prod_list_plantype', type: 'text' },
|
{ name: 'prod_list_plantype', type: 'text' },
|
||||||
{ name: 'prod_list_category', type: 'text', required: true },
|
{ name: 'prod_list_category', type: 'text', required: true },
|
||||||
{ name: 'prod_list_sort', type: 'number' },
|
{ name: 'prod_list_sort', type: 'number' },
|
||||||
@@ -47,7 +49,9 @@ const collections = [
|
|||||||
{ name: 'prod_list_tags', type: 'text' },
|
{ name: 'prod_list_tags', type: 'text' },
|
||||||
{ name: 'prod_list_status', type: 'text' },
|
{ name: 'prod_list_status', type: 'text' },
|
||||||
{ name: 'prod_list_basic_price', type: 'number' },
|
{ name: 'prod_list_basic_price', type: 'number' },
|
||||||
|
{ 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)',
|
||||||
@@ -106,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;
|
||||||
|
|||||||
Reference in New Issue
Block a user