diff --git a/.workbuddy/memory/2026-08-23.md b/.workbuddy/memory/2026-08-23.md new file mode 100644 index 0000000..c15bc21 --- /dev/null +++ b/.workbuddy/memory/2026-08-23.md @@ -0,0 +1,31 @@ +# 2026-08-23 工作日志 + +## 订单"确认身份"卡住 —— 根因与最终修复(已落地) + +### 根因(完整调用链,后端为命门) +1. 前端列表 `pages/order/index.vue:93`、`pages/order/detail.vue:528` 用 `receiptStatus==20 && idCode==null` 判定"确认身份"。`idCode` 前端从不赋值,必须由后端写入。 +2. 后端 `OpenEquipmentController.receipt()` → `verifyIdCard()` 调 `IdcardServiceImpl`(蜜堂有信/阿里云市场,已失效的第三方 OCR)。OCR 调不通 → `order.setIdCode()` 永不执行 → `idCode` 恒为 null → 列表卡死。 +3. 更关键:**`shop_order` 表根本没有 `id_code` / `real_name` / `address` 三列**(实体 `Order` 声明了字段但列不存在)。一旦 `setIdCode` 就 SQL 报错。 +4. `OrderReceiptParam` 也没有 `idCode` 字段,所以前端发 `idCode` 后端不读(早期前端直连 OCR 方案实际无效)。 + +### 最终方案:后端百度 OCR(密钥服务端,安全) +- 用户给了百度密钥(API Key / Secret Key)+ 后端路径 `/Users/gxwebsoft/JAVA/yunxinwei-api`,明确"换接口"。 +- 把失效的 `IdcardService` 替换为百度身份证识别,由后端从上传的正面图识别并写 `idCode`。 + +### 后端改动(已 BUILD SUCCESS) +- 新增 `common/core/utils/BaiduOcrService.java`:用 apiKey/secretKey 换 access_token(进程内缓存),调 `aip.baidubce.com/rest/2.0/ocr/v1/idcard` 取 `words_result['公民身份号码']`(校验 18 位格式),回填 `Order.idCode/realName/address` 及 `User`。 +- 新增 `common/core/utils/BaiduIdCardResult.java`(结果 VO)。 +- `OpenEquipmentController.java`:`verifyIdCard()` 改用 `baiduOcrService.recognizeFront(front)`;移除 `IdcardService`/`IdcardRespVO` 依赖(旧实现仍在注释块内)。 +- `shop/param/OrderReceiptParam.java`:新增非必填 `idCode`(前端手动填写兜底通道,经 BeanCopier 写库)。 +- `application.yml`:新增 `baidu.ocr.api-key/secret-key`(用户密钥已写入)。 +- **新增 `数据库脚本/alter_shop_order_idcard.sql`:必须先在 `yunxinwei` 库执行 `ALTER TABLE shop_order ADD id_code/real_name/address`,否则 `updateById` 会因列不存在而失败。** 这是部署前必做的人工步骤。 + +### 前端改动(收敛,去掉暴露密钥的直连 OCR) +- `config.js`:移除前端 `baiduOcr` 密钥块。 +- `pages/order/delivery.vue`:移除 `getBaiduToken`/`recognizeIdCard` 及 `chooseImage` 里的 OCR 触发;`idCode` 改为可选项(有值才格式校验);新增可手动填写的身份证号输入框(兜底);`handleSubmit` 改为仅当 `idCode` 非空才随请求发送(避免空串覆盖后端识别结果)。 + +### 实测验证要点(交用户部署后确认) +- 部署后端前先跑 SQL 加列。 +- 提交收货后回列表,应出现"去支付"(`idCode` 非空)。 +- 若 OCR 偶尔失败,用户可手动填身份证号兜底。 +- 不再需要小程序加百度域名白名单(OCR 走服务端)。 diff --git a/config.js b/config.js index 8027dbb..a8da863 100644 --- a/config.js +++ b/config.js @@ -17,7 +17,6 @@ module.exports = { - // 注册用户的默认角色ID // roleId: 81 // 商户编号 diff --git a/pages/order/delivery.vue b/pages/order/delivery.vue index 85e1755..9fe4e63 100644 --- a/pages/order/delivery.vue +++ b/pages/order/delivery.vue @@ -62,6 +62,7 @@ + @@ -127,12 +128,21 @@ message: '请输入单位地址', trigger: ['blur', 'change'] }], - homeAddress: [{ - required: true, - message: '请输入家庭地址', - trigger: ['blur', 'change'] - }], - } + homeAddress: [{ + required: true, + message: '请输入家庭地址', + trigger: ['blur', 'change'] + }], + idCode: [{ + validator: (rule, value, callback) => { + // 允许为空(身份证号由后端 OCR 自动识别);有值时才校验格式 + if (!value) return true + return /^\d{17}[\dXx]$/.test(value || '') + }, + message: '身份证号格式不正确', + trigger: ['blur', 'change'] + }], +} // 图片槽位说明(与 orderSourceData 下标一一对应) const imageSlotLabels = ['身份证正面', '身份证反面', '人车合照', '车子照片', '安装照片'] @@ -157,6 +167,8 @@ emergentUser: '', officeAddress: '', homeAddress: '', + // 身份证号(上传正面后由 OCR 自动识别回填,也可手动修改) + idCode: '', // 按钮禁用 // disabled: false, submitText: '证件已上传并确认收货', @@ -269,7 +281,7 @@ handleSubmit() { const that = this const app = this - const { orderId, orderSourceData, captcha, receiptPhone, emergentUser, officeAddress, homeAddress } = app + const { orderId, orderSourceData, captcha, receiptPhone, emergentUser, officeAddress, homeAddress, idCode } = app // 1) 校验 5 张图是否都已上传 const missingIndex = [] @@ -285,15 +297,21 @@ return } - receiptOrder({ - orderId: orderId, - orderSourceData: JSON.stringify(orderSourceData), - captcha: captcha, - receiptPhone: receiptPhone, - emergentUser: emergentUser, - officeAddress: officeAddress, - homeAddress: homeAddress - }).then(result => { + // 2) 身份证号优先由后端 OCR 自动识别并写库;前端仅在用户手动填写时才随请求带上(避免空串覆盖后端结果) + const params = { + orderId: orderId, + orderSourceData: JSON.stringify(orderSourceData), + captcha: captcha, + receiptPhone: receiptPhone, + emergentUser: emergentUser, + officeAddress: officeAddress, + homeAddress: homeAddress + } + if (idCode) { + params.idCode = idCode + } + + receiptOrder(params).then(result => { if(result.code == 0) { that.$success("确认收货成功!") // uni.navigateBack() @@ -335,9 +353,9 @@ } else { app.orderSourceData.push(res.data.url) } - console.log('app.orderSourceData', app.orderSourceData) - app.$success(`${slotLabel}上传成功`) - }).catch(err => { + console.log('app.orderSourceData', app.orderSourceData) + app.$success(`${slotLabel}上传成功`) + }).catch(err => { app.$toast(formatErrorMsg((err && (err.errMsg || err.message)) || `${slotLabel}上传失败,请重试`), 2500) }).finally(() => { uni.hideLoading() @@ -347,6 +365,7 @@ }); }, + } }