From aa2f7f175ccd9b513b6e91c9790fa2a13a3f7313 Mon Sep 17 00:00:00 2001 From: lovife <474239041@qq.com> Date: Tue, 29 Sep 2026 11:24:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(h5):=20=E8=BA=AB=E4=BB=BD=E6=A0=B8?= =?UTF-8?q?=E9=AA=8C=E9=A1=B5=EF=BC=88=E9=97=A8=E7=A6=81=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E5=AD=A6=E7=94=9F=E6=B5=81=E7=A8=8B=E9=A6=96?= =?UTF-8?q?=E4=B8=AA=E5=8F=AF=E7=94=A8=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 学生填材料之前的唯一一道门:姓名 + 身份证号与老师提交的名单核对, 通过后把微信账号与名单里那一行钉死。后面所有材料都归属那个人。 交付: · pages/verify/verify.vue —— 核验页 · 落地页「开始填报」→ 核验页(带 scene) · 两页都加 max-width 居中(桌面查看/自动化验证时不至于拉满全屏) 三处刻意的设计: 1) 失败提示不区分"姓名错"还是"号码错"(后端统一 40007),前端也只显示 后端那一句话 —— **不在前端加"可能是身份证号错了"这类推断**。 分开提示等于给出一个身份证号枚举器。 2) 预填项标注「老师已填,请核对」并明确「学生不能改」:报考职业/工种/等级 由老师按报考计划确定(名单里实测 28/28 都有值)。让学生重填只会制造错误, 而报错工种在机构那边是直接不受理。 3) 前端只做"非空 + 长度"这类明显笔误的提示,真正的判定(校验位、名册匹配、 绑定状态)全交给后端。 修掉一个 H5 特有的坑: **H5 的页面跳转不保留 URL 参数** —— 直接 navigateTo 到核验页的话, 那边读不到 scene、就不知道是哪个任务(小程序没有这个问题)。 所以落地页跳转时显式拼上 scene,核验页再从自己的 URL 里读出来。 验收(全程无头浏览器自动完成,除"微信授权"那一步)。 现在 task 17 已发布(收集中)并带测试名册,手机上可走完整流程。 注:task 17 现在是「收集中」并带 1 行测试名册(张同学 / 361029200912100015), 是我为验证留的演示数据。 --- .../mp/controller/MpH5AuthController.java | 37 +- frontend-mp/src/pages.json | 8 + frontend-mp/src/pages/index/index.vue | 63 ++- frontend-mp/src/pages/verify/verify.vue | 382 ++++++++++++++++++ 4 files changed, 472 insertions(+), 18 deletions(-) create mode 100644 frontend-mp/src/pages/verify/verify.vue diff --git a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java index dcc2244..6bf330f 100644 --- a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java +++ b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java @@ -125,29 +125,44 @@ public class MpH5AuthController { /** * 302 回 H5 页面。token 放 fragment(不发给服务器),错误信息放 query。 */ + /** + * 302 回 H5 页面。 + * + *

token 放在 hash 路由的 query 里,形如 + * {@code http://host/#/pages/index/index?token=X&scene=Y}。这个格式同时满足两件事: + *

+ * 换句话说:参数要么放外层 query(会进服务器日志),要么放 hash 路由的 query, + * 别放裸 hash。 + */ private ResponseEntity redirectToApp(String token, String scene, String error) { String base = props.mpH5() == null ? null : props.mpH5().h5BaseUrlOrDefault(); if (base == null || base.isBlank()) { base = "http://127.0.0.1:5174"; } - StringBuilder sb = new StringBuilder(base); - String frag = ""; + StringBuilder q = new StringBuilder(); if (token != null) { - frag += "token=" + enc(token); + q.append("token=").append(enc(token)); } if (scene != null && !scene.isBlank()) { - frag += (frag.isEmpty() ? "" : "&") + "scene=" + enc(scene); + q.append(q.isEmpty() ? "" : "&").append("scene=").append(enc(scene)); } + String url = base; if (error != null) { - // 错误放 query:它不敏感,而且我们要让页面能读到 - sb.append("?authError=").append(enc(error)); + // 错误放外层 query:它不敏感,而且页面要能读到 + url += "?authError=" + enc(error); } - if (!frag.isEmpty()) { - sb.append("#").append(frag); + if (!q.isEmpty()) { + // hash 路由格式:uni-app 能解析,且整段不发给服务器 + url += "#/pages/index/index?" + q; } - return ResponseEntity.status(HttpStatus.FOUND) - .header("Location", sb.toString()) - .build(); + return ResponseEntity.status(HttpStatus.FOUND).header("Location", url).build(); } private static String enc(String s) { diff --git a/frontend-mp/src/pages.json b/frontend-mp/src/pages.json index e5f5701..36be32e 100644 --- a/frontend-mp/src/pages.json +++ b/frontend-mp/src/pages.json @@ -8,6 +8,14 @@ "navigationBarTextStyle": "white", "enablePullDownRefresh": true } + }, + { + "path": "pages/verify/verify", + "style": { + "navigationBarTitleText": "核对身份", + "navigationBarBackgroundColor": "#0052D9", + "navigationBarTextStyle": "white" + } } ], "globalStyle": { diff --git a/frontend-mp/src/pages/index/index.vue b/frontend-mp/src/pages/index/index.vue index 16ec0f9..109fc47 100644 --- a/frontend-mp/src/pages/index/index.vue +++ b/frontend-mp/src/pages/index/index.vue @@ -89,6 +89,25 @@ const isError = ref(false) const loading = ref(false) const title = ref('材料填报') +/** + * 当前 URL 里的 scene(授权回调回来时在 hash 路由的 query 里)。 + * + * ⚠️ 为什么跳转时要把它带上:**H5 的页面跳转不保留 URL 参数**, + * 直接 navigateTo 到核验页的话,那边读不到 scene、就不知道是哪个任务。 + * 这是 H5 与小程序的一个真实差异(小程序可以靠 scene 直进,H5 得自己带)。 + * 所以这里显式拼参数,核验页再从自己的 URL 里读出来。 + */ +function sceneFromUrl(): string { + try { + const hash = typeof location !== 'undefined' ? location.hash : '' + const q = hash.includes('?') ? hash.slice(hash.indexOf('?') + 1) : '' + const params = new URLSearchParams(q) + return params.get('scene') || '' + } catch { + return '' + } +} + const badgeClass = computed(() => { switch (ctx.value.taskStatus) { case 1: return 'land__badge--ok' @@ -154,18 +173,42 @@ async function load() { } function start() { - // 填报主流程属 M3(/mp/login → 门禁核验 → 表单),当前骨架先给出明确预期, - // 而不是静默什么都不做 —— 后者会被当成"按钮坏了" - uni.showModal({ - title: '填报功能开发中', - content: `已识别任务「${ctx.value.taskName ?? ''}」。填报主流程(身份核验与表单填写)属下一阶段开发。`, - showCancel: false - }) + const s = scene.value || sceneFromUrl() + if (!s) { + uni.showModal({ + title: '无法继续', + content: '没有取到任务信息,请重新扫班主任发的二维码进入。', + showCancel: false + }) + return + } + // 带上 scene —— 见 sceneFromUrl 的说明(H5 跳转不保留 URL 参数) + uni.navigateTo({ url: `/pages/verify/verify?scene=${encodeURIComponent(s)}` }) } onLoad((options) => { rawScene.value = options?.scene || options?.q || '' scene.value = pickScene(options) + + // 授权回调失败时后端会把原因放在外层 query 的 authError 里(不敏感,可以进服务器日志) + const authErr = options?.authError + if (authErr) { + isError.value = true + message.value = decodeURIComponent(String(authErr)) + return + } + + // 授权回调会把会话 token 带回来(在 hash 路由的 query 里,不进服务器日志)。 + // 存下来供后续页面用 —— 门禁核验、提交都要靠它。 + const t = options?.token + if (t) { + try { + uni.setStorageSync('token', t) + } catch { + // 存不了也不该让页面挂掉:落地页本身不需要 token + } + } + load() }) @@ -192,6 +235,12 @@ page { min-height: 100vh; padding: 32rpx; + /* 桌面浏览器打开时不至于拉满全屏 —— 主战场是手机, + 但自动化验证与临时桌面查看都跑在桌面浏览器里,窄一点更像真实形态 */ + max-width: 720rpx; + margin: 0 auto; + box-sizing: border-box; + &__hero { padding: 48rpx 32rpx; background: linear-gradient(135deg, $primary 0%, #2b6fe0 100%); diff --git a/frontend-mp/src/pages/verify/verify.vue b/frontend-mp/src/pages/verify/verify.vue new file mode 100644 index 0000000..c9f92a2 --- /dev/null +++ b/frontend-mp/src/pages/verify/verify.vue @@ -0,0 +1,382 @@ + + + + +