Commit Graph

5 Commits

Author SHA1 Message Date
lovife 8ef09733b1 feat(file): 材料上传的前端界面 + 列出/删除接口
后端:GET /mp/files/submissions/{id}(列出我的材料)+ DELETE /mp/files/{id}(软删)。
  两者都把归属条件(submitter_id)写进 SQL 的 WHERE —— 与门禁、草稿同一套写法,
  少一个条件就是"能看/能删别人的材料"。

前端(填报页的材料区):
  · 按材料项列出:名称、必传标记、格式白名单与体积上限、来源引导语
  · 已传的显示「原名 → 规范名」与「压缩前 → 压缩后」体积(学生要能看见系统做了什么)
  · 传完**立刻处理**并显示结果

  ⚠️ 为什么上传后马上处理而不是留到提交时:学生要能看到"压到了多大、叫什么名字"
     才算完成。若把处理留到提交时,他上传时会以为"传上去了",提交时才发现体积超标 ——
     而那时他可能已经在回家的路上。

  · 学生能自己删(传错了不该只能找班主任)。已提交的填报单不给增删,与草稿同口径。

⚠️ 本轮**未完成浏览器肉眼验证**:构建产物里确实有材料区的代码
   (产物 grep 到「需要上传的材料」),但浏览器加载的是**缓存的旧 chunk**
   (实测 .fill__wrap 只有 4 个子元素,没有材料卡),我用带时间戳的方式绕缓存没成功。
   代码与产物是对的,需要一次硬刷新(Ctrl+Shift+R)或清缓存后再确认。
   这条记下来,不要当成"已验证"。
2026-09-29 11:59:58 +08:00
lovife 465700969a feat(h5): 填报页(三态 + 条件级联 + 失焦自动存草稿)
学生终于能在手机上真正填一份表了。

三态渲染(契约 §9.2):
  locked=true                → 只读灰底 + 「如有误请联系班主任」
  prefilled=true,locked=false → 可编辑 + 橙字「老师已填,请核对」
  其余                        → 普通空白
实测:姓名/身份证号/联系电话/报考职业/工种/等级 只读,学历可改并标注来源。

条件级联(visibleWhen)在前端求值,实测正确:
  「是否在校生=是」→ 在读学校/所学专业/入学时间 出现;
  「是否补考」未填 → 补考三科不出现。

**隐藏 ≠ 清空**:学生把「是否补考」改回"否",已填的补考科目仍留在草稿里
(万一他改回来)。要不要清空由后端在提交时判断 —— 前端不该替用户做不可逆的事。

保存策略:**失焦即存单个字段**。不做"整表提交"——学生填到一半切出去接电话,
回来整表丢失是最让人恼火的失败。单字段保存的代价是一次请求,
远小于"白填一遍"。
进页面时若还没有草稿会先建一份(否则第一次失焦保存会因 submissionId 为空而静默不存)。

前端拦住只读字段的保存请求(能省一次必然失败的请求),但**服务端那道拒绝才是真的**。

顺带:核验页的「开始填写材料」接上了(带 taskId + scene —— H5 跳转不保留 URL 参数)。

验收:构建产物含 pages-fill-fill;用无头浏览器实测渲染,
三态与两处级联均正确,抬头显示「请在 2026-10-15 前提交」。
2026-09-29 11:43:45 +08:00
lovife aa2f7f175c 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),
   是我为验证留的演示数据。
2026-09-29 11:24:32 +08:00
lovife 2659cd5a8c feat(h5): H5 交给后端托管 + 修掉"相对地址在 uni.request 下失败"
背景:配好了花生壳穿透 http://394nb68658le.vicp.fun → 本机 8080。
但 H5 原来跑在 127.0.0.1:5174,手机访问不到 —— 所以把 H5 静态文件
交给后端一起服务(同一个域名)。这样顺带也不再需要 CORS,
而且正是正式部署的形态。

改动:
· application-dev.yml 加 spring.web.resources.static-locations,
  默认指向 frontend-mp/dist/build/h5/,可用 H5_STATIC_DIR 覆盖。
  ⚠️ 前缀必须是 spring.(我一开始写成了 fill.,表现是"页面 404 而应用
  照常启动、没有任何报错")。
· fill.mp-h5.callback-url / h5-base-url 改成穿透域名。
· config/index.ts:API 地址按端区分(api 地址两端策略不同,写死一个值
  一定会在某一边出错):
    H5     → location.origin(绝对同源)
    小程序 → http://localhost:8080(没有"同域"概念,必须绝对地址)
  用 uni-app 条件编译区分,而不是运行期判断(后者会把两套地址都打进包里)。

⚠️ 修掉一个只在真实穿透下才会暴露的 bug:
· 原来 H5 硬编码 http://localhost:8080。从公网域名打开时变成跨域请求 → 被 CORS 拦
  → 页面显示"网络请求失败",而**从本机打开却是好的**。这个 bug 只有走真实
  穿透/真机才会暴露 —— 这正是"要在真环境验证"的价值。
· 改成相对路径后**仍然失败**(request:fail),但同样地址用原生 XHR / fetch
  都是 200。也就是说这是 uni.request 自身对相对路径的处理差异,不是服务端
  或跨域问题 —— 而它长得像"网络不通",很容易往 CORS / 域名 / 后端去查,白绕一大圈。
  最终用 location.origin(绝对同源)绕开。

验证:经公网 http://394nb68658le.vicp.fun/?scene=t1-k17
→ 显示「示范职业学校 / 2026年9月机电专业认定材料收集 / 尚未发布」,
无头浏览器截图确认(不依赖人工)。
※ 首次加载约 27 秒 —— 穿透的带宽限制(花生壳免费档),不是代码问题。
2026-09-29 11:11:49 +08:00
lovife 0bc7ee02dc feat(mp): 小程序端骨架 + 扫码落地页(scene 识别租户)
小程序端从零建起来了,并在微信开发者工具里真机模拟跑通:
扫 scene=t1-k17 → 显示「示范职业学校 / 2026年9月机电专业认定材料收集 / 尚未发布」。

交付:
· frontend-mp/(uni-app,官方 uni-preset-vue#vite-ts 模板),appid 已填
· src/pages/index/index.vue 扫码落地页:读 scene → 调后端 → 显示
  「哪个学校 / 填什么 / 现在能不能填」
· GET /api/v1/mp/scene-context(匿名):登录前的落地页查询
· 主题色与 PC 管理端同一套(腾讯云 TDesign 色板),两端视觉一致

为什么落地页必然是匿名接口:
学生扫开的第一屏要回答"这是哪个学校、要填什么、能不能填",
而这发生在绑定身份之前。它与契约里的 POST /mp/login(code+scene 换 session)
不是一回事 —— 那个要换 openid,属 M3。

⚠️ 一个必踩的坑(撞上并绕开了):
FillTenantLineHandler.getTenantId() 在**匿名请求下直接抛 40101**,
所以不能用 taskMapper.selectById()。正确写法是专用 Mapper + 每条查询
@InterceptorIgnore(tenantLine="true") 并显式带 tenant_id(项目注释称此为
一次代码审查点)。见 module/mp/mapper/MpSceneMapper.java —— 把这几条集中在
一个文件里,正是为了让"关掉框架保护后由人保证隔离"这件事有地方审。

⚠️ scene 可穷举(t1-k1、t1-k2…),所以匿名接口:
① 只返回落地页必须的最少信息,不往里加"顺带能查出来"的东西;
② 按 IP 限流(20 次/分钟,Redis INCR,复用项目的 GATE_RATE_LIMITED 40071)。
E2E 专门验了"前 20 次通过、第 21 次起被限、清掉计数器立刻恢复"。

状态文案分五种而不是合并成"当前不可填报":学生的下一步动作完全不同 ——
「已截止」要找班主任、「已暂停」要等通知、「尚未发布」是根本不该现在扫。

顺带记两个环境坑(写进交接摘要):
· 微信开发者工具的「服务端口」默认关闭,HBuilderX 运行时报
  `IDE service port disabled` 然后退出,**调不起开发者工具**。
  解法:开发者工具 → 设置 → 安全设置 → 服务端口 → 开启;
  或直接「导入项目」dist/dev/mp-weixin 绕过。
· HBuilderX 把这个项目当 CLI 型项目,产物在 dist/dev/mp-weixin,
  **不是** unpackage/。模拟扫码要在「编译模式」里设 进入场景=扫描二维码、
  启动参数 scene=t1-k17。
· <style lang="scss"> 需要装 sass(模板没带),已 pnpm add -D sass。

⚠️ 对上线计划最重要的一条结论(已写进交接摘要):
开发版(develop)只有开发者能扫、且会过期;体验版(trial)只有体验成员能扫;
**只有正式版(release)学生才能扫**,而它需要提交审核,涉及小程序类目与资质。
所以 C1「小程序资质法务确认」不是"以后再说",它直接决定学生能不能开始填报。

已知缺口:
· 填报主流程(/mp/login 门禁核验 表单填写)属 M3;
  现在点「开始填报」给的是明确的"开发中"提示,不是静默无反应。
· frontend-mp 还没套项目既有的 ESLint/Stylelint 规范。

验收:e2e-m1-scene.py 31/31;八条 E2E 合计 389 项断言全通过;
微信开发者工具里人工确认页面渲染与 scene 传递正确。
2026-09-29 10:29:14 +08:00