rencaika_merchant/docs/pages/12-相机扫码.md
@@ -17,13 +17,13 @@ | SC-002 | 原生层覆盖 | 扫描框、遮罩、返回按钮和状态卡使用 `cover-view` / `cover-image`。 | 已完成,待真机验证 | | SC-003 | 扫码解析 | 扫码原文仅用于请求 `POST /api/merchant/verification/scan`,不写日志或持久缓存。 | 已接入 | | SC-004 | 二次确认核销 | 解析成功后显示脱敏持卡人信息并要求确认;确认请求携带幂等键。 | 已接入 | | SC-005 | 状态恢复 | 支持核验中、确认中、核销成功、失败重试、重新扫描和相机拒绝。 | 已完成 | | SC-005 | 状态恢复 | 支持核验中、确认中、核销成功、普通失败重试、非当前商户独立失败页和相机拒绝。 | 已完成 | | SC-006 | 返回 | 返回时取消当前请求并回到首页。 | 已完成 | ## 操作说明 1. 从首页进入相机扫码页,授权后将人才动态码置于扫描框内。 2. 扫码结果先由后台解析;解析失败可重试或重新扫描。 2. 扫码结果先由后台解析;非当前商户等商户范围错误进入独立失败结果页,其他解析失败可重试或重新扫描。 3. 解析成功后确认核销。成功后展示核销结果;失败时保留当前结果并可重试。 4. 点击左上角返回会取消当前请求并回到首页。 @@ -38,6 +38,7 @@ - [x] 首页扫码核销入口可跳转到相机扫码页。 - [x] 已实现扫描、服务端解析、二次确认、成功反馈和失败重试。 - [x] 非当前商户二维码使用独立核销失败页提示,返回后恢复扫码状态。 - [x] 扫码不会直接消耗动态码;仅确认成功才写入核销记录。 - [x] 重复确认通过幂等键返回同一次核销结果。 - [ ] 已真机验证相机授权、二维码识别、权限拒绝、原生层覆盖和网络重试。 rencaika_merchant/docs/pages/22-核销失败.md
New file @@ -0,0 +1,36 @@ # 核销失败 ## 基本信息 | 项目 | 内容 | | --- | --- | | 路由 | `/pages/verification-failure/verification-failure` | | 页面目标 | 明确告知商户二维码无法在当前商户核销,并提供单一返回路径。 | | 进入方式 | 相机扫码接口返回非当前商户相关的核销拒绝信息。 | | 负责人/更新时间 | 待指定 / 2026-08-14 | ## 功能点 | 编号 | 功能 | 说明 | 当前状态 | | --- | --- | --- | --- | | VF-001 | 失败结果 | 独立展示“核销失败”和统一拒绝原因,不在相机页使用弹窗。 | 已完成 | | VF-002 | 返回 | 顶部返回和页面“返回”按钮均回到扫码页,并恢复可扫描状态。 | 已完成 | ## 操作说明 1. 商户扫描不属于当前商户或不可核销的二维码。 2. 系统进入独立核销失败页,展示统一错误说明。 3. 点击顶部返回或页面“返回”按钮,回到相机页重新扫描。 ## 异常处理 | 场景 | 用户可见提示 | 恢复方式 | | --- | --- | --- | | 无法返回扫码页 | 保持当前失败结果 | 自动返回商户端首页 | ## 验收清单 - [x] 页面结构与核销成功结果页保持一致。 - [x] 页面只保留“返回”主操作,不提供重试核销。 - [x] 返回扫码页后可重新扫描。 - [ ] 微信真机安全区、返回流程和视觉效果已验证。 rencaika_merchant/docs/pages/README.md
@@ -2,7 +2,7 @@ ## 当前约定 截至 2026-08-12,商户端 `pages.json` 已注册 26 个页面,均已纳入操作说明。协议列表和协议详情共用 `08-商户协议.md`,因此当前共有 25 份页面说明。新增页面或既有页面交互发生变化时,必须同步更新: 截至 2026-08-14,商户端 `pages.json` 已注册 27 个页面。新增页面或既有页面交互发生变化时,必须同步更新: - `docs/开发进度与注意事项.md` - `docs/项目接口对接说明.md` @@ -34,6 +34,7 @@ | 19 | 进件待审核 | `/pages/merchant-onboarding-pending/merchant-onboarding-pending` | 待审核结果与返回流程首版已完成 | | 20 | 提现提交成功 | `/pages/withdrawal-success/withdrawal-success` | 提交结果与返回提现页流程已完成 | | 21 | 结算管理 | `/pages/settlement-management/settlement-management` | 结算汇总、列表与导出已接入 | | 22 | 核销失败 | `/pages/verification-failure/verification-failure` | 非当前商户二维码独立失败结果页已完成 | - 文档编号必须唯一,页面目录、路由和文档标题应保持一致。 - 页面有多种业务状态时,可在同一说明中用“状态差异”说明,或增加明确命名的补充文档。 rencaika_merchant/docs/开发进度与注意事项.md
@@ -17,12 +17,12 @@ | uni-app X 工程骨架 | 已完成 | 使用 `.uvue` + `.uts`,已配置微信小程序目标。 | | 微信小程序 AppID | 开发配置已应用 | 当前暂用人才端 `wx6ae2cadfea28fce1`;正式发布前必须替换为商户端独立 AppID。 | | 请求与会话基础层 | 已完成首版 | 已建立 `config/api.uts`、`services/http.uts`、`services/session.uts` 和扫码业务服务,统一处理环境、请求头、超时、响应错误、`traceId` 与 401。 | | 页面路由 | 已完成首版 | `pages.json` 已注册 26 个页面;页面源码和当前代码内跳转目标均存在,均为自定义导航样式。 | | 页面路由 | 已完成首版 | `pages.json` 已注册 27 个页面;页面源码和当前代码内跳转目标均存在,均为自定义导航样式。 | | 登录页视觉 | 已完成 | 已按设计稿实现欢迎区、插画、表单、协议和错误提示样式;密码框使用微信安全密码输入类型,避免 iOS 保存密码提示。 | | 登录页演示流程 | 已完成 | 账号、密码和协议校验通过后直接进入首页;未接真实鉴权。 | | 首页视觉 | 已完成 | 已实现经营汇总、统计卡、扫码核销主操作和底部导航。 | | 首页数据 | 静态演示 | 当前展示设计示例数值,未接经营数据接口。 | | 扫码核销 | 前端解析流程已完成 | 首页可进入原生后置相机扫码页;已读取真实扫码结果并调用统一服务,补齐核验中、成功、失败、重试、重新扫描和相机拒绝状态;后台联调与真机验证待完成。 | | 扫码核销 | 前端解析流程已完成 | 首页可进入原生后置相机扫码页;已读取真实扫码结果并调用统一服务,补齐核验中、成功、普通失败重试、非当前商户独立失败页、重新扫描和相机拒绝状态;真机验证待完成。 | | 订单管理 | 已接入订单列表 | 已接入团购、到店消费、免费入住订单的分页、稳定状态枚举、日期范围、脱敏字段、失败重试和开发演示数据开关;详情页仍沿用现有视觉结构。 | | 待支付/已完成订单详情 | 已完成视觉与交互首版 | 已实现待支付、已完成两种状态、订单金额明细、人才权益信息与返回订单列表;团购已完成补齐撤销期限提示、二次确认、提交锁定和状态回退接口,待后台联调。 | | 待核销订单详情 | 已完成视觉与静态交互首版 | 已实现商品摘要、订单金额明细、人才权益信息与固定确认核销操作。 | @@ -122,6 +122,7 @@ | 2026-07-28 | 新增商户进件和进件待审核页面;提现入口根据未进件、待审核、审核通过三种状态分流,补齐企业/个人表单、日期与证件校验和敏感信息保护。HBuilderX 5.07 编译通过,共 21 个页面。 | | 2026-07-28 | 新增提现提交成功页面;提现确认后替换填写页进入成功提醒,顶部返回和页面按钮均回到商户提现页。HBuilderX 5.07 微信小程序编译通过,共 22 个页面。 | | 2026-08-12 | 新增商户中心结算管理页,接入后台现有商户结算统计接口,支持按日/月查看汇总、分页记录与 Excel 导出;补充页面路由、菜单入口和接口文档。 | | 2026-08-14 | 商户端扫码核销新增独立失败结果页;非当前商户相关拒绝不再停留在相机页弹窗,返回后自动恢复扫码状态。 | | 2026-07-28 | 团购已完成详情补齐撤销核销流程:已过后台结算周期时提示不可撤销,期限内二次确认后调用带幂等键的撤销接口,成功后按后端响应退回订单状态;到店消费仍不展示撤销操作。HBuilderX 5.07 微信小程序编译通过。 | | 2026-07-28 | 集中优化可独立完成的 P1:订单状态改用稳定枚举并补齐按 ID 本地详情映射;商品路由携带商品/审核记录 ID,详情补齐表单绑定、校验、草稿与未保存提醒;登录协议入口接通;版本统一为 V1.0.0;缓存与会话分级清理;新增通用页面反馈组件。HBuilderX 5.07 微信小程序编译通过。 | | 2026-07-28 | 集中优化可独立完成的 P2:首页、订单、商户中心统一使用动态安全区底部导航;新增一键静态检查/编译脚本、脱敏接口 Mock 契约、错误诊断与追踪编号复制;未引用图片移出运行时 `static/`。静态检查与 HBuilderX 5.07 微信小程序编译通过。 | rencaika_merchant/docs/项目接口对接说明.md
@@ -125,7 +125,7 @@ 1. 优先调用微信扫码能力,扫码结果只用于发起后端核验,不在客户端推导权益可用性。 2. 核销确认必须由服务端保证幂等;同一核销码重复提交应返回可识别的已处理结果。 3. 页面仅展示业务必要的脱敏用户信息,不缓存完整人才码、身份证号、手机号或订单敏感字段。 4. 扫码解析失败、已使用、已过期、无权限和网络超时均需给出明确且可恢复的提示。 4. 扫码解析失败、已使用、已过期、无权限和网络超时均需给出明确且可恢复的提示;非当前商户相关拒绝使用独立失败结果页,返回后恢复扫码。 5. 涉及金额、补贴和核销结果的所有最终判断均以后端返回为准。 ## 统一异常处理要求 @@ -136,6 +136,7 @@ | 登录失效 | 清理会话并回登录页 | 重新登录 | | 无门店权限 | 说明当前账号无可操作门店 | 联系管理员或切换账号 | | 核销码无效/过期 | 显示具体原因,不显示成功态 | 重新扫码 | | 二维码不属于当前商户 | 进入独立核销失败页并展示统一拒绝原因 | 点击“返回”后重新扫码 | | 重复核销 | 显示已核销时间或记录入口 | 查看核销详情 | | 服务端未知错误 | 显示通用失败提示并保留 `traceId` | 稍后重试或联系支持 | rencaika_merchant/docs/项目说明.md
@@ -11,7 +11,7 @@ - [待处理事项](./待处理事项.md):本轮静态审计结论、上线阻塞、后端依赖和真机验收清单。 - [项目接口对接说明](./项目接口对接说明.md):建议接口边界、通用协议和异常处理要求。 - [开发进度与注意事项](./开发进度与注意事项.md):已完成页面、实现约定和更新记录。 - [页面操作说明索引](./pages/README.md):22 个注册页面对应的操作和验收说明。 - [页面操作说明索引](./pages/README.md):27 个注册页面的操作和验收说明索引。 ## 技术框架 @@ -60,6 +60,7 @@ │ ├─ order-detail/order-detail.uvue # 待支付与已完成订单详情 │ ├─ verification-detail/verification-detail.uvue # 待核销订单详情 │ ├─ scan/scan.uvue # 相机扫码核销 │ ├─ verification-failure/verification-failure.uvue # 核销失败结果 │ ├─ merchant/merchant.uvue # 商户中心 │ ├─ withdrawal/withdrawal.uvue # 商户提现 │ ├─ settlement-management/settlement-management.uvue # 结算管理 @@ -97,6 +98,7 @@ │ ├─ 经营数据总览 │ ├─ 扫码核销入口 │ │ └─ 相机扫码 /pages/scan/scan │ │ └─ 核销失败 /pages/verification-failure/verification-failure │ ├─ 订单管理 /pages/orders/orders │ │ ├─ 团购订单 │ │ │ └─ 待核销订单详情 /pages/verification-detail/verification-detail rencaika_merchant/pages.json
@@ -179,6 +179,14 @@ } }, { "path": "pages/verification-failure/verification-failure", "style": { "navigationStyle": "custom", "backgroundColor": "#F6F5F2", "disableScroll": true } }, { "path": "pages/consume-input/consume-input", "style": { "navigationStyle": "custom", rencaika_merchant/pages/scan/scan.uvue
@@ -36,6 +36,7 @@ let lastScanResult = '' let openedManualConsumption = false let openedConfirmation = false let openedVerificationFailure = false let cancelVerificationRequest: (() => void) | null = null const cancelPendingRequest = () => { @@ -51,8 +52,30 @@ uni.navigateBack() } const isMerchantScopeFailure = (message: string): boolean => { return message.indexOf('当前商户') >= 0 || message.indexOf('本商户') >= 0 } const openVerificationFailure = () => { openedVerificationFailure = true uni.navigateTo({ url: '/pages/verification-failure/verification-failure', fail: () => { openedVerificationFailure = false scanState.value = 'error' statusTitle.value = '核销失败' statusMessage.value = '二维码已失效、不可核销或不属于当前商户' canRetryVerification.value = false } }) } const showVerificationError = (error: HttpError) => { cancelVerificationRequest = null if (isMerchantScopeFailure(error.message)) { openVerificationFailure() return } scanState.value = 'error' statusTitle.value = error.code == 'STORE_CONTEXT_MISSING' ? '门店信息缺失' : '核验失败' statusMessage.value = error.message + (error.traceId.length > 0 ? '\n追踪号:' + error.traceId : '') @@ -68,6 +91,10 @@ cancelVerificationRequest = scanVerificationCode(rawResult, (result: VerificationScanResult, traceId: string) => { cancelVerificationRequest = null if (!result.canVerify) { if (isMerchantScopeFailure(result.message)) { openVerificationFailure() return } scanState.value = 'error' statusTitle.value = result.title statusMessage.value = result.message + (traceId.length > 0 ? '\n追踪号:' + traceId : '') @@ -150,9 +177,10 @@ }) onShow(() => { if (!openedManualConsumption && !openedConfirmation) return if (!openedManualConsumption && !openedConfirmation && !openedVerificationFailure) return openedManualConsumption = false openedConfirmation = false openedVerificationFailure = false resetScanning() }) </script> rencaika_merchant/pages/verification-failure/verification-failure.uvue
New file @@ -0,0 +1,31 @@ <template> <view class="page"> <view class="navigation-bar"> <view class="back-button" @tap="goBack"><image class="back-icon" src="/static/icons/lucide/back-left.svg" mode="aspectFit"></image></view> </view> <view class="result-content"> <view class="failure-shell"><image class="failure-icon" src="/static/icons/lucide/x-orange.svg" mode="aspectFit"></image></view> <text class="result-title">核销失败</text> <text class="result-message">二维码已失效、不可核销或不属于当前商户</text> <view class="return-button" @tap="goBack"><text>返回</text></view> </view> </view> </template> <script setup lang="uts"> const goBack = () => uni.navigateBack({ delta: 1, fail: () => uni.reLaunch({ url: '/pages/index/index' }) }) </script> <style> page { background-color: #F6F5F2; } .page { display: flex; width: 100%; height: 100vh; flex-direction: column; background-color: #F6F5F2; } .navigation-bar { display: flex; height: 176rpx; flex: none; justify-content: center; align-items: flex-end; padding-bottom: 26rpx; box-sizing: border-box; background-color: #FFFFFF; position: relative; } .back-button { display: flex; width: 72rpx; height: 72rpx; justify-content: center; align-items: center; position: absolute; left: 24rpx; bottom: 14rpx; } .back-icon { width: 38rpx; height: 38rpx; } .result-content { display: flex; flex: 1; padding: 222rpx 64rpx 0; flex-direction: column; align-items: center; box-sizing: border-box; } .failure-shell { display: flex; width: 144rpx; height: 144rpx; justify-content: center; align-items: center; border-width: 12rpx; border-style: solid; border-color: #FFD7C8; border-radius: 78rpx; background-color: #FFE9DF; box-sizing: border-box; } .failure-icon { width: 76rpx; height: 76rpx; } .result-title { margin-top: 38rpx; color: #273345; font-size: 32rpx; line-height: 45rpx; font-weight: 700; } .result-message { width: 540rpx; margin-top: 12rpx; color: #9AA6B8; font-size: 24rpx; line-height: 36rpx; text-align: center; } .return-button { display: flex; width: 216rpx; height: 68rpx; margin-top: 34rpx; justify-content: center; align-items: center; border-width: 1rpx; border-style: solid; border-color: #FFCCAA; border-radius: 36rpx; color: #FF6B3C; font-size: 25rpx; line-height: 35rpx; font-weight: 600; background-color: #FFF4E8; } </style> rencaika_merchant/static/icons/lucide/x-orange.svg
New file @@ -0,0 +1,16 @@ <!-- @license lucide-static v0.468.0 - ISC --> <svg class="lucide lucide-x" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#FF6B2C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" > <path d="M18 6 6 18" /> <path d="m6 6 12 12" /> </svg> talent-card-admin/src/style.css
@@ -334,6 +334,15 @@ padding: 24px; } .admin-main > :not(.main-breadcrumb), .admin-main .page-stack, .admin-main .panel { box-sizing: border-box; width: 100%; min-width: 0; max-width: 100%; } .main-breadcrumb { display: flex; align-items: center; @@ -492,6 +501,15 @@ .merchant-create-page { display: grid; gap: 14px; } .merchant-create-page > *, .merchant-create-page .parking-address-table-wrapper, .merchant-create-page .coupon-product-select-table, .merchant-create-page .health-check-table-row > div { box-sizing: border-box; min-width: 0; max-width: 100%; } .merchant-create-tabs { @@ -4699,6 +4717,8 @@ .el-table { --el-table-header-bg-color: #f8fafc; --el-table-header-text-color: #374151; width: 100%; max-width: 100%; border-radius: 8px; } talent-card-admin/src/views/MerchantCreateView.vue
@@ -2488,7 +2488,7 @@ <el-button type="primary" size="small" @click="markParkingLocation(row)">{{ row.latitude ? '已标点' : '未标点' }}</el-button> </template> </el-table-column> <el-table-column label="操作" width="100" align="center"> <el-table-column label="操作" width="100" fixed="right" align="center"> <template #default="{ $index }"> <el-button link type="danger" @click="removeParkingAddress($index)">删除</el-button> </template> @@ -2537,7 +2537,7 @@ </el-checkbox-group> </template> </el-table-column> <el-table-column label="操作" min-width="160" align="center"> <el-table-column label="操作" width="160" fixed="right" align="center"> <template #default="{ $index, row }"> <el-button v-if="row.isNew || row.isEditing" link type="primary" @click="saveApprovalRow($index)">保存</el-button> <el-button v-else link type="primary" @click="editApprovalRow($index)">编辑</el-button> @@ -2558,7 +2558,7 @@ <el-table :data="schoolAddresses" border> <el-table-column label="名称" prop="name" /> <el-table-column label="地址" prop="address" /> <el-table-column label="操作" width="120" align="center"> <el-table-column label="操作" width="120" fixed="right" align="center"> <template #default="{ $index }"> <el-button link type="danger" @click="removeSchool($index)">删除</el-button> <el-button link type="primary" @click="openSchoolDialog($index)">详情</el-button> @@ -2637,7 +2637,7 @@ <el-table-column label="状态" min-width="90"> <template #default="{ row }">{{ productAuditStatusText(row.auditStatus) }}</template> </el-table-column> <el-table-column label="操作" width="164" align="center"> <el-table-column label="操作" width="164" fixed="right" align="center"> <template #default="{ row }"> <div class="merchant-products-operation"> <el-button link @click="editProduct(row)">编辑</el-button> @@ -2794,7 +2794,7 @@ </el-checkbox-group> </template> </el-table-column> <el-table-column label="操作" width="80" align="center"> <el-table-column label="操作" width="80" fixed="right" align="center"> <template #default="{ $index }"> <el-button link type="primary" @click="removeHealthCheckItem($index)">删除</el-button> </template> @@ -2977,7 +2977,7 @@ <el-table-column label="状态" min-width="90"> <template #default="{ row }">{{ couponStatusText(row) }}</template> </el-table-column> <el-table-column label="操作" width="164" align="center"> <el-table-column label="操作" width="164" fixed="right" align="center"> <template #default="{ row }"> <div class="merchant-coupons-operation"> <el-button link @click="editCoupon(row)">编辑</el-button> talent-card-admin/src/views/OrderDetailView.vue
@@ -152,7 +152,7 @@ <el-table-column label="商品金额" min-width="130" align="right"> <template #default="{ row }">{{ money(row.amount) }}</template> </el-table-column> <el-table-column label="操作" width="100" align="center"> <el-table-column label="操作" width="100" fixed="right" align="center"> <template #default="{ row }"> <el-button link type="primary" :disabled="!row.coverUrl" @click="previewImage(row.coverUrl)">查看</el-button> </template> talent-card-admin/src/views/ProductView.vue
@@ -201,7 +201,7 @@ <el-table-column label="状态" width="120" align="center"> <template #default="{ row }">{{ auditStatusText(row.auditStatus) }}</template> </el-table-column> <el-table-column label="操作" width="180" align="center"> <el-table-column label="操作" width="180" fixed="right" align="center"> <template #default="{ row }"> <el-button link type="primary" @click="viewDetail(row)">查看</el-button> </template> talent-card-admin/src/views/TalentArticleManagementView.vue
@@ -66,7 +66,7 @@ <el-table-column label="发布时间" width="170" align="center"><template #default="{ row }">{{ formatTime(row.publishTime) }}</template></el-table-column> <el-table-column prop="viewCount" label="观看数" width="100" align="center" /> <el-table-column label="状态" width="100" align="center"><template #default="{ row }">{{ statusText(row.status) }}</template></el-table-column> <el-table-column label="操作" width="180" align="center"><template #default="{ row }"><el-button link type="primary" @click="toggleStatus(row)">{{ Number(row.status) === 1 ? '禁用' : '启用' }}</el-button><el-button link type="primary" @click="editArticle(row)">编辑</el-button></template></el-table-column> <el-table-column label="操作" width="180" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click="toggleStatus(row)">{{ Number(row.status) === 1 ? '禁用' : '启用' }}</el-button><el-button link type="primary" @click="editArticle(row)">编辑</el-button></template></el-table-column> </el-table> <div class="article-management-pagination"><el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size" background layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" :total="pagination.total" @change="load" /></div> </section>