diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..476dc6f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/yunxinwei-uniapp.iml b/.idea/yunxinwei-uniapp.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/yunxinwei-uniapp.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.workbuddy/memory/2026-07-26.md b/.workbuddy/memory/2026-07-26.md new file mode 100644 index 0000000..fad1133 --- /dev/null +++ b/.workbuddy/memory/2026-07-26.md @@ -0,0 +1,45 @@ +# 2026-07-26 + +## 修复 `pages/order/delivery.vue` 身份证上传失败 Bug + +**用户报告**:页面提交时弹"请上传身份证正面照片.FAILUREFP03333"。 + +**根因分析**: +- 错误码 `FAILUREFP03333` 和提示文本"请上传身份证正面照片"都是后端 `POST /open/equipment/receipt` 返回的,前端代码无此内容。 +- `orderSourceData` 数组下标与图片含义是固定绑定:0=身份证正面、1=身份证反面、2=人车合照、3=车子照片、4=安装照片。 +- 截图里只看到"其他"模块 3 张图,未见"身份证信息"模块——用户大概率跳过了身份证正反面上传就点了提交。 + +**修复内容**(`pages/order/delivery.vue`): +1. 在 `handleSubmit` 提交前增加客户端必传校验:5 个图片槽位任一为空就拦截,并按"身份证正面 > 身份证反面 > 其他"优先级提示用户补图。 +2. 过滤后端返回的错误码尾巴(如 `.FAILUREFP03333`),新增 `formatErrorMsg()` 工具函数。 +3. 优化占位图:未上传时显示"+ 点击上传身份证正面(人像面)"等说明,替代原来的灰色 `not-dealer.png` 占位。 +4. `chooseImage` 加 `uni.showLoading` 上传中提示,成功/失败分别 toast 明确文案。 +5. 字段标签明确化:"正面/反面"→"身份证正面(人像面)/身份证反面(国徽面)"。 + +**关于 `FAILUREFP03333` 的判断**: +- 非前端错误码(前端 grep 无任何匹配)。 +- 推测是后端在调用阿里云内容安全 / 第三方 OCR(Face++、百度、腾讯云天御等)做身份证内容审核时返回的状态码。 +- 真正根因在后端,前端只能加校验和友好提示,无法根本解决"图片本身被识别为非身份证"的场景。 +- 建议后端同事确认 `/open/equipment/receipt` 接口的身份证正反面校验逻辑,以及是否依赖第三方图片识别服务。 + +## 后端 `yunxinwei-api` 同步修复(FAILUREFP03333 根因) + +**项目路径**:`/Users/gxwebsoft/JAVA/yunxinwei-api` +**文件**:`src/main/java/com/gxwebsoft/open/controller/OpenEquipmentController.java` + +**根因确认**: +- `receipt()` 接口(原 402-461 行)调用 `idcardService.verify(front, back)` 做阿里云市场 OCR(`miitangs14.market.alicloudapi.com/v1/tools/ocr/idCard`),非 `FP00000` 时把 `verify.getMessage() + verify.getCode()` 直接拼进用户提示 → 用户看到"请上传身份证正面照片.FAILUREFP03333"。 +- `idcardService.verify` 在 OCR 接口异常时返回 `null`,原代码紧接着 `verify.getCode()` 会 NPE → 500。 +- `images.get(0/1)` 在图片不足 2 张时会 `IndexOutOfBounds`;用户跳过身份证上传只传 3 张其他图时,`images[0]` 被当成身份证人像面去 OCR → 失败但提示语仍是"请上传身份证正面照片"。 + +**修复内容**: +1. 抽出私有方法 `verifyIdCard(orderSourceData, order)`,把身份证识别与实名回填逻辑内聚。 +2. JSON 解析加 try-catch,失败返回"身份证图片数据格式有误"。 +3. 校验 `images` 至少 2 张且 `images[0]`/`images[1]` 非空,否则返回"请上传身份证正面和反面照片"(不再越界)。 +4. `verify` 调用加 try-catch + `null` 判断,避免 NPE;OCR 异常返回友好提示。 +5. **内部错误码 `FAILUREFP03333` 不再回吐给用户**,仅 `log.warn` 记录;用户提示改为"身份证正面照片识别失败,请重新上传清晰的身份证人像面照片" / "身份证反面照片识别失败,请重新上传清晰的身份证国徽面照片"。 +6. `userService.getById(...)` 补 `null` 防护,避免回填空指针。 + +**验证**:`./mvnw -o -q compile` 离线编译通过(EXIT=0,依赖已本地缓存)。 + +**前后端配合结论**:前端(delivery.vue)做提交前必传校验 + 友好占位,后端(OpenEquipmentController)做空安全 + 内部码隔离 + 分正反面提示,两端共同消除该 Bug。 diff --git a/pages/order/delivery.vue b/pages/order/delivery.vue index ea752b3..85e1755 100644 --- a/pages/order/delivery.vue +++ b/pages/order/delivery.vue @@ -6,34 +6,49 @@ - + - 身份证信息 + 身份证信息(必传) - - - + + + + + + 点击上传身份证正面(人像面) + - - - + + + + + + 点击上传身份证反面(国徽面) + - 其他 + 其他凭证(必传) - - - + + + + + + 点击上传人车合照 + - - - + + + + + + 点击上传车子照片 + - - - + + + + + + 点击上传安装完成照片 + @@ -118,6 +133,15 @@ trigger: ['blur', 'change'] }], } + + // 图片槽位说明(与 orderSourceData 下标一一对应) + const imageSlotLabels = ['身份证正面', '身份证反面', '人车合照', '车子照片', '安装照片'] + + // 过滤后端返回的错误码尾巴(如 ".FAILUREFP03333"),让提示更友好 + function formatErrorMsg(msg) { + if (!msg) return '操作失败,请稍后重试' + return String(msg).replace(/\.[A-Z0-9]{4,}\s*$/, '').trim() || '操作失败,请稍后重试' + } export default { data() { @@ -241,70 +265,87 @@ app.getData() }) }, */ - // 表单提交 - handleSubmit() { - // if(orderSourceData.length < 5) { - // return - // } - const that = this - const app = this - const { orderId,orderSourceData,captcha,receiptPhone,emergentUser,officeAddress, homeAddress } = app - - - receiptOrder({ - orderId: orderId, - orderSourceData: JSON.stringify(orderSourceData), - captcha: captcha, - receiptPhone: receiptPhone, - emergentUser: emergentUser, - officeAddress: officeAddress, - homeAddress: homeAddress - }).then(result => { - if(result.code == 0) { - that.$success("确认收货成功!") - // uni.navigateBack() - setTimeout(()=>{ - that.$navTo('pages/order/index',{}, 'redirectTo') - }, 1000) - } else { - that.$toast(result.message) - } - }).catch(err => { - that.$toast(err.message) - }) - }, - - // 上传图片 - chooseImage(id) { - const app = this - const { orderId } = this - const type = 'photo' + id - // 选择图片 - uni.chooseImage({ - count: 1, - sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有 - sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 - success(chooseImageRes) { - const tempFilePaths = chooseImageRes.tempFilePaths; - - uploadFile({ - filePath: tempFilePaths[0], - fileType: 'image', - name: 'file' - }).then(res => { + // 表单提交 + handleSubmit() { + const that = this + const app = this + const { orderId, orderSourceData, captcha, receiptPhone, emergentUser, officeAddress, homeAddress } = app + + // 1) 校验 5 张图是否都已上传 + const missingIndex = [] + for (let i = 0; i < imageSlotLabels.length; i++) { + if (!orderSourceData[i]) { + missingIndex.push(i) + } + } + if (missingIndex.length > 0) { + // 优先提示最关键的那张(身份证正面) + const firstMissing = missingIndex.includes(0) ? 0 : missingIndex[0] + that.$toast(`请先上传${imageSlotLabels[firstMissing]}`, 2500) + return + } + + receiptOrder({ + orderId: orderId, + orderSourceData: JSON.stringify(orderSourceData), + captcha: captcha, + receiptPhone: receiptPhone, + emergentUser: emergentUser, + officeAddress: officeAddress, + homeAddress: homeAddress + }).then(result => { + if(result.code == 0) { + that.$success("确认收货成功!") + // uni.navigateBack() + setTimeout(()=>{ + that.$navTo('pages/order/index',{}, 'redirectTo') + }, 1000) + } else { + that.$toast(formatErrorMsg(result.message), 2500) + } + }).catch(err => { + that.$toast(formatErrorMsg((err && err.message) || '提交失败,请稍后重试'), 2500) + }) + }, + + // 上传图片 + chooseImage(id) { + const app = this + const { orderId } = this + const type = 'photo' + id + // 选择图片 + uni.chooseImage({ + count: 1, + sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有 + sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 + success(chooseImageRes) { + const tempFilePaths = chooseImageRes.tempFilePaths; + const slotLabel = imageSlotLabels[id - 1] || '图片' + uni.showLoading({ title: '上传中...', mask: true }) + + uploadFile({ + filePath: tempFilePaths[0], + fileType: 'image', + name: 'file' + }).then(res => { console.log('res', res) - // app.orderSourceData.push(res.data.url) - if(app.orderSourceData[id-1]){ - app.orderSourceData.splice(id-1, 1, res.data.url) - } else { - app.orderSourceData.push(res.data.url) - } - console.log('app.orderSourceData', app.orderSourceData) - }) - - } - }); - }, + // app.orderSourceData.push(res.data.url) + if(app.orderSourceData[id-1]){ + app.orderSourceData.splice(id-1, 1, res.data.url) + } else { + app.orderSourceData.push(res.data.url) + } + console.log('app.orderSourceData', app.orderSourceData) + app.$success(`${slotLabel}上传成功`) + }).catch(err => { + app.$toast(formatErrorMsg((err && (err.errMsg || err.message)) || `${slotLabel}上传失败,请重试`), 2500) + }).finally(() => { + uni.hideLoading() + }) + + } + }); + }, } @@ -334,6 +375,39 @@ background: #fff; } + /* 上传图片(已上传时) */ + .upload-img { + width: 300rpx; + height: 200rpx; + border-radius: 8rpx; + } + + /* 上传占位符(未上传时) */ + .upload-placeholder { + width: 300rpx; + height: 200rpx; + border: 2rpx dashed #c8c8c8; + border-radius: 8rpx; + background: #fafafa; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #999; + + .placeholder-icon { + font-size: 56rpx; + line-height: 1; + color: #c0c4cc; + } + + .placeholder-text { + font-size: 22rpx; + margin-top: 10rpx; + color: #909399; + } + } + /* 底部操作栏 */ .footer { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..8472722 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,86 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@dcloudio/uni-ui': + specifier: ^1.4.28 + version: 1.5.12 + async-validator: + specifier: ^4.2.5 + version: 4.2.5 + dayjs: + specifier: ^1.11.10 + version: 1.11.21 + luch-request: + specifier: ^3.0.8 + version: 3.1.1 + devDependencies: + js: + specifier: ^0.1.0 + version: 0.1.0 + js-md5: + specifier: ^0.7.3 + version: 0.7.3 + +packages: + + '@dcloudio/types@2.6.12': + resolution: {integrity: sha512-mrCMwcINy1IFjU9VUqLeWBkj404yWs5paLDttBcA+eqUjanuUQbBcTVPqlrGgkyzLXDcV2oDDZRSNxNpXi4kMQ==} + + '@dcloudio/uni-ui@1.5.12': + resolution: {integrity: sha512-mGDl2OZSz7D8xcUAzJegWDHOqB4MEFBSW9Esb/oJiu2/3Gk9+P/Z4bA4JZ9jv9VWBYbMrYwaTfK1Z728kABdYg==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + commander@1.1.1: + resolution: {integrity: sha512-71Rod2AhcH3JhkBikVpNd0pA+fWsmAaVoti6OR38T76chA7vE3pSerS0Jor4wDw+tOueD2zLVvFOw5H0Rcj7rA==} + engines: {node: '>= 0.6.x'} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + js-md5@0.7.3: + resolution: {integrity: sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==} + + js@0.1.0: + resolution: {integrity: sha512-ZBbGYOpact8QAH9RprFWL4RAESYwbDodxiuDjOnzwzzk9pBzKycoifGuUrHHcDixE/eLMKPHRaXenTgu1qXBqA==} + hasBin: true + + keypress@0.1.0: + resolution: {integrity: sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA==} + + luch-request@3.1.1: + resolution: {integrity: sha512-p7+mlcEtgRcd0OfXC4XZbyiwSr1XgCeqNT7LlVUjnk7InYl/8d5Rk7BUqAYNA2WRafI1wRIUQWRWZRpeUwWR0w==} + +snapshots: + + '@dcloudio/types@2.6.12': {} + + '@dcloudio/uni-ui@1.5.12': {} + + async-validator@4.2.5: {} + + commander@1.1.1: + dependencies: + keypress: 0.1.0 + + dayjs@1.11.21: {} + + js-md5@0.7.3: {} + + js@0.1.0: + dependencies: + commander: 1.1.1 + + keypress@0.1.0: {} + + luch-request@3.1.1: + dependencies: + '@dcloudio/types': 2.6.12