feat(qrcode): M1-06 填报二维码(接入真实微信小程序)
学生端的入口终于通了:班主任把码发到班级群,学生扫码进填报页。
这条 E2E 是本仓库唯一打外网的测试 —— 真实调微信生成小程序码并取回图片。
交付:
· GET /tasks/{id}/qrcode?type=MINI[&orgId] (§7.12)
· GET /qrcodes/{scene} 匿名图片接口(<img src> 带不了 Authorization 头,
必须匿名。图里只有一串场景值,不含学生信息)
· 微信小程序凭据接入 + access_token 与二维码的 Redis 缓存
微信账号(已实测确认,不是靠猜):
· wxf01d3fa60f87f0b7 是【小程序】:小程序码可生成、小程序订阅消息可用、
公众号的用户管理接口返回 48001(交叉验证)
· wx5d3ae5c1c21fef24 是【公众号测试号】:做不了小程序码,但能做网页授权
+ JS-SDK —— 正好是「H5 保底」需要的能力。两条路都有号了
三处刻意的设计:
1) access_token 走 Redis 缓存(TTL 7000 秒,比官方 7200 少 200 避开边界)。
token 获取接口有日调用限额(约 2000 次),每次生成码都取一次,
几十个班一起用就会撞限额 —— 表现是"早上还能用,下午全班的码都生成不出来"。
2) 同一个 scene 的图缓存 30 天:图是一样的,而生成接口本身也有日限额,
列表页每渲染一次就可能请求一次。
3) 微信 errcode 翻译成人话:40164(IP 不在白名单)与 40001(appsecret 错)
是完全不同的排查方向,只给一个数字等于让运维从错的方向查起。
匿名图片接口**只读缓存、绝不现场调微信**:否则任何人都能用构造出来的 scene
刷爆当天额度,而额度用完是全租户的码都生成不出来 —— 等于整个填报入口瘫痪。
开发期 env-version 设成 develop(配置项 WX_MP_ENV_VERSION)。这个小程序若尚未
发布,用 release 生成的码扫开提示「小程序未发布」,而接口不会报错 ——
只会让人以为"码生成得不对"。
本轮踩的 3 个坑,都是"假设错了但不报错":
1) 微信返回的是 JPEG 不是 PNG。探测脚本按 PNG 魔数判断,把一次成功报成了失败
(接口 200、91KB、看着一切正常)。服务代码里按 Content-Type/是不是图片判断。
2) 业务错误返回 HTTP 200 —— 项目约定只有 401/403/429/5xxxx 才占用真 HTTP 状态码,
业务码在响应体的 code 里(ErrorCode.httpStatus())。我的 E2E 按 HTTP 状态码判
业务失败,于是把"业务失败"当成了"成功"。
3) 我写的断言又一次恒为真:check(..., "max-age" in (...) or True, "")。
这与上一轮刚记进摘要的"断言要能真的失败"是同一类错 —— 说明那条教训我只是
写下来了,没有真的在写断言时自查。已改成真的读响应头来判。
另外 fetch_* 辅助函数成功时返回 err=None,而断言在成功路径上写 err[:120],
把"本该通过的用例"崩掉了;已改成成功时返回空串。
⚠️ 凭据位置与安全:
app-secret 放在 application-dev.yml(会入库),用 ${WX_MP_APP_SECRET:...} 形式,
test/prod 必须用环境变量覆盖。它与 jwt/crypto/rsa 那几把自研密钥性质不同:
那几个泄露只影响本系统,appsecret 泄露等于别人能以这个小程序身份调用所有接口。
生产环境务必单独申请凭据 + 走环境变量。
已知缺口:
· shortUrl 恒为 null:短链需要短链服务,而「H5 保底」路线还没定。
不返回一个"其实很长"的链接冒充短链 —— 字段叫 shortUrl 却给 80 个字符,
会让调用方以为可以直接打印。
· 图存在 Redis,没走对象存储(后端还没接 MinIO,容器在跑代码没接)。
· 小程序正式使用还需两步人工操作:服务器域名白名单加 API 域名、确认小程序已发布。
· 前端还没有二维码入口(属 M1-07~M1-10)。
验收:e2e-m1-qrcode.py 32/32(真实调微信);七条 E2E 合计 358 项断言全通过。
This commit is contained in:
+58
-1
@@ -51,8 +51,9 @@
|
||||
| **M1-02 覆盖班级与班主任** | ✅ `GET/PUT /tasks/{id}/orgs`(整体替换 + 软移除 + 历史行复用)+ 前端「配置班级」抽屉(勾选班级、主/副班主任、应填人数);**`e2e-m1-orgs.py` → 48/48** |
|
||||
| **M1-04 材料项定义** | ✅ 材料项 CRUD(§7.10)+ 三个内置模板(`TPL_SCHOOL_ROLL`/`TPL_SOCIAL`/`TPL_ZHUANXIANG`)+ 材料目录接口;**`e2e-m1-materials.py` → 68/68** |
|
||||
| **M1-03 表单 schema 版本化** | ✅ `GET/PUT /tasks/{id}/form-schema`(版本只增不改 + `baseVersion` 乐观锁 40025 + 内置字段不可删 40026)+ 官方 21 项内置字段目录;**`e2e-m1-form.py` → 48/48** |
|
||||
| **M1-06 填报二维码** | ✅ `GET /tasks/{id}/qrcode`(真调微信生成小程序码)+ 匿名图片接口 `GET /qrcodes/{scene}`;**`e2e-m1-qrcode.py` → 32/32(唯一打外网的 E2E)** |
|
||||
| PC 前端 | ✅ 已换 Gi Admin Pro + 腾讯云主题;已实现:登录 / 工作台 / 组织架构 / 租户管理 / 账号管理 / **收集任务(含配置班级)** |
|
||||
| 待做 | M1 剩余(M1-05 规则模板、M1-06 二维码、**M1-07~M1-10 前端**——表单设计器 M1-07、材料规则界面 M1-09);小程序(uni-app)未创建;CI/CD;H5 保底预研 |
|
||||
| 待做 | M1 剩余(M1-05 规则模板 + **M1-07~M1-10 前端**——表单设计器 M1-07、材料规则界面 M1-09);小程序(uni-app)未创建;CI/CD |
|
||||
|
||||
---
|
||||
|
||||
@@ -323,6 +324,62 @@ Flyway 迁移 1~6 全部 success。
|
||||
|
||||
---
|
||||
|
||||
## 一·九、M1-06 填报二维码(2026-09-29 上午,未提交)
|
||||
|
||||
> 范围:`GET /tasks/{id}/qrcode`(§7.12)+ 匿名图片 `GET /qrcodes/{scene}`、
|
||||
> 微信小程序凭据接入、access_token 与二维码的 Redis 缓存。
|
||||
|
||||
**验收**:`python scripts/e2e-m1-qrcode.py` → **32/32**;
|
||||
**这是本仓库唯一一条打外网的 E2E**(真实调微信生成小程序码)。
|
||||
七条 E2E 合计 **358 项断言**全通过(36 + 55 + 71 + 48 + 68 + 48 + 32)。
|
||||
|
||||
### 微信账号与凭据
|
||||
|
||||
| 项 | 值 / 位置 |
|
||||
|---|---|
|
||||
| 小程序 appid | `wxf01d3fa60f87f0b7`(**已实测确认是小程序**:小程序码可生成、小程序订阅消息可用、公众号的用户管理接口返回 48001) |
|
||||
| 凭据位置 | `application-dev.yml` 的 `fill.mini-program`,用 `${WX_MP_APP_SECRET:...}` 形式,**test/prod 必须用环境变量覆盖** |
|
||||
| ⚠️ 安全 | appsecret 与上述几把自研密钥**性质不同**:jwt/crypto/rsa 泄露只影响本系统,appsecret 泄露等于别人能以这个小程序身份调用所有接口。**生产环境务必单独申请凭据 + 走环境变量** |
|
||||
| 另一个号 | `wx5d3ae5c1c21fef24` 是**公众号测试号**(实测确认),做不了小程序码,但**能做网页授权 + JS-SDK** → 正好是「H5 保底」需要的能力。两条路都有了 |
|
||||
|
||||
**开发期 `env-version` 设为 `develop`**(配置项 `WX_MP_ENV_VERSION`)。原因写在配置注释里:
|
||||
这个小程序若尚未发布,用 `release` 生成的码扫开会提示「小程序未发布」,而**接口不会报错** ——
|
||||
只会让人以为"码生成得不对",白查半天。
|
||||
|
||||
### 三处刻意的设计
|
||||
|
||||
| # | 设计 | 为什么 |
|
||||
|:--:|---|---|
|
||||
| 1 | **access_token 走 Redis 缓存(TTL 7000 秒)** | token 7200 秒有效,而**获取接口有日调用限额**(约 2000 次)。每次生成码都取一次 token,几十个班一起用就会撞限额,表现是"早上还能用,下午全班的码都生成不出来"。7000 而非 7200 是为了避开"刚取到就过期"的边界 |
|
||||
| 2 | **同一个 scene 的图缓存 30 天** | 图是一样的,而生成接口本身也有日限额;列表页每渲染一次就可能请求一次 |
|
||||
| 3 | **微信 errcode 翻译成人话** | 原始返回只有一个数字,而 `40164`(IP 不在白名单)与 `40001`(appsecret 错)是完全不同的排查方向。不翻译的话运维看到的是"生成二维码失败" |
|
||||
|
||||
**匿名图片接口只读缓存、绝不现场调微信**:否则任何人都能用构造出来的 scene 刷爆当天的
|
||||
调用额度,而额度用完是**全租户的码都生成不出来** —— 等于整个填报入口瘫痪。
|
||||
|
||||
### 本轮踩的 3 个坑(都是"假设错了但不报错")
|
||||
|
||||
| # | 坑 | 说明 |
|
||||
|:--:|---|---|
|
||||
| 1 | **微信返回的是 JPEG,不是 PNG** | 探测时我按 PNG 魔数判断,把一次成功报成了失败(接口 200、91KB 数据、看着一切正常)。**这个坑在服务代码里也留了注释**:按 Content-Type 或"是不是图片"来判断,别按某一种格式的魔数 |
|
||||
| 2 | **业务错误返回 HTTP 200** | 项目约定是"只有 401/403/429/5xxxx 才占用真 HTTP 状态码",业务错误码在响应体的 `code` 里(见 `ErrorCode.httpStatus()`)。我的 E2E 按"HTTP 状态码 != 200"判断业务失败,于是把"业务失败"当成了"成功" |
|
||||
| 3 | **我写的断言又一次恒为真** | `check("...", "max-age" in (...) or True, "")` —— `or True` 让它永远通过。**这与上一轮记进摘要的"断言要能真的失败"是同一类错**,说明那条教训我只是写下来了、没有真的在写断言时自查。已修,并把"必带 Cache-Control"改成真的读响应头来判 |
|
||||
|
||||
> 另外还修了一处自己给自己挖的坑:`fetch_*` 辅助函数成功时返回 `err=None`,
|
||||
> 而我的断言在**成功路径**上写 `err[:120]`,于是"本该通过的用例"把脚本崩掉了。
|
||||
> 已改成成功时返回空串。
|
||||
|
||||
### 已知缺口
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| **`shortUrl` 恒为 `null`** | 短链需要一个短链服务(发号 + 跳转表 + 域名),而「H5 保底」这条路线本身还没定。**不返回一个"其实很长"的链接冒充短链** —— 字段叫 `shortUrl` 却给 80 个字符,会让调用方以为可以直接打印 |
|
||||
| 二维码图存在 Redis,没走对象存储 | 后端**还没接 MinIO**(容器在跑,代码没接)。当前由本服务的匿名接口提供图片。接入对象存储后可以把这里换成返回 OSS 地址 |
|
||||
| 小程序正式使用还需两步**人工操作** | ① 服务器域名白名单加我们的 API 域名(否则小程序里请求不通);② 确认小程序已发布(扫 `release` 的码验证),开发期用 `develop` |
|
||||
| 前端没有二维码入口 | 属 M1-07~M1-10 的前端范围。当前可通过接口取码 |
|
||||
|
||||
---
|
||||
|
||||
**关键实现决策(不要在后继开发中改回去)**
|
||||
|
||||
| 决策 | 理由 |
|
||||
|
||||
Reference in New Issue
Block a user