test(release): verify Android production routes read-only
This commit is contained in:
+2
-1
@@ -8,7 +8,8 @@
|
||||
- **2026-07-29,发布预检支持定向后端/schema 范围和冻结物核验,未发布**:`release-preflight.sh` 不再把远端后端或 schema 核验强制绑定到管理端/H5 静态匹配;定向后端发布可只启用 `RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true`,且只要求本地后端 JAR 新鲜。完整包口径仍必须同时启用远端静态、后端和 schema 三项。新增范围回归脚本,校验缺少 `RELEASE_REMOTE_URL` 时失败关闭、定向模式不回退为静态耦合、完整模式命令仍保留;`feece9bb` 再新增 `RELEASE_ARTIFACT_COMMIT` 与 `RELEASE_LOCAL_BACKEND_PATH`,允许从后续文档提交上安全地核验冻结提交及其实际候选 JAR。提交 `566f222e` 后只重建后端并实跑生产只读定向预检:输出没有管理端/H5 哈希,根站、租户、移动首页、64/64 schema 和生产禁 DDL 门禁通过,最终仅因现网/候选 AIHR 模块哈希 `15f5513a…` / `c93460e8…` 不同而按预期失败。随后以 `dc20b920` 和发布 ZIP 解压 JAR 再跑后端只读预检,确认候选整包哈希 `8c676f2f…`,仍仅在同一模块差异处失败;这证明范围和冻结物选择正确,不代表后端已经发布。本项没有执行生产同步、重启或数据库变更。
|
||||
- **2026-07-29,Android 人工验收新增只读统一取证**:新增 `capture-android-acceptance.ps1`,只读取指定设备当前包版本、前台 Activity、四项敏感权限、UI 层级、截图和应用退出记录,证据限定写入 Git 已忽略的 `output/`;静态回归拒绝安装、启动、停止、清数据、授权、撤权和输入类命令。脚本已在 `emulator-5554` 实跑,确认 `0.1.10 (110)` / API 35 的原生隐私标题与拒绝/同意按钮可见、手机号登录不可见、四项权限均未授权、退出记录无 Crash/ANR。本项只记录用户选择前状态,未代替用户点击隐私选项。
|
||||
- **2026-07-29,定向后端发布/回滚工具与只读计划,未发布**:新增固定 `YCWY:/opt/wygj/app/ruoyi-admin.jar` 和 `wygj-aihr.service` 的 `release-backend.sh`;`plan`/`rollback-plan` 默认只读,`deploy`/`rollback` 必须使用完整 SHA-256 授权串且 Git 工作区干净。执行链包含旧包与清单备份、同目录候选校验、原子切换、systemd 重启、后端 + schema 发布后预检、失败自动恢复、回滚前安全备份和恢复前目标哈希复核;发现并发改包时拒绝自动覆盖,所有备份均保留。离线回归覆盖授权前失败关闭、六段远端 Bash 语法、无删除操作,并在临时目录实跑备份→切换→恢复→并发拒绝→回滚安全备份链。冻结候选实跑只读计划确认候选 JAR `8c676f2f…`、现网 JAR `46c99cff…`、目标/服务/空间、三个公开入口、64/64 schema 和生产禁 DDL 均符合要求;只输出授权串,没有创建远端备份、上传、切换、重启或改库。
|
||||
- **2026-07-29,8 月 1 日 APK 生产接口兼容性只读核验,未发布**:线上与候选 `ruoyi-aihr` 均含 819 个非目录/非 Manifest 条目,0 个增删、775 个逐字节相同、44 个不同;短信登录、移动首页、查/练/问、直通车和 ASR/TTS 对应的 9 个关键控制器 class 哈希逐一相同。未登录 GET 探针得到公开首页业务码 `200`、5 个鉴权入口 `401`、13 个仅 POST 入口 `405`,没有发送业务 POST 或写入数据。44 个差异仅来自 `AihrModelSeedService`、`AihrPracticeSeedService`、`AihrSopSeedService` 及内部类,内容是禁止兜底文案冒充已创建/派发工单、阻止含占位内容或不完整五维评分的场景发布;因此现网接口壳可承接 APK,但候选业务语义红线仍需授权后定向发布后端。管理端/H5 与 schema 不属于该定向后端发布;本次没有执行生产同步、重启或数据库变更。
|
||||
- **2026-07-29,8 月 1 日生产 API 无登录探针固化**:新增 GET-only 脚本覆盖 APK 依赖的 20 个入口;实跑得到公开移动首页 `1×200`、鉴权 GET `6×401`、仅 POST 入口 `13×405`。`/api/knowledge/answer-feedback` 因同一路径另有运营 GET,按真实控制器归入鉴权组。回归禁止请求体、表单、授权头、手机号和短信验证码入口;本次未申请验证码、未登录、未上传、未发送业务 POST 或写入数据。该结果证明路由与失败关闭契约,不替代认证态业务闭环。
|
||||
- **2026-07-29,8 月 1 日 APK 生产接口兼容性只读核验,未发布**:线上与候选 `ruoyi-aihr` 均含 819 个非目录/非 Manifest 条目,0 个增删、775 个逐字节相同、44 个不同;短信登录、移动首页、查/练/问、直通车和 ASR/TTS 对应的 9 个关键控制器 class 哈希逐一相同。最新统一无登录 GET 探针得到公开首页业务码 `1×200`、鉴权入口 `6×401`、仅 POST 入口 `13×405`,没有触发短信、登录、业务 POST 或数据写入。44 个差异仅来自 `AihrModelSeedService`、`AihrPracticeSeedService`、`AihrSopSeedService` 及内部类,内容是禁止兜底文案冒充已创建/派发工单、阻止含占位内容或不完整五维评分的场景发布;因此现网接口壳可承接 APK,但候选业务语义红线仍需授权后定向发布后端。管理端/H5 与 schema 不属于该定向后端发布;本次没有执行生产同步、重启或数据库变更。
|
||||
- **2026-07-26,阿里云百炼 ASR 生产热修复**:`POST /api/ai/asr` 新增 `dashscope/qianwen + qwen3-asr-flash` 的 `/compatible-mode/v1/chat/completions` + Base64 `input_audio` 分支,原 SiliconFlow `/audio/transcriptions` multipart 路径继续兼容,ASR 超时放宽至 30 秒。定向 Maven 测试 10 项通过;后端 JAR(SHA-256 `46c99cff75d21f8536a71c3c058b6437e99dbf9ff4bae3542b44471f3f34ad84`)已发布,生产 ASR 已切换至 `dashscope/qwen3-asr-flash`,旧 `siliconflow/TeleAI/TeleSpeechASR` 已停用。生产接口获得正确转写与受控 `ossId`,Android HBuilderX 标准基座“按住说话”请求成功,线上日志无 403/ASR 异常。回滚目录为 `/opt/wygj/backups/asr-hotfix-20260726210122`;本次未发布静态资源、未变更 schema,也不代表完整包一致性或原生 App 全项验收。
|
||||
- **2026-07-26,移动端编译链升级与 uni-ui 解析修复,未发布**:本机 HBuilderX GUI 已升级为 `5.22.2026071707`,CLI 为 `5.22.2026072503-alpha`;`mobile-uni` 的 App、App-Plus、H5 与 Vite 编译包统一到对应的 `3.0.0-alpha-5020220260725001` 批次,消除 5.15/5.21 混用产生的两套 `uni-cli-shared` 状态隔离,使登录页 `<uni-forms>`、`<uni-easyinput>` 等组件重新由 `pages.json` easycom 规则正常解析。H5 与 App 均以 `Compiler version: 5.22(vue3)` 构建通过;浏览器和 vivo V2443A(Android 15)真机均确认手机号、验证码标签及输入框正常。HBuilder 5.22 标准基座 `versionName=15.22` 已自动更新并成功运行,测试手机号配合固定验证码登录成功,可进入员工首页并切换“问”页,未出现编译器/SDK 版本不匹配弹窗或组件解析告警。升级后仍须重建并安装 5.22 自定义运行基座完成原生权限全清单;`verify:app-release` 当前因缺少含正式法务 HTTPS 链接的 `androidPrivacy.json` 按预期阻断。本项未发布生产 H5,也未云打包 APK/IPA,手机上的 `com.yincheng.wygj` 正式包未被覆盖。
|
||||
- **2026-07-26,全网 AI 本地库修复,未发布**:补齐 `reset-dev-db.sh` 遗漏的全网 AI 基表、数据库密钥和千问字段三项正式迁移,并增加迁移顺序及幂等回归,避免重置本地库后再次缺失 `aihr_web_search_provider`;当前本地库为 137 张表,管理端代理下的提供方列表接口验证通过。
|
||||
|
||||
@@ -360,6 +360,12 @@ Android 人工验收每完成一个步骤,用 `.\scripts\capture-android-accep
|
||||
--approval 'ROLLBACK_BACKEND:<完整备份JAR-SHA256>'
|
||||
```
|
||||
|
||||
发布前后均可重复运行下列无登录安全探针。它只对 APK 依赖的 20 个入口发送 GET:1 个公开首页必须返回业务 `200`,6 个鉴权 GET 必须返回 `401`,13 个仅允许 POST 的入口用 GET 必须返回 `405`。`/api/knowledge/answer-feedback` 同时存在员工 POST 和运营 GET,因此归入鉴权 GET。脚本明确排除短信验证码入口,不登录、不上传、不提交业务请求;通过只证明路由存在且无登录访问失败关闭,不能替代认证态业务验收:
|
||||
|
||||
```bash
|
||||
./scripts/verify-aug1-production-api-readonly.sh
|
||||
```
|
||||
|
||||
## MVP 页面验证
|
||||
|
||||
登录后侧栏应只展示以下入口:
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
| Android 统一取证 | 新增只读脚本并通过状态变更命令回归;实跑确认前台原生隐私弹窗标题与拒绝/同意按钮可见、登录不可见、四项敏感权限为 `false`、退出记录无 Crash/ANR,证据在忽略目录 `output/aug1-rc/android-acceptance/` |
|
||||
| 首次隐私弹窗 | 最新包已在虚拟机干净启动显示原生弹窗;前一候选包的两个 HTTPS 链接已在 vivo 系统浏览器打开,仍需用户亲自执行拒绝/同意反例 |
|
||||
| 法务静态页 | `https://peilian.njzhmj.top/legal/terms.html` 与 `https://peilian.njzhmj.top/legal/privacy.html` 已部署并可访问;发布前站点备份为 `/opt/wygj/backups/www-20260728224710` |
|
||||
| 生产 API 只读契约 | 线上与候选包的 9 个关键控制器 class 哈希逐一相同,短信登录、移动首页、查/练/问、直通车和 ASR/TTS 路由均存在;未登录探针得到公开首页 `200`、5 个鉴权入口 `401`、13 个仅 POST 入口 `405`,全程未发送业务 POST、未创建账号或数据 |
|
||||
| 生产 API 只读契约 | 线上与候选包的 9 个关键控制器 class 哈希逐一相同,短信登录、移动首页、查/练/问、直通车和 ASR/TTS 路由均存在;可重复 GET-only 探针覆盖 20 个 APK 入口,得到公开首页 `1×200`、鉴权入口 `6×401`、仅 POST 入口 `13×405`。`answer-feedback` 因同路径另有运营 GET 归入鉴权组;脚本排除短信验证码,不登录、不上传、不发送业务 POST |
|
||||
| 定向发布预检 | `566f222e` 已支持只核对后端 + schema;`feece9bb` 进一步允许以冻结提交和解压后的候选 JAR 为准。两次实际只读运行均跳过管理端/H5,根站、租户、移动首页和生产禁 DDL 门禁通过;后端 + schema 模式另通过 64/64 schema。冻结物模式确认本地 JAR SHA-256 为 `8c676f2f…`,最终均仅因现网/候选模块哈希 `15f5513a…` / `c93460e8…` 不同而按预期失败 |
|
||||
| 后端发布/回滚计划 | `release-backend.sh` 固定生产 JAR 与 systemd 服务,默认只读,部署/回滚分别要求完整哈希授权串和干净工作区;备份、原子切换、发布后预检、失败恢复及并发改包保护均有回归。冻结 JAR 实跑只读计划通过:候选 `8c676f2f…`、现网 `46c99cff…`,服务 active、空间充足、三个公开入口和 64/64 schema 通过,全程未备份、上传、重启或改库 |
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SCRIPT="$ROOT_DIR/scripts/verify-aug1-production-api-readonly.sh"
|
||||
DEV_SETUP="$ROOT_DIR/docs/DEV_SETUP.md"
|
||||
AUTH_CONTROLLER="$ROOT_DIR/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/AuthController.java"
|
||||
MOBILE_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java"
|
||||
KNOWLEDGE_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/controller/AihrKnowledgeQueryController.java"
|
||||
SOP_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java"
|
||||
PRACTICE_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java"
|
||||
AGENT_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/agent/AihrAgentController.java"
|
||||
DIRECT_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/direct/AihrDirectController.java"
|
||||
SPEECH_CONTROLLER="$ROOT_DIR/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java"
|
||||
|
||||
bash -n "$SCRIPT"
|
||||
|
||||
probe_count="$(grep -Ec '^(public|auth|method)_[^|]+\|/[^|]+\|[0-9]+$' "$SCRIPT")"
|
||||
[[ "$probe_count" -eq 20 ]]
|
||||
[[ "$(grep -Ec '^public_[^|]+\|/[^|]+\|200$' "$SCRIPT")" -eq 1 ]]
|
||||
[[ "$(grep -Ec '^auth_[^|]+\|/[^|]+\|401$' "$SCRIPT")" -eq 6 ]]
|
||||
[[ "$(grep -Ec '^method_[^|]+\|/[^|]+\|405$' "$SCRIPT")" -eq 13 ]]
|
||||
|
||||
grep -Fq 'side_effects=GET-only' "$SCRIPT"
|
||||
grep -Fq 'sms-code endpoint intentionally excluded' "$SCRIPT"
|
||||
grep -Fq './scripts/verify-aug1-production-api-readonly.sh' "$DEV_SETUP"
|
||||
|
||||
grep -Fq '@PostMapping("/mobile/sms-login")' "$AUTH_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/home/{role}")' "$MOBILE_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/me")' "$MOBILE_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/query")' "$KNOWLEDGE_CONTROLLER"
|
||||
grep -Fq '@PostMapping(value = "/query-media"' "$KNOWLEDGE_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/resources/{attachmentId}/download-link")' "$KNOWLEDGE_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/position-sop")' "$SOP_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/answer-feedback")' "$SOP_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/answer-feedback")' "$SOP_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/summary-card")' "$SOP_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/scenarios")' "$PRACTICE_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/start")' "$PRACTICE_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/turn")' "$PRACTICE_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/finish")' "$PRACTICE_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/messages")' "$AGENT_CONTROLLER"
|
||||
grep -Fq '@PostMapping(value = "/messages/media"' "$AGENT_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/channels")' "$DIRECT_CONTROLLER"
|
||||
grep -Fq '@GetMapping("/mine")' "$DIRECT_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/feedback")' "$DIRECT_CONTROLLER"
|
||||
grep -Fq '@PostMapping(value = "/asr"' "$SPEECH_CONTROLLER"
|
||||
grep -Fq '@PostMapping("/tts")' "$SPEECH_CONTROLLER"
|
||||
|
||||
for forbidden in \
|
||||
'curl -X' \
|
||||
'--request' \
|
||||
'--data' \
|
||||
'--form' \
|
||||
'phonenumber=' \
|
||||
'Authorization:' \
|
||||
'/resource/sms/code'; do
|
||||
if grep -Fq -- "$forbidden" "$SCRIPT"; then
|
||||
echo "FAIL: read-only production API verifier contains forbidden request material: $forbidden" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo 'PASS: August 1 production API verifier is GET-only and excludes SMS/login side effects'
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
base_url="${AUG1_PRODUCTION_API_URL:-https://peilian.njzhmj.top/prod-api}"
|
||||
base_url="${base_url%/}"
|
||||
[[ "$base_url" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[^[:space:]]*)?$ ]] || {
|
||||
echo "verify-aug1-production-api-readonly: AUG1_PRODUCTION_API_URL must be an HTTPS URL" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
read -r -d '' probes <<'PROBES' || true
|
||||
public_mobile_home|/api/aihr/mobile/home/user|200
|
||||
auth_mobile_me|/api/aihr/mobile/me|401
|
||||
auth_position_sop|/api/knowledge/position-sop|401
|
||||
auth_practice_scenarios|/api/train/practice/scenarios|401
|
||||
auth_direct_channels|/api/aihr/direct/channels|401
|
||||
auth_direct_mine|/api/aihr/direct/mine?pageNum=1&pageSize=1|401
|
||||
method_sms_login|/auth/mobile/sms-login|405
|
||||
method_knowledge_query|/api/knowledge/query|405
|
||||
method_knowledge_media|/api/knowledge/query-media|405
|
||||
method_resource_download_link|/api/knowledge/resources/0/download-link|405
|
||||
auth_answer_feedback|/api/knowledge/answer-feedback|401
|
||||
method_summary_card|/api/knowledge/summary-card|405
|
||||
method_practice_start|/api/train/practice/start|405
|
||||
method_practice_turn|/api/train/practice/turn|405
|
||||
method_practice_finish|/api/train/practice/finish|405
|
||||
method_agent_messages|/api/aihr/agent/messages|405
|
||||
method_agent_media|/api/aihr/agent/messages/media|405
|
||||
method_direct_feedback|/api/aihr/direct/feedback|405
|
||||
method_asr|/api/ai/asr|405
|
||||
method_tts|/api/ai/tts|405
|
||||
PROBES
|
||||
|
||||
total=0
|
||||
public_count=0
|
||||
auth_count=0
|
||||
method_count=0
|
||||
|
||||
while IFS='|' read -r label path expected_code; do
|
||||
[[ -n "$label" ]] || continue
|
||||
response="$(
|
||||
curl -sS --max-time 15 \
|
||||
-H 'Accept: application/json' \
|
||||
-w $'\n__AUG1_HTTP__%{http_code}' \
|
||||
"$base_url$path"
|
||||
)" || {
|
||||
echo "verify-aug1-production-api-readonly: GET failed for $label $path" >&2
|
||||
exit 1
|
||||
}
|
||||
http_line="${response##*$'\n'__AUG1_HTTP__}"
|
||||
body="${response%$'\n'__AUG1_HTTP__*}"
|
||||
[[ "$http_line" == "200" ]] || {
|
||||
echo "verify-aug1-production-api-readonly: $label expected HTTP 200, got $http_line" >&2
|
||||
exit 1
|
||||
}
|
||||
business_code="$(
|
||||
printf '%s' "$body" |
|
||||
sed -nE 's/^[[:space:]]*\{[[:space:]]*"code"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p'
|
||||
)"
|
||||
[[ "$business_code" == "$expected_code" ]] || {
|
||||
echo "verify-aug1-production-api-readonly: $label expected business code $expected_code, got ${business_code:-missing}" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "$label=HTTP/200 business/$business_code GET $path"
|
||||
total=$((total + 1))
|
||||
case "$expected_code" in
|
||||
200) public_count=$((public_count + 1)) ;;
|
||||
401) auth_count=$((auth_count + 1)) ;;
|
||||
405) method_count=$((method_count + 1)) ;;
|
||||
*)
|
||||
echo "verify-aug1-production-api-readonly: unsupported expected code $expected_code" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done <<<"$probes"
|
||||
|
||||
[[ "$total" -eq 20 ]] || {
|
||||
echo "verify-aug1-production-api-readonly: expected 20 probes, ran $total" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$public_count" -eq 1 && "$auth_count" -eq 6 && "$method_count" -eq 13 ]] || {
|
||||
echo "verify-aug1-production-api-readonly: unexpected probe distribution $public_count/$auth_count/$method_count" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "aug1_production_api_readonly=passed total=$total public=$public_count auth_closed=$auth_count method_closed=$method_count"
|
||||
echo "side_effects=GET-only; sms-code endpoint intentionally excluded; no login, token, upload or business POST"
|
||||
Reference in New Issue
Block a user