feat: 添加购物车与订单管理页面及相关API支持
- 新增购物车与订单管理页面,包含用户列表、购物车详情和订单记录展示功能。 - 实现用户搜索、刷新、重置和退出登录功能。 - 新增购物车和订单数据表结构初始化脚本,包含字段、索引及权限规则设置。 - 实现数据表的创建与更新逻辑,并进行结构校验。
This commit is contained in:
52
docs/pb_tbl_cart.md
Normal file
52
docs/pb_tbl_cart.md
Normal file
@@ -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 字段清单里单独声明。
|
||||
54
docs/pb_tbl_order.md
Normal file
54
docs/pb_tbl_order.md
Normal file
@@ -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 字段清单里单独声明。
|
||||
@@ -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`,如需货币精度策略建议后续统一为“分”或固定小数位。
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/create.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/cart/create', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
guards.duplicateGuard(e)
|
||||
|
||||
const payload = guards.validateCartMutationBody(e, false)
|
||||
const data = cartOrderService.createCart(authState, payload)
|
||||
|
||||
return success(e, '创建购物车记录成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('创建购物车记录失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/delete.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/cart/delete', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
guards.duplicateGuard(e)
|
||||
|
||||
const payload = guards.validateCartDeleteBody(e)
|
||||
const data = cartOrderService.deleteCart(authState.openid, payload.cart_id)
|
||||
|
||||
return success(e, '删除购物车记录成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('删除购物车记录失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
23
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js
Normal file
23
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/detail.js
Normal file
@@ -0,0 +1,23 @@
|
||||
routerAdd('POST', '/api/cart/detail', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
const payload = guards.validateCartDetailBody(e)
|
||||
const data = cartOrderService.getCartDetail(authState.openid, payload.cart_id)
|
||||
|
||||
return success(e, '查询购物车详情成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('查询购物车详情失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/list.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/cart/list', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
const payload = guards.validateCartListBody(e)
|
||||
const data = cartOrderService.listCarts(authState.openid, payload)
|
||||
|
||||
return success(e, '查询购物车列表成功', {
|
||||
items: data,
|
||||
})
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('查询购物车列表失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/cart/update.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/cart/update', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
guards.duplicateGuard(e)
|
||||
|
||||
const payload = guards.validateCartMutationBody(e, true)
|
||||
const data = cartOrderService.updateCart(authState.openid, payload)
|
||||
|
||||
return success(e, '更新购物车记录成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('更新购物车记录失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/create.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/order/create', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
guards.duplicateGuard(e)
|
||||
|
||||
const payload = guards.validateOrderMutationBody(e, false)
|
||||
const data = cartOrderService.createOrder(authState, payload)
|
||||
|
||||
return success(e, '创建订单成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('创建订单失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/delete.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/order/delete', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
guards.duplicateGuard(e)
|
||||
|
||||
const payload = guards.validateOrderDeleteBody(e)
|
||||
const data = cartOrderService.deleteOrder(authState.openid, payload.order_id)
|
||||
|
||||
return success(e, '删除订单成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('删除订单失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
23
pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js
Normal file
23
pocket-base/bai_api_pb_hooks/bai_api_routes/order/detail.js
Normal file
@@ -0,0 +1,23 @@
|
||||
routerAdd('POST', '/api/order/detail', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
const payload = guards.validateOrderDetailBody(e)
|
||||
const data = cartOrderService.getOrderDetail(authState.openid, payload.order_id)
|
||||
|
||||
return success(e, '查询订单详情成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('查询订单详情失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/list.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/order/list', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
const payload = guards.validateOrderListBody(e)
|
||||
const data = cartOrderService.listOrders(authState.openid, payload)
|
||||
|
||||
return success(e, '查询订单列表成功', {
|
||||
items: data,
|
||||
})
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('查询订单列表失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js
Normal file
25
pocket-base/bai_api_pb_hooks/bai_api_routes/order/update.js
Normal file
@@ -0,0 +1,25 @@
|
||||
routerAdd('POST', '/api/order/update', function (e) {
|
||||
const guards = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/middlewares/requestGuards.js`)
|
||||
const cartOrderService = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js`)
|
||||
const logger = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/logger.js`)
|
||||
const { success, fail } = require(`${__hooks}/bai_api_pb_hooks/bai_api_shared/utils/response.js`)
|
||||
|
||||
try {
|
||||
guards.requireJson(e)
|
||||
const authState = guards.requireAuthUser(e)
|
||||
guards.duplicateGuard(e)
|
||||
|
||||
const payload = guards.validateOrderMutationBody(e, true)
|
||||
const data = cartOrderService.updateOrder(authState.openid, payload)
|
||||
|
||||
return success(e, '更新订单成功', data)
|
||||
} catch (err) {
|
||||
const status = (err && err.statusCode) || (err && err.status) || 400
|
||||
logger.error('更新订单失败', {
|
||||
status: status,
|
||||
errMsg: (err && err.message) || '未知错误',
|
||||
data: (err && err.data) || {},
|
||||
})
|
||||
return fail(e, (err && err.message) || '操作失败', (err && err.data) || {}, status)
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
pushOrUpdate(mapped, indexByName, item.name || item.key, item.value, item.sort, i + 1, i + 1)
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
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
|
||||
pushOrUpdate(result, indexByName, keys[i], source[keys[i]], '', i + 1, i + 1)
|
||||
}
|
||||
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 })
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
try {
|
||||
documentService.getAttachmentDetail(value)
|
||||
documentService.getAttachmentDetail(values[i])
|
||||
} catch (_err) {
|
||||
throw createAppError(400, fieldName + ' 对应附件不存在:' + value)
|
||||
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))
|
||||
|
||||
312
pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js
Normal file
312
pocket-base/bai_web_pb_hooks/pages/cart-order-manage.js
Normal file
@@ -0,0 +1,312 @@
|
||||
routerAdd('GET', '/manage/cart-order-manage', function (e) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>购物车与订单管理</title>
|
||||
<script>
|
||||
(function () {
|
||||
var token = localStorage.getItem('pb_manage_token') || ''
|
||||
var isLoggedIn = localStorage.getItem('pb_manage_logged_in') === '1'
|
||||
if (!token || !isLoggedIn) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%); color: #1f2937; }
|
||||
.container { max-width: 1520px; margin: 0 auto; padding: 24px 14px 40px; }
|
||||
.panel { background: rgba(255,255,255,0.97); border: 1px solid #e5e7eb; box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); border-radius: 20px; padding: 18px; }
|
||||
.panel + .panel { margin-top: 14px; }
|
||||
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.toolbar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto auto; gap: 10px; }
|
||||
.btn { border: none; border-radius: 12px; padding: 10px 16px; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-light { background: #f8fafc; color: #334155; border: 1px solid #dbe3f0; }
|
||||
.btn-danger { background: #dc2626; color: #fff; }
|
||||
.status { margin-top: 12px; min-height: 24px; font-size: 14px; }
|
||||
.status.success { color: #15803d; }
|
||||
.status.error { color: #b91c1c; }
|
||||
input { width: 100%; border: 1px solid #cbd5e1; border-radius: 12px; padding: 10px 12px; font-size: 14px; background: #fff; }
|
||||
.layout { display: grid; grid-template-columns: minmax(340px, 420px) minmax(0, 1fr); gap: 14px; align-items: start; }
|
||||
.user-list { display: grid; gap: 10px; max-height: 72vh; overflow: auto; }
|
||||
.user-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; cursor: pointer; }
|
||||
.user-card.active { border-color: #2563eb; background: #eff6ff; box-shadow: inset 0 0 0 1px #bfdbfe; }
|
||||
.user-name { font-size: 18px; font-weight: 700; color: #0f172a; }
|
||||
.user-meta { margin-top: 6px; color: #64748b; font-size: 13px; word-break: break-all; }
|
||||
.user-stats { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.stat-tag { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px; background: #dbeafe; color: #1d4ed8; font-size: 12px; font-weight: 700; }
|
||||
.detail-header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; align-items: flex-start; margin-bottom: 14px; }
|
||||
.detail-title { margin: 0; font-size: 22px; }
|
||||
.detail-meta { color: #64748b; font-size: 13px; line-height: 1.7; }
|
||||
.summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||
.summary-card { border: 1px solid #dbe3f0; border-radius: 16px; padding: 14px; background: #f8fbff; }
|
||||
.summary-label { color: #64748b; font-size: 13px; }
|
||||
.summary-value { margin-top: 8px; font-size: 22px; font-weight: 700; color: #0f172a; }
|
||||
.section + .section { margin-top: 14px; }
|
||||
.section-title { margin: 0 0 10px; font-size: 18px; color: #0f172a; }
|
||||
.table-wrap { overflow: auto; border: 1px solid #dbe3f0; border-radius: 16px; background: #fff; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead { background: #eff6ff; }
|
||||
th, td { padding: 12px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
|
||||
.muted { color: #64748b; font-size: 12px; }
|
||||
.empty { text-align: center; padding: 24px; color: #64748b; }
|
||||
@media (max-width: 1080px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.toolbar { grid-template-columns: 1fr; }
|
||||
.summary-grid { grid-template-columns: 1fr; }
|
||||
table, thead, tbody, th, td, tr { display: block; }
|
||||
thead { display: none; }
|
||||
tr { border-bottom: 1px solid #e5e7eb; }
|
||||
td { display: flex; justify-content: space-between; gap: 10px; }
|
||||
td::before { content: attr(data-label); font-weight: 700; color: #475569; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<section class="panel">
|
||||
<h1 style="margin-top:0;">购物车与订单管理</h1>
|
||||
<div class="actions">
|
||||
<a class="btn btn-light" href="/pb/manage">返回主页</a>
|
||||
<button class="btn btn-light" id="reloadBtn" type="button">刷新</button>
|
||||
<button class="btn btn-danger" id="logoutBtn" type="button">退出登录</button>
|
||||
</div>
|
||||
<div class="status" id="status"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="toolbar">
|
||||
<input id="keywordInput" placeholder="按用户名 / openid / 手机号 / users_id 搜索" />
|
||||
<button class="btn btn-primary" id="searchBtn" type="button">搜索</button>
|
||||
<button class="btn btn-light" id="resetBtn" type="button">重置</button>
|
||||
<button class="btn btn-light" id="refreshBtn" type="button">重新加载</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="layout">
|
||||
<section class="panel">
|
||||
<h2 style="margin-top:0;">用户列表</h2>
|
||||
<div id="userList" class="user-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div id="detailWrap"></div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '/pb/api'
|
||||
const tokenKey = 'pb_manage_token'
|
||||
const state = {
|
||||
users: [],
|
||||
selectedOpenid: '',
|
||||
}
|
||||
|
||||
const statusEl = document.getElementById('status')
|
||||
const keywordInput = document.getElementById('keywordInput')
|
||||
const userListEl = document.getElementById('userList')
|
||||
const detailWrapEl = document.getElementById('detailWrap')
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function setStatus(message, type) {
|
||||
statusEl.textContent = message || ''
|
||||
statusEl.className = 'status' + (type ? ' ' + type : '')
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return localStorage.getItem(tokenKey) || ''
|
||||
}
|
||||
|
||||
async function requestJson(path, payload) {
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
window.location.replace('/pb/manage/login')
|
||||
throw new Error('登录状态已失效,请重新登录')
|
||||
}
|
||||
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
body: JSON.stringify(payload || {}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (!res.ok) {
|
||||
throw new Error((data && (data.errMsg || data.message)) || '请求失败')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function getSelectedUser() {
|
||||
return state.users.find(function (item) {
|
||||
return normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||
}) || null
|
||||
}
|
||||
|
||||
function renderUserList() {
|
||||
if (!state.users.length) {
|
||||
userListEl.innerHTML = '<div class="empty">暂无匹配用户。</div>'
|
||||
return
|
||||
}
|
||||
|
||||
userListEl.innerHTML = state.users.map(function (item) {
|
||||
const isActive = normalizeText(item.openid) === normalizeText(state.selectedOpenid)
|
||||
return '<div class="user-card' + (isActive ? ' active' : '') + '" data-openid="' + escapeHtml(item.openid) + '">'
|
||||
+ '<div class="user-name">' + escapeHtml(item.users_name || item.users_id || item.openid) + '</div>'
|
||||
+ '<div class="user-meta">openid:' + escapeHtml(item.openid || '-') + '</div>'
|
||||
+ '<div class="user-meta">手机号:' + escapeHtml(item.users_phone || '-') + '</div>'
|
||||
+ '<div class="user-meta">users_id:' + escapeHtml(item.users_id || '-') + '</div>'
|
||||
+ '<div class="user-stats">'
|
||||
+ '<span class="stat-tag">购物车 ' + escapeHtml(item.cart_count || 0) + '</span>'
|
||||
+ '<span class="stat-tag">购物数量 ' + escapeHtml(item.cart_total_quantity || 0) + '</span>'
|
||||
+ '<span class="stat-tag">订单 ' + escapeHtml(item.order_count || 0) + '</span>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function renderCartTable(items) {
|
||||
if (!items.length) {
|
||||
return '<div class="empty">当前用户暂无购物车记录。</div>'
|
||||
}
|
||||
|
||||
return '<div class="table-wrap"><table><thead><tr>'
|
||||
+ '<th>商品名称</th><th>型号</th><th>数量</th><th>单价</th><th>状态</th><th>加入时间</th><th>购物车名</th>'
|
||||
+ '</tr></thead><tbody>'
|
||||
+ items.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="商品名称"><div>' + escapeHtml(item.product_name || item.cart_product_id || '-') + '</div><div class="muted">' + escapeHtml(item.cart_product_id || '-') + '</div></td>'
|
||||
+ '<td data-label="型号">' + escapeHtml(item.product_modelnumber || '-') + '</td>'
|
||||
+ '<td data-label="数量">' + escapeHtml(item.cart_product_quantity || 0) + '</td>'
|
||||
+ '<td data-label="单价">¥' + escapeHtml(item.cart_at_price || 0) + '</td>'
|
||||
+ '<td data-label="状态">' + escapeHtml(item.cart_status || '-') + '</td>'
|
||||
+ '<td data-label="加入时间">' + escapeHtml(item.cart_create || item.created || '-') + '</td>'
|
||||
+ '<td data-label="购物车名">' + escapeHtml(item.cart_number || '-') + '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
+ '</tbody></table></div>'
|
||||
}
|
||||
|
||||
function renderOrderTable(items) {
|
||||
if (!items.length) {
|
||||
return '<div class="empty">当前用户暂无订单记录。</div>'
|
||||
}
|
||||
|
||||
return '<div class="table-wrap"><table><thead><tr>'
|
||||
+ '<th>订单编号</th><th>来源</th><th>来源ID</th><th>金额</th><th>状态</th><th>下单时间</th>'
|
||||
+ '</tr></thead><tbody>'
|
||||
+ items.map(function (item) {
|
||||
return '<tr>'
|
||||
+ '<td data-label="订单编号"><div>' + escapeHtml(item.order_number || item.order_id || '-') + '</div><div class="muted">' + escapeHtml(item.order_id || '-') + '</div></td>'
|
||||
+ '<td data-label="来源">' + escapeHtml(item.order_source || '-') + '</td>'
|
||||
+ '<td data-label="来源ID">' + escapeHtml(item.order_source_id || '-') + '</td>'
|
||||
+ '<td data-label="金额">¥' + escapeHtml(item.order_amount || 0) + '</td>'
|
||||
+ '<td data-label="状态">' + escapeHtml(item.order_status || '-') + '</td>'
|
||||
+ '<td data-label="下单时间">' + escapeHtml(item.order_create || item.created || '-') + '</td>'
|
||||
+ '</tr>'
|
||||
}).join('')
|
||||
+ '</tbody></table></div>'
|
||||
}
|
||||
|
||||
function renderDetail() {
|
||||
const user = getSelectedUser()
|
||||
if (!user) {
|
||||
detailWrapEl.innerHTML = '<div class="empty">请选择左侧用户查看购物车与订单详情。</div>'
|
||||
return
|
||||
}
|
||||
|
||||
detailWrapEl.innerHTML = '<div class="detail-header">'
|
||||
+ '<div>'
|
||||
+ '<h2 class="detail-title">' + escapeHtml(user.users_name || user.users_id || user.openid) + '</h2>'
|
||||
+ '<div class="detail-meta">openid:' + escapeHtml(user.openid || '-') + '</div>'
|
||||
+ '<div class="detail-meta">users_id:' + escapeHtml(user.users_id || '-') + '</div>'
|
||||
+ '<div class="detail-meta">手机号:' + escapeHtml(user.users_phone || '-') + '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '<div class="summary-grid">'
|
||||
+ '<div class="summary-card"><div class="summary-label">购物车记录数</div><div class="summary-value">' + escapeHtml(user.cart_count || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">购物车商品总数</div><div class="summary-value">' + escapeHtml(user.cart_total_quantity || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">订单数</div><div class="summary-value">' + escapeHtml(user.order_count || 0) + '</div></div>'
|
||||
+ '<div class="summary-card"><div class="summary-label">订单总金额</div><div class="summary-value">¥' + escapeHtml(user.order_total_amount || 0) + '</div></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="section"><h3 class="section-title">当前购物车详情</h3>' + renderCartTable(Array.isArray(user.carts) ? user.carts : []) + '</div>'
|
||||
+ '<div class="section"><h3 class="section-title">订单记录</h3>' + renderOrderTable(Array.isArray(user.orders) ? user.orders : []) + '</div>'
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
setStatus('正在加载用户购物车与订单数据...', '')
|
||||
try {
|
||||
const data = await requestJson('/cart-order/manage-users', {
|
||||
keyword: normalizeText(keywordInput.value),
|
||||
})
|
||||
state.users = Array.isArray(data.items) ? data.items : []
|
||||
if (!state.users.length) {
|
||||
state.selectedOpenid = ''
|
||||
} else if (!getSelectedUser()) {
|
||||
state.selectedOpenid = normalizeText(state.users[0].openid)
|
||||
}
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus('加载完成,共 ' + state.users.length + ' 位用户。', 'success')
|
||||
} catch (err) {
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
setStatus(err.message || '加载失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
userListEl.addEventListener('click', function (event) {
|
||||
const target = event.target && event.target.closest ? event.target.closest('[data-openid]') : null
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
state.selectedOpenid = normalizeText(target.getAttribute('data-openid'))
|
||||
renderUserList()
|
||||
renderDetail()
|
||||
})
|
||||
|
||||
document.getElementById('searchBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('reloadBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadUsers)
|
||||
document.getElementById('resetBtn').addEventListener('click', function () {
|
||||
keywordInput.value = ''
|
||||
loadUsers()
|
||||
})
|
||||
document.getElementById('logoutBtn').addEventListener('click', function () {
|
||||
localStorage.removeItem('pb_manage_token')
|
||||
localStorage.removeItem('pb_manage_logged_in')
|
||||
localStorage.removeItem('pb_manage_login_account')
|
||||
localStorage.removeItem('pb_manage_login_time')
|
||||
window.location.replace('/pb/manage/login')
|
||||
})
|
||||
|
||||
loadUsers()
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return e.html(200, html)
|
||||
})
|
||||
@@ -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 '<tr>'
|
||||
+ '<td data-label="字典名称"><input class="inline-input" data-field="dict_name" data-name="' + escapeHtml(item.dict_name) + '" value="' + escapeHtml(item.dict_name) + '" /></td>'
|
||||
+ '<td data-label="字典名称"><input class="inline-input immutable-input" data-field="dict_name" data-name="' + escapeHtml(item.dict_name) + '" data-immutable-name="' + escapeHtml(item.dict_name) + '" value="' + escapeHtml(item.dict_name) + '" title="字典名称创建后不可修改,可选中复制" /></td>'
|
||||
+ '<td data-label="启用"><select class="inline-input" data-field="dict_word_is_enabled" data-name="' + escapeHtml(item.dict_name) + '"><option value="true"' + (item.dict_word_is_enabled ? ' selected' : '') + '>启用</option><option value="false"' + (!item.dict_word_is_enabled ? ' selected' : '') + '>禁用</option></select><div class="' + enabledClass + '" style="margin-top:8px;">' + enabledText + '</div></td>'
|
||||
+ '<td data-label="备注"><textarea class="inline-input" data-field="dict_word_remark" data-name="' + escapeHtml(item.dict_name) + '">' + escapeHtml(item.dict_word_remark) + '</textarea></td>'
|
||||
+ '<td data-label="枚举项">' + renderItemsPreview(item.items, previewKey, state.expandedPreviewKey === previewKey) + '</td>'
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
<section class="panel">
|
||||
<h2>文档列表</h2>
|
||||
<div class="toolbar" style="margin-bottom: 12px;">
|
||||
<div>
|
||||
<label for="listTitleKeyword">标题模糊搜索</label>
|
||||
<input id="listTitleKeyword" placeholder="请输入标题关键字" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="listTypeFilter">类型筛选</label>
|
||||
<select id="listTypeFilter">
|
||||
<option value="">全部类型</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="searchBtn" type="button">查询</button>
|
||||
<button class="btn btn-light" id="clearSearchBtn" type="button">清空筛选</button>
|
||||
</div>
|
||||
<div style="overflow:auto;">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -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 = ['<option value="">请选择数据来源字典</option>']
|
||||
.concat(state.dictionaries.map(function (dict) {
|
||||
.concat(getDocumentTypeSourceDictionaries().map(function (dict) {
|
||||
return '<option value="' + escapeHtml(dict.system_dict_id) + '"' + (currentValue === dict.system_dict_id ? ' selected' : '') + '>' + escapeHtml(dict.dict_name) + '</option>'
|
||||
}))
|
||||
.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 = ['<option value="">全部类型</option>']
|
||||
.concat(options.map(function (item) {
|
||||
return '<option value="' + escapeHtml(item.value) + '"' + (item.value === currentValue ? ' selected' : '') + '>' + escapeHtml(item.label) + '</option>'
|
||||
}))
|
||||
.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')
|
||||
|
||||
@@ -54,6 +54,10 @@ routerAdd('GET', '/manage', function (e) {
|
||||
<h2>SDK 权限管理</h2>
|
||||
<a class="btn" href="/pb/manage/sdk-permission-manage">进入权限管理</a>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h2>购物车与订单</h2>
|
||||
<a class="btn" href="/pb/manage/cart-order-manage">进入购物车与订单管理</a>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="module">
|
||||
|
||||
@@ -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) {
|
||||
<tr>
|
||||
<th style="width:40%;">属性名</th>
|
||||
<th style="width:45%;">属性值</th>
|
||||
<th style="width:15%;">操作</th>
|
||||
<th style="width:22%;">操作 / 排序</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="paramsBody"></tbody>
|
||||
@@ -204,8 +213,8 @@ routerAdd('GET', '/manage/product-manage', function (e) {
|
||||
</div>
|
||||
<div style="margin-top:10px;">
|
||||
<label for="paramsJsonInput">批量导入/导出 JSON</label>
|
||||
<textarea id="paramsJsonInput" placeholder='示例:{"属性名":"属性值","电压":"220v"}'></textarea>
|
||||
<div class="hint">仅支持 JSON 对象格式。导入时按属性名增量合并:同名覆盖、不同名新增,不会清空当前参数表。</div>
|
||||
<textarea id="paramsJsonInput" placeholder='示例:[{"sort":1,"name":"功率","value":"120W"},{"sort":2,"name":"电压","value":"220V"}]'></textarea>
|
||||
<div class="hint">推荐使用数组格式:每项包含 sort、name、value。也兼容旧对象格式。导入时按属性名增量合并:同名覆盖、不同名新增,不会清空当前参数表。</div>
|
||||
</div>
|
||||
<div class="param-actions" style="margin-top:10px;">
|
||||
<button class="btn btn-light" id="addParamBtn" type="button">新增参数行</button>
|
||||
@@ -216,17 +225,17 @@ routerAdd('GET', '/manage/product-manage', function (e) {
|
||||
<div class="full">
|
||||
<label>产品图标(保存到 tbl_attachments)</label>
|
||||
<div class="thumb-wrap">
|
||||
<div>
|
||||
<img id="iconPreview" class="thumb" src="" alt="icon" style="display:none;" />
|
||||
<div id="iconEmpty" class="muted">暂无图标</div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:260px;">
|
||||
<input id="prodListIconFile" type="file" accept="image/*" />
|
||||
<div class="hint">选择后会在保存时上传,并写入 prod_list_icon。</div>
|
||||
<div class="thumb-upload-panel">
|
||||
<input id="prodListIconFile" type="file" accept="image/*" multiple />
|
||||
<div class="hint">支持多图上传。保存时会上传到附件表,并按上传后附件 ID 用 | 写入 prod_list_icon。</div>
|
||||
<div class="actions" style="margin-top:8px;">
|
||||
<button class="btn btn-light" id="clearIconBtn" type="button">清空图标</button>
|
||||
<button class="btn btn-light" id="clearIconBtn" type="button">清空全部图标</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="thumb-preview-panel">
|
||||
<div id="iconPreviewList" class="thumb-grid"></div>
|
||||
<div id="iconEmpty" class="muted">暂无图标</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="full">
|
||||
@@ -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(
|
||||
'<div class="thumb-card">'
|
||||
+ '<img class="thumb" src="' + escapeHtml(item.attachments_url || '') + '" alt="icon-' + (i + 1) + '" />'
|
||||
+ '<div class="thumb-caption">已保存</div>'
|
||||
+ '<button class="btn btn-light thumb-remove" type="button" data-icon-remove-existing="' + i + '">移除</button>'
|
||||
+ '</div>'
|
||||
)
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.pendingIconFiles.length; i += 1) {
|
||||
const item = state.pendingIconFiles[i]
|
||||
cards.push(
|
||||
'<div class="thumb-card">'
|
||||
+ '<img class="thumb" src="' + escapeHtml(item.previewUrl || '') + '" alt="pending-icon-' + (i + 1) + '" />'
|
||||
+ '<div class="thumb-caption">待上传</div>'
|
||||
+ '<button class="btn btn-light thumb-remove" type="button" data-icon-remove-pending="' + escapeHtml(item.key) + '">移除</button>'
|
||||
+ '</div>'
|
||||
)
|
||||
}
|
||||
|
||||
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 '<tr>'
|
||||
+ '<td data-label="属性名"><input data-param-name="' + index + '" value="' + escapeHtml(row.name) + '" placeholder="属性名" /></td>'
|
||||
+ '<td data-label="属性值"><input data-param-value="' + index + '" value="' + escapeHtml(row.value) + '" placeholder="属性值" /></td>'
|
||||
+ '<td data-label="操作"><button class="btn btn-light" type="button" data-param-remove="' + index + '">删除</button></td>'
|
||||
+ '<td data-label="操作 / 排序"><div class="param-op">'
|
||||
+ '<input data-param-sort="' + index + '" type="number" min="1" step="1" value="' + escapeHtml(row.sort || '') + '" placeholder="正整数排序" />'
|
||||
+ '<button class="btn btn-light" type="button" data-param-remove="' + index + '">删除</button>'
|
||||
+ '</div></td>'
|
||||
+ '</tr>'
|
||||
}).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')
|
||||
if (Array.isArray(parsed)) {
|
||||
const invalidRow = findInvalidParameterSortRow(parsed)
|
||||
if (invalidRow) {
|
||||
setStatus('导入失败:第 ' + invalidRow + ' 项 sort 必须为正整数。', '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
|
||||
}
|
||||
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 === fields.iconFile) {
|
||||
const file = target.files && target.files.length ? target.files[0] : null
|
||||
clearPendingIconPreview()
|
||||
if (!file) {
|
||||
setIconPreview(state.currentIconAttachment ? (state.currentIconAttachment.attachments_url || '') : '')
|
||||
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
|
||||
}
|
||||
|
||||
state.pendingIconFile = {
|
||||
if (target === fields.iconFile) {
|
||||
const fileList = target.files ? Array.prototype.slice.call(target.files) : []
|
||||
if (!fileList.length) {
|
||||
return
|
||||
}
|
||||
|
||||
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 () {
|
||||
|
||||
@@ -4,7 +4,13 @@ info:
|
||||
version: 1.0.0-wx
|
||||
description: |
|
||||
面向微信端的小程序接口文档。
|
||||
本文档只包含微信登录、微信资料完善,以及微信端会共用的系统接口。
|
||||
本文档包含微信登录、微信资料完善,以及微信小程序侧会直接调用的业务接口。
|
||||
|
||||
微信小程序调用适配说明:
|
||||
- 除 `/pb/api/wechat/login` 外,其余购物车 / 订单接口都需要在请求头中携带 `Authorization: Bearer <token>`
|
||||
- `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
|
||||
|
||||
|
||||
282
script/pocketbase.cart-order.js
Normal file
282
script/pocketbase.cart-order.js
Normal file
@@ -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();
|
||||
Reference in New Issue
Block a user