diff --git a/docs/pb_tbl_order.md b/docs/pb_tbl_order.md index 1646a79..4876d7c 100644 --- a/docs/pb_tbl_order.md +++ b/docs/pb_tbl_order.md @@ -10,8 +10,8 @@ 当前结构按“一个订单一条记录”设计: -- `order_id` 是订单业务 ID -- `order_number` 是订单编号,建议按“用户名+年月日时分秒”或统一编号规则生成 +- `order_id` 是订单业务 ID,由数据库自动生成(`ORDER-时间戳-随机串`) +- `order_number` 是订单编号,由 PocketBase 原生字段自动生成纯数字编号(`[0-9]{18}`) - `order_snap` 使用 JSON 完整保存下单时的商品、数量、价格、折扣等快照,避免后续商品数据变化影响历史订单 ## 字段清单 @@ -19,14 +19,16 @@ | 字段名 | 类型 | 必填 | 说明 | | :--- | :--- | :---: | :--- | | `id` | `text` | 是 | PocketBase 记录主键 | -| `order_id` | `text` | 是 | 订单业务 ID,唯一标识 | -| `order_number` | `text` | 是 | 订单编号,建议业务侧自动生成并保证可追踪 | +| `order_id` | `text` | 是 | 订单业务 ID,唯一标识;数据库自动生成,格式:`ORDER-[0-9]{13}-[A-Za-z0-9]{6}` | +| `order_number` | `text` | 是 | 订单编号,PocketBase 原生自动生成 18 位纯数字(`[0-9]{18}`),唯一可追踪 | | `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_product_quantity` | `number` | 否 | 订单商品总数量,允许为空,不建立索引 | +| `order_pay_status` | `text` | 否 | 订单支付状态,允许为空,不建立索引 | | `order_amount` | `number` | 是 | 订单总金额 | | `order_remark` | `text` | 否 | 订单备注 | | `is_delete` | `number` | 否 | 软删除标记,`0` 表示未删除,`1` 表示已删除,默认 `0` | diff --git a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js index 391ce2b..f52c8a5 100644 --- a/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js +++ b/pocket-base/bai_api_pb_hooks/bai_api_shared/services/cartOrderService.js @@ -608,8 +608,7 @@ 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)) + // order_id / order_number are generated by PocketBase native field rules. record.set('order_owner', authState.openid) record.set('order_source', normalizeOrderSource(payload.order_source)) record.set('order_status', normalizeOrderStatus(payload.order_status)) diff --git a/pocket-base/spec/openapi-wx/cart.yaml b/pocket-base/spec/openapi-wx/cart.yaml index a32587d..308d586 100644 --- a/pocket-base/spec/openapi-wx/cart.yaml +++ b/pocket-base/spec/openapi-wx/cart.yaml @@ -232,6 +232,74 @@ paths: application/json: schema: $ref: '#/components/schemas/PocketBaseNativeError' + /pb/api/batch: + post: + operationId: postPocketBaseCartBatchUpdate + tags: [购物车] + summary: 批量更新购物车记录 + description: | + 使用 PocketBase 原生 batch 接口批量更新 `tbl_cart` 记录。 + + 说明: + - 这是 PocketBase 原生批处理接口,不是 hooks。 + - 单次请求可提交多条 `PATCH /api/collections/tbl_cart/records/{recordId}` 子请求。 + - 每条子请求仍按原生 collection rule 校验权限。 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseCartBatchRequest' + example: + requests: + - method: PATCH|string + url: /api/collections/tbl_cart/records/PocketBase记录主键|string + headers: + Content-Type: application/json|string + body: + cart_number: 购物车名称或分组号|string + cart_product_quantity: 产品数量|integer + cart_status: 购物车状态|string + cart_at_price: 加入购物车时价格|number + cart_remark: 备注|string + - method: PATCH|string + url: /api/collections/tbl_cart/records/PocketBase记录主键|string + headers: + Content-Type: application/json|string + body: + cart_status: 购物车状态|string + cart_remark: 备注|string + responses: + '200': + description: 批量处理成功 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseBatchResponse' + '400': + description: 批处理参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '401': + description: token 无效或已过期 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '403': + description: 无权访问其中一条或多条记录 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' + '500': + description: 服务端错误 + content: + application/json: + schema: + $ref: '#/components/schemas/PocketBaseNativeError' components: schemas: PocketBaseNativeError: @@ -397,6 +465,57 @@ components: type: array items: $ref: '#/components/schemas/PocketBaseCartRecord' + PocketBaseCartBatchItem: + type: object + required: [method, url, body] + properties: + method: + type: string + description: HTTP 方法 + example: PATCH|string + url: + type: string + description: PocketBase 子请求路径 + example: /api/collections/tbl_cart/records/PocketBase记录主键|string + headers: + type: object + description: 子请求头 + additionalProperties: + type: string + example: + Content-Type: application/json|string + body: + $ref: '#/components/schemas/PocketBaseCartUpdateRequest' + PocketBaseCartBatchRequest: + type: object + required: [requests] + properties: + requests: + type: array + description: 批量子请求列表 + items: + $ref: '#/components/schemas/PocketBaseCartBatchItem' + PocketBaseBatchResponseItem: + type: object + properties: + status: + type: [integer, string] + description: 子请求响应状态码 + example: 子请求状态码|integer + body: + type: object + description: 子请求响应体 + additionalProperties: true + example: + 字段名|string: 字段值|string + PocketBaseBatchResponse: + type: object + properties: + items: + type: array + description: 批处理结果列表 + items: + $ref: '#/components/schemas/PocketBaseBatchResponseItem' PocketBaseProductListExpand: type: object description: tbl_product_list 关联展开后的记录对象(示例字段) diff --git a/pocket-base/spec/openapi-wx/order.yaml b/pocket-base/spec/openapi-wx/order.yaml index 1e0d65c..c404302 100644 --- a/pocket-base/spec/openapi-wx/order.yaml +++ b/pocket-base/spec/openapi-wx/order.yaml @@ -82,6 +82,9 @@ paths: operationId: postPocketBaseOrderRecord tags: [订单] summary: 创建订单记录 + description: | + `order_id` 与 `order_number` 由 PocketBase 原生字段自动生成。 + 创建请求不应传入这两个字段,响应中返回生成结果。 requestBody: required: true content: @@ -89,8 +92,6 @@ paths: schema: $ref: '#/components/schemas/PocketBaseOrderCreateRequest' example: - order_id: 订单业务ID|string - order_number: 订单编号|string order_owner: 订单所有者openid|string order_source: 订单来源|string order_status: 订单状态|string @@ -271,10 +272,10 @@ components: properties: order_id: type: string - description: 订单业务ID + description: 订单业务ID(PocketBase 原生自动生成) order_number: type: string - description: 订单编号 + description: 订单编号(PocketBase 原生自动生成的 18 位纯数字) order_create: type: string description: 订单创建时间 @@ -299,6 +300,12 @@ components: type: object additionalProperties: true description: 订单快照 + order_product_quantity: + type: [number, integer] + description: 订单商品总数量(可为空) + order_pay_status: + type: string + description: 订单支付状态 order_amount: type: [number, integer] description: 订单金额 @@ -315,6 +322,8 @@ components: order_source_id: 来源关联业务ID|string order_snap: 字段名|string: 字段值|string + order_product_quantity: 订单商品总数量|number + order_pay_status: 订单支付状态|string order_amount: 订单金额|number order_remark: 备注|string PocketBaseOrderRecord: @@ -323,12 +332,10 @@ components: - $ref: '#/components/schemas/PocketBaseOrderFields' PocketBaseOrderCreateRequest: type: object - required: [order_id, order_number, order_owner, order_source, order_status, order_source_id, order_snap, order_amount] + description: | + 创建订单时无需传入 `order_id`、`order_number`,由 PocketBase 原生字段自动生成。 + required: [order_owner, order_source, order_status, order_source_id, order_snap, order_amount] properties: - order_id: - type: string - order_number: - type: string order_owner: type: string order_source: @@ -345,6 +352,10 @@ components: items: type: object additionalProperties: true + order_product_quantity: + type: [number, integer] + order_pay_status: + type: string order_amount: type: [number, integer] order_remark: @@ -370,6 +381,10 @@ components: items: type: object additionalProperties: true + order_product_quantity: + type: [number, integer] + order_pay_status: + type: string order_amount: type: [number, integer] order_remark: diff --git a/script/package.json b/script/package.json index 9332a01..7ec77e7 100644 --- a/script/package.json +++ b/script/package.json @@ -12,7 +12,10 @@ "init:scheme": "node pocketbase.scheme.js", "migrate:file-fields": "node pocketbase.file-fields-to-attachments.js", "migrate:add-is-delete-field": "node pocketbase.add-is-delete-field.js", + "migrate:add-order-product-quantity-field": "node pocketbase.add-order-product-quantity-field.js", + "migrate:add-order-pay-status-field": "node pocketbase.add-order-pay-status-field.js", "migrate:apply-soft-delete-rules": "node pocketbase.apply-soft-delete-rules.js", + "migrate:ensure-order-autogen-fields": "node pocketbase.ensure-order-autogen-fields.js", "migrate:ensure-cart-order-autogen-id": "node pocketbase.ensure-cart-order-autogen-id.js", "migrate:cart-active-unique-index": "node pocketbase.cart-active-unique-index.js", "migrate:product-params-array": "node migrate-product-parameters-to-array.js", diff --git a/script/pocketbase.add-order-pay-status-field.js b/script/pocketbase.add-order-pay-status-field.js new file mode 100644 index 0000000..2b0992b --- /dev/null +++ b/script/pocketbase.add-order-pay-status-field.js @@ -0,0 +1,187 @@ +import { createRequire } from 'module'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import PocketBase from 'pocketbase'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let runtimeConfig = {}; +try { + runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); +} catch (_error) { + runtimeConfig = {}; +} + +function readEnvFile(filePath) { + if (!fs.existsSync(filePath)) return {}; + + const content = fs.readFileSync(filePath, 'utf8'); + const result = {}; + + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + + const index = line.indexOf('='); + if (index === -1) continue; + + const key = line.slice(0, index).trim(); + const value = line.slice(index + 1).trim(); + result[key] = value; + } + + return result; +} + +const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env')); +const PB_URL = String( + process.env.PB_URL + || backendEnv.POCKETBASE_API_URL + || runtimeConfig.POCKETBASE_API_URL + || 'http://127.0.0.1:8090' +).replace(/\/+$/, ''); +const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; + +if (!AUTH_TOKEN) { + console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行 tbl_order 字段迁移。'); + process.exit(1); +} + +const pb = new PocketBase(PB_URL); + +const TARGET_FIELD = { + name: 'order_pay_status', + type: 'text', + required: false, +}; + +function normalizeFieldPayload(field, overrides) { + const payload = Object.assign({}, field, overrides || {}); + + if (payload.type === 'number') { + if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) { + payload.onlyInt = !!payload.onlyInt; + } + } + + if (payload.type === 'autodate') { + payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true; + payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false; + } + + if (payload.type === 'file') { + payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0; + payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0; + payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null; + } + + if (payload.type === 'relation') { + payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 1; + payload.cascadeDelete = typeof payload.cascadeDelete === 'boolean' ? payload.cascadeDelete : false; + } + + return payload; +} + +function buildCollectionPayload(collection, fields) { + return { + name: collection.name, + type: collection.type, + listRule: collection.listRule, + viewRule: collection.viewRule, + createRule: collection.createRule, + updateRule: collection.updateRule, + deleteRule: collection.deleteRule, + fields, + indexes: collection.indexes || [], + }; +} + +async function migrateField() { + const collection = await pb.collections.getOne('tbl_order'); + const fields = Array.isArray(collection.fields) ? collection.fields : []; + const existingField = fields.find((field) => field && field.name === TARGET_FIELD.name); + + const fieldReady = !!existingField + && existingField.type === TARGET_FIELD.type + && existingField.required === TARGET_FIELD.required; + + if (fieldReady) { + return { + action: 'skipped', + field: { + name: existingField.name, + type: existingField.type, + required: !!existingField.required, + }, + }; + } + + const nextFields = fields + .filter((field) => field && field.name !== 'id') + .map((field) => { + if (field.name === TARGET_FIELD.name) { + return normalizeFieldPayload(field, TARGET_FIELD); + } + return normalizeFieldPayload(field); + }); + + if (!existingField) { + nextFields.push(normalizeFieldPayload(null, TARGET_FIELD)); + } + + await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields)); + + return { + action: existingField ? 'normalized' : 'added', + }; +} + +async function verifyField() { + const collection = await pb.collections.getOne('tbl_order'); + const field = (collection.fields || []).find((item) => item && item.name === TARGET_FIELD.name); + + const result = { + name: field ? field.name : '', + type: field ? field.type : '', + required: !!(field && field.required), + ok: !!field && field.type === TARGET_FIELD.type && field.required === TARGET_FIELD.required, + }; + + if (!result.ok) { + throw new Error('order_pay_status 字段校验失败: ' + JSON.stringify(result, null, 2)); + } + + return result; +} + +async function main() { + try { + console.log(`🔄 正在连接 PocketBase: ${PB_URL}`); + pb.authStore.save(AUTH_TOKEN, null); + console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); + + const migrated = await migrateField(); + console.log('📝 迁移结果:'); + console.log(JSON.stringify(migrated, null, 2)); + + const verified = await verifyField(); + console.log('📝 校验结果:'); + console.log(JSON.stringify(verified, null, 2)); + + console.log('🎉 tbl_order.order_pay_status 字段已就绪。'); + } catch (error) { + console.error('❌ tbl_order 字段迁移失败:'); + console.error('status:', error && error.status); + console.error('message:', error && error.message); + console.error('response:', error && error.response); + console.error('originalError:', error && error.originalError); + console.error('stack:', error && error.stack); + process.exitCode = 1; + } +} + +main(); diff --git a/script/pocketbase.add-order-product-quantity-field.js b/script/pocketbase.add-order-product-quantity-field.js new file mode 100644 index 0000000..025bc81 --- /dev/null +++ b/script/pocketbase.add-order-product-quantity-field.js @@ -0,0 +1,187 @@ +import { createRequire } from 'module'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import PocketBase from 'pocketbase'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let runtimeConfig = {}; +try { + runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); +} catch (_error) { + runtimeConfig = {}; +} + +function readEnvFile(filePath) { + if (!fs.existsSync(filePath)) return {}; + + const content = fs.readFileSync(filePath, 'utf8'); + const result = {}; + + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + + const index = line.indexOf('='); + if (index === -1) continue; + + const key = line.slice(0, index).trim(); + const value = line.slice(index + 1).trim(); + result[key] = value; + } + + return result; +} + +const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env')); +const PB_URL = String( + process.env.PB_URL + || backendEnv.POCKETBASE_API_URL + || runtimeConfig.POCKETBASE_API_URL + || 'http://127.0.0.1:8090' +).replace(/\/+$/, ''); +const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; + +if (!AUTH_TOKEN) { + console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行 tbl_order 字段迁移。'); + process.exit(1); +} + +const pb = new PocketBase(PB_URL); + +const TARGET_FIELD = { + name: 'order_product_quantity', + type: 'number', + required: false, +}; + +function normalizeFieldPayload(field, overrides) { + const payload = Object.assign({}, field, overrides || {}); + + if (payload.type === 'number') { + if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) { + payload.onlyInt = !!payload.onlyInt; + } + } + + if (payload.type === 'autodate') { + payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true; + payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false; + } + + if (payload.type === 'file') { + payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0; + payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0; + payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null; + } + + if (payload.type === 'relation') { + payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 1; + payload.cascadeDelete = typeof payload.cascadeDelete === 'boolean' ? payload.cascadeDelete : false; + } + + return payload; +} + +function buildCollectionPayload(collection, fields) { + return { + name: collection.name, + type: collection.type, + listRule: collection.listRule, + viewRule: collection.viewRule, + createRule: collection.createRule, + updateRule: collection.updateRule, + deleteRule: collection.deleteRule, + fields, + indexes: collection.indexes || [], + }; +} + +async function migrateField() { + const collection = await pb.collections.getOne('tbl_order'); + const fields = Array.isArray(collection.fields) ? collection.fields : []; + const existingField = fields.find((field) => field && field.name === TARGET_FIELD.name); + + const fieldReady = !!existingField + && existingField.type === TARGET_FIELD.type + && existingField.required === TARGET_FIELD.required; + + if (fieldReady) { + return { + action: 'skipped', + field: { + name: existingField.name, + type: existingField.type, + required: !!existingField.required, + }, + }; + } + + const nextFields = fields + .filter((field) => field && field.name !== 'id') + .map((field) => { + if (field.name === TARGET_FIELD.name) { + return normalizeFieldPayload(field, TARGET_FIELD); + } + return normalizeFieldPayload(field); + }); + + if (!existingField) { + nextFields.push(normalizeFieldPayload(null, TARGET_FIELD)); + } + + await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields)); + + return { + action: existingField ? 'normalized' : 'added', + }; +} + +async function verifyField() { + const collection = await pb.collections.getOne('tbl_order'); + const field = (collection.fields || []).find((item) => item && item.name === TARGET_FIELD.name); + + const result = { + name: field ? field.name : '', + type: field ? field.type : '', + required: !!(field && field.required), + ok: !!field && field.type === TARGET_FIELD.type && field.required === TARGET_FIELD.required, + }; + + if (!result.ok) { + throw new Error('order_product_quantity 字段校验失败: ' + JSON.stringify(result, null, 2)); + } + + return result; +} + +async function main() { + try { + console.log(`🔄 正在连接 PocketBase: ${PB_URL}`); + pb.authStore.save(AUTH_TOKEN, null); + console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); + + const migrated = await migrateField(); + console.log('📝 迁移结果:'); + console.log(JSON.stringify(migrated, null, 2)); + + const verified = await verifyField(); + console.log('📝 校验结果:'); + console.log(JSON.stringify(verified, null, 2)); + + console.log('🎉 tbl_order.order_product_quantity 字段已就绪。'); + } catch (error) { + console.error('❌ tbl_order 字段迁移失败:'); + console.error('status:', error && error.status); + console.error('message:', error && error.message); + console.error('response:', error && error.response); + console.error('originalError:', error && error.originalError); + console.error('stack:', error && error.stack); + process.exitCode = 1; + } +} + +main(); diff --git a/script/pocketbase.cart-order.js b/script/pocketbase.cart-order.js index 4431db6..3a8071e 100644 --- a/script/pocketbase.cart-order.js +++ b/script/pocketbase.cart-order.js @@ -81,13 +81,15 @@ async function buildCollections() { deleteRule: `${OWNER_AUTH_RULE} && ${ORDER_OWNER_MATCH_RULE}`, fields: [ { name: 'order_id', type: 'text', required: true, autogeneratePattern: 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}' }, - { name: 'order_number', type: 'text', required: true }, + { name: 'order_number', type: 'text', required: true, autogeneratePattern: '[0-9]{18}' }, { 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_product_quantity', type: 'number', required: false }, + { name: 'order_pay_status', type: 'text', required: false }, { name: 'order_amount', type: 'number', required: true }, { name: 'order_remark', type: 'text' }, { name: 'is_delete', type: 'number', default: 0, min: 0, max: 1, onlyInt: true }, diff --git a/script/pocketbase.ensure-order-autogen-fields.js b/script/pocketbase.ensure-order-autogen-fields.js new file mode 100644 index 0000000..59a11d7 --- /dev/null +++ b/script/pocketbase.ensure-order-autogen-fields.js @@ -0,0 +1,209 @@ +import { createRequire } from 'module'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import PocketBase from 'pocketbase'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +let runtimeConfig = {}; +try { + runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); +} catch (_error) { + runtimeConfig = {}; +} + +function readEnvFile(filePath) { + if (!fs.existsSync(filePath)) return {}; + + const content = fs.readFileSync(filePath, 'utf8'); + const result = {}; + + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + + const index = line.indexOf('='); + if (index === -1) continue; + + const key = line.slice(0, index).trim(); + const value = line.slice(index + 1).trim(); + result[key] = value; + } + + return result; +} + +const backendEnv = readEnvFile(path.resolve(__dirname, '..', 'back-end', '.env')); +const PB_URL = String( + process.env.PB_URL + || backendEnv.POCKETBASE_API_URL + || runtimeConfig.POCKETBASE_API_URL + || 'http://127.0.0.1:8090' +).replace(/\/+$/, ''); +const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; + +if (!AUTH_TOKEN) { + console.error('❌ 缺少 POCKETBASE_AUTH_TOKEN,无法执行 tbl_order 自动编号迁移。'); + process.exit(1); +} + +const pb = new PocketBase(PB_URL); + +const ORDER_ID_PATTERN = 'ORDER-[0-9]{13}-[A-Za-z0-9]{6}'; +const ORDER_NUMBER_PATTERN = '[0-9]{18}'; + +function normalizeFieldPayload(field, overrides) { + const payload = Object.assign({}, field, overrides || {}); + + if (payload.type === 'number') { + if (Object.prototype.hasOwnProperty.call(payload, 'onlyInt')) payload.onlyInt = !!payload.onlyInt; + } + + if (payload.type === 'autodate') { + payload.onCreate = typeof payload.onCreate === 'boolean' ? payload.onCreate : true; + payload.onUpdate = typeof payload.onUpdate === 'boolean' ? payload.onUpdate : false; + } + + if (payload.type === 'file') { + payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 0; + payload.maxSize = typeof payload.maxSize === 'number' ? payload.maxSize : 0; + payload.mimeTypes = Array.isArray(payload.mimeTypes) ? payload.mimeTypes : null; + } + + if (payload.type === 'relation') { + payload.maxSelect = typeof payload.maxSelect === 'number' ? payload.maxSelect : 1; + payload.cascadeDelete = typeof payload.cascadeDelete === 'boolean' ? payload.cascadeDelete : false; + } + + return payload; +} + +function buildCollectionPayload(collection, fields) { + return { + name: collection.name, + type: collection.type, + listRule: collection.listRule, + viewRule: collection.viewRule, + createRule: collection.createRule, + updateRule: collection.updateRule, + deleteRule: collection.deleteRule, + fields, + indexes: collection.indexes || [], + }; +} + +async function migrateOrderFields() { + const collection = await pb.collections.getOne('tbl_order'); + const fields = Array.isArray(collection.fields) ? collection.fields : []; + + const orderIdField = fields.find((field) => field && field.name === 'order_id'); + const orderNumberField = fields.find((field) => field && field.name === 'order_number'); + + if (!orderIdField || !orderNumberField) { + throw new Error('tbl_order 缺少 order_id 或 order_number 字段'); + } + + const orderIdReady = orderIdField.type === 'text' && String(orderIdField.autogeneratePattern || '') === ORDER_ID_PATTERN; + const orderNumberReady = orderNumberField.type === 'text' + && String(orderNumberField.autogeneratePattern || '') === ORDER_NUMBER_PATTERN; + + if (orderIdReady && orderNumberReady) { + return { + action: 'skipped', + order_id: { + type: orderIdField.type, + autogeneratePattern: String(orderIdField.autogeneratePattern || ''), + }, + order_number: { + type: orderNumberField.type, + }, + }; + } + + const nextFields = fields + .filter((field) => field && field.name !== 'id') + .map((field) => { + if (field.name === 'order_id') { + return normalizeFieldPayload(field, { + type: 'text', + required: true, + autogeneratePattern: ORDER_ID_PATTERN, + }); + } + + if (field.name === 'order_number') { + return normalizeFieldPayload(field, { + type: 'text', + required: true, + autogeneratePattern: ORDER_NUMBER_PATTERN, + pattern: '', + }); + } + + return normalizeFieldPayload(field); + }); + + await pb.collections.update(collection.id, buildCollectionPayload(collection, nextFields)); + + return { + action: 'updated', + }; +} + +async function verifyOrderFields() { + const collection = await pb.collections.getOne('tbl_order'); + const orderIdField = (collection.fields || []).find((field) => field && field.name === 'order_id'); + const orderNumberField = (collection.fields || []).find((field) => field && field.name === 'order_number'); + + const result = { + order_id: { + type: orderIdField ? orderIdField.type : '', + autogeneratePattern: String(orderIdField && orderIdField.autogeneratePattern || ''), + ok: !!orderIdField && orderIdField.type === 'text' && String(orderIdField.autogeneratePattern || '') === ORDER_ID_PATTERN, + }, + order_number: { + type: orderNumberField ? orderNumberField.type : '', + autogeneratePattern: String(orderNumberField && orderNumberField.autogeneratePattern || ''), + ok: !!orderNumberField + && orderNumberField.type === 'text' + && String(orderNumberField.autogeneratePattern || '') === ORDER_NUMBER_PATTERN, + }, + }; + + if (!result.order_id.ok || !result.order_number.ok) { + throw new Error('tbl_order 自动编号规则校验失败: ' + JSON.stringify(result, null, 2)); + } + + return result; +} + +async function main() { + try { + console.log(`🔄 正在连接 PocketBase: ${PB_URL}`); + pb.authStore.save(AUTH_TOKEN, null); + console.log('✅ 已使用 POCKETBASE_AUTH_TOKEN 载入认证状态。'); + + const migrated = await migrateOrderFields(); + console.log('📝 迁移结果:'); + console.log(JSON.stringify(migrated, null, 2)); + + const verified = await verifyOrderFields(); + console.log('📝 校验结果:'); + console.log(JSON.stringify(verified, null, 2)); + + console.log('🎉 tbl_order 自动编号规则已就绪。'); + } catch (error) { + console.error('❌ tbl_order 自动编号迁移失败:'); + console.error('status:', error && error.status); + console.error('message:', error && error.message); + console.error('response:', error && error.response); + console.error('originalError:', error && error.originalError); + console.error('stack:', error && error.stack); + process.exitCode = 1; + } +} + +main(); \ No newline at end of file