From 91fcdcd65ada990a205fd48a3416b45773211125 Mon Sep 17 00:00:00 2001 From: XuJiacheng Date: Fri, 3 Apr 2026 10:50:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B4=AD=E7=89=A9?= =?UTF-8?q?=E8=BD=A6=E4=B8=8E=E8=AE=A2=E5=8D=95=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=8F=8A=E7=9B=B8=E5=85=B3API=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增购物车与订单管理页面,包含用户列表、购物车详情和订单记录展示功能。 - 实现用户搜索、刷新、重置和退出登录功能。 - 新增购物车和订单数据表结构初始化脚本,包含字段、索引及权限规则设置。 - 实现数据表的创建与更新逻辑,并进行结构校验。 --- docs/pb_tbl_cart.md | 52 ++ docs/pb_tbl_order.md | 54 ++ docs/pb_tbl_product_list.md | 9 +- pocket-base/bai-api-main.pb.js | 11 + pocket-base/bai-web-main.pb.js | 1 + .../bai_api_routes/cart-order/manage-users.js | 25 + .../bai_api_routes/cart/create.js | 25 + .../bai_api_routes/cart/delete.js | 25 + .../bai_api_routes/cart/detail.js | 23 + .../bai_api_routes/cart/list.js | 25 + .../bai_api_routes/cart/update.js | 25 + .../bai_api_routes/order/create.js | 25 + .../bai_api_routes/order/delete.js | 25 + .../bai_api_routes/order/detail.js | 23 + .../bai_api_routes/order/list.js | 25 + .../bai_api_routes/order/update.js | 25 + .../middlewares/requestGuards.js | 156 +++- .../services/cartOrderService.js | 614 ++++++++++++ .../services/dictionaryService.js | 6 +- .../services/documentService.js | 18 +- .../bai_api_shared/services/productService.js | 186 ++-- .../pages/cart-order-manage.js | 312 +++++++ .../pages/dictionary-manage.js | 86 +- .../bai_web_pb_hooks/pages/document-manage.js | 143 ++- pocket-base/bai_web_pb_hooks/pages/index.js | 4 + .../bai_web_pb_hooks/pages/product-manage.js | 365 ++++++-- pocket-base/spec/openapi-wx.yaml | 880 +++++++++++++++++- script/pocketbase.cart-order.js | 282 ++++++ 28 files changed, 3279 insertions(+), 171 deletions(-) create mode 100644 docs/pb_tbl_cart.md create mode 100644 docs/pb_tbl_order.md create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js create mode 100644 pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js create mode 100644 pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js create mode 100644 script/pocketbase.cart-order.js 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..946d636 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` | 否 | 排序值(同分类内按升序) | @@ -47,9 +47,10 @@ ## 补充约定 -- `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_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..051c734 100644 --- a/pocket-base/bai-api-main.pb.js +++ b/pocket-base/bai-api-main.pb.js @@ -41,6 +41,17 @@ 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/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-users.js b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js new file mode 100644 index 0000000..db77bf0 --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_routes/cart-order/manage-users.js @@ -0,0 +1,25 @@ +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, '查询用户购物车与订单成功', { + 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/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_shared/middlewares/requestGuards.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js index d9ba824..a82b8a9 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 @@ -355,16 +355,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 +387,7 @@ function normalizeProductParameters(value) { result.push({ name: name, value: normalizedValue, + sort: normalizedSort, }) } @@ -381,7 +397,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 +409,7 @@ 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 @@ -439,7 +455,7 @@ 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), @@ -460,6 +476,129 @@ 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 validateDocumentHistoryListBody(e) { const payload = parseBody(e) @@ -635,6 +774,15 @@ module.exports = { validateProductDetailBody, validateProductMutationBody, validateProductDeleteBody, + validateCartListBody, + validateCartDetailBody, + validateCartMutationBody, + validateCartDeleteBody, + validateOrderListBody, + validateOrderDetailBody, + validateOrderMutationBody, + validateOrderDeleteBody, + validateCartOrderManageListBody, 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..88c1a6b --- /dev/null +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js @@ -0,0 +1,614 @@ +const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`) +const { createAppError } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/appError.js`) + +function buildBusinessId(prefix) { + return prefix + '-' + new Date().getTime() + '-' + $security.randomString(6) +} + +function 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 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', '', '-created', 500, 0) + const cartRecords = $app.findRecordsByFilter('tbl_cart', '', '-cart_create', 1000, 0) + const orderRecords = $app.findRecordsByFilter('tbl_order', '', '-order_create', 1000, 0) + const groupedCarts = {} + const groupedOrders = {} + + for (let i = 0; i < cartRecords.length; i += 1) { + const owner = cartRecords[i].getString('cart_owner') + if (!groupedCarts[owner]) { + groupedCarts[owner] = [] + } + groupedCarts[owner].push(exportCartRecord(cartRecords[i])) + } + + for (let i = 0; i < orderRecords.length; i += 1) { + const owner = orderRecords[i].getString('order_owner') + if (!groupedOrders[owner]) { + groupedOrders[owner] = [] + } + groupedOrders[owner].push(exportOrderRecord(orderRecords[i])) + } + + const result = [] + for (let i = 0; i < userRecords.length; i += 1) { + const item = exportManageUser(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 result +} + +module.exports = { + listCarts, + getCartDetail, + createCart, + updateCart, + deleteCart, + listOrders, + getOrderDetail, + createOrder, + updateOrder, + deleteOrder, + listManageUsersCartOrders, +} 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..ff29b36 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 @@ -192,9 +192,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 +210,7 @@ function updateDictionary(payload) { } logger.info('字典修改成功', { - dict_name: payload.dict_name, + dict_name: immutableName, original_dict_name: payload.original_dict_name, }) 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..0df1726 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 @@ -436,6 +436,7 @@ 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 = [] @@ -448,10 +449,23 @@ function listDocuments(payload) { || 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 (matchedKeyword && 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..f5b71d7 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 @@ -37,6 +37,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 +80,52 @@ 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 buildCategoryRankMap(records) { const grouped = {} for (let i = 0; i < records.length; i += 1) { @@ -118,16 +174,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 +193,8 @@ function normalizeParameters(value) { result.push({ name: name, value: normalizedValue, + sort: normalizedSort, + __inputIndex: inputIndex, }) } @@ -144,9 +204,9 @@ 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') { @@ -155,10 +215,10 @@ function normalizeParameters(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]], '', i + 1, i + 1) } - return result + return sortParameterRows(result) } function normalizeParametersForOutput(value) { @@ -166,6 +226,30 @@ 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 { @@ -191,16 +275,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,20 +291,9 @@ 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') { @@ -243,18 +309,19 @@ function normalizeParametersForOutput(value) { const indexByName = {} const keys = Object.keys(source) for (let i = 0; i < keys.length; i += 1) { - const name = normalizeText(keys[i]) - if (!name) { - 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 }) + pushOrUpdate(result, indexByName, keys[i], source[keys[i]], '', i + 1, i + 1) + } + + return sortParameterRows(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 +329,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,16 +352,20 @@ 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) : {} @@ -305,9 +378,12 @@ function exportProductRecord(record, extra) { 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, @@ -399,7 +475,7 @@ 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)) @@ -448,7 +524,7 @@ 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)) 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..ad1e7ab --- /dev/null +++ b/pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js @@ -0,0 +1,312 @@ +routerAdd('GET', '/manage/cart-order-manage', function (e) { + const html = ` + + + + + 购物车与订单管理 + + + + +
+
+

购物车与订单管理

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

用户列表

+
+
+ +
+
+
+
+
+ + + +` + + 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..479aa5e 100644 --- a/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/dictionary-manage.js @@ -42,6 +42,7 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { .badge-off { background: #fee2e2; color: #991b1b; } .muted { color: #64748b; font-size: 13px; } .inline-input { min-width: 120px; } + .immutable-input { background: #fffdf7; } .link { color: #2563eb; text-decoration: none; font-weight: 600; } .status { margin: 14px 0 0; min-height: 24px; font-size: 14px; } .status.success { color: #15803d; } @@ -570,7 +571,7 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { const enabledText = item.dict_word_is_enabled ? '启用' : '禁用' const previewKey = String(index) return '' - + '' + + '' + '
' + enabledText + '
' + '' + '' + renderItemsPreview(item.items, previewKey, state.expandedPreviewKey === previewKey) + '' @@ -593,6 +594,15 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { state.enumCounter = 1 modalTitle.textContent = mode === 'create' ? '新增字典' : '编辑枚举项' dictNameInput.value = record ? record.dict_name : '' + if (mode === 'edit' && record) { + dictNameInput.setAttribute('data-immutable-name', record.dict_name || '') + dictNameInput.classList.add('immutable-input') + dictNameInput.title = '字典名称创建后不可修改,可选中复制' + } else { + dictNameInput.removeAttribute('data-immutable-name') + dictNameInput.classList.remove('immutable-input') + dictNameInput.title = '' + } enabledInput.value = record && !record.dict_word_is_enabled ? 'false' : 'true' remarkInput.value = record ? (record.dict_word_remark || '') : '' state.items = record && Array.isArray(record.items) && record.items.length @@ -802,7 +812,7 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { normalizeItemEnums() const items = state.items const payload = { - dict_name: dictNameInput.value.trim(), + dict_name: state.mode === 'edit' ? state.editingOriginalName : dictNameInput.value.trim(), original_dict_name: state.editingOriginalName, dict_word_parent_id: '', dict_word_is_enabled: enabledInput.value === 'true', @@ -838,7 +848,7 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { const payload = { original_dict_name: targetName, - dict_name: (row.find(function (el) { return el.getAttribute('data-field') === 'dict_name' }) || {}).value || targetName, + dict_name: targetName, dict_word_is_enabled: ((row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_is_enabled' }) || {}).value || 'true') === 'true', dict_word_remark: (row.find(function (el) { return el.getAttribute('data-field') === 'dict_word_remark' }) || {}).value || '', dict_word_parent_id: '', @@ -1028,6 +1038,76 @@ routerAdd('GET', '/manage/dictionary-manage', function (e) { renderItemsEditor() } + function isImmutableNameInput(target) { + return !!(target && target.matches && target.matches('input[data-immutable-name]')) + } + + function restoreImmutableInputValue(target) { + if (!isImmutableNameInput(target)) { + return + } + target.value = target.getAttribute('data-immutable-name') || '' + } + + function shouldAllowImmutableKey(event) { + if (event.ctrlKey || event.metaKey || event.altKey) { + return true + } + + const key = event.key || '' + return key === 'Tab' + || key === 'Shift' + || key === 'Control' + || key === 'Meta' + || key === 'Alt' + || key === 'CapsLock' + || key === 'Escape' + || key === 'Enter' + || key === 'ArrowLeft' + || key === 'ArrowRight' + || key === 'ArrowUp' + || key === 'ArrowDown' + || key === 'Home' + || key === 'End' + || key === 'PageUp' + || key === 'PageDown' + } + + document.addEventListener('beforeinput', function (event) { + if (!isImmutableNameInput(event.target)) { + return + } + event.preventDefault() + }) + + document.addEventListener('paste', function (event) { + if (!isImmutableNameInput(event.target)) { + return + } + event.preventDefault() + }) + + document.addEventListener('drop', function (event) { + if (!isImmutableNameInput(event.target)) { + return + } + event.preventDefault() + }) + + document.addEventListener('keydown', function (event) { + if (!isImmutableNameInput(event.target)) { + return + } + if (shouldAllowImmutableKey(event)) { + return + } + event.preventDefault() + }) + + document.addEventListener('input', function (event) { + restoreImmutableInputValue(event.target) + }) + document.getElementById('listBtn').addEventListener('click', loadList) document.getElementById('detailBtn').addEventListener('click', loadDetail) document.getElementById('resetBtn').addEventListener('click', function () { 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..4cd7a1d 100644 --- a/pocket-base/bai_web_pb_hooks/pages/document-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/document-manage.js @@ -22,7 +22,8 @@ routerAdd('GET', '/manage/document-manage', function (e) { .panel + .panel { margin-top: 14px; } h1, h2 { margin-top: 0; } p { color: #4b5563; line-height: 1.7; } - .actions, .form-actions, .toolbar, .table-actions, .file-actions { display: flex; flex-wrap: wrap; gap: 12px; } + .actions, .form-actions, .table-actions, .file-actions { display: flex; flex-wrap: wrap; gap: 12px; } + .toolbar { display: grid; grid-template-columns: 1.4fr 1fr auto auto; gap: 10px; } .btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; } .btn-primary { background: #2563eb; color: #fff; } .btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; } @@ -252,6 +253,20 @@ routerAdd('GET', '/manage/document-manage', function (e) {

文档列表

+
+
+ + +
+
+ + +
+ + +
@@ -313,6 +328,8 @@ routerAdd('GET', '/manage/document-manage', function (e) { const applicationScenariosTagsEl = document.getElementById('applicationScenariosTags') const hotelTypeOptionsEl = document.getElementById('hotelTypeOptions') const hotelTypeTagsEl = document.getElementById('hotelTypeTags') + const listTitleKeywordEl = document.getElementById('listTitleKeyword') + const listTypeFilterEl = document.getElementById('listTypeFilter') const imageViewerEl = document.getElementById('imageViewer') const imageViewerImgEl = document.getElementById('imageViewerImg') const loadingMaskEl = document.getElementById('loadingMask') @@ -379,6 +396,10 @@ routerAdd('GET', '/manage/document-manage', function (e) { applicationScenarios: [], hotelType: [], }, + listFilters: { + titleKeyword: '', + type: '', + }, } function setStatus(message, type) { @@ -647,14 +668,58 @@ routerAdd('GET', '/manage/document-manage', function (e) { function renderDocumentTypeSourceOptions() { const currentValue = String(state.selections.documentTypeSource || '') documentTypeSourceEl.innerHTML = [''] - .concat(state.dictionaries.map(function (dict) { + .concat(getDocumentTypeSourceDictionaries().map(function (dict) { return '' })) .join('') } + function getDocumentTypeSourceDictionaries() { + return state.dictionaries.filter(function (dict) { + const dictName = String(dict && dict.dict_name ? dict.dict_name : '') + if (!dictName) { + return false + } + + return dictName.indexOf('文档-') === 0 && dictName.indexOf('文档-文档类型') !== -1 + }) + } + + function buildDocumentTypeFilterOptions() { + return getDocumentTypeSourceDictionaries() + .map(function (dict) { + const sourceId = String(dict && dict.system_dict_id ? dict.system_dict_id : '').trim() + if (!sourceId) { + return null + } + + return { + value: sourceId, + label: dict.dict_name || sourceId, + } + }) + .filter(function (item) { + return !!item + }) + } + + function renderListTypeFilterOptions() { + if (!listTypeFilterEl) { + return + } + + const currentValue = String(state.listFilters.type || '') + const options = buildDocumentTypeFilterOptions() + listTypeFilterEl.innerHTML = [''] + .concat(options.map(function (item) { + return '' + })) + .join('') + } + function renderDictionarySelectors() { renderDocumentTypeSourceOptions() + renderListTypeFilterOptions() const sourceDict = getDocumentTypeSourceDictionary() const sourceItems = sourceDict && Array.isArray(sourceDict.items) ? sourceDict.items : [] @@ -1037,11 +1102,29 @@ routerAdd('GET', '/manage/document-manage', function (e) { renderAttachmentEditors() } + function syncListFiltersFromInputs() { + state.listFilters.titleKeyword = listTitleKeywordEl ? String(listTitleKeywordEl.value || '').trim() : '' + state.listFilters.type = listTypeFilterEl ? String(listTypeFilterEl.value || '').trim() : '' + } + + function applyListFiltersToInputs() { + if (listTitleKeywordEl) { + listTitleKeywordEl.value = state.listFilters.titleKeyword || '' + } + if (listTypeFilterEl) { + listTypeFilterEl.value = state.listFilters.type || '' + } + } + async function loadDocuments() { + syncListFiltersFromInputs() setStatus('正在加载文档列表...', '') showLoading('正在加载文档列表...') try { - const data = await requestJson('/document/list', {}) + const data = await requestJson('/document/list', { + title_keyword: state.listFilters.titleKeyword, + document_type: state.listFilters.type, + }) state.list = data.items || [] renderTable() setStatus('文档列表已刷新,共 ' + state.list.length + ' 条。', 'success') @@ -1052,6 +1135,41 @@ routerAdd('GET', '/manage/document-manage', function (e) { } } + async function submitDocumentSilently() { + if (state.mode !== 'create' && state.mode !== 'edit') { + return true + } + + if (!fields.documentTitle.value.trim()) { + return false + } + + if (!buildDocumentTypeStorageValue()) { + return false + } + + try { + await submitDocument() + return true + } catch (_err) { + return false + } + } + + async function saveAndHideEditorBeforeListQuery() { + if (state.mode === 'create' || state.mode === 'edit') { + try { + await submitDocumentSilently() + } catch (_error) {} + enterIdleMode() + } + } + + async function queryDocumentsWithAutoSave() { + await saveAndHideEditorBeforeListQuery() + await loadDocuments() + } + function resetForm() { fields.documentTitle.value = '' fields.documentStatus.value = '有效' @@ -1358,7 +1476,24 @@ routerAdd('GET', '/manage/document-manage', function (e) { updateSelection(fieldName, target.value, !!target.checked) }) - document.getElementById('reloadBtn').addEventListener('click', loadDocuments) + document.getElementById('reloadBtn').addEventListener('click', queryDocumentsWithAutoSave) + document.getElementById('searchBtn').addEventListener('click', queryDocumentsWithAutoSave) + document.getElementById('clearSearchBtn').addEventListener('click', function () { + state.listFilters.titleKeyword = '' + state.listFilters.type = '' + applyListFiltersToInputs() + queryDocumentsWithAutoSave() + }) + if (listTitleKeywordEl) { + listTitleKeywordEl.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + queryDocumentsWithAutoSave() + } + }) + } + if (listTypeFilterEl) { + listTypeFilterEl.addEventListener('change', queryDocumentsWithAutoSave) + } document.getElementById('createModeBtn').addEventListener('click', function () { enterCreateMode() setStatus('已切换到新建模式。', 'success') diff --git a/pocket-base/bai_web_pb_hooks/pages/index.js b/pocket-base/bai_web_pb_hooks/pages/index.js index 0fc5caa..47063b1 100644 --- a/pocket-base/bai_web_pb_hooks/pages/index.js +++ b/pocket-base/bai_web_pb_hooks/pages/index.js @@ -54,6 +54,10 @@ routerAdd('GET', '/manage', function (e) {

SDK 权限管理

进入权限管理 +
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 4ba8ae8..2a63eaf 100644 --- a/pocket-base/bai_web_pb_hooks/pages/product-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/product-manage.js @@ -55,7 +55,16 @@ routerAdd('GET', '/manage/product-manage', function (e) { th, td { padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; } .empty { text-align: center; padding: 24px; color: #64748b; } .thumb { width: 84px; height: 84px; border-radius: 10px; border: 1px solid #dbe3f0; object-fit: cover; background: #fff; } - .thumb-wrap { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; } + .thumb-wrap { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; } + .thumb-upload-panel { flex: 1; min-width: 320px; } + .thumb-preview-panel { flex: 1.2; min-width: 320px; } + .thumb-grid { display: flex; flex-wrap: wrap; gap: 12px; } + .thumb-card { width: 104px; } + .thumb-caption { margin-top: 6px; font-size: 12px; color: #64748b; text-align: center; } + .thumb-remove { width: 100%; margin-top: 6px; } + .param-op { display: flex; align-items: center; gap: 10px; } + .param-op input { width: 120px; flex: 0 0 120px; } + .param-op .btn { flex: 0 0 auto; white-space: nowrap; } .muted { color: #64748b; font-size: 12px; } .modal-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 16px; background: rgba(15, 23, 42, 0.42); z-index: 9999; } .modal-mask.show { display: flex; } @@ -196,7 +205,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
- + @@ -204,8 +213,8 @@ routerAdd('GET', '/manage/product-manage', function (e) {
- -
仅支持 JSON 对象格式。导入时按属性名增量合并:同名覆盖、不同名新增,不会清空当前参数表。
+ +
推荐使用数组格式:每项包含 sort、name、value。也兼容旧对象格式。导入时按属性名增量合并:同名覆盖、不同名新增,不会清空当前参数表。
@@ -216,17 +225,17 @@ routerAdd('GET', '/manage/product-manage', function (e) {
-
- -
暂无图标
-
-
- -
选择后会在保存时上传,并写入 prod_list_icon。
+
+ +
支持多图上传。保存时会上传到附件表,并按上传后附件 ID 用 | 写入 prod_list_icon。
- +
+
+
+
暂无图标
+
@@ -320,7 +329,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { const copyModelInputEl = document.getElementById('copyModelInput') const loadingMaskEl = document.getElementById('loadingMask') const loadingTextEl = document.getElementById('loadingText') - const iconPreviewEl = document.getElementById('iconPreview') + const iconPreviewListEl = document.getElementById('iconPreviewList') const iconEmptyEl = document.getElementById('iconEmpty') const statusFilterEl = document.getElementById('statusFilter') const categoryFilterEl = document.getElementById('categoryFilter') @@ -366,8 +375,8 @@ routerAdd('GET', '/manage/product-manage', function (e) { tags: [], }, parameterRows: [], - currentIconAttachment: null, - pendingIconFile: null, + currentIconAttachments: [], + pendingIconFiles: [], copySourceProductId: '', } @@ -779,25 +788,108 @@ routerAdd('GET', '/manage/product-manage', function (e) { }).join('') } - function setIconPreview(url) { - if (!url) { - iconPreviewEl.style.display = 'none' - iconPreviewEl.src = '' - iconEmptyEl.style.display = 'block' - return + function normalizePositiveInteger(value) { + const text = normalizeText(value) + if (!text) { + return null } - iconPreviewEl.style.display = 'block' - iconPreviewEl.src = url - iconEmptyEl.style.display = 'none' + + const num = Number(text) + if (!Number.isFinite(num) || num <= 0 || Math.floor(num) !== num) { + return null + } + + return num + } + + function buildParameterRow(name, value, sort) { + return { + name: normalizeText(name), + value: value === null || typeof value === 'undefined' ? '' : String(value), + sort: normalizePositiveInteger(sort), + } + } + + function findInvalidParameterSortRow(rows) { + for (let i = 0; i < rows.length; i += 1) { + const rawSort = normalizeText(rows[i] && rows[i].sort) + if (!rawSort) { + continue + } + if (normalizePositiveInteger(rawSort) === null) { + return i + 1 + } + } + return 0 } function clearPendingIconPreview() { - if (state.pendingIconFile && state.pendingIconFile.previewUrl) { + for (let i = 0; i < state.pendingIconFiles.length; i += 1) { + const item = state.pendingIconFiles[i] + if (!item || !item.previewUrl) { + continue + } + try { - URL.revokeObjectURL(state.pendingIconFile.previewUrl) + URL.revokeObjectURL(item.previewUrl) } catch (_error) {} } - state.pendingIconFile = null + state.pendingIconFiles = [] + } + + function getCollectedIconIds() { + return state.currentIconAttachments.map(function (item) { + return normalizeText(item && item.attachments_id) + }).filter(function (item) { + return !!item + }) + } + + function renderIconPreview() { + const cards = [] + + for (let i = 0; i < state.currentIconAttachments.length; i += 1) { + const item = state.currentIconAttachments[i] + cards.push( + '
' + + 'icon-' + (i + 1) + '' + + '
已保存
' + + '' + + '
' + ) + } + + for (let i = 0; i < state.pendingIconFiles.length; i += 1) { + const item = state.pendingIconFiles[i] + cards.push( + '
' + + 'pending-icon-' + (i + 1) + '' + + '
待上传
' + + '' + + '
' + ) + } + + iconPreviewListEl.innerHTML = cards.join('') + iconEmptyEl.style.display = cards.length ? 'none' : 'block' + } + + function sortParameterList(rows) { + return rows + .slice() + .sort(function (a, b) { + const sortA = normalizePositiveInteger(a.sort) + const sortB = normalizePositiveInteger(b.sort) + const safeSortA = sortA === null ? Number.MAX_SAFE_INTEGER : sortA + const safeSortB = sortB === null ? Number.MAX_SAFE_INTEGER : sortB + if (safeSortA !== safeSortB) { + return safeSortA - safeSortB + } + return Number(a.__inputIndex || 0) - Number(b.__inputIndex || 0) + }) + .map(function (item) { + return buildParameterRow(item.name, item.value, item.sort) + }) } function renderParameterRows() { @@ -810,7 +902,10 @@ routerAdd('GET', '/manage/product-manage', function (e) { return '
' + '' + '' - + '' + + '' + '' }).join('') } @@ -830,25 +925,15 @@ routerAdd('GET', '/manage/product-manage', function (e) { return } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - setStatus('JSON 仅支持对象格式,例如:{"属性名":"属性值","电压":"220v"}。', 'error') - return - } - - const incomingRows = [] - const keys = Object.keys(parsed) - for (let i = 0; i < keys.length; i += 1) { - const name = normalizeText(keys[i]) - if (!name) { - continue + if (Array.isArray(parsed)) { + const invalidRow = findInvalidParameterSortRow(parsed) + if (invalidRow) { + setStatus('导入失败:第 ' + invalidRow + ' 项 sort 必须为正整数。', 'error') + return } - const value = parsed[keys[i]] - incomingRows.push({ - name: name, - value: value === null || typeof value === 'undefined' ? '' : String(value), - }) } + const incomingRows = normalizeParameterRows(parsed) if (!incomingRows.length) { setStatus('导入内容为空,请至少提供一个有效属性。', 'error') return @@ -871,10 +956,10 @@ routerAdd('GET', '/manage/product-manage', function (e) { const row = incomingRows[i] const idx = existingIndexMap[row.name] if (typeof idx === 'number') { - state.parameterRows[idx].value = row.value + state.parameterRows[idx] = buildParameterRow(row.name, row.value, row.sort) updatedCount += 1 } else { - state.parameterRows.push({ name: row.name, value: row.value }) + state.parameterRows.push(buildParameterRow(row.name, row.value, row.sort)) existingIndexMap[row.name] = state.parameterRows.length - 1 addedCount += 1 } @@ -887,6 +972,17 @@ routerAdd('GET', '/manage/product-manage', function (e) { function collectParameterArray() { const result = [] const indexByName = {} + let nextAutoSort = 1 + + function getAutoSort() { + while (result.some(function (item) { return Number(item.sort) === nextAutoSort })) { + nextAutoSort += 1 + } + const current = nextAutoSort + nextAutoSort += 1 + return current + } + for (let i = 0; i < state.parameterRows.length; i += 1) { const name = normalizeText(state.parameterRows[i].name) const value = normalizeText(state.parameterRows[i].value) @@ -894,41 +990,45 @@ routerAdd('GET', '/manage/product-manage', function (e) { continue } + const rawSort = normalizeText(state.parameterRows[i].sort) + const explicitSort = normalizePositiveInteger(rawSort) + if (rawSort && explicitSort === null) { + throw new Error('第 ' + (i + 1) + ' 行参数排序必须为正整数') + } + const sortValue = explicitSort === null ? getAutoSort() : explicitSort const existingIndex = indexByName[name] if (typeof existingIndex === 'number') { result[existingIndex].value = value + result[existingIndex].sort = sortValue } else { indexByName[name] = result.length result.push({ name: name, value: value, + sort: sortValue, + __inputIndex: i + 1, }) } } - return result + + return sortParameterList(result) } async function exportParametersToJson() { - const rows = collectParameterArray() - const exportObject = {} - - for (let i = 0; i < rows.length; i += 1) { - const name = normalizeText(rows[i].name) - if (!name) { - continue - } - exportObject[name] = rows[i].value === null || typeof rows[i].value === 'undefined' - ? '' - : String(rows[i].value) + let rows = [] + try { + rows = collectParameterArray() + } catch (err) { + setStatus(err.message || '参数导出失败', 'error') + return } - const keys = Object.keys(exportObject) - if (!keys.length) { + if (!rows.length) { setStatus('当前参数表为空,暂无可导出的内容。', 'error') return } - const jsonText = JSON.stringify(exportObject, null, 2) + const jsonText = JSON.stringify(rows, null, 2) fields.paramsJsonInput.value = jsonText let copied = false @@ -961,18 +1061,19 @@ routerAdd('GET', '/manage/product-manage', function (e) { const rows = [] const indexByName = {} - function upsert(nameValue, rawValue) { + function upsert(nameValue, rawValue, rawSort, inputIndex) { const name = normalizeText(nameValue) if (!name) { return } - const normalizedValue = rawValue === null || typeof rawValue === 'undefined' ? '' : String(rawValue) + + const currentRow = buildParameterRow(name, rawValue, rawSort || inputIndex) const existingIndex = indexByName[name] if (typeof existingIndex === 'number') { - rows[existingIndex].value = normalizedValue + rows[existingIndex] = Object.assign({ __inputIndex: inputIndex }, currentRow) } else { indexByName[name] = rows.length - rows.push({ name: name, value: normalizedValue }) + rows.push(Object.assign({ __inputIndex: inputIndex }, currentRow)) } } @@ -982,26 +1083,25 @@ routerAdd('GET', '/manage/product-manage', function (e) { if (!item) { continue } - upsert(item.name || item.key, item.value) + upsert(item.name || item.key, item.value, item.sort, i + 1) } - return rows + return sortParameterList(rows) } if (typeof source !== 'object') { return [] } - // Some PocketBase/Goja payloads are object-like values; roundtrip makes keys enumerable. try { source = JSON.parse(JSON.stringify(source)) } catch (_error) {} const keys = Object.keys(source) for (let i = 0; i < keys.length; i += 1) { - upsert(keys[i], source[keys[i]]) + upsert(keys[i], source[keys[i]], i + 1, i + 1) } - return rows + return sortParameterList(rows) } function updateEditorMode() { @@ -1041,9 +1141,9 @@ routerAdd('GET', '/manage/product-manage', function (e) { state.selections.powerSupply = [] state.selections.tags = [] state.parameterRows = [] - state.currentIconAttachment = null + state.currentIconAttachments = [] clearPendingIconPreview() - setIconPreview('') + renderIconPreview() renderProductStatusSelector() renderAllMultiOptionLists() renderTagOptions() @@ -1086,14 +1186,16 @@ routerAdd('GET', '/manage/product-manage', function (e) { state.selections.powerSupply = splitPipe(item.prod_list_power_supply) state.selections.tags = splitPipe(item.prod_list_tags) state.parameterRows = normalizeParameterRows(item.prod_list_parameters) - state.currentIconAttachment = item.prod_list_icon_attachment || null + state.currentIconAttachments = Array.isArray(item.prod_list_icon_attachments) + ? item.prod_list_icon_attachments.slice() + : (item.prod_list_icon_attachment ? [item.prod_list_icon_attachment] : []) clearPendingIconPreview() renderProductStatusSelector() renderAllMultiOptionLists() renderTagOptions() renderParameterRows() - setIconPreview(item.prod_list_icon_url || '') + renderIconPreview() renderSortRankHint() } @@ -1257,6 +1359,37 @@ routerAdd('GET', '/manage/product-manage', function (e) { } } + async function submitProductSilently() { + if (state.mode !== 'create' && state.mode !== 'edit') { + return true + } + + if (!normalizeText(fields.name.value)) { + return false + } + + try { + await submitProduct() + return true + } catch (_err) { + return false + } + } + + async function saveAndHideEditorBeforeProductQuery() { + if (state.mode === 'create' || state.mode === 'edit') { + try { + await submitProductSilently() + } catch (_error) {} + enterIdleMode() + } + } + + async function queryProductsWithAutoSave() { + await saveAndHideEditorBeforeProductQuery() + await loadProducts() + } + async function enterEditMode(productId) { showLoading('正在加载产品详情...') try { @@ -1305,22 +1438,27 @@ routerAdd('GET', '/manage/product-manage', function (e) { showLoading(state.mode === 'edit' ? '正在保存产品修改...' : '正在创建产品...') const uploadedAttachments = [] - let finalIconId = state.currentIconAttachment && state.currentIconAttachment.attachments_id - ? state.currentIconAttachment.attachments_id - : '' + let finalIconIds = getCollectedIconIds() try { - if (state.pendingIconFile && state.pendingIconFile.file) { - const uploaded = await uploadAttachment(state.pendingIconFile.file) + for (let i = 0; i < state.pendingIconFiles.length; i += 1) { + const pendingFile = state.pendingIconFiles[i] + if (!pendingFile || !pendingFile.file) { + continue + } + + const uploaded = await uploadAttachment(pendingFile.file) uploadedAttachments.push(uploaded) - finalIconId = uploaded.attachments_id || '' + if (uploaded && uploaded.attachments_id) { + finalIconIds.push(uploaded.attachments_id) + } } const payload = { prod_list_id: state.mode === 'edit' ? state.editingId : '', prod_list_name: normalizeText(fields.name.value), prod_list_modelnumber: normalizeText(fields.model.value), - prod_list_icon: finalIconId, + prod_list_icon: joinPipe(finalIconIds), prod_list_description: normalizeText(fields.description.value), prod_list_feature: normalizeText(fields.feature.value), prod_list_parameters: collectParameterArray(), @@ -1332,6 +1470,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { prod_list_tags: joinPipe(state.selections.tags), prod_list_status: normalizeText(state.productStatus) || '有效', prod_list_basic_price: normalizeText(fields.basicPrice.value), + prod_list_sort: normalizeText(fields.sort.value), prod_list_remark: normalizeText(fields.remark.value), } @@ -1474,19 +1613,30 @@ routerAdd('GET', '/manage/product-manage', function (e) { return } + if (target.matches('input[data-param-sort]')) { + const index = Number(target.getAttribute('data-param-sort')) + if (Number.isInteger(index) && state.parameterRows[index]) { + state.parameterRows[index].sort = target.value + } + return + } + if (target === fields.iconFile) { - const file = target.files && target.files.length ? target.files[0] : null - clearPendingIconPreview() - if (!file) { - setIconPreview(state.currentIconAttachment ? (state.currentIconAttachment.attachments_url || '') : '') + const fileList = target.files ? Array.prototype.slice.call(target.files) : [] + if (!fileList.length) { return } - state.pendingIconFile = { - file: file, - previewUrl: URL.createObjectURL(file), + for (let i = 0; i < fileList.length; i += 1) { + const file = fileList[i] + state.pendingIconFiles.push({ + key: 'pending-' + Date.now() + '-' + i + '-' + Math.random().toString(36).slice(2, 8), + file: file, + previewUrl: URL.createObjectURL(file), + }) } - setIconPreview(state.pendingIconFile.previewUrl) + fields.iconFile.value = '' + renderIconPreview() } }) @@ -1502,15 +1652,42 @@ routerAdd('GET', '/manage/product-manage', function (e) { state.parameterRows.splice(index, 1) renderParameterRows() } + return + } + + if (target.matches('button[data-icon-remove-existing]')) { + const index = Number(target.getAttribute('data-icon-remove-existing')) + if (Number.isInteger(index) && index >= 0 && index < state.currentIconAttachments.length) { + state.currentIconAttachments.splice(index, 1) + renderIconPreview() + } + return + } + + if (target.matches('button[data-icon-remove-pending]')) { + const key = normalizeText(target.getAttribute('data-icon-remove-pending')) + const nextFiles = [] + for (let i = 0; i < state.pendingIconFiles.length; i += 1) { + const item = state.pendingIconFiles[i] + if (item.key === key) { + try { + URL.revokeObjectURL(item.previewUrl) + } catch (_error) {} + continue + } + nextFiles.push(item) + } + state.pendingIconFiles = nextFiles + renderIconPreview() } }) document.getElementById('reloadBtn').addEventListener('click', function () { - loadProducts() + queryProductsWithAutoSave() }) document.getElementById('searchBtn').addEventListener('click', function () { - loadProducts() + queryProductsWithAutoSave() }) document.getElementById('createModeBtn').addEventListener('click', function () { @@ -1522,7 +1699,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { }) document.getElementById('addParamBtn').addEventListener('click', function () { - state.parameterRows.push({ name: '', value: '' }) + state.parameterRows.push(buildParameterRow('', '', '')) renderParameterRows() }) @@ -1544,10 +1721,10 @@ routerAdd('GET', '/manage/product-manage', function (e) { } document.getElementById('clearIconBtn').addEventListener('click', function () { - state.currentIconAttachment = null + state.currentIconAttachments = [] clearPendingIconPreview() fields.iconFile.value = '' - setIconPreview('') + renderIconPreview() }) document.getElementById('copyCancelBtn').addEventListener('click', function () { diff --git a/pocket-base/spec/openapi-wx.yaml b/pocket-base/spec/openapi-wx.yaml index b6ffa77..32433e7 100644 --- a/pocket-base/spec/openapi-wx.yaml +++ b/pocket-base/spec/openapi-wx.yaml @@ -4,7 +4,13 @@ info: version: 1.0.0-wx description: | 面向微信端的小程序接口文档。 - 本文档只包含微信登录、微信资料完善,以及微信端会共用的系统接口。 + 本文档包含微信登录、微信资料完善,以及微信小程序侧会直接调用的业务接口。 + + 微信小程序调用适配说明: + - 除 `/pb/api/wechat/login` 外,其余购物车 / 订单接口都需要在请求头中携带 `Authorization: Bearer ` + - `token` 取自 `/pb/api/wechat/login` 成功返回的认证 token + - 小程序端应统一使用 HTTPS + JSON 请求体,不依赖 Cookie / Session + - 购物车与订单接口的 owner 字段由服务端根据当前 token 自动绑定到 `tbl_auth_users.openid` license: name: Proprietary identifier: LicenseRef-Proprietary @@ -26,9 +32,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 +67,9 @@ paths: $ref: '#/components/schemas/ErrorResponse' /pb/api/system/refresh-token: post: + security: + - BearerAuth: [] + - {} operationId: postSystemRefreshToken tags: - 系统 @@ -113,6 +128,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /pb/api/wechat/login: post: + security: [] operationId: postWechatLogin tags: - 微信认证 @@ -165,6 +181,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' /pb/api/wechat/profile: post: + security: + - BearerAuth: [] operationId: postWechatProfile tags: - 微信认证 @@ -984,7 +1002,591 @@ paths: application/json: schema: $ref: '#/components/schemas/PocketBaseNativeError' + /pb/api/cart/list: + post: + operationId: postCartList + tags: + - 购物车 + summary: 查询当前登录用户的购物车列表 + description: | + 返回当前 `Authorization` 对应 openid 名下的购物车记录。 + 小程序端不需要传 `cart_owner`,服务端会自动基于 token 过滤。 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CartListRequest' + responses: + '200': + description: 查询成功 + content: + application/json: + schema: + $ref: '#/components/schemas/CartListResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/cart/detail: + post: + operationId: postCartDetail + tags: + - 购物车 + summary: 查询当前登录用户的购物车详情 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CartDetailRequest' + responses: + '200': + description: 查询成功 + content: + application/json: + schema: + $ref: '#/components/schemas/CartDetailResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 购物车记录不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/cart/create: + post: + operationId: postCartCreate + tags: + - 购物车 + summary: 创建购物车记录 + description: | + `cart_owner`、`cart_create` 由服务端自动处理。 + 小程序端只需要提交商品、数量、价格和可选备注。 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CartCreateRequest' + responses: + '200': + description: 创建成功 + content: + application/json: + schema: + $ref: '#/components/schemas/CartMutationResponse' + '400': + description: 请求参数错误或产品不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: 请求过于频繁 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/cart/update: + post: + operationId: postCartUpdate + tags: + - 购物车 + summary: 更新购物车记录 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CartUpdateRequest' + responses: + '200': + description: 更新成功 + content: + application/json: + schema: + $ref: '#/components/schemas/CartMutationResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 购物车记录不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: 请求过于频繁 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/cart/delete: + post: + operationId: postCartDelete + tags: + - 购物车 + summary: 删除购物车记录 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CartDeleteRequest' + responses: + '200': + description: 删除成功 + content: + application/json: + schema: + $ref: '#/components/schemas/CartDeleteResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 购物车记录不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: 请求过于频繁 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/order/list: + post: + operationId: postOrderList + tags: + - 订单 + summary: 查询当前登录用户的订单列表 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderListRequest' + responses: + '200': + description: 查询成功 + content: + application/json: + schema: + $ref: '#/components/schemas/OrderListResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/order/detail: + post: + operationId: postOrderDetail + tags: + - 订单 + summary: 查询当前登录用户的订单详情 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderDetailRequest' + responses: + '200': + description: 查询成功 + content: + application/json: + schema: + $ref: '#/components/schemas/OrderDetailResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 订单记录不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/order/create: + post: + operationId: postOrderCreate + tags: + - 订单 + summary: 创建订单 + description: | + `order_owner`、`order_create` 由服务端自动处理。 + 小程序端需要提交订单来源、来源 ID、订单快照和订单金额。 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderCreateRequest' + responses: + '200': + description: 创建成功 + content: + application/json: + schema: + $ref: '#/components/schemas/OrderMutationResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: 请求过于频繁 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/order/update: + post: + operationId: postOrderUpdate + tags: + - 订单 + summary: 更新订单 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderUpdateRequest' + responses: + '200': + description: 更新成功 + content: + application/json: + schema: + $ref: '#/components/schemas/OrderMutationResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 订单记录不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: 请求过于频繁 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /pb/api/order/delete: + post: + operationId: postOrderDelete + tags: + - 订单 + summary: 删除订单 + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrderDeleteRequest' + responses: + '200': + description: 删除成功 + content: + application/json: + schema: + $ref: '#/components/schemas/OrderDeleteResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: token 缺失、无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 无权访问目标数据 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 订单记录不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: 请求体不是 JSON + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '429': + description: 请求过于频繁 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT schemas: ApiResponseBase: type: object @@ -1038,6 +1640,281 @@ components: errMsg: 失败原因提示 | string data: 任意错误字段: 错误附加信息 | object + CartRecord: + type: object + required: + - cart_id + - cart_number + - cart_create + - cart_owner + - cart_product_id + - cart_product_quantity + - cart_status + - cart_at_price + properties: + pb_id: + type: string + cart_id: + type: string + cart_number: + type: string + cart_create: + type: string + description: 购物车项创建时间 + cart_owner: + type: string + description: 当前登录用户 openid + cart_product_id: + type: string + cart_product_quantity: + type: integer + cart_status: + type: string + cart_at_price: + type: number + cart_remark: + type: string + product_name: + type: string + product_modelnumber: + type: string + product_basic_price: + type: + - number + - 'null' + created: + type: string + updated: + type: string + CartListRequest: + type: object + properties: + keyword: + type: string + description: 按购物车编号、商品 ID、商品名称模糊搜索 + cart_status: + type: string + cart_number: + type: string + CartDetailRequest: + type: object + required: + - cart_id + properties: + cart_id: + type: string + CartCreateRequest: + type: object + required: + - cart_product_id + - cart_product_quantity + - cart_at_price + properties: + cart_number: + type: string + cart_product_id: + type: string + cart_product_quantity: + type: integer + cart_status: + type: string + cart_at_price: + type: number + cart_remark: + type: string + CartUpdateRequest: + type: object + required: + - cart_id + properties: + cart_id: + type: string + cart_number: + type: string + cart_product_id: + type: string + cart_product_quantity: + type: integer + cart_status: + type: string + cart_at_price: + type: number + cart_remark: + type: string + CartDeleteRequest: + type: object + required: + - cart_id + properties: + cart_id: + type: string + CartListResponse: + type: object + required: + - items + properties: + items: + type: array + items: + $ref: '#/components/schemas/CartRecord' + CartDetailResponse: + $ref: '#/components/schemas/CartRecord' + CartMutationResponse: + $ref: '#/components/schemas/CartRecord' + CartDeleteResponse: + type: object + required: + - cart_id + properties: + cart_id: + type: string + OrderRecord: + type: object + required: + - order_id + - order_number + - order_create + - order_owner + - order_source + - order_status + - order_source_id + - order_snap + - order_amount + properties: + pb_id: + type: string + order_id: + type: string + order_number: + type: string + order_create: + type: string + description: 订单创建时间 + order_owner: + type: string + description: 当前登录用户 openid + order_source: + type: string + order_status: + type: string + order_source_id: + type: string + order_snap: + description: 下单快照 + oneOf: + - type: object + additionalProperties: true + - type: array + items: + type: object + additionalProperties: true + order_amount: + type: number + order_remark: + type: string + created: + type: string + updated: + type: string + OrderListRequest: + type: object + properties: + keyword: + type: string + description: 按订单编号、订单 ID、来源 ID 模糊搜索 + order_status: + type: string + order_source: + type: string + OrderDetailRequest: + type: object + required: + - order_id + properties: + order_id: + type: string + OrderCreateRequest: + type: object + required: + - order_source + - order_source_id + - order_snap + - order_amount + properties: + order_number: + type: string + order_source: + type: string + order_status: + type: string + order_source_id: + type: string + order_snap: + oneOf: + - type: object + additionalProperties: true + - type: array + items: + type: object + additionalProperties: true + order_amount: + type: number + order_remark: + type: string + OrderUpdateRequest: + type: object + required: + - order_id + properties: + order_id: + type: string + order_number: + type: string + order_source: + type: string + order_status: + type: string + order_source_id: + type: string + order_snap: + oneOf: + - type: object + additionalProperties: true + - type: array + items: + type: object + additionalProperties: true + order_amount: + type: number + order_remark: + type: string + OrderDeleteRequest: + type: object + required: + - order_id + properties: + order_id: + type: string + OrderListResponse: + type: object + required: + - items + properties: + items: + type: array + items: + $ref: '#/components/schemas/OrderRecord' + OrderDetailResponse: + $ref: '#/components/schemas/OrderRecord' + OrderMutationResponse: + $ref: '#/components/schemas/OrderRecord' + OrderDeleteResponse: + type: object + required: + - order_id + properties: + order_id: + type: string CompanyInfo: anyOf: - type: object @@ -2449,4 +3326,3 @@ components: errMsg: 业务提示信息 | string data: total_users: 用户总数 | integer - 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();
属性名 属性值操作操作 / 排序
' + + '' + + '' + + '