docs: 部署与运维手册 + 用户手册与客服话术
docs/09:环境与发布、密钥清单、迁移与回滚、排障、首次接入 CI 的检查单、本地能验到哪一步、 压测记录(含未测项)。 docs/10:学生/班主任/管理员三份操作手册 + 高频问题标准回答 + 30 分钟培训提纲 + 上线首日检查单;文案与错误码逐条取自代码并回核(抓到两处与实现不符), 核对固化成了 check-manual.py 并进 CI。
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
# 09 · 部署与运维
|
||||
|
||||
> 对应里程碑 **M0-04(工程化)** 与 **M7-10(灰度发布 + 生产环境准备 + 数据迁移预案)**。
|
||||
> 这份文档回答四个问题:**怎么发**、**密钥怎么办**、**升级与回滚怎么做**、**出事看哪里**。
|
||||
|
||||
---
|
||||
|
||||
## §1 四套环境
|
||||
|
||||
| 环境 | profile | 配置来源 | 数据库 | 存储 | 用途 |
|
||||
|---|---|---|---|---|---|
|
||||
| 本地 | `dev` | `application-dev.yml`(**含 dev 密钥,已提交**) | `xt-pg` 容器 | `local` | 开发与 E2E |
|
||||
| 集成 | `test` | `application-test.yml`(**无密钥**,全走环境变量) | CI 临时容器 / 联调库 | `local` 或 `qiniu` | CI 与联调 |
|
||||
| 预发 | `prod` + 灰度开关 | `application-prod.yml` | 独立库 | `qiniu` | 客户验收 |
|
||||
| 生产 | `prod` | `application-prod.yml` | 独立库 | `qiniu` | 正式 |
|
||||
|
||||
**⚠️ dev 与 test/prod 的密钥不能混用。** `dev` 的 JWT / AES 密钥在仓库里能查到,
|
||||
拿它能伪造任意用户的 token(含平台运营)。这件事由 `ProdEnvCheck` 在启动时拦下 ——
|
||||
见 §3.4。
|
||||
|
||||
---
|
||||
|
||||
## §2 怎么发
|
||||
|
||||
### 2.1 本地
|
||||
|
||||
```bash
|
||||
cd deploy && docker compose up -d # xt-pg(5432) + xt-redis(16379)
|
||||
cd backend
|
||||
mvn -DskipTests package
|
||||
java -jar xt-server/target/xt-server-1.0.0-SNAPSHOT.jar --spring.profiles.active=dev
|
||||
```
|
||||
|
||||
- ⚠️ **工作目录必须是 `backend/`**:两处相对路径依赖它 ——
|
||||
`application-local.yml`(本地私有配置,不提交)在这里;
|
||||
H5 静态目录配的是 `file:../frontend-mp/dist/build/h5/`
|
||||
- 跑的是 **jar 不是热加载**:改后端要重新 package(先停进程,否则 jar 被占用)
|
||||
- H5 没有独立 dev server:`cd frontend-mp && pnpm build:h5`,然后访问
|
||||
`http://localhost:8080`(由后端托管)
|
||||
|
||||
### 2.2 CI(GitLab)
|
||||
|
||||
`.gitlab-ci.yml` 三个阶段:
|
||||
|
||||
| 阶段 | 做什么 | 为什么这样切 |
|
||||
|---|---|---|
|
||||
| `check` | 契约对账、实体列对账、文档一致性、**e2e 脚本语法扫描** | 都不需要数据库,几十秒出结果,先在最快的阶段挡掉低级错误 |
|
||||
| `build` | 后端打包、管理端 typecheck/build、**H5 build** | H5 产物是后面 e2e 的输入,不能跳过 |
|
||||
| `e2e` | 起 PG+Redis 容器 → 起后端(`test` profile)→ **依次跑全部 `scripts/e2e-*.py`** | 这个仓库**没有单元测试**,E2E 就是主验证手段;一个只跑编译的流水线在这里等于没跑 |
|
||||
|
||||
另外单独一个 job `生产环境自检` 跑 `scripts/e2e-prodguard.py` ——
|
||||
它验的是"**配错时拒绝启动**",而这恰好是最容易被后来的改动悄悄破坏、
|
||||
且破坏后**一切看起来都正常**的一件事。
|
||||
|
||||
**密钥策略:e2e 阶段每次随机生成密钥,不存任何 CI 变量。**
|
||||
好处不只是省事 —— 它顺带证明**应用没有偷偷依赖某个 dev 默认值**(依赖了这里就红)。
|
||||
|
||||
### 2.3 生产发布
|
||||
|
||||
```bash
|
||||
# ① 构建(CI 里做,产物是 xt-server.jar)
|
||||
mvn -DskipTests package
|
||||
|
||||
# ② 先把迁移在一个只读副本上试跑(见 §4.2)
|
||||
java -jar xt-server.jar --spring.profiles.active=prod \
|
||||
--spring.flyway.enabled=true --spring.main.web-application-type=none
|
||||
|
||||
# ③ 滚动发布:先起新实例、探活通过、再摘旧实例
|
||||
curl -sf http://127.0.0.1:8080/actuator/health # 探活用这个,不是 /
|
||||
```
|
||||
|
||||
⚠️ **滚动发布期间会有两个版本同时在跑**,所以:
|
||||
- **迁移必须是"向后兼容"的**(只加列/加表,不改列类型、不删列)——
|
||||
否则旧实例会在新库上出错。改列要走"两次发布":这次加新列并双写,下次删旧列。
|
||||
- **H5 静态产物要一起发**:它是后端托管的,前后端不同步会出现
|
||||
"页面调用了还不存在的接口"。
|
||||
|
||||
---
|
||||
|
||||
## §3 密钥与凭据
|
||||
|
||||
### 3.1 清单
|
||||
|
||||
| 变量 | 用途 | 生成方式 | 轮换影响 |
|
||||
|---|---|---|---|
|
||||
| `XT_JWT_SECRET` | 登录 token 的 HS512 签名 | `openssl rand -base64 64` | **所有人被登出**(要做就安排在低峰) |
|
||||
| `XT_AES_KEY` | **证件号加密** | `openssl rand -base64 32` | ⚠️ **不能直接换** —— 见 3.2 |
|
||||
| `XT_HMAC_KEY` | 开放接口签名 / 去重 hash | `openssl rand -base64 48` | 接入方要改签名密钥;`id_card_hash` 类的去重值会变 |
|
||||
| `XT_RSA_PRIVATE_KEY` | 登录密码解密(PKCS#8 单行 base64) | `openssl genpkey -algorithm RSA ...` | 公钥在前端,**要同时换**(接口 `/auth/public-key` 会下发新公钥) |
|
||||
| `PG_PASSWORD` / `REDIS_PASSWORD` | 基础设施 | 运维 | 重启即生效 |
|
||||
| `QINIU_*` | 对象存储 | 七牛控制台 | 换 key 不影响已存文件(URL 是签名的) |
|
||||
| `WX_*` | 微信小程序 / 公众号 | 微信后台 | 换 secret 后旧 access_token 立刻失效(**服务自己会重新取**) |
|
||||
|
||||
### 3.2 ⚠️ AES 密钥不能"直接换"
|
||||
|
||||
`collect_roster.id_card_no` / `collect_file` 里的证件号是 **AES 密文**。
|
||||
换密钥之后**旧密文解不开**,而系统不会报错 —— 它只会显示一串乱码,
|
||||
或者门禁核验永远匹配不上(`id_card_hash` 也变了)。
|
||||
|
||||
正确做法(本项目尚未实现,属二期工程):
|
||||
1. 新密钥加进来(配置支持"当前密钥 + 历史密钥列表");
|
||||
2. 写一个离线任务用旧密钥解密、新密钥重加密,并**同时重算 hash 与 mask**;
|
||||
3. 全部迁完之后,旧密钥才下线。
|
||||
|
||||
**在此之前:`XT_AES_KEY` 一旦启用就不要换。** 这条比 token 那条更硬 ——
|
||||
token 换了只是"重新登录一次",这个换了是"全校证件号读不出来"。
|
||||
|
||||
### 3.3 密钥轮换的记录
|
||||
|
||||
轮换是**运维动作**,要留痕(谁、什么时候、为什么)。项目里的操作日志表
|
||||
`sys_operation_log` 可以承接这类记录,但**轮换本身在系统外发生**,
|
||||
所以还应有独立的运维记录 —— 建议直接用 GitLab 的 issue 或运维台账。
|
||||
|
||||
### 3.4 启动自检(`ProdEnvCheck`)
|
||||
|
||||
`prod` profile 启动时会先自检,**任何一条不通过就拒绝启动**:
|
||||
|
||||
| 检查 | 不检查会怎样 |
|
||||
|---|---|
|
||||
| 六个必需环境变量齐全 | 直接起不来(但报错是一堆栈,不是清单 —— 所以这里提前拦) |
|
||||
| `stub-enabled` 不为 `true` | **任何人可以登录任何学生** |
|
||||
| 存储 provider 不是 `local` | 多副本下**必然丢文件**,且丢的时候没有任何报错 |
|
||||
| JWT 密钥不是 dev 那把、且 ≥ 32 字符 | 拿仓库里那把就能**伪造任意用户的 token** |
|
||||
| AES 密钥不是 dev 那把 | 证件号加密形同虚设 |
|
||||
| `springdoc.api-docs.enabled` 不为 `true` | 全部接口与字段暴露给公网 |
|
||||
|
||||
实现是 `EnvironmentPostProcessor`(跑在任何 bean 之前)。
|
||||
⚠️ **不能写成 `ApplicationRunner`**:那样缺密钥时 `JwtService` 构造先炸,
|
||||
用户看到的是一段 56 行栈,而不是这份清单(这是实测踩过的)。
|
||||
|
||||
---
|
||||
|
||||
## §4 升级、迁移与回滚
|
||||
|
||||
### 4.1 迁移
|
||||
|
||||
Flyway 管理,迁移文件在 `backend/xt-server/src/main/resources/db/migration/`。
|
||||
**已发布的迁移文件不许改**(`validate-on-migrate: true` 会校验)——
|
||||
这是保护:它保证"生产跑的迁移"与"仓库里的"逐字节一致。
|
||||
|
||||
⚠️ **生产禁止 `flyway clean`**(`clean-disabled: true`):
|
||||
它是"清空整个库再重建",在生产上执行等于把所有学校的填报数据删掉。
|
||||
|
||||
### 4.2 升级前必做
|
||||
|
||||
```bash
|
||||
# ① 备份(**这是唯一的后悔药**)
|
||||
pg_dump -Fc -h $PG_HOST -U $PG_USER $PG_DB > backup-$(date +%F-%H%M).dump
|
||||
|
||||
# ② 在副本上试跑迁移:先恢复备份到临时库,再对临时库跑一次
|
||||
createdb xt_dryrun && pg_restore -d xt_dryrun backup-*.dump
|
||||
java -jar xt-server.jar --spring.profiles.active=prod \
|
||||
--spring.datasource.url=jdbc:postgresql://.../xt_dryrun \
|
||||
--spring.main.web-application-type=none # 只跑迁移,不起 web
|
||||
```
|
||||
|
||||
②的价值在于:**迁移失败的最好时机是"还没碰生产库"的时候**。
|
||||
|
||||
### 4.3 回滚
|
||||
|
||||
| 情形 | 怎么做 |
|
||||
|---|---|
|
||||
| 只换了应用版本、没跑新迁移 | 把上一个 jar 换回来,重启。**最快** |
|
||||
| 跑了新迁移、但迁移是"只加列/加表" | 换回旧 jar 即可(旧版本不认识新列,不影响) |
|
||||
| 跑了新迁移、且迁移**改了列类型或删了列** | ⚠️ 换回旧 jar 也救不了,必须**恢复备份** —— 所以 §2.3 要求迁移必须向后兼容 |
|
||||
| 只是某个开关配错 | 改环境变量重启(`ProdEnvCheck` 会先校验一遍) |
|
||||
|
||||
**发布前把"回滚要多久"算清楚**,并写在发布单上。恢复一个几十 GB 的库
|
||||
不是"几分钟"的事。
|
||||
|
||||
### 4.4 灰度
|
||||
|
||||
一期建议按**租户**灰度(不是按流量):客户是学校,一个学校要么全在新版本、
|
||||
要么全在旧版本 —— 按流量切会让同一个学校里一半学生走新代码、一半走旧代码,
|
||||
而"同一份填报单被两个版本各写一半"是最难排查的一类问题。
|
||||
|
||||
```bash
|
||||
XT_TENANT_DOMAIN_ENABLED=true # 如果要按域名区分单位
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §5 出事看哪里
|
||||
|
||||
| 症状 | 先看 | 常见原因 |
|
||||
|---|---|---|
|
||||
| 服务起不来 | 启动日志里的 `生产环境启动自检未通过` | §3.4 的六条 |
|
||||
| 学生上传失败 | `xt-server.log` 里 `MaterialFileService` | 存储密钥错 / 空间名错(开 `XT_STORAGE_SELF_CHECK=true` 能在启动时暴露) |
|
||||
| 页面 404 但接口正常 | `spring.web.resources.static-locations` | H5 构建产物没放对目录(**应用不会报错**) |
|
||||
| 定时任务没生效 | 手工触发一次:`POST /api/v1/schedules/run {"job":"close"}` | 见下 |
|
||||
| 「查不到数据」但库里有 | 租户上下文 | 定时任务/匿名接口的线程里没有登录用户,租户拦截器默认拒绝 |
|
||||
|
||||
**定时任务的排查办法**:`POST /api/v1/schedules/run` 可以手工触发一轮
|
||||
(`close` / `urge` / `timeout` / `raw`),运维补跑也用它。
|
||||
|
||||
⚠️ 定时任务最安静的一类故障是"**跑了但什么都没做**":
|
||||
候选查询的 SQL 别名没加双引号时会被 PostgreSQL 折成小写,
|
||||
`row.get("tenantId")` 得到 `null`,异常被 catch 吞掉,日志只有一行 warn。
|
||||
所以四个任务都加了候选行自检 —— 缺 `tenantId` 会把"实际的键"打进日志。
|
||||
|
||||
---
|
||||
|
||||
## §6 首次接入 CI 的检查单
|
||||
|
||||
`.gitlab-ci.yml` 里这几件事**在本地 Windows 上验不到**,第一次上 runner 时要重点看:
|
||||
|
||||
- [ ] `services: postgres / redis` 的容器别名能否解析(`PG_HOST=postgres`)
|
||||
- [ ] runner 镜像里 `apt-get install python3 python3-pip curl postgresql-client` 可用
|
||||
- [ ] `pip3 install openpyxl requests` 可用(E2E 脚本要用)
|
||||
- [ ] `openssl rand -base64 64` 生成的随机串里含 `/` `+` `=` 时,
|
||||
作为**环境变量**传给 Java 没问题(shell 里安全;哪天改用 `.env` 文件则要转义)
|
||||
- [ ] `artifacts` 的路径与保留期
|
||||
- [ ] `maven:3.9-eclipse-temurin-21` 与 `node:20` 的镜像拉取速度
|
||||
- [ ] **`scripts/check-test-profile.py` 要先过**:它验的就是"CI 那个 profile 能不能起来"。
|
||||
它红了就是配置问题,不要去查 E2E 脚本
|
||||
- [ ] 密钥生成命令的长度要对:`openssl rand -base64 32` → 44 字符(AES-256 要 32 字节)。
|
||||
长度不对时 `ProdEnvCheck` 会点名报出来("解出来 30 字节,需要 32 字节")
|
||||
- [ ] **shell 片段的语法只能在 Linux 上验**:本次开发机的 `cmd.exe` 解析不了
|
||||
单引号 pathspec(`'frontend-pc/**/*.vue'`),所以"lint 改动文件"那段
|
||||
是照 Linux 语义写的,首次跑要留意
|
||||
|
||||
---
|
||||
|
||||
## §7 为什么没有 GitHub Actions
|
||||
|
||||
仓库里原本有一个模板自带的 `frontend-pc/.github/workflows/deploy.yml`
|
||||
(部署到 GitHub Pages)。它在本项目**不可能工作**,已删除:
|
||||
|
||||
1. 远端是 **GitLab**(`gitlab.jxyunge.com`),不是 GitHub;
|
||||
2. 它用 `npm ci` / `npm run build`,而本项目用 **pnpm**(仓库里是 `pnpm-lock.yaml`);
|
||||
3. 它把管理端当**静态站**发到 Pages —— 而这个前端**必须连着后端**才有意义
|
||||
(登录、接口、权限全部依赖 `/api`),发上去只会得到一个打不开的演示页。
|
||||
|
||||
留着它的害处不是"多一个文件",而是**它会让人以为"我们有部署流水线"**。
|
||||
|
||||
---
|
||||
|
||||
## §8 本地能验到哪一步
|
||||
|
||||
`.gitlab-ci.yml` 是配置文件,**"写了没验"的 CI 最糟**(第一次真跑才红,而那时所有人都在等)。
|
||||
所以本轮把能本地跑的都跑了一遍:
|
||||
|
||||
| 阶段 | 本地怎么验 | 结果 |
|
||||
|---|---|---|
|
||||
| `check` | 直接跑那三个校验脚本 + 把 CI 里的 heredoc 片段原样执行 | ✅ |
|
||||
| `build` | `mvn package`、`pnpm typecheck`、`pnpm build` | ✅ |
|
||||
| `e2e` | 起 `test` profile 确认能起来(`check-test-profile.py`);全套 E2E 在 `dev` 上跑 | ✅ |
|
||||
| `生产环境自检` | **真的起三次 jar**(三种误配形态) | ✅ 9/9 |
|
||||
|
||||
**本地验不到的**(要在真实 runner 上跑):
|
||||
`services:` 的容器别名解析、`apt-get`/`pip3` 在 runner 镜像里可用性、
|
||||
artifacts 路径、以及**所有 shell 片段的语法**(开发机是 Windows,
|
||||
`cmd.exe` 解析不了单引号 pathspec —— 所以"只 lint 改动文件"那段是照 Linux 语义写的)。
|
||||
这几条都在 §6 的检查单里。
|
||||
|
||||
---
|
||||
|
||||
## §9 压测记录(M7-09)
|
||||
|
||||
脚本:`python scripts/load-test.py [--rows N] [--only gate|read|upload|export] [--with-upload]`
|
||||
|
||||
⚠️ **本机是"负载生成器 + 后端 + PG + Redis 全在同一台机器(且 PG/Redis 在 Docker 里)"**,
|
||||
所以绝对值偏悲观;**看比例与形状,不要看绝对值**。
|
||||
另一条同样重要:**谈响应时间必须带规模**(同一个接口在 60 行与 2000 行上是两件事),
|
||||
所以报告里每个数字都带规模。
|
||||
|
||||
### 9.1 结果(规模:名册 2000 人)
|
||||
|
||||
| 场景 | 结果 |
|
||||
|---|---|
|
||||
| **门禁核验**(60 人同一出口 IP 同时扫码) | **60/60 成功**(修复前 **20/60**,见 9.3);并发下 p50 218ms / p95 327ms |
|
||||
| **读接口**(20 并发 × 8 轮 = 640 请求,四类混合) | **0 失败**;单请求中位 **36~47ms**(与页大小 20/50/200 **无关**);20 并发下 p50 156~375ms |
|
||||
| **名册列表的 SQL 条数** | **恒为 3 条**(`selectPage` + `count` + 组织名映射),**与 pageSize 无关 → 没有 N+1** |
|
||||
| **DB 侧耗时** | 分页查一页 **1.2ms**、计数 **0.66ms**、组织映射 **0.06ms** |
|
||||
| **导出** | 前置检查正确拦住(`40035`,见 9.4);大包打包**未测**(需要真实审批过的数据) |
|
||||
| **材料上传** | **默认不跑**(见 9.4) |
|
||||
|
||||
### 9.2 结论:后端没有 N+1,DB 不是瓶颈
|
||||
|
||||
三条独立证据:
|
||||
|
||||
1. **数 SQL 条数**:一次名册列表请求恒 3 条 SQL,pageSize 从 5 到 200 都是 3 条;
|
||||
2. **单请求延迟与页大小无关**(20/50/200 的中位分别是 47/36/44ms)——
|
||||
如果是"按行做重活",这三个数会明显拉开;
|
||||
3. **DB 侧 2ms 以内**(`EXPLAIN ANALYZE`)。
|
||||
|
||||
所以并发下的 p50 156~375ms 是**排队效应**(20 个请求抢同一台机器的 CPU 与网络),
|
||||
不是单请求成本。**要提吞吐得先加机器/拆进程,而不是改 SQL。**
|
||||
|
||||
### 9.3 ⚠️ 门禁限流曾经会挡住整间教室(已修)
|
||||
|
||||
压测第一次跑出来的是:60 个**不同**学生同时核验(全部正确),
|
||||
**20 个成功、40 个被限流**(`40071`)。原因是 IP 限流 20 次/分钟,
|
||||
而它计的是**所有请求**(成功也算)。
|
||||
|
||||
真实场景里这有多严重:**一个班的学生坐在同一间教室、连同一个 WiFi**,
|
||||
出口就是同一个公网 IP。开学第一次收材料,后 40 个学生看到的是
|
||||
「操作过于频繁,请稍后再试」—— 而他们不知道自己该等多久、也不知道为什么别人能进。
|
||||
|
||||
**修法:分开两个计数器,因为目的不同。**
|
||||
|
||||
| 计数器 | 计什么 | 目的 | 限额 |
|
||||
|---|---|---|---|
|
||||
| 失败(按人) | 只计失败 | 防**猜** | 10 次/小时 |
|
||||
| 失败(按 IP) | 只计失败 | 防**枚举** | 20 次/分钟 |
|
||||
| 成功(按 IP) | 只计成功 | 防"批量取数据" | 200 次/分钟 |
|
||||
|
||||
关键洞见:**枚举攻击产生的是失败,而教室产生的是成功。**
|
||||
把成功也算进防枚举的额度里,等于用防攻击的代价挡住正常用户 ——
|
||||
而挡住的量级恰好是"一个班"。
|
||||
|
||||
⚠️ 另有一条**刻意不计**的失败:`GATE_ALREADY_BOUND`(两个人抢同一个名册项)。
|
||||
开学第一天几十人同时扫码必然出现几例,把它算进额度里会让整间教室在几秒内被限流,
|
||||
**而没有一个人做错了什么**。
|
||||
|
||||
### 9.4 两条使用注意
|
||||
|
||||
- **上传压测默认关闭**(`--with-upload` 才跑):本机 `application-local.yml` 里
|
||||
`provider: qiniu`,所以上传落的是**线上那个空间**(bucket=tools-matrix)。
|
||||
一次压测 40 张图打过去,七牛回了 `status:614`。两件事都不对:
|
||||
压测不该依赖外部服务(网络抖动会让"性能结论"变成对七牛的评价),
|
||||
也不该往真实桶里灌测试文件(删库不删桶)。
|
||||
**要跑就先把存储切成本地**:`XT_STORAGE_PROVIDER=local` 重启后端。
|
||||
- **导出创建被 `40035` 拦住不是失败**:那是铁律(`canExport=false` 时禁止导出)
|
||||
在服务端的正确生效。脚本会把这种情形报成「被正确拦住」,
|
||||
并**断言创建接口也必须拒绝** —— 界面禁用按钮只是第一道,服务端是最后一道。
|
||||
|
||||
### 9.5 还没测的
|
||||
|
||||
| 场景 | 为什么没测 | 怎么测 |
|
||||
|---|---|---|
|
||||
| 导出**大包打包**(几百人 / 1GB+) | 需要真实审批过的数据,而造那种数据要跑完整审核链路 | 用 staging 的真实批次,量 `ExportService` 的打包耗时与内存峰值 |
|
||||
| 上传并发(CPU 密集的压缩) | 见 9.4(会打到外部存储) | 切本地存储后 `--with-upload` |
|
||||
| 5000 人名册(PRD 提到的导入上限) | 导入本身是同步的,压测里造 5000 行要 ~20s | `--rows 5000`,重点是**导入**耗时与内存 |
|
||||
| 长时间稳定性(内存泄漏、连接泄漏) | 一次性压测测不出来 | 持续 1~2 小时的低强度压测,看 RSS 与 Hikari 活跃连接数是否单调上升 |
|
||||
@@ -0,0 +1,280 @@
|
||||
# 10 · 用户手册与客服话术
|
||||
|
||||
> 对应里程碑 **M7-11(用户手册 / 培训材料 / 客服话术)**。
|
||||
> 这份文档写给**三种人**:要操作系统的学校老师与管理员、要回答问题的客服、要讲课的培训人。
|
||||
>
|
||||
> ⚠️ 文中的每一句提示文案、每一个错误码都**取自代码**(`ErrorCode.java` 与各 Service 的文案),
|
||||
> 不是"应该长这样"的设想 —— 客服照着它回答,才对得上用户屏幕上真正看到的东西。
|
||||
|
||||
---
|
||||
|
||||
## §1 三种角色,三个入口
|
||||
|
||||
| 角色 | 入口 | 设备 | 主要做什么 |
|
||||
|---|---|---|---|
|
||||
| **学生** | 扫学校/班主任发的**二维码** | 手机(微信小程序 / H5) | 核对身份 → 填表 → 上传材料 → 提交 |
|
||||
| **班主任** | 微信小程序(同一套码,登录后进教师端) | 手机 | 看本班进度、审本班材料、代传、催报 |
|
||||
| **学校管理员** | 电脑浏览器打开管理端 | PC | 建任务、配表单与材料、导名册、终审、导出交付包 |
|
||||
|
||||
**⚠️ 学生与老师的小程序是同一个**,靠账号角色区分。学生第一次要"核验身份"(填姓名+证件号),
|
||||
这一步就是**门禁**:名册里没有的人进不去(这是产品铁律,见 §5.1)。
|
||||
|
||||
---
|
||||
|
||||
## §2 学生操作手册
|
||||
|
||||
### 2.1 完整流程(约 3 分钟)
|
||||
|
||||
```
|
||||
扫码 → 核验身份(姓名+证件号)→ 填表 → 传材料 → 提交 → 等审核
|
||||
↑ ↓
|
||||
被驳回时回来改 ←──── 老师驳回并说明原因
|
||||
```
|
||||
|
||||
### 2.2 每一步会发生什么
|
||||
|
||||
**① 扫码**
|
||||
- 扫**班主任发的那个码**。每个任务、每个班可能有不同的码。
|
||||
- 扫错码(别的任务/别的班)→ 系统提示「这不在你负责的班级里」这类信息,
|
||||
**不会**让你填了才发现不对。
|
||||
|
||||
**② 核验身份**
|
||||
- 填**姓名 + 身份证号**,要与学校给的名册**完全一致**。
|
||||
- ⚠️ 核验不通过时,系统**故意不告诉你错在哪**(是姓名错还是号码错)——
|
||||
这是防探测:说得越细,别人越容易试出你班上同学的信息。
|
||||
提示统一是「**没有找到你的信息,请核对后重试**」。
|
||||
- 试太多次会被暂时拦住:「这个账号试得太多次了,请 X 分钟后再试,或联系班主任核对姓名与证件号」。
|
||||
⚠️ 如果提示的是「**当前网络下尝试核验的人较多,请稍等 1 分钟再试**」,
|
||||
那不是你的问题 —— 是同一个 WiFi 下有人多次试错,等一分钟即可。
|
||||
- 如果提示「**该信息已被登记**」(`40072`):说明这个名册项已经被**另一个微信号**绑定过。
|
||||
常见原因:你用两个微信号都扫了码。**联系班主任处理**,不要重复试。
|
||||
- 如果名册里确实没有你 → 点「**申请加入名单**」,由管理员审批后入册(见 2.4)。
|
||||
|
||||
**③ 填表**
|
||||
- 表里有一部分**已经替你填好了**(报考职业/工种/等级/学校/专业等)——
|
||||
那是老师从名册带过来的,**核对一下**,不对就改。
|
||||
- 带红色标记的是必填。
|
||||
|
||||
**④ 传材料**
|
||||
- 拍照或从相册选,**文件名乱没关系** —— 系统会自动改名成「姓名-证件号-材料名」。
|
||||
- 系统会把图压到 **1MB 以内**(清晰度足够认定机构看)。
|
||||
- 体积压不下来时会提示「**这张图太复杂,压到最小还是超过 1M,建议重新拍一张更清晰的**」。
|
||||
- ⚠️ 每个材料项有**份数上限**(如身份证最多 2 份)。**重复上传会各占一份** ——
|
||||
系统不会自动去重(它只记录文件的哈希用于版本追溯)。传错了就先**删掉上一份**再传。
|
||||
|
||||
**⑤ 提交**
|
||||
- 提交前系统会**体检一遍**:红色问题必须改,黄色问题可以确认后继续。
|
||||
- ⚠️ 提交按钮点了没反应时,先看页面上有没有**红色清单** —— 那是被拦住了,不是网络问题。
|
||||
|
||||
**⑥ 被驳回之后**
|
||||
- 老师驳回时会写原因,你会在**消息**里看到。
|
||||
- 回到「我的任务」→ 点「**撤回修改**」→ 改完重新提交。
|
||||
- ⚠️ 同一个任务**最多被驳回 3 次**(`40010 驳回次数已达上限`)。
|
||||
到上限后请直接联系班主任,不要反复提交。
|
||||
|
||||
### 2.3 你可能会看到的提示(照抄给客服)
|
||||
|
||||
| 你看到的 | 什么意思 | 怎么办 |
|
||||
|---|---|---|
|
||||
| 没有找到你的信息,请核对后重试 | 姓名或证件号与名册不符(系统不说是哪个) | 找班主任核对名册上的写法 |
|
||||
| 操作过于频繁,请稍后再试 | 试错次数太多(同一个人 1 小时 10 次) | 等几分钟,或让班主任核对 |
|
||||
| 当前网络下尝试核验的人较多 | **不是你的问题**:同一 WiFi 有人多次试错 | 等 1 分钟 |
|
||||
| 该信息已被登记 | 这个名册项已被另一个微信号绑定 | 联系班主任解绑或处理 |
|
||||
| 你已在名单中,请返回重新核验 | 你已经在名册里了,不用申请加入 | 返回上一步直接核验 |
|
||||
| 你已提交过申请,请耐心等待 | 加入申请重复提交 | 等管理员审批 |
|
||||
| 这份材料可能不是你的,请重新上传 | 系统发现材料上的证件号与你不一致 | 确认没传错人的材料,重传 |
|
||||
| 已进入审核,需先撤回才能删材料 | 你已提交,材料被锁 | 点「撤回修改」后再改 |
|
||||
| 已通过终审,不可撤回 | 已经审完了 | 联系学校管理员 |
|
||||
| 任务已截止、已归档或已暂停 | 这个任务不能再填了 | 联系班主任 |
|
||||
|
||||
### 2.4 名册里没有我(申请加入名单)
|
||||
|
||||
1. 核验页点「**申请加入名单**」;
|
||||
2. 填姓名、身份证号、班级、联系电话,可附说明;
|
||||
3. 管理员在管理端「**加入申请**」里审批;
|
||||
4. 通过后你会收到消息,回到核验页重新核验即可。
|
||||
- ⚠️ 申请**次数有上限**(`40037`),别反复提交;重复提交会提示「你已提交过申请,请耐心等待」。
|
||||
|
||||
---
|
||||
|
||||
## §3 班主任操作手册(手机)
|
||||
|
||||
### 3.1 每天看什么
|
||||
|
||||
打开小程序 → **看板**:本班应填 / 已提交 / 待审 / 异常。
|
||||
「异常」是系统体检出来的问题(证件号不符、材料看不清等),**优先处理这些**,
|
||||
因为它们最终会被终审驳回。
|
||||
|
||||
### 3.2 审核(核心操作)
|
||||
|
||||
- 列表是**一张一张的卡片**,一眼一份材料。
|
||||
- **右滑 = 通过**(原型铁律:通过可以滑,因为它是"没什么要说"的那个动作)。
|
||||
- **驳回必须点按钮**(不能滑)—— 因为驳回**必须写原因**,而手势会把"写原因"这一步跳过。
|
||||
- 驳回时给的原因会**原样发给学生**,所以写清楚"哪里不对、怎么改",
|
||||
比如「身份证照片反光,请重拍」而不是「不合格」。
|
||||
|
||||
### 3.3 代传材料
|
||||
|
||||
学生不会用手机、或材料在他手上时:进学生详情 → 「**代传**」→ 选材料 → 拍照上传。
|
||||
- 代传会**留痕**(记录是谁代传的),学生端会显示「由 X 老师代传」。
|
||||
- 代传原因**是选填的**(不是必填)—— 但建议填一句,
|
||||
因为它会挂到审核时间轴上,**学生也看得到**(比如"学生手机坏了,家长送来的纸质件")。
|
||||
|
||||
### 3.4 催报
|
||||
|
||||
- 「一键催报」催本班**还没提交**的人。
|
||||
- ⚠️ 已经交了的人**不会**收到"请尽快完成填报" —— 那会让他们以为自己没交成功。
|
||||
- 同一个人 **24 小时内只会被催一次**(防骚扰)。
|
||||
- 没核对过身份的人**催不到**(他没有账号),列表里会标出来:
|
||||
「这个学生还没扫码核对身份,无法触达(先让他扫二维码)」。
|
||||
|
||||
---
|
||||
|
||||
## §4 学校管理员操作手册(PC)
|
||||
|
||||
### 4.1 开通一个任务:五件套
|
||||
|
||||
进「**收集任务**」→ 新增,然后**在任务这一行上**依次点这几个链接:
|
||||
|
||||
| 顺序 | 点什么 | 做什么 | 不做会怎样 |
|
||||
|---|---|---|---|
|
||||
| 1 | **表单设计** | 内置字段锁定、扩展字段自由加 | 用默认表单也能跑 |
|
||||
| 2 | **材料规则** | 每个材料项的格式、体积、份数;命名模板 | 不配必传材料 → **发不出去**(`40018`) |
|
||||
| 3 | **配置班级** | 哪些班参加、每班班主任是谁 | 不配 → **发不出去**(`40019`) |
|
||||
| 4 | **名单管理** | 导 Excel 名册(先下载模板) | 名册为空 → **发不出去**(`40016`) |
|
||||
| 5 | **发布** | 发布后学生才能扫码填 | — |
|
||||
|
||||
⚠️ **发布前系统会拦一次**,把上面缺的项逐条列出来。这是有意的:
|
||||
没名册的任务发出去,学生扫进来会发现"我不在名单里",比发不出去更糟。
|
||||
|
||||
名册导入的小技巧:
|
||||
- 表头可以不在第 1 行,系统会**自动找表头**(认不出会让你指定行号);
|
||||
- 先点「**体检**」(`dryRun`)看报告,再正式导入;
|
||||
- 报告里 `created/failed/skipped` 与每行的错误原因都会列出来。
|
||||
|
||||
### 4.2 审核与终审
|
||||
|
||||
- 班主任先审(初审)→ 管理员在「**审核**」里终审。
|
||||
- 可以**批量通过**(一次最多 200 条,`40011`)。
|
||||
- ⚠️ 存在**红色问题**的单子不允许通过(`40032`)—— 那不是系统刁难,
|
||||
而是这些问题到认定机构那边一定会被打回。
|
||||
|
||||
### 4.3 导出交付包
|
||||
|
||||
「**导出中心**」→ 选任务 → 看**前置检查**:
|
||||
- 检查不通过时按钮**直接禁用**并列出不合格清单(不是弹个警告还能点)。
|
||||
- 通过后导出,产物保留 **7 天**,过期后需重新导出(会明确提示「导出包已过期(保留 7 天),请重新导出」)。
|
||||
- 几百人以上会自动**分卷**(`-part1.zip`、`-part2.zip`…),每一卷都是**完整可解**的压缩包。
|
||||
|
||||
### 4.4 历史整理器(把以前那堆乱文件夹变成能用的数据)
|
||||
|
||||
「配置 → **历史整理器**」→ 上传一个 zip:
|
||||
1. 填任务名;
|
||||
2. **关联一个已有任务的名册**(⚠️ 这一步决定了报告的价值,见下);
|
||||
3. 等它跑完 → 看报告。
|
||||
|
||||
报告里最该看的一句是「**N 个文件里,X 个系统全自动处理,只有 Y 个必须你亲自判断**」。
|
||||
- **有关联名册**才有「证件号校验」与「疑似串档检测」—— 而"疑似串档 1 例"是报告里最有价值的一条;
|
||||
- 不关联也能跑,但只能做文件名规范化。
|
||||
|
||||
### 4.5 其它
|
||||
|
||||
| 要做什么 | 去哪 |
|
||||
|---|---|
|
||||
| 加/停用账号、重置密码 | 组织与账号 → 账号管理 |
|
||||
| 改学校/院系/班级结构 | 组织与账号 → 组织架构(也支持 Excel 导入) |
|
||||
| 加一个下拉选项(如多了种学历写法) | 配置 → 字典管理(**内置项不可改名**,要加就追加自定义项) |
|
||||
| 查"谁在什么时候看了证件号/导出了什么" | 平台运维 → 操作日志 |
|
||||
| 查"通知到底发出去没有" | 收集管理 → 通知记录 |
|
||||
| 看某个任务的分班进度 | 工作台 → 数据看板(选一个任务) |
|
||||
|
||||
---
|
||||
|
||||
## §5 客服话术
|
||||
|
||||
### 5.1 三条必须能讲清楚的产品规则
|
||||
|
||||
1. **必须先有名册**(门禁铁律)。名单外的人填不了 —— 这是为了防冒名与串档,
|
||||
不是系统限制。名单外的人走「申请加入名单」。
|
||||
2. **材料是自动改名与自动压缩的**。学生传的文件名乱、体积大都不是问题;
|
||||
交付物是压缩后的"规范件"。
|
||||
3. **驳回必须写原因**,原因会原样发给学生。所以客服看到"学生说不知道为什么被驳回",
|
||||
要先去审核记录里看老师写了什么。
|
||||
|
||||
### 5.2 高频问题 → 标准回答
|
||||
|
||||
**Q:学生说"扫码进去说找不到我的信息"**
|
||||
A:这是名册门禁。请班主任核对名册上他的**姓名写法**与**身份证号**(常见是生僻字、
|
||||
空格、身份证号末位 X 大小写)。系统**故意不告诉**是姓名错还是号码错。
|
||||
如果确实不在名册里,让他点「申请加入名单」。
|
||||
|
||||
**Q:学生说"提示操作过于频繁"**
|
||||
A:分两种,看文案:
|
||||
· 「这个账号试得太多次了」→ 他自己试错超过 10 次,等一会儿或让班主任核对;
|
||||
· 「当前网络下尝试核验的人较多」→ **不是他的问题**(同 WiFi 有人试错),等 1 分钟。
|
||||
开学当天全班同时扫码是正常的,系统已按"只对失败计数"处理。
|
||||
|
||||
**Q:学生说"提示该信息已被登记"**
|
||||
A:这个名册项已经绑定了**另一个微信号**。常见于学生换了微信号、或用家人手机扫过。
|
||||
需要管理员在后台解绑后重新核验。
|
||||
|
||||
**Q:学生说"传了材料但老师那边看不到"**
|
||||
A:先确认他**提交了没有**(传材料 ≠ 提交)。让他看「我的任务」里的状态:
|
||||
草稿 = 还没提交。另外,已提交后材料被锁(`40030`),要改必须先「撤回修改」;
|
||||
如果他是"传了两次同一张",那会**各占一份**(系统不去重),撞上份数上限时要先删一份。
|
||||
|
||||
**Q:老师说"导出的按钮是灰的"**
|
||||
A:那是**前置检查没通过**,页面上会列出不合格清单(`40035`)。
|
||||
通常是还有单子没审完、或存在不合格文件。把清单里的问题处理掉即可。
|
||||
|
||||
**Q:老师说"导出包下载不了了"**
|
||||
A:导出包只保留 **7 天**。过期后重新导出即可(会提示「导出包已过期(保留 7 天),请重新导出」)。
|
||||
已提交的数据不会因为导出包过期而丢失。
|
||||
|
||||
**Q:老师问"为什么这个学生催不到"**
|
||||
A:他**还没扫码核对身份**,系统里没有他的账号,发不了消息。
|
||||
列表里会明确标出这一点:「这个学生还没扫码核对身份,无法触达(先让他扫二维码)」。
|
||||
|
||||
**Q:管理员问"学生被驳回 3 次之后怎么办"**
|
||||
A:系统上限是 3 次(防反复折腾)。到上限后由班主任/管理员直接处理
|
||||
(代传材料或线下收集),不要再让学生提交。
|
||||
|
||||
### 5.3 出错时客服要收集的四样信息
|
||||
|
||||
1. **学生/老师的账号**(用户名或手机号);
|
||||
2. **任务名**(页面上就有,如「2026年9月机电认定」);
|
||||
3. **屏幕上的完整提示文字**(照抄,不要转述 —— 提示文案与错误码是一一对应的);
|
||||
4. **发生时间**(精确到分钟)。
|
||||
|
||||
有了这四样,技术同学可以直接去「操作日志」与「通知记录」里定位。
|
||||
|
||||
---
|
||||
|
||||
## §6 培训材料(30 分钟一节课的提纲)
|
||||
|
||||
**对象**:学校管理员 + 班主任。**准备**:一台电脑(管理员)、每人一台手机(班主任)。
|
||||
|
||||
| 时间 | 内容 | 关键动作 |
|
||||
|---|---|---|
|
||||
| 0–3′ | 这个东西解决什么问题 | 一句:**把"收材料、改名、压缩、催交、打包"这五件手工活变成一次配置** |
|
||||
| 3–8′ | 管理员建一个演示任务 | 五件套走一遍(表单 → 材料 → 班级 → 名册 → 发布),**故意漏一项**,让系统把 `40016` 报出来 |
|
||||
| 8–13′ | 导名册 | 强调"先体检再导入";演示一行错数据被报告抓出来 |
|
||||
| 13–20′ | 班主任端:审一份 | **右滑通过**、**驳回必须写原因**(让每人写一条原因,互相看写得清不清楚) |
|
||||
| 20–25′ | 代传与催报 | 演示"催不到"的那个提示,讲清"没核验身份就触达不到" |
|
||||
| 25–30′ | 导出与答疑 | 演示前置检查拦截;把 §5.2 的高频问题当堂过一遍 |
|
||||
|
||||
**课后必须发出去的两样**:① 班主任的**二维码**;② 学生版的**三步图示**(扫码 → 核验 → 提交)。
|
||||
|
||||
---
|
||||
|
||||
## §7 上线首日检查单
|
||||
|
||||
- [ ] 每个班的名册都**导进去了**(名册为空的任务发不出去)
|
||||
- [ ] 每个班都**配了班主任**(没配 → 审核没人管,超时提醒也找不到人)
|
||||
- [ ] 每个任务都**配了必传材料**(不配发不出去)
|
||||
- [ ] 二维码**发到了每个班**(且是**当前任务**的码,不是上一次的)
|
||||
- [ ] 至少让 1 个学生**完整走一遍**(核验 → 提交),确认他能看到任务
|
||||
- [ ] 让 1 个班主任**审一份**(右滑通过、驳回各一次),确认消息能到学生那里
|
||||
- [ ] 管理员确认「工作台 → 数据看板」能看到这个任务的进度
|
||||
- [ ] ⚠️ 提前跟班主任说一句:**开学当天全班同时扫码,属正常**;
|
||||
若出现「当前网络下尝试核验的人较多」,等 1 分钟再扫
|
||||
Reference in New Issue
Block a user