diff --git a/docs/api_response_field_notes.md b/docs/api_response_field_notes.md index 26857df..71349a5 100644 --- a/docs/api_response_field_notes.md +++ b/docs/api_response_field_notes.md @@ -19,6 +19,7 @@ | `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` | diff --git a/docs/pb_tbl_auth_users.md b/docs/pb_tbl_auth_users.md index 4e7f916..3a36321 100644 --- a/docs/pb_tbl_auth_users.md +++ b/docs/pb_tbl_auth_users.md @@ -24,7 +24,7 @@ | `users_idtype` | `text` | 否 | 身份来源类型或证件类型 | | `users_id_number` | `text` | 否 | 证件号 | | `users_phone` | `text` | 否 | 手机号 | -| `users_level` | `text` | 否 | 用户等级文本 | +| `users_level` | `text` | 否 | 用户等级枚举值,保存“数据-会员等级”字典 enum | | `users_type` | `text` | 否 | 用户类型 | | `company_id` | `text` | 否 | 所属公司业务 ID | | `users_parent_id` | `text` | 否 | 上级用户业务 ID | @@ -63,4 +63,5 @@ - 本表为 `auth` collection,除上述字段外还受 PocketBase 原生鉴权机制约束。 - 图片类字段统一只保存 `tbl_attachments.attachments_id`。 - 登录接口返回的 token 来源于本表 auth record 的原生签发能力,可直接给 PocketBase SDK 使用。 +- 新用户注册时,`users_level` 默认保持为空;已有用户后续登录 / 更新流程也不会自动改写该字段。 - PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。 diff --git a/docs/pb_tbl_cart.md b/docs/pb_tbl_cart.md new file mode 100644 index 0000000..5324b2a --- /dev/null +++ b/docs/pb_tbl_cart.md @@ -0,0 +1,52 @@ +# 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` | 否 | 备注 | + +## 索引 + +| 索引名 | 类型 | 说明 | +| :--- | :--- | :--- | +| `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 做原生访问控制。 +- `cart_product_quantity`、`cart_at_price` 使用 `number`,数量正整数与价格精度建议在 hooks / API 层统一校验。 +- 当购物车被清空时,建议业务侧将历史记录 `cart_status` 置为 `无效`,而不是直接覆盖有效记录。 +- `cart_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。 +- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。 diff --git a/docs/pb_tbl_order.md b/docs/pb_tbl_order.md new file mode 100644 index 0000000..66706c8 --- /dev/null +++ b/docs/pb_tbl_order.md @@ -0,0 +1,54 @@ +# 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` | 否 | 订单备注 | + +## 索引 + +| 索引名 | 类型 | 说明 | +| :--- | :--- | :--- | +| `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 做原生访问控制。 +- `order_amount` 使用 `number`,货币精度策略建议后续统一为“分”或固定小数位。 +- `order_create` 由数据库自动写入,接口层不需要也不应允许客户端自行填值。 +- PocketBase 系统字段 `created`、`updated` 仍然存在,只是不在 collection 字段清单里单独声明。 diff --git a/docs/pb_tbl_product_list.md b/docs/pb_tbl_product_list.md index e542694..1d4ad8e 100644 --- a/docs/pb_tbl_product_list.md +++ b/docs/pb_tbl_product_list.md @@ -16,10 +16,10 @@ | `prod_list_id` | `text` | 是 | 产品列表业务 ID,唯一标识 | | `prod_list_name` | `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_feature` | `text` | 否 | 产品特色 | -| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 数组,格式为 `[ { "name": "属性名", "value": "属性值" } ]`) | +| `prod_list_parameters` | `json` | 否 | 产品参数(JSON 数组,格式为 `[ { "sort": 1, "name": "属性名", "value": "属性值" } ]`) | | `prod_list_plantype` | `text` | 否 | 产品方案 | | `prod_list_category` | `text` | 是 | 产品分类(必填,单选) | | `prod_list_sort` | `number` | 否 | 排序值(同分类内按升序) | @@ -29,6 +29,7 @@ | `prod_list_tags` | `text` | 否 | 产品标签(辅助检索,以 `|` 聚合) | | `prod_list_status` | `text` | 否 | 产品状态(有效 / 过期 / 主推等) | | `prod_list_basic_price` | `number` | 否 | 基础价格 | +| `prod_list_vip_price` | `json` | 否 | 会员价数组,格式为 `[{"viplevel":"会员等级枚举值","price":1999}]` | | `prod_list_remark` | `text` | 否 | 备注 | ## 索引 @@ -47,9 +48,12 @@ ## 补充约定 -- `prod_list_icon` 仅保存附件业务 ID,真实文件统一在 `tbl_attachments`。 +- `prod_list_icon` 仅保存附件业务 ID,真实文件统一在 `tbl_attachments`;多图时按上传顺序使用 `|` 聚合。 - 当前预构建脚本中已将 `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` 时应保持和存储格式一致(Markdown 渲染器或受控 HTML 渲染),避免直接注入未净化内容。 - `prod_list_basic_price` 使用 `number`,如需货币精度策略建议后续统一为“分”或固定小数位。 diff --git a/pocket-base/bai-api-main.pb.js b/pocket-base/bai-api-main.pb.js index b61c1b7..19b8819 100644 --- a/pocket-base/bai-api-main.pb.js +++ b/pocket-base/bai-api-main.pb.js @@ -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/update.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/sdk-permission/context.js`) require(`${__hooks}/bai_api_pb_hooks/bai_api_routes/sdk-permission/role-save.js`) diff --git a/pocket-base/bai-web-main.pb.js b/pocket-base/bai-web-main.pb.js index 699a94a..ab0fad4 100644 --- a/pocket-base/bai-web-main.pb.js +++ b/pocket-base/bai-web-main.pb.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/dictionary-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-chat.pb.js`) require(`${__hooks}/bai_chat_alm_hooks/bai-sql-lab.pb.js`) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-user-update.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-user-update.js new file mode 100644 index 0000000..d0c1081 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-user-update.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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js new file mode 100644 index 0000000..665f33d --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js new file mode 100644 index 0000000..39cf94d --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js new file mode 100644 index 0000000..715a352 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js new file mode 100644 index 0000000..960d74d --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js new file mode 100644 index 0000000..70375d7 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js new file mode 100644 index 0000000..02be668 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js new file mode 100644 index 0000000..5750c50 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js new file mode 100644 index 0000000..5b0a578 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js new file mode 100644 index 0000000..d960b4f --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js new file mode 100644 index 0000000..d38f984 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js new file mode 100644 index 0000000..3959ba4 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js @@ -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) + } +}) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js index ae04fde..0934409 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/sdk-permission/context.js @@ -1,15 +1,28 @@ routerAdd('POST', '/api/sdk-permission/context', function (e) { 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 { 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`) - guards.requireJson(e) - guards.requireManagePlatformUser(e) + try { + guards.requireJson(e) + guards.requireManagePlatformUser(e) - const payload = guards.validateSdkPermissionContextBody(e) - const data = permissionService.getManagementContext(payload.keyword) + const payload = guards.validateSdkPermissionContextBody(e) + 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) + } }) diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/.env b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/.env index 4e78de5..b44b228 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/.env +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/.env @@ -1,6 +1,6 @@ NODE_ENV=production 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 #正式服 diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js index 2b778d8..92a6d04 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js @@ -2,7 +2,7 @@ module.exports = { NODE_ENV: 'production', APP_VERSION: '0.1.21', APP_BASE_URL: 'https://bai-api.blv-oa.com', - POCKETBASE_API_URL: 'https://bai-api.blv-oa.com/pb', + POCKETBASE_API_URL: 'http://127.0.0.1:8090', POCKETBASE_AUTH_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyIsImV4cCI6MTgwNTk2MzM4NywiaWQiOiJmcnl2dG1qNnlwcHlmMXAiLCJyZWZyZXNoYWJsZSI6ZmFsc2UsInR5cGUiOiJhdXRoIn0.R34MTotuFBpevqbbUtvQ3GPa0Z1mpY8eQwZgDdmfhMo', WECHAT_APPID: 'wx3bd7a7b19679da7a', WECHAT_SECRET: '57e40438c2a9151257b1927674db10e1', diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js index d9ba824..437666d 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js @@ -107,14 +107,32 @@ function validateSystemRefreshBody(e) { } function requireManagePlatformUser(e) { - const authUser = requireAuthUser(e) - const idType = authUser.authRecord.getString('users_idtype') + const authHeader = e.request.header.get('Authorization') || '' + 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') { throw createAppError(403, '仅平台管理用户可访问') } - return authUser + return { + openid: authRecord.getString('openid') || '', + authRecord: authRecord, + } } function validateDictionaryListBody(e) { @@ -355,16 +373,31 @@ function normalizeProductParameters(value) { const result = [] 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() if (!name) { return } const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue) + const normalizedSort = normalizeParameterSort(rawSort, fallbackSort) const existingIndex = indexByName[name] if (typeof existingIndex === 'number') { result[existingIndex].value = normalizedValue + result[existingIndex].sort = normalizedSort return } @@ -372,6 +405,7 @@ function normalizeProductParameters(value) { result.push({ name: name, value: normalizedValue, + sort: normalizedSort, }) } @@ -381,7 +415,7 @@ function normalizeProductParameters(value) { if (!item) { continue } - upsert(item.name || item.key, item.value) + upsert(item.name || item.key, item.value, item.sort, result.length + 1) } return result } @@ -393,7 +427,58 @@ function normalizeProductParameters(value) { const keys = Object.keys(value) 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 @@ -439,10 +524,11 @@ function validateProductMutationBody(e, isUpdate) { prod_list_id: payload.prod_list_id || '', prod_list_name: payload.prod_list_name || '', 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_feature: payload.prod_list_feature || '', 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_category: payload.prod_list_category || '', prod_list_sort: typeof payload.prod_list_sort === 'undefined' ? 0 : payload.prod_list_sort, @@ -452,6 +538,7 @@ function validateProductMutationBody(e, isUpdate) { prod_list_tags: payload.prod_list_tags || '', 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_vip_price: normalizeProductVipPrice(payload.prod_list_vip_price), prod_list_remark: payload.prod_list_remark || '', } } @@ -460,6 +547,157 @@ function validateProductDeleteBody(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) { const payload = parseBody(e) @@ -635,6 +873,16 @@ module.exports = { validateProductDetailBody, validateProductMutationBody, validateProductDeleteBody, + validateCartListBody, + validateCartDetailBody, + validateCartMutationBody, + validateCartDeleteBody, + validateOrderListBody, + validateOrderDetailBody, + validateOrderMutationBody, + validateOrderDeleteBody, + validateCartOrderManageListBody, + validateCartOrderManageUserUpdateBody, validateDocumentHistoryListBody, validateSdkPermissionContextBody, validateSdkPermissionRoleBody, diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js new file mode 100644 index 0000000..e67e7d9 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js @@ -0,0 +1,783 @@ +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 productInfo = buildProductInfo(findProductRecordByBusinessId(record.cart_product_id)) + + return { + pb_id: String(record.id || ''), + cart_id: String(record.cart_id || ''), + cart_number: String(record.cart_number || ''), + cart_create: String(record.cart_create || ''), + cart_owner: String(record.cart_owner || ''), + cart_product_id: String(record.cart_product_id || ''), + cart_product_quantity: Number(record.cart_product_quantity || 0), + cart_status: String(record.cart_status || ''), + cart_at_price: Number(record.cart_at_price || 0), + cart_remark: String(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.created || ''), + updated: String(record.updated || ''), + } +} + +function exportAdminOrderRecord(record) { + return { + pb_id: String(record.id || ''), + order_id: String(record.order_id || ''), + order_number: String(record.order_number || ''), + order_create: String(record.order_create || ''), + order_owner: String(record.order_owner || ''), + order_source: String(record.order_source || ''), + order_status: String(record.order_status || ''), + order_source_id: String(record.order_source_id || ''), + order_snap: parseJsonFieldForOutput(record.order_snap), + order_amount: Number(record.order_amount || 0), + order_remark: String(record.order_remark || ''), + created: String(record.created || ''), + updated: String(record.updated || ''), + } +} + +function exportAdminManageUser(userRecord, groupedCarts, groupedOrders) { + const openid = String(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.id || ''), + openid: openid, + users_id: String(userRecord.users_id || ''), + users_name: String(userRecord.users_name || ''), + users_phone: String(userRecord.users_phone || ''), + users_level: String(userRecord.users_level || ''), + users_level_name: userService.resolveUserLevelName(String(userRecord.users_level || '')), + users_type: String(userRecord.users_type || ''), + users_idtype: String(userRecord.users_idtype || ''), + users_id_number: String(userRecord.users_id_number || ''), + users_status: String(userRecord.users_status || ''), + users_rank_level: userRecord.users_rank_level === null || typeof userRecord.users_rank_level === 'undefined' + ? null + : Number(userRecord.users_rank_level), + users_auth_type: userRecord.users_auth_type === null || typeof userRecord.users_auth_type === 'undefined' + ? null + : Number(userRecord.users_auth_type), + users_tag: String(userRecord.users_tag || ''), + company_id: String(userRecord.company_id || ''), + users_parent_id: String(userRecord.users_parent_id || ''), + users_promo_code: String(userRecord.users_promo_code || ''), + usergroups_id: String(userRecord.usergroups_id || ''), + users_picture: String(userRecord.users_picture || ''), + users_id_pic_a: String(userRecord.users_id_pic_a || ''), + users_id_pic_b: String(userRecord.users_id_pic_b || ''), + users_title_picture: String(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.created || ''), + updated: String(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 = String(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 = String(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, +} diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js index 747689c..092394c 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/dictionaryService.js @@ -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) { const records = $app.findRecordsByFilter('tbl_system_dict', 'dict_name = {:dictName}', '', 1, 0, { dictName: dictName, @@ -157,6 +168,15 @@ function getDictionaryByName(dictName) { return exportDictionaryRecord(record) } +function getDictionaryItemsByName(dictName) { + const dictionary = getDictionaryByName(dictName) + if (!dictionary.dict_word_is_enabled) { + return [] + } + + return sortDictionaryItems(dictionary.items) +} + function createDictionary(payload) { ensureDictionaryNameUnique(payload.dict_name) @@ -192,9 +212,9 @@ function updateDictionary(payload) { 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_parent_id', payload.dict_word_parent_id) record.set('dict_word_remark', payload.dict_word_remark) @@ -210,7 +230,7 @@ function updateDictionary(payload) { } logger.info('字典修改成功', { - dict_name: payload.dict_name, + dict_name: immutableName, original_dict_name: payload.original_dict_name, }) @@ -245,6 +265,7 @@ function deleteDictionary(dictName) { module.exports = { listDictionaries, getDictionaryByName, + getDictionaryItemsByName, createDictionary, updateDictionary, deleteDictionary, diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js index 4202646..bbb8796 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/documentService.js @@ -435,23 +435,30 @@ function deleteAttachment(attachmentId) { function listDocuments(payload) { const allRecords = $app.findRecordsByFilter('tbl_document', '', '', 500, 0) - const keyword = String(payload.keyword || '').toLowerCase() + const titleKeyword = String(payload.title_keyword || '').toLowerCase().trim() const status = String(payload.status || '') const type = String(payload.document_type || '') const result = [] for (let i = 0; i < allRecords.length; i += 1) { const item = exportDocumentRecord(allRecords[i]) - const matchedKeyword = !keyword - || item.document_id.toLowerCase().indexOf(keyword) !== -1 - || item.document_title.toLowerCase().indexOf(keyword) !== -1 - || item.document_subtitle.toLowerCase().indexOf(keyword) !== -1 - || item.document_summary.toLowerCase().indexOf(keyword) !== -1 - || item.document_keywords.toLowerCase().indexOf(keyword) !== -1 + const matchedTitleKeyword = !titleKeyword + || item.document_title.toLowerCase().indexOf(titleKeyword) !== -1 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) } } diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js index 1544735..d046510 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/productService.js @@ -1,6 +1,9 @@ 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 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) { return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6) @@ -37,6 +40,16 @@ function normalizeSortValue(value) { } function normalizePipeValues(value) { + if (Array.isArray(value)) { + return value + .map(function (item) { + return normalizeText(item) + }) + .filter(function (item) { + return !!item + }) + } + return String(value || '') .split('|') .map(function (item) { @@ -70,6 +83,64 @@ function normalizeRequiredCategory(value) { 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) { const grouped = {} for (let i = 0; i < records.length; i += 1) { @@ -118,16 +189,18 @@ function normalizeParameters(value) { const result = [] const indexByName = {} - function upsert(nameValue, rawValue) { + function upsert(nameValue, rawValue, rawSort, fallbackSort, inputIndex) { const name = normalizeText(nameValue) if (!name) { return } const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue) + const normalizedSort = normalizePositiveSort(rawSort, 'prod_list_parameters.sort', fallbackSort) const existingIndex = indexByName[name] if (typeof existingIndex === 'number') { result[existingIndex].value = normalizedValue + result[existingIndex].sort = normalizedSort return } @@ -135,6 +208,8 @@ function normalizeParameters(value) { result.push({ name: name, value: normalizedValue, + sort: normalizedSort, + __inputIndex: inputIndex, }) } @@ -144,21 +219,21 @@ function normalizeParameters(value) { if (!item) { 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') { throw createAppError(400, 'prod_list_parameters 必须是数组或对象') } - const keys = Object.keys(value) + const keys = safeObjectKeys(value) 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) { @@ -166,10 +241,37 @@ function normalizeParametersForOutput(value) { 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 if (typeof source === 'string') { try { source = JSON.parse(source) + if (source === null) { + return [] + } } catch (_error) { const raw = normalizeText(source) if (raw.indexOf('map[') === 0 && raw.endsWith(']')) { @@ -191,16 +293,9 @@ function normalizeParametersForOutput(value) { continue } const val = pair.slice(separatorIndex + 1) - const normalizedValue = val === null || typeof val === 'undefined' ? '' : String(val) - const existingIndex = indexByName[key] - if (typeof existingIndex === 'number') { - result[existingIndex].value = normalizedValue - } else { - indexByName[key] = result.length - result.push({ name: key, value: normalizedValue }) - } + pushOrUpdate(result, indexByName, key, val, '', i + 1, i + 1) } - return result + return sortParameterRows(result) } return [] } @@ -214,47 +309,165 @@ function normalizeParametersForOutput(value) { if (!item) { continue } - const name = normalizeText(item.name || item.key) - if (!name) { - continue - } - const normalizedValue = item.value === null || typeof item.value === 'undefined' ? '' : String(item.value) - const existingIndex = indexByName[name] - if (typeof existingIndex === 'number') { - mapped[existingIndex].value = normalizedValue - } else { - indexByName[name] = mapped.length - mapped.push({ name: name, value: normalizedValue }) - } + pushOrUpdate(mapped, indexByName, item.name || item.key, item.value, item.sort, i + 1, i + 1) } - return mapped + return sortParameterRows(mapped) } if (typeof source !== 'object') { return [] } + if (source === null) { + return [] + } + // Some PocketBase/Goja map-like values are not directly enumerable; roundtrip to plain object. try { source = JSON.parse(JSON.stringify(source)) } catch (_error) {} + if (source === null || typeof source !== 'object') { + return [] + } + const result = [] const indexByName = {} - const keys = Object.keys(source) + const keys = safeObjectKeys(source) for (let i = 0; i < keys.length; i += 1) { - const name = normalizeText(keys[i]) - if (!name) { + pushOrUpdate(result, indexByName, keys[i], source[keys[i]], '', i + 1, i + 1) + } + + 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 } - const current = source[keys[i]] - const normalizedValue = current === null || typeof current === 'undefined' ? '' : String(current) - const existingIndex = indexByName[name] - if (typeof existingIndex === 'number') { - result[existingIndex].value = normalizedValue - } else { - indexByName[name] = result.length - result.push({ name: name, value: normalizedValue }) + result[key] = { + value: key, + label: String(items[i].description || ''), + } + } + + return result +} + +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 +475,17 @@ function normalizeParametersForOutput(value) { } function ensureAttachmentExists(attachmentId, fieldName) { - const value = normalizeText(attachmentId) - if (!value) { + const values = normalizeAttachmentIdList(attachmentId) + if (!values.length) { return } - try { - documentService.getAttachmentDetail(value) - } catch (_err) { - throw createAppError(400, fieldName + ' 对应附件不存在:' + value) + for (let i = 0; i < values.length; i += 1) { + try { + documentService.getAttachmentDetail(values[i]) + } catch (_err) { + throw createAppError(400, fieldName + ' 对应附件不存在:' + values[i]) + } } } @@ -283,34 +498,51 @@ function findProductRecordByBusinessId(productId) { } function exportProductRecord(record, extra) { - const iconId = record.getString('prod_list_icon') - let iconAttachment = null + const iconIds = normalizeAttachmentIdList(record.getString('prod_list_icon')) + const iconAttachments = [] + const iconUrls = [] - if (iconId) { + for (let i = 0; i < iconIds.length; i += 1) { try { - iconAttachment = documentService.getAttachmentDetail(iconId) + const attachment = documentService.getAttachmentDetail(iconIds[i]) + iconAttachments.push(attachment) + iconUrls.push(attachment.attachments_url || '') } catch (_error) { - iconAttachment = null + continue } } + const firstIconAttachment = iconAttachments.length ? iconAttachments[0] : null 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 parametersFromRaw = normalizeParametersForOutput(parametersRaw) 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 { pb_id: record.id, 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_icon: iconId, - prod_list_icon_attachment: iconAttachment, - prod_list_icon_url: iconAttachment ? iconAttachment.attachments_url : '', + prod_list_icon: iconIds.join('|'), + prod_list_icon_ids: iconIds, + 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_feature: record.getString('prod_list_feature'), prod_list_parameters: parameters, + prod_list_function: functions, prod_list_plantype: record.getString('prod_list_plantype'), prod_list_category: record.getString('prod_list_category'), prod_list_sort: Number(record.get('prod_list_sort') || 0), @@ -320,6 +552,7 @@ function exportProductRecord(record, extra) { prod_list_power_supply: record.getString('prod_list_power_supply'), prod_list_tags: record.getString('prod_list_tags'), 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_remark: record.getString('prod_list_remark'), created: String(record.created || ''), @@ -328,42 +561,57 @@ function exportProductRecord(record, extra) { } function listProducts(payload) { - const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0) - const keyword = normalizeText(payload.keyword).toLowerCase() - const status = normalizeText(payload.status) - const category = normalizeText(payload.prod_list_category) - const result = [] + try { + const allRecords = $app.findRecordsByFilter('tbl_product_list', '', '', 500, 0) + const keyword = normalizeText(payload.keyword).toLowerCase() + const status = normalizeText(payload.status) + const category = normalizeText(payload.prod_list_category) + const result = [] - const allItems = [] - for (let i = 0; i < allRecords.length; i += 1) { - allItems.push(exportProductRecord(allRecords[i])) - } - - const rankMap = buildCategoryRankMap(allItems) - - for (let i = 0; i < allItems.length; i += 1) { - const source = allItems[i] - const item = Object.assign({}, source, { - prod_list_category_rank: rankMap[source.prod_list_id] || 0, - }) - const matchedKeyword = !keyword - || item.prod_list_id.toLowerCase().indexOf(keyword) !== -1 - || item.prod_list_name.toLowerCase().indexOf(keyword) !== -1 - || item.prod_list_modelnumber.toLowerCase().indexOf(keyword) !== -1 - || item.prod_list_tags.toLowerCase().indexOf(keyword) !== -1 - const matchedStatus = !status || item.prod_list_status === status - const matchedCategory = !category || item.prod_list_category === category - - if (matchedKeyword && matchedStatus && matchedCategory) { - result.push(item) + const allItems = [] + for (let i = 0; i < allRecords.length; i += 1) { + try { + 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) + + for (let i = 0; i < allItems.length; i += 1) { + const source = allItems[i] + const item = Object.assign({}, source, { + prod_list_category_rank: rankMap[source.prod_list_id] || 0, + }) + const matchedKeyword = !keyword + || item.prod_list_id.toLowerCase().indexOf(keyword) !== -1 + || item.prod_list_name.toLowerCase().indexOf(keyword) !== -1 + || item.prod_list_modelnumber.toLowerCase().indexOf(keyword) !== -1 + || item.prod_list_tags.toLowerCase().indexOf(keyword) !== -1 + const matchedStatus = !status || item.prod_list_status === status + const matchedCategory = !category || item.prod_list_category === category + + if (matchedKeyword && matchedStatus && matchedCategory) { + result.push(item) + } + } + + result.sort(function (a, b) { + return String(b.updated || '').localeCompare(String(a.updated || '')) + }) + + return result + } catch (err) { + logger.error('产品列表构建失败,返回空列表兜底', { + errMsg: (err && err.message) || '未知错误', + }) + return [] } - - result.sort(function (a, b) { - return String(b.updated || '').localeCompare(String(a.updated || '')) - }) - - return result } function getProductDetail(productId) { @@ -399,10 +647,11 @@ function createProduct(_userOpenid, payload) { record.set('prod_list_id', targetProductId) record.set('prod_list_name', normalizeText(payload.prod_list_name)) 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_feature', normalizeText(payload.prod_list_feature)) 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_category', normalizeRequiredCategory(payload.prod_list_category)) record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort)) @@ -412,6 +661,7 @@ function createProduct(_userOpenid, payload) { record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags)) 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_vip_price', normalizeVipPriceRows(payload.prod_list_vip_price)) record.set('prod_list_remark', normalizeText(payload.prod_list_remark)) try { @@ -448,10 +698,11 @@ function updateProduct(_userOpenid, payload) { record.set('prod_list_name', normalizeText(payload.prod_list_name)) 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_feature', normalizeText(payload.prod_list_feature)) 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_category', normalizeRequiredCategory(payload.prod_list_category)) record.set('prod_list_sort', normalizeSortValue(payload.prod_list_sort)) @@ -461,6 +712,7 @@ function updateProduct(_userOpenid, payload) { record.set('prod_list_tags', joinUniquePipeValues(payload.prod_list_tags)) 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_vip_price', normalizeVipPriceRows(payload.prod_list_vip_price)) record.set('prod_list_remark', normalizeText(payload.prod_list_remark)) try { diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js index b85807b..ea6578b 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/sdkPermissionService.js @@ -100,6 +100,7 @@ function uniqueList(values) { function listRoles() { const records = $app.findRecordsByFilter(ROLE_COLLECTION, '', 'role_name', 500, 0) + return records.map(function (record) { return { pb_id: record.id, @@ -353,7 +354,7 @@ function saveCollectionRules(payload) { function listUsers(keyword, roleMap) { 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 usergroupsId = record.getString('usergroups_id') const role = roleMap && roleMap[usergroupsId] ? roleMap[usergroupsId] : null diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js index b2b211c..cef3271 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/userService.js @@ -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 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 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 GUEST_USER_TYPE = '游客' const REGISTERED_USER_TYPE = '注册用户' const WECHAT_ID_TYPE = 'WeChat' const MANAGE_PLATFORM_ID_TYPE = 'ManagePlatform' +const USER_LEVEL_DICT_NAME = '数据-会员等级' const mutationLocks = {} function buildUserId() { @@ -42,6 +44,74 @@ function isAllProfileFieldsEmpty(record) { 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) { if (mutationLocks[lockKey]) { throw createAppError(429, '请求过于频繁,请稍后重试') @@ -275,6 +345,7 @@ function enrichUser(userRecord) { users_phone: userRecord.getString('users_phone'), users_phone_masked: maskPhone(userRecord.getString('users_phone')), users_level: userRecord.getString('users_level'), + users_level_name: resolveUserLevelName(userRecord.getString('users_level')), users_tag: userRecord.getString('users_tag'), users_picture: userPicture.id, users_picture_url: userPicture.url, @@ -438,7 +509,6 @@ function registerPlatformUser(payload) { record.set('users_name', payload.users_name) record.set('users_phone', payload.users_phone) 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_tag', payload.users_tag || '') 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) { const userRecord = findUserByOpenid(openid) if (!userRecord) { @@ -690,8 +853,11 @@ module.exports = { authenticatePlatformUser, ensureAuthToken, updateWechatUserProfile, + updateManagedUserProfile, refreshAuthToken, issueAuthToken, registerPlatformUser, + resolveUserLevelName, + getUserLevelOptions, } diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js index 1697d51..52f2400 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/utils/response.js @@ -53,7 +53,11 @@ function successWithToken(e, msg, data, token, code) { function fail(e, msg, data, code) { 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 = { diff --git a/pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js b/pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js new file mode 100644 index 0000000..0cd139b --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js @@ -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) +}) diff --git a/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js b/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js index 4474d7c..d4984dc 100644 --- a/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js @@ -1,1081 +1,9 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { - const html = ` - - - - - 字典管理 - - - - -
-
-

字典管理

-
- 返回主页 - - -
-
-
- -
-
- - - - - -
-
- - - - - - - - - - - - - - -
字典名称启用备注枚举项创建时间操作
暂无数据,请先查询。
-
-
-
- - - -
- - 预览原图 -
- -
-
-
-
处理中,请稍候...
-
-
- - - -` + const html = $template.loadFiles( + __hooks + '/bai_web_pb_hooks/views/dictionary-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) }) diff --git a/pocket-base/bai_web_pb_hooks/pages/document-manage.js b/pocket-base/bai_web_pb_hooks/pages/document-manage.js index ccf84eb..6893a6c 100644 --- a/pocket-base/bai_web_pb_hooks/pages/document-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/document-manage.js @@ -1,1439 +1,9 @@ routerAdd('GET', '/manage/document-manage', function (e) { - const html = ` - - - - - 文档管理 - - - - -
-
-

文档管理

-
- 返回主页 - - - -
-
-
- -
-
-
-

新增文档

-
-
当前模式:新建
-
-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
-
-
- - -
- - -
-
系统会根据生效日期和到期日期自动切换状态;都不填写时默认有效。
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
可不选,也可多选。
-
-
-
-
-
- -
-
可不选,也可多选。
-
-
-
-
-
- -
-
可不选,也可多选。
-
-
-
-
-
- -
-
可不选,也可多选。
-
-
-
-
-
- - -
-
-
-
-

图片附件

- -
-
拖拽图片到这里,或点击选择文件
- -
-
-
-
-
-

视频附件

- -
-
拖拽视频到这里,或点击选择文件
- -
-
-
-
-
-

文件附件

- -
-
拖拽文件到这里,或点击选择文件
- -
-
-
-
-
-
- - - -
-
-
- -
-

文档列表

-
- - - - - - - - - - - - - -
标题类型/状态附件链接更新时间操作
暂无数据,请先刷新列表。
-
-
-
- -
- - 预览原图 -
- -
-
-
-
处理中,请稍候...
-
-
- - - -` + const html = $template.loadFiles( + __hooks + '/bai_web_pb_hooks/views/document-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) }) diff --git a/pocket-base/bai_web_pb_hooks/pages/index.js b/pocket-base/bai_web_pb_hooks/pages/index.js index 0fc5caa..7afe66b 100644 --- a/pocket-base/bai_web_pb_hooks/pages/index.js +++ b/pocket-base/bai_web_pb_hooks/pages/index.js @@ -1,94 +1,9 @@ routerAdd('GET', '/manage', function (e) { - const html = ` - - - - - 管理主页 - - - - -
-
-

管理主页

-
-

平台管理

-
- - - - -
-
-
-

AI 管理

-
- - - -
-
-
- -
-
-
- - -` + const html = $template.loadFiles( + __hooks + '/bai_web_pb_hooks/views/index.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) }) diff --git a/pocket-base/bai_web_pb_hooks/pages/login.js b/pocket-base/bai_web_pb_hooks/pages/login.js index 683b54a..85d3cb0 100644 --- a/pocket-base/bai_web_pb_hooks/pages/login.js +++ b/pocket-base/bai_web_pb_hooks/pages/login.js @@ -1,157 +1,9 @@ function renderLoginPage(e) { - const html = ` - - - - - 登录 - - - - -
-
-

后台登录

-

请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。

-
-
- - -
-
- - -
- -
-
-
-
- - -` + const html = $template.loadFiles( + __hooks + '/bai_web_pb_hooks/views/login.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) } diff --git a/pocket-base/bai_web_pb_hooks/pages/product-manage.js b/pocket-base/bai_web_pb_hooks/pages/product-manage.js index 2f84922..0fb8374 100644 --- a/pocket-base/bai_web_pb_hooks/pages/product-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/product-manage.js @@ -1,1461 +1,9 @@ routerAdd('GET', '/manage/product-manage', function (e) { - const html = ` - - - - - 产品管理 - - - - -
-
-

产品管理

-
- 返回主页 - - - -
-
-
- -
-
- - - - -
-
- -
-
-

新增产品

-
当前模式:新建
-
-
-
- - -
-
- - -
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
- - -
-
- - -
-
- - -
请先选择产品分类后查看当前分类内排序位次。
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- - -
-
- -
-
-
- - -
-
-
-
-
- -
- - - - - - - - - -
属性名属性值操作
-
-
- - -
仅支持 JSON 对象格式。导入时按属性名增量合并:同名覆盖、不同名新增,不会清空当前参数表。
-
-
- - - -
-
-
- -
-
- -
暂无图标
-
-
- -
选择后会在保存时上传,并写入 prod_list_icon。
-
- -
-
-
-
-
- - -
-
-
- - - -
-
-
- -
-

产品列表

-
- - - - - - - - - - - - - - -
名称/型号分类信息标签状态/价格更新时间操作
暂无数据,请先查询。
-
-
-
- -
-
-
-
处理中,请稍候...
-
-
- - - -` + const html = $template.loadFiles( + __hooks + '/bai_web_pb_hooks/views/product-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) }) diff --git a/pocket-base/bai_web_pb_hooks/pages/sdk-permission-manage.js b/pocket-base/bai_web_pb_hooks/pages/sdk-permission-manage.js index 3fdef03..a769ad9 100644 --- a/pocket-base/bai_web_pb_hooks/pages/sdk-permission-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/sdk-permission-manage.js @@ -1,745 +1,9 @@ routerAdd('GET', '/manage/sdk-permission-manage', function (e) { - const html = ` - - - - - SDK 权限管理 - - - - -
-
-

SDK 权限管理

-

这里管理的是 tbl_auth_users 用户通过 PocketBase SDK 直连数据库时的业务权限。ManagePlatform 会被视为你的业务管理员,但不会自动变成 PocketBase 原生 _superusers

-
- 返回主页 - - - -
-
-
- -
-
加载中...
-
- -
-

角色管理

-
- - - - -
角色 ID 由系统自动生成,页面不显示。
- -
-
- - - - - - - - - - - - - -
名称编码状态备注操作
暂无角色。
-
-
- -
-

用户授权

-
- - -
-
- - - - - - - - - - - - - -
用户身份类型当前角色授权操作
暂无用户。
-
-
- -
-

Collection 直连权限

-
- -
这里是按“当前配置角色”逐表分配 CRUD 权限。下面依次对应“列表、详情、新增、修改、删除”五种权限,勾选后会立即保存。公开访问或自定义规则的操作不会显示授权勾选框。
-
-
- - - - - - - - - - -
集合当前角色权限
暂无集合。
-
-
-
- -
-
-
-
处理中,请稍候...
-
-
- - - -` + const html = $template.loadFiles( + __hooks + '/bai_web_pb_hooks/views/sdk-permission-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) }) diff --git a/pocket-base/bai_web_pb_hooks/shared/theme-body.html b/pocket-base/bai_web_pb_hooks/shared/theme-body.html new file mode 100644 index 0000000..bfdbf43 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/shared/theme-body.html @@ -0,0 +1,32 @@ +{{ define "theme_body" }} + + +{{ end }} diff --git a/pocket-base/bai_web_pb_hooks/shared/theme-head.html b/pocket-base/bai_web_pb_hooks/shared/theme-head.html new file mode 100644 index 0000000..d79c550 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/shared/theme-head.html @@ -0,0 +1,186 @@ +{{ define "theme_head" }} + + +{{ end }} diff --git a/pocket-base/bai_web_pb_hooks/views/cart-order-manage.html b/pocket-base/bai_web_pb_hooks/views/cart-order-manage.html new file mode 100644 index 0000000..b2ee0d7 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/cart-order-manage.html @@ -0,0 +1,407 @@ + + + + + + 用户信息及订单管理 + + + {{ template "theme_head" . }} + + +
+
+

用户信息及订单管理

+
+ 返回主页 + +
+
+
+ +
+
+ + + + +
+
+ +
+
+

用户列表

+
+
+ +
+
+
+
+
+ + + {{ template "theme_body" . }} + + diff --git a/pocket-base/bai_web_pb_hooks/views/dictionary-manage.html b/pocket-base/bai_web_pb_hooks/views/dictionary-manage.html new file mode 100644 index 0000000..6f9db2b --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/dictionary-manage.html @@ -0,0 +1,1151 @@ + + + + + + 字典管理 + + + {{ template "theme_head" . }} + + +
+
+

字典管理

+
+ 返回主页 + +
+
+
+ +
+
+ + + + + +
+
+ + + + + + + + + + + + + + +
字典名称启用备注枚举项创建时间操作
暂无数据,请先查询。
+
+
+
+ + + +
+ + 预览原图 +
+ +
+
+
+
处理中,请稍候...
+
+
+ + + {{ template "theme_body" . }} + + diff --git a/pocket-base/bai_web_pb_hooks/views/document-manage.html b/pocket-base/bai_web_pb_hooks/views/document-manage.html new file mode 100644 index 0000000..0fff527 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/document-manage.html @@ -0,0 +1,1579 @@ + + + + + + 文档管理 + + + {{ template "theme_head" . }} + + +
+
+

文档管理

+
+ 返回主页 + + +
+
+
+ +
+
+
+

新增文档

+
+
当前模式:新建
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+ + +
+ + +
+
系统会根据生效日期和到期日期自动切换状态;都不填写时默认有效。
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
可不选,也可多选。
+
+
+
+
+
+ +
+
可不选,也可多选。
+
+
+
+
+
+ +
+
可不选,也可多选。
+
+
+
+
+
+ +
+
可不选,也可多选。
+
+
+
+
+
+ + +
+
+
+
+

图片附件

+ +
+
拖拽图片到这里,或点击选择文件
+ +
+
+
+
+
+

视频附件

+ +
+
拖拽视频到这里,或点击选择文件
+ +
+
+
+
+
+

文件附件

+ +
+
拖拽文件到这里,或点击选择文件
+ +
+
+
+
+
+
+ + + +
+
+
+ +
+

文档列表

+
+
+ + +
+
+ + +
+ + +
+
+ + + + + + + + + + + + + +
标题类型/状态附件链接更新时间操作
暂无数据,请先刷新列表。
+
+
+
+ +
+ + 预览原图 +
+ +
+
+
+
处理中,请稍候...
+
+
+ + + {{ template "theme_body" . }} + + diff --git a/pocket-base/bai_web_pb_hooks/views/index.html b/pocket-base/bai_web_pb_hooks/views/index.html new file mode 100644 index 0000000..ea0f606 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/index.html @@ -0,0 +1,96 @@ + + + + + + 管理主页 + + + {{ template "theme_head" . }} + + +
+
+

管理主页

+
+

平台管理

+
+ + + + + +
+
+
+

AI 管理

+
+ + + +
+
+
+ +
+
+
+ + {{ template "theme_body" . }} + + diff --git a/pocket-base/bai_web_pb_hooks/views/login.html b/pocket-base/bai_web_pb_hooks/views/login.html new file mode 100644 index 0000000..d539c18 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/login.html @@ -0,0 +1,155 @@ + + + + + + 登录 + + + {{ template "theme_head" . }} + + +
+
+

后台登录

+

请输入平台账号(邮箱或手机号)与密码,登录成功后会自动跳转到管理首页。

+
+
+ + +
+
+ + +
+ +
+
+
+
+ + {{ template "theme_body" . }} + + \ No newline at end of file diff --git a/pocket-base/bai_web_pb_hooks/views/product-manage.html b/pocket-base/bai_web_pb_hooks/views/product-manage.html new file mode 100644 index 0000000..ca49c01 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/product-manage.html @@ -0,0 +1,2323 @@ + + + + + + 产品管理 + + + {{ template "theme_head" . }} + + +
+
+

产品管理

+
+ 返回主页 + + +
+
+
+ +
+
+ + + + +
+
+ +
+
+

新增产品

+
当前模式:新建
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
请先选择产品分类后查看当前分类内排序位次。
+
+
+
+ +
+ + + + + + + + +
会员等级会员价格
+
+
按“数据-会员等级”字典自动生成行,一行对应一个会员等级,只需要填写价格。
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+
+
+ +
+ + + + + + + + + +
属性名属性值操作 / 排序
+
+
+ + +
推荐使用数组格式:每项包含 sort、name、value。也兼容旧对象格式。导入时按属性名增量合并:同名覆盖、不同名新增,不会清空当前参数表。
+
+
+ + + +
+
+
+ +
+ + + + + + + + + +
功能名功能说明操作 / 排序
+
+
+ + +
推荐使用数组格式:每项包含 sort、name、value。也兼容旧对象格式。导入时按功能名增量合并:同名覆盖、不同名新增,不会清空当前功能表。
+
+
+ + + +
+
+
+ +
+
+ +
支持多图上传。保存时会上传到附件表,并按上传后附件 ID 用 | 写入 prod_list_icon。
+
+ +
+
+
+
+
暂无图标
+
+
+
+
+ + +
+
+
+ + + +
+
+
+ +
+

产品列表

+
+ + + + + + + + + + + + + + +
名称/型号分类信息标签状态/价格更新时间操作
暂无数据,请先查询。
+
+
+
+ + + +
+
+
+
处理中,请稍候...
+
+
+ + + {{ template "theme_body" . }} + + diff --git a/pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html b/pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html new file mode 100644 index 0000000..5b42163 --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/views/sdk-permission-manage.html @@ -0,0 +1,735 @@ + + + + + + SDK 权限管理 + + + {{ template "theme_head" . }} + + +
+
+

SDK 权限管理

+

这里管理的是 tbl_auth_users 用户通过 PocketBase SDK 直连数据库时的业务权限。ManagePlatform 会被视为你的业务管理员,但不会自动变成 PocketBase 原生 _superusers

+
+ 返回主页 + + +
+
+
+ +
+
加载中...
+
+ +
+

角色管理

+
+ + + + +
角色 ID 由系统自动生成,页面不显示。
+ +
+
+ + + + + + + + + + + + + +
名称编码状态备注操作
暂无角色。
+
+
+ +
+

用户授权

+
+ + +
+
+ + + + + + + + + + + + + +
用户身份类型当前角色授权操作
暂无用户。
+
+
+ +
+

Collection 直连权限

+
+ +
这里是按“当前配置角色”逐表分配 CRUD 权限。下面依次对应“列表、详情、新增、修改、删除”五种权限,勾选后会立即保存。公开访问或自定义规则的操作不会显示授权勾选框。
+
+
+ + + + + + + + + + +
集合当前角色权限
暂无集合。
+
+
+
+ +
+
+
+
处理中,请稍候...
+
+
+ + + {{ template "theme_body" . }} + + \ No newline at end of file diff --git a/pocket-base/spec/openapi-manage.yaml b/pocket-base/spec/openapi-manage.yaml index 7505f7f..29ad05a 100644 --- a/pocket-base/spec/openapi-manage.yaml +++ b/pocket-base/spec/openapi-manage.yaml @@ -229,6 +229,9 @@ components: users_level: type: string description: "用户等级" + users_level_name: + type: string + description: "用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析" users_tag: type: string description: "用户标签" @@ -293,6 +296,7 @@ components: 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 @@ -1230,6 +1234,7 @@ paths: 创建平台用户 auth record。 服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。 前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。 + 首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。 注册成功后直接返回 PocketBase 原生 auth token。 requestBody: required: true @@ -1260,6 +1265,7 @@ paths: 前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。 仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。 服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。 + 返回体中的 `user.users_level_name` 为服务端按“数据-会员等级”字典实时解析后的等级名称。 登录成功后直接返回 PocketBase 原生 auth token。 requestBody: required: true @@ -1803,6 +1809,3 @@ paths: description: "非 ManagePlatform 用户无权访问" '415': description: "请求体必须为 application/json" - - - diff --git a/pocket-base/spec/openapi-wx.yaml b/pocket-base/spec/openapi-wx.yaml index b6ffa77..0f609db 100644 --- a/pocket-base/spec/openapi-wx.yaml +++ b/pocket-base/spec/openapi-wx.yaml @@ -4,7 +4,14 @@ info: version: 1.0.0-wx description: | 面向微信端的小程序接口文档。 - 本文档只包含微信登录、微信资料完善,以及微信端会共用的系统接口。 + 本文档包含微信登录、微信资料完善,以及微信小程序侧会直接调用的业务接口。 + + 微信小程序调用适配说明: + - 除 `/pb/api/wechat/login` 外,调用购物车 / 订单的 PocketBase 原生 records API 时都需要在请求头中携带 `Authorization: Bearer ` + - `token` 取自 `/pb/api/wechat/login` 成功返回的认证 token + - 小程序端应统一使用 HTTPS,不依赖 Cookie / Session + - 购物车与订单当前文档展示的是 PocketBase 原生 `/pb/api/collections/.../records` 接口,不是自定义 hooks API + - 按当前 collection 规则,创建 `tbl_cart` / `tbl_order` 记录时,客户端必须显式提交 owner 字段,且值必须等于当前 token 对应的 `openid` license: name: Proprietary identifier: LicenseRef-Proprietary @@ -26,9 +33,15 @@ tags: description: 通过 PocketBase 原生 records API 访问 `tbl_product_list` - name: 文档信息 description: 通过 PocketBase 原生 records API 访问 `tbl_document` + - name: 购物车 + description: 微信小程序侧购物车 CRUD 接口 + - name: 订单 + description: 微信小程序侧订单 CRUD 接口 +security: [] paths: /pb/api/system/users-count: post: + security: [] operationId: postSystemUsersCount tags: - 系统 @@ -55,6 +68,9 @@ paths: $ref: '#/components/schemas/ErrorResponse' /pb/api/system/refresh-token: post: + security: + - BearerAuth: [] + - {} operationId: postSystemRefreshToken tags: - 系统 @@ -113,6 +129,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /pb/api/wechat/login: post: + security: [] operationId: postWechatLogin tags: - 微信认证 @@ -120,6 +137,7 @@ paths: description: | 使用微信 `users_wx_code` 换取微信 openid。 若 `tbl_auth_users` 中不存在对应用户,则自动创建新 auth 用户并返回 token。 + 首次注册时,`users_level` 默认保持为空,不自动写入会员等级。 requestBody: required: true content: @@ -165,6 +183,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' /pb/api/wechat/profile: post: + security: + - BearerAuth: [] operationId: postWechatProfile tags: - 微信认证 @@ -736,13 +756,16 @@ paths: value: <属性值>| prod_list_plantype: <产品方案>| prod_list_category: <产品分类>| - prod_list_sort: <排序值>| + prod_list_sort: 10 prod_list_comm_type: <通讯类型>| prod_list_series: <产品系列>| prod_list_power_supply: <供电方式>| prod_list_tags: <产品标签>| prod_list_status: <产品状态>| - prod_list_basic_price: <基础价格>| + prod_list_basic_price: 1999 + prod_list_vip_price: + - viplevel: VIP1 + price: 1899 prod_list_remark: <备注>| '400': description: 查询参数错误 @@ -984,7 +1007,501 @@ paths: application/json: schema: $ref: '#/components/schemas/PocketBaseNativeError' + /pb/api/collections/tbl_cart/records: + get: + operationId: getPocketBaseCartRecords + tags: + - 购物车 + summary: 查询当前用户购物车列表或按业务 ID 精确查询 + description: | + 使用 PocketBase 原生 records list 接口查询 `tbl_cart`。 + + 当前线上权限规则: + - `listRule = @request.auth.id != "" && cart_owner = @request.auth.openid` + - 因此调用方只能读到 `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: '#/components/schemas/PocketBaseCartListResponse' + '400': + description: 查询参数错误或不满足 listRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '403': + description: 集合规则被锁定或服务端权限设置异常 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/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: '#/components/schemas/PocketBaseCartCreateRequest' + responses: + '200': + description: 创建成功 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseCartRecord' + '400': + description: 参数错误、违反字段约束或不满足 createRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '403': + description: 集合规则被锁定或服务端权限设置异常 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + /pb/api/collections/tbl_cart/records/{recordId}: + 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: '#/components/schemas/PocketBaseCartUpdateRequest' + responses: + '200': + description: 更新成功 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseCartRecord' + '400': + description: 参数错误或违反字段约束 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '404': + description: 记录不存在或不满足 updateRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/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: '#/components/schemas/PocketBaseNativeError' + '404': + description: 记录不存在或不满足 deleteRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + /pb/api/collections/tbl_order/records: + get: + operationId: getPocketBaseOrderRecords + tags: + - 订单 + summary: 查询当前用户订单列表或按业务 ID 精确查询 + description: | + 使用 PocketBase 原生 records list 接口查询 `tbl_order`。 + + 当前线上权限规则: + - `listRule = @request.auth.id != "" && order_owner = @request.auth.openid` + - 因此调用方只能读到 `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: '#/components/schemas/PocketBaseOrderListResponse' + '400': + description: 查询参数错误或不满足 listRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '403': + description: 集合规则被锁定或服务端权限设置异常 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/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: '#/components/schemas/PocketBaseOrderCreateRequest' + responses: + '200': + description: 创建成功 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseOrderRecord' + '400': + description: 参数错误、违反字段约束或不满足 createRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '403': + description: 集合规则被锁定或服务端权限设置异常 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + /pb/api/collections/tbl_order/records/{recordId}: + 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: '#/components/schemas/PocketBaseOrderUpdateRequest' + responses: + '200': + description: 更新成功 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseOrderRecord' + '400': + description: 参数错误或违反字段约束 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '404': + description: 记录不存在或不满足 updateRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/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: '#/components/schemas/PocketBaseNativeError' + '404': + description: 记录不存在或不满足 deleteRule + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT schemas: ApiResponseBase: type: object @@ -1038,6 +1555,286 @@ components: errMsg: 失败原因提示 | string data: 任意错误字段: 错误附加信息 | object + 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: '#/components/schemas/PocketBaseRecordBase' + - $ref: '#/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: '#/components/schemas/PocketBaseCartRecord' + 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: '#/components/schemas/PocketBaseRecordBase' + - $ref: '#/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: '#/components/schemas/PocketBaseOrderRecord' CompanyInfo: anyOf: - type: object @@ -1657,7 +2454,7 @@ components: - number - integer description: 排序值(同分类内按升序) - example: <排序值>| + example: 10 prod_list_comm_type: type: string description: 通讯类型 @@ -1683,7 +2480,24 @@ components: - number - integer description: 基础价格 - example: <基础价格>| + example: 1999 + prod_list_vip_price: + type: array + description: 会员价数组,每项包含会员等级枚举值与价格 + items: + type: object + properties: + viplevel: + type: string + example: <会员等级枚举值>| + price: + type: + - number + - integer + example: 1899 + example: + - viplevel: VIP1 + price: 1899 prod_list_remark: type: string description: 备注 @@ -1709,13 +2523,16 @@ components: value: <属性值>| prod_list_plantype: <产品方案>| prod_list_category: <产品分类>| - prod_list_sort: <排序值>| + prod_list_sort: 10 prod_list_comm_type: <通讯类型>| prod_list_series: <产品系列>| prod_list_power_supply: <供电方式>| prod_list_tags: <产品标签>| prod_list_status: <产品状态>| - prod_list_basic_price: <基础价格>| + prod_list_basic_price: 1999 + prod_list_vip_price: + - viplevel: VIP1 + price: 1899 prod_list_remark: <备注>| PocketBaseProductListListResponse: type: object @@ -1772,13 +2589,16 @@ components: value: <属性值>| prod_list_plantype: <产品方案>| prod_list_category: <产品分类>| - prod_list_sort: <排序值>| + prod_list_sort: 10 prod_list_comm_type: <通讯类型>| prod_list_series: <产品系列>| prod_list_power_supply: <供电方式>| prod_list_tags: <产品标签>| prod_list_status: <产品状态>| - prod_list_basic_price: <基础价格>| + prod_list_basic_price: 1999 + prod_list_vip_price: + - viplevel: VIP1 + price: 1899 prod_list_remark: <备注>| PocketBaseDocumentFields: type: object @@ -2079,6 +2899,9 @@ components: users_level: type: string description: 用户等级 + users_level_name: + type: string + description: 用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析 users_tag: type: string description: 用户标签 @@ -2143,6 +2966,7 @@ components: 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 @@ -2291,6 +3115,7 @@ components: 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 @@ -2369,6 +3194,7 @@ components: 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 @@ -2449,4 +3275,3 @@ components: errMsg: 业务提示信息 | string data: total_users: 用户总数 | integer - diff --git a/pocket-base/spec/openapi.yaml b/pocket-base/spec/openapi.yaml index 585d7a8..2709804 100644 --- a/pocket-base/spec/openapi.yaml +++ b/pocket-base/spec/openapi.yaml @@ -205,6 +205,9 @@ components: users_level: type: string description: 用户等级 + users_level_name: + type: string + description: 用户等级名称,按 `users_level -> 数据-会员等级` 字典描述实时解析 users_tag: type: string description: 用户标签 @@ -269,6 +272,7 @@ components: 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 @@ -1249,6 +1253,7 @@ paths: 使用微信 code 换取微信侧 openid,并写入统一身份字段 `tbl_auth_users.openid`。 若 `tbl_auth_users` 中不存在对应用户则自动创建 auth record,随后返回 PocketBase 原生 auth token。 首次注册创建时会写入 `users_idtype = WeChat`。 + 首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。 返回的 `token` 可直接用于 PocketBase SDK 与当前 hooks 接口调用。 requestBody: required: true @@ -1281,6 +1286,7 @@ paths: 创建平台用户 auth record。 服务端会自动生成 GUID 并写入统一身份字段 `openid`,同时写入 `users_idtype = ManagePlatform`。 前端以 `users_phone + password/passwordConfirm` 注册,但服务端仍会创建 PocketBase 原生 auth 用户。 + 首次注册创建时,`users_level` 默认保持为空,不自动写入会员等级。 注册成功后直接返回 PocketBase 原生 auth token。 requestBody: required: true @@ -1311,6 +1317,7 @@ paths: 前端使用平台注册时保存的 `邮箱或手机号 + password` 登录。 仅允许 `users_idtype = ManagePlatform` 的用户通过该接口登录。 服务端会根据 `login_account` 自动判断邮箱或手机号,并定位平台用户,再使用该用户的 PocketBase 原生 identity(当前为 `email`)执行原生 password auth。 + 返回体中的 `user.users_level_name` 为服务端按“数据-会员等级”字典实时解析后的等级名称。 登录成功后直接返回 PocketBase 原生 auth token。 requestBody: required: true @@ -1882,5 +1889,3 @@ paths: description: 非 ManagePlatform 用户无权访问 '415': description: 请求体必须为 application/json - - diff --git a/script/add-product-function-field.js b/script/add-product-function-field.js new file mode 100644 index 0000000..c7637e3 --- /dev/null +++ b/script/add-product-function-field.js @@ -0,0 +1,149 @@ +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); + + if (existingField && existingField.type === targetFieldType) { + 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.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); +}); diff --git a/script/database_schema.md b/script/database_schema.md index 152ead3..639ae04 100644 --- a/script/database_schema.md +++ b/script/database_schema.md @@ -84,7 +84,7 @@ | users_id_number | text | 证件号 | | users_phone | text | 用户电话号码 | | users_wx_openid | text | 微信号 | -| users_level | text | 用户等级 | +| users_level | text | 用户等级枚举值(新用户默认空) | | users_type | text | 用户类型 | | users_tag | text | 用户标签 | | users_status | text | 用户状态 | diff --git a/script/package.json b/script/package.json index 1cb5ffc..7f3796a 100644 --- a/script/package.json +++ b/script/package.json @@ -11,6 +11,7 @@ "init:dictionary": "node pocketbase.dictionary.js", "migrate:file-fields": "node pocketbase.file-fields-to-attachments.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-owner-sync": "node test-company-owner-sync.js" }, diff --git a/script/pocketbase.cart-order.js b/script/pocketbase.cart-order.js new file mode 100644 index 0000000..34eb88d --- /dev/null +++ b/script/pocketbase.cart-order.js @@ -0,0 +1,282 @@ +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'; + +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}`, + viewRule: `${OWNER_AUTH_RULE} && ${CART_OWNER_MATCH_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 }, + { 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' }, + ], + 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}`, + viewRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_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 }, + { 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' }, + ], + 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(); diff --git a/script/pocketbase.product-list.js b/script/pocketbase.product-list.js index a4bdbc5..85e0f2a 100644 --- a/script/pocketbase.product-list.js +++ b/script/pocketbase.product-list.js @@ -38,6 +38,7 @@ const collections = [ { name: 'prod_list_description', type: 'text' }, { name: 'prod_list_feature', type: 'text' }, { name: 'prod_list_parameters', type: 'json' }, + { name: 'prod_list_function', type: 'json' }, { name: 'prod_list_plantype', type: 'text' }, { name: 'prod_list_category', type: 'text', required: true }, { name: 'prod_list_sort', type: 'number' }, @@ -47,6 +48,7 @@ const collections = [ { name: 'prod_list_tags', type: 'text' }, { name: 'prod_list_status', type: 'text' }, { name: 'prod_list_basic_price', type: 'number' }, + { name: 'prod_list_vip_price', type: 'json' }, { name: 'prod_list_remark', type: 'text' }, ], indexes: [