feat(h5): 身份核验页(门禁)—— 学生流程首个可用闭环
学生填材料之前的唯一一道门:姓名 + 身份证号与老师提交的名单核对, 通过后把微信账号与名单里那一行钉死。后面所有材料都归属那个人。 交付: · 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), 是我为验证留的演示数据。
This commit is contained in:
+26
-11
@@ -125,29 +125,44 @@ public class MpH5AuthController {
|
||||
/**
|
||||
* 302 回 H5 页面。token 放 <b>fragment</b>(不发给服务器),错误信息放 query。
|
||||
*/
|
||||
/**
|
||||
* 302 回 H5 页面。
|
||||
*
|
||||
* <p>token 放在 <b>hash 路由的 query</b> 里,形如
|
||||
* {@code http://host/#/pages/index/index?token=X&scene=Y}。这个格式同时满足两件事:
|
||||
* <ul>
|
||||
* <li><b>安全</b>:整段在 {@code #} 之后,<b>不会发给服务器</b> ——
|
||||
* 不进 Nginx access log、不进 Referer。</li>
|
||||
* <li><b>能用</b>:uni-app H5 的 {@code onLoad} 解析的是<b>路由</b>的 query。
|
||||
* 实测写成裸 hash({@code #token=X&scene=Y})时,它既不是外层查询串、
|
||||
* 也不是合法的路由路径 —— 两边都不认,页面读不到任何参数,
|
||||
* 表现是"授权明明成功了,落地页却说 scene 没获取到"。</li>
|
||||
* </ul>
|
||||
* 换句话说:<b>参数要么放外层 query(会进服务器日志),要么放 hash 路由的 query</b>,
|
||||
* 别放裸 hash。
|
||||
*/
|
||||
private ResponseEntity<Void> 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) {
|
||||
|
||||
Reference in New Issue
Block a user