fix(release): close fixed-code mobile auth gaps
This commit is contained in:
+2
-1
@@ -4,7 +4,8 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- **2026-07-29,Android 法务交互按企业内测体验收口,候选升级为 `0.1.12 (112)`,未发布生产**:取消启动阶段的 DCloud 原生隐私确认,统一由登录页协议勾选承担同意门槛;协议默认未勾选,服务协议与隐私政策可点击查看,未勾选时发送验证码和登录均在请求前失败关闭。两个公开 HTTPS 地址进入版本化 `.env`,npm 与 HBuilderX 云打包共用;源码、App-Plus 产物和 APK 门禁同时检查 `prompt=none`、登录法务链接、未勾选拦截、网络安全与敏感值。DCloud 独立内测 APK `com.yincheng.wygj 0.1.12 (112)` 大小 `33,921,458` 字节,SHA-256 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB`,API 35、v2 测试签名、`usesCleartextTraffic=false`;本机 Android 15/API 35 虚拟机全新安装后确认原生隐私层不可见、四项敏感权限未授权、无 Crash/ANR。干净提交 `b9e08789` 的统一 RC 为 `releaseEligible=true`:移动端 199 项、后端 698 项、类型检查、H5/App、管理端、后端打包和 APK 深检全部通过,证据位于 `output/aug1-rc/0.1.12-112-b9e08789-frozen/`。双提交交付包 `bangdao-aug1-0.1.12-112-b9e08789-ops6bf365d5.zip` 大小 `192,926,061` 字节、SHA-256 `E8014A0C5BD56B66B198710E75E2B8DDEC86002D1E3BC6CAF56FB3454A199C33`,14 个条目及内部哈希逐条通过;新后端 JAR `3F9F4506…` 的生产只读计划也通过目标、服务、公开入口、64/64 schema 和生产禁 DDL 检查,现网 JAR 仍为 `46C99CFF…`,未执行部署。该 x86_64 虚拟机对 ARM-only DCloud Weex 运行时出现 IPC 初始化超时,因此不把空白 WebView 作为页面通过;登录页链接/未勾选反例及认证业务仍须 ARM64 真机验收。本项没有发布后端、H5、数据库或新的生产静态资源。
|
||||
- **2026-07-29,移动端固定码认证与身份失败关闭收口,候选升级为 `0.1.13 (113)`,未发布生产**:生产验证只选择已存在 `app_user` 且双向唯一的在职组织主体;固定码请求、登录、非空项目范围、认证首页、训练历史/画像、五通道、我的反馈、大喇叭、考试、案例/全网只读入口、退出、旧令牌拒绝、等待防刷窗口后的再次获取和重新登录全部通过,应用日志确认两次验证码请求均命中“未真实发送短信”分支。脚本不输出手机号、固定码或令牌,不触发自动注册,不提交反馈、训练、附件、ASR/TTS 或 Agent 请求。后端 `/api/aihr/mobile/me` 与认证首页遇到身份缺失、组织查询异常、无在职身份或后台账号时改为失败关闭;前端角色和岗位加载失败清空缓存并展示明确错误,不再回退普通员工、陈旧岗位或公开首页。移动端 199 项、`ruoyi-aihr` 700 项和类型检查通过;DCloud 云端证书 APK 大小 `33,921,543` 字节、SHA-256 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0`,API 35、v2 测试签名、禁止明文 HTTP、包内业务资源/登录法务/身份失败关闭和敏感值深检通过。5 个场景、30 道题与五通道处理人统一签认单已落地,但正式内容仍为 `0/5`、`0/30`,五个处理角色有效绑定均为 `0`。本项未发布后端、H5、数据库或生产静态资源;新 APK 还须在干净提交上完成统一 RC 冻结。
|
||||
- **2026-07-29,Android 法务交互按企业内测体验收口,候选升级为 `0.1.12 (112)`,未发布生产**:取消启动阶段的 DCloud 原生隐私确认,统一由登录页协议勾选承担同意门槛;协议默认未勾选,服务协议与隐私政策可点击查看,未勾选时发送验证码和登录均在请求前失败关闭。两个公开 HTTPS 地址进入版本化 `.env`,npm 与 HBuilderX 云打包共用;源码、App-Plus 产物和 APK 门禁同时检查 `prompt=none`、登录法务链接、未勾选拦截、网络安全与敏感值。DCloud 独立内测 APK `com.yincheng.wygj 0.1.12 (112)` 大小 `33,921,458` 字节,SHA-256 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB`,API 35、v2 测试签名、`usesCleartextTraffic=false`;本机 Android 15/API 35 虚拟机全新安装后确认原生隐私层不可见、四项敏感权限未授权、无 Crash/ANR。干净提交 `b9e08789` 的统一 RC 为 `releaseEligible=true`:移动端 199 项、后端 698 项、类型检查、H5/App、管理端、后端打包和 APK 深检全部通过,证据位于 `output/aug1-rc/0.1.12-112-b9e08789-frozen/`。双提交交付包 `bangdao-aug1-0.1.12-112-b9e08789-ops6bf365d5.zip` 大小 `192,926,061` 字节、SHA-256 `E8014A0C5BD56B66B198710E75E2B8DDEC86002D1E3BC6CAF56FB3454A199C33`,14 个条目及内部哈希逐条通过;新后端 JAR `3F9F4506…` 的生产只读计划也通过目标、服务、公开入口、64/64 schema 和生产禁 DDL 检查,现网 JAR 仍为 `46C99CFF…`,未执行部署。当前 AVD 为 `x86_64`,APK 只含 `arm64-v8a/armeabi-v7a`,DCloud Weex 在跨架构转译时实际报 `spinWaitPeer timeout`、`sender is null` 和 IPC timeout;390×844 Playwright 验证仅作为布局、链接和请求前拦截的补充证据,不替代 App 运行或认证业务验收,也不因此修改发布 ABI、增加降级链或启用兜底内容。证据位于 `output/playwright/aug1-login-consent/`。本项没有发布后端、H5、数据库或新的生产静态资源。
|
||||
- **2026-07-28 至 2026-07-29,前一版 Android 受控内测候选包与法务链路,历史总体 No-Go**:补齐登录页协议默认不勾选与法务地址失败关闭、App-Plus 原生隐私配置生成/校验、APK 包名/版本/API/签名/哈希/包内资源一致性/敏感值门禁、DCloud 云证书自定义基座和独立测试包流程;《服务协议》《隐私政策》静态页已部署到 `/legal/`,发布前整站备份为 `/opt/wygj/backups/www-20260728224710`。客户端法务校验已移除 App-Plus 不保证支持的浏览器 `URL` 构造器,测试验证码也不再以源码常量进入安装包;五个文件选择服务与九个消费页面进一步统一“用户取消/平台失败”分流,取消后不再残留上传或转写错误;直通车首次弱网加载失败后可原位重试,失败时清空旧通道并禁止提交。当时独立 APK `com.yincheng.wygj 0.1.10 (110)` 的 `targetSdkVersion=35`,SHA-256 为 `20CB16B4E9D024DBF5EEEF577BFBA3B4DF5E5E78B597BABB99CB7450C7AE5034`;Android 原生网络安全配置经 APK 反编译确认 `usesCleartextTraffic=false`、`networkSecurityConfig` 引用和系统证书信任配置均已进入安装包。本机 `Medium_Phone` Android 15/API 35 虚拟机已完成从 `0.1.9` 保留数据覆盖升级和冷启动,真实显示原生隐私弹窗,四项敏感权限保持未授权且未见 FATAL/ANR;`0.1.6` 已在 vivo V2443A(Android 15)完成覆盖安装和冷启动。代码候选提交 `dc20b920` 已在干净工作区通过统一 RC 门禁,`releaseEligible=true`:移动端 198 项测试、类型检查、H5/App 构建、管理端构建、`ruoyi-aihr` 698 项全量测试、后端打包及 APK 强化门禁全部通过;最小交付包 SHA-256 为 `4BAA79B2CA2C4D486B6194288D90022D5F994A4D8C8E05D7C2DDD87F9BF5B3B7`,解压后 9 个文件、其中 8 个载荷逐文件复验通过。只读生产预检确认根站、租户接口、移动首页和 64/64 schema 门禁通过,但生产后端模块、管理端和移动 H5 与本地候选仍不一致;用户隐私选择、权限反例、认证态核心业务和正式内容也未完成,故当时 Go/No-Go 为 **No-Go**。本项没有新增生产发布、服务重启、数据库或静态资源变更。
|
||||
- **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。本项只记录用户选择前状态,未代替用户点击隐私选项。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- 品牌:管理端与员工端统一显示“帮道”,继续使用现有红色品牌图标。
|
||||
- 基础链路:租户列表、验证码、`000000 / admin / admin123` 加密登录已验证。
|
||||
- 管理端:除 AI 面试、训练、案例、SOP 和资料处理外,已包含大喇叭发布、单文件提炼与多岗位解读、五渠道直通反馈收件箱、内容/任务运营、问题榜运营、知识空间、全网 AI 配置和组织权限;员工个人训练、主管复盘、团队难题、成长证据与训练录音仅在认证 APP 按项目范围访问。
|
||||
- 用户侧:`mobile-uni/` 承载员工、候选人和主管三端;“问”已改为数字师傅 Agent,文字、ASR 转写和图片/视频统一先做意图规划,再按服务端权限调用知识、训练、待办、记忆、媒体或全网工具,寒暄和不完整表达不再机械落入 RAG。现有能力还包括手机号登录、30 分钟短期多轮、原文件/视频受控交付、大喇叭公司文件摘要/下载/多岗位解读/追问、五渠道直通反馈、“记一下”确认卡、三级字号、训练/题目反馈、问题榜与积分、岗位考试、案例、候选面试/资料、主管复盘/派发及学员模式。Agent 代码已随 2026-07-25 完整包部署;2026-07-29 当前候选包为 `0.1.12 (112)`,启动阶段不再展示 DCloud 原生隐私确认,法务同意统一由登录页承担:协议默认未勾选、两个正文可点击、未勾选时验证码和登录均失败关闭。包体已通过 API 35、登录法务链接、包内资源一致性、敏感值、禁止明文 HTTP 和网络安全资源门禁;本机 Android 15/API 35 虚拟机全新安装后确认无原生隐私弹窗、四项敏感权限未授权且无 Crash/ANR。提交 `b9e08789` 的统一 RC 证据为 `releaseEligible=true`,移动端 199 项、`ruoyi-aihr` 698 项测试及全部构建通过。该 x86_64 虚拟机无法可靠运行仅含 ARM ABI 的 DCloud Weex 页面,因此登录页点击和认证业务仍须在 ARM64 真机完成;正式内容仍为 0/0,生产后端、业务签认和发布授权也未闭环,当前 Go/No-Go 仍为 No-Go。旧 `mobile/` 只用于历史回溯。
|
||||
- 用户侧:`mobile-uni/` 承载员工、候选人和主管三端;“问”已改为数字师傅 Agent,文字、ASR 转写和图片/视频统一先做意图规划,再按服务端权限调用知识、训练、待办、记忆、媒体或全网工具,寒暄和不完整表达不再机械落入 RAG。现有能力还包括手机号登录、30 分钟短期多轮、原文件/视频受控交付、大喇叭公司文件摘要/下载/多岗位解读/追问、五渠道直通反馈、“记一下”确认卡、三级字号、训练/题目反馈、问题榜与积分、岗位考试、案例、候选面试/资料、主管复盘/派发及学员模式。Agent 代码已随 2026-07-25 完整包部署;2026-07-29 当前 DCloud 云端证书内测 APK 为 `0.1.13 (113)`,SHA-256 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0`,启动阶段不展示 DCloud 原生隐私确认,法务同意统一由登录页承担。该版进一步让认证身份、角色和组织岗位异常失败关闭,不再回退为普通员工或公开首页;移动端 199 项、`ruoyi-aihr` 700 项、类型检查、App-Plus 资源和 APK 深检已通过。生产现有在职 APP 账号的固定码链路已经完整通过首次获取/登录、认证只读主路径、退出、旧令牌失效、等待防刷窗口后的再次获取和重新登录,应用日志确认两次均未真实发送短信。当前 x86_64 AVD 对仅含 ARM ABI 的 DCloud Weex 运行时出现 IPC 超时;浏览器截图只作布局和交互补充证据,不替代 App 运行、认证业务或设备能力验收,也不因此修改发布 ABI、增加降级路径或启用兜底内容。正式可发布场景仍为 `0/5`、有据标准答案为 `0/30`,五个处理角色有效绑定均为 `0`;生产后端、业务签认和发布授权尚未闭环,当前 Go/No-Go 仍为 No-Go。旧 `mobile/` 只用于历史回溯。
|
||||
- 英雄路径:AI 面试(真模型出题/评分)、三角色对练(真 LLM 客户回复+评分,ASR/TTS 配置后支持语音)、案例沉淀(真实语音上传+ASR 转写+模型整理)均为真模型优先、未配置或失败回退本地兜底;生产短音频 ASR 当前为阿里云百炼 `qwen3-asr-flash`,SiliconFlow audio 接口保留兼容。SOP 知识库已接中文关键词 + MySQL Fulltext + Qdrant 混合召回 + rerank 重排,支持 txt/md/PDF/Word/Excel/PPT/图片/视频上传解析入库(图片走 vision OCR,视频走 ffmpeg 抽音轨转写+关键帧描述),资料处理中心支持 ZIP 异步展开后逐文件入队;模型配置页可查看向量库维度/点数并一键重建。
|
||||
- 资料处理:已新增解析任务状态页,按 `aihr_knowledge_attach` 和 `aihr_knowledge_fragment` 展示等待解析、解析中、已完成、失败、片段数和向量化状态;任务列表按当前筛选结果前端分页(10/20/50 条,筛选或刷新回到第一页)。页面只保留“批量导入”,通过异步队列处理多文件或 ZIP,不提供浏览器目录选择和服务端目录导入入口。后端目录导入接口仅保留为运维/调试能力。
|
||||
- 组织人员:`aihr_org_snapshot` 本地 reset 保留演示 seed;生产通过 `POST /api/aihr/org/sync` 同步外部组织。2026-07-25 记录的生产快照为 3417 人、2938 人在职且均有姓名、3392 个手机号映射、175 名在职主管和 20 名项目经理;姓名通过稳定 `employee_id` 定向补齐,未执行全量覆盖。当前上游 `employee_number` 与既有主体不一致且手机号大量脱敏,修复契约前禁止强制全量同步;人数属于时点事实,发布或验收前应重新查询。
|
||||
|
||||
+23
-19
@@ -92,10 +92,11 @@ public class AihrMobileController {
|
||||
}
|
||||
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||
String identity = currentAppUsername();
|
||||
if (loginUser == null
|
||||
|| !UserType.APP_USER.getUserType().equals(loginUser.getUserType())
|
||||
|| identity.isBlank()) {
|
||||
return R.ok(mobileSeedService.publicHome(role));
|
||||
if (loginUser == null || identity.isBlank()) {
|
||||
throw new ServiceException("移动端登录身份缺失,请重新登录");
|
||||
}
|
||||
if (!UserType.APP_USER.getUserType().equals(loginUser.getUserType())) {
|
||||
throw new ServiceException("后台管理账号不能使用移动端员工首页");
|
||||
}
|
||||
String resolvedIdentity = identity;
|
||||
if (supervisorRoleRequested(role)) {
|
||||
@@ -114,36 +115,39 @@ public class AihrMobileController {
|
||||
public R<IdentityResponse> me() {
|
||||
String phone = currentAppUsername();
|
||||
if (phone.isBlank()) {
|
||||
return R.ok(new IdentityResponse("user", "员工端", "", "一线", List.of()));
|
||||
throw new ServiceException("移动端登录身份缺失,请重新登录");
|
||||
}
|
||||
// APP username is a phone number. Validate it before it reaches the
|
||||
// organization lookup so a phone-shaped external ID cannot borrow
|
||||
// another employee's role or project scope.
|
||||
mobileSeedService.requireMobileIdentity(phone);
|
||||
List<OrgPersonRow> people = List.of();
|
||||
var row = java.util.Optional.<OrgPersonRow>empty();
|
||||
List<OrgPersonRow> people;
|
||||
try {
|
||||
var snapshotPeople = orgSyncService.activeByMobilePhone(phone);
|
||||
people = snapshotPeople;
|
||||
row = snapshotPeople.stream()
|
||||
.filter(person -> "主管".equals(person.positionLevel()) || "项目经理".equals(person.positionLevel()))
|
||||
.findFirst()
|
||||
.or(() -> snapshotPeople.stream().findFirst());
|
||||
people = orgSyncService.activeByMobilePhone(phone);
|
||||
} catch (RuntimeException ex) {
|
||||
log.warn("mobile identity lookup failed, fallback to employee role(处理错误已隐藏)");
|
||||
log.warn("mobile identity lookup failed closed: {}", ex.getClass().getSimpleName());
|
||||
throw new ServiceException("组织身份加载失败,请稍后重试");
|
||||
}
|
||||
if (row.isEmpty()) {
|
||||
return R.ok(new IdentityResponse("user", "员工端", "", "一线", List.of()));
|
||||
return R.ok(identityResponse(people));
|
||||
}
|
||||
|
||||
static IdentityResponse identityResponse(List<OrgPersonRow> people) {
|
||||
if (people == null || people.isEmpty()) {
|
||||
throw new ServiceException("未找到有效的在职组织身份,请联系管理员");
|
||||
}
|
||||
var person = row.get();
|
||||
var person = people.stream()
|
||||
.filter(item -> "主管".equals(item.positionLevel()) || "项目经理".equals(item.positionLevel()))
|
||||
.findFirst()
|
||||
.or(() -> people.stream().findFirst())
|
||||
.orElseThrow(() -> new ServiceException("未找到有效的在职组织身份,请联系管理员"));
|
||||
boolean supervisor = "主管".equals(person.positionLevel()) || "项目经理".equals(person.positionLevel());
|
||||
return R.ok(new IdentityResponse(
|
||||
return new IdentityResponse(
|
||||
supervisor ? "supervisor" : "user",
|
||||
supervisor ? "主管端" : "员工端",
|
||||
person.positionName(),
|
||||
person.positionLevel(),
|
||||
projectOptions(people)
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
static List<ProjectResponse> projectOptions(List<OrgPersonRow> people) {
|
||||
|
||||
+26
-1
@@ -1,12 +1,14 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import org.dromara.aihr.domain.AihrOrgSyncDto.OrgPersonRow;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@Tag("dev")
|
||||
class AihrMobileControllerTest {
|
||||
@@ -27,8 +29,31 @@ class AihrMobileControllerTest {
|
||||
assertEquals("星河湾一期", projects.get(1).projectName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityResponsePrefersSupervisorAndKeepsProjectOptions() {
|
||||
var rows = List.of(
|
||||
row(1L, "P2", "星河湾二期", "一线"),
|
||||
row(2L, "P1", "星河湾一期", "主管")
|
||||
);
|
||||
|
||||
var response = AihrMobileController.identityResponse(rows);
|
||||
|
||||
assertEquals("supervisor", response.role());
|
||||
assertEquals("主管端", response.roleLabel());
|
||||
assertEquals(List.of("P1", "P2"), response.projects().stream().map(item -> item.projectCode()).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityResponseFailsClosedWithoutActiveOrganizationRows() {
|
||||
assertThrows(ServiceException.class, () -> AihrMobileController.identityResponse(List.of()));
|
||||
}
|
||||
|
||||
private static OrgPersonRow row(Long id, String projectCode, String projectName) {
|
||||
return row(id, projectCode, projectName, "一线");
|
||||
}
|
||||
|
||||
private static OrgPersonRow row(Long id, String projectCode, String projectName, String positionLevel) {
|
||||
return new OrgPersonRow(id, projectCode, projectName, "客服部", "employee-1", "王敏",
|
||||
"生活顾问", "一线", "active", "2026-07-21");
|
||||
"生活顾问", positionLevel, "active", "2026-07-21");
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -1602,8 +1602,11 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertTrue(controllerSource.contains("mobileSeedService.practiceAlerts(supervisorScopeExtPartyId())"));
|
||||
assertTrue(controllerSource.contains("mobileSeedService.createPracticeAssignment(request, supervisorScopeExtPartyId())"));
|
||||
assertTrue(controllerSource.contains("orgSyncService.activeByMobilePhone(phone)"));
|
||||
assertTrue(controllerSource.contains("mobile identity lookup failed, fallback to employee role"));
|
||||
assertTrue(controllerSource.contains("mobile identity lookup failed closed"));
|
||||
assertTrue(controllerSource.contains("catch (RuntimeException ex)"));
|
||||
assertTrue(controllerSource.contains("throw new ServiceException(\"组织身份加载失败,请稍后重试\")"));
|
||||
assertTrue(controllerSource.contains("throw new ServiceException(\"未找到有效的在职组织身份,请联系管理员\")"));
|
||||
assertFalse(controllerSource.contains("fallback to employee role"));
|
||||
assertTrue(controllerSource.contains("private String supervisorScopeExtPartyId()"));
|
||||
assertTrue(controllerSource.contains("if (isSystemOperator())"));
|
||||
assertTrue(controllerSource.contains("后台管理账号不能使用主管带教接口"));
|
||||
@@ -1623,6 +1626,11 @@ public class AihrPracticeSeedServiceTest {
|
||||
assertTrue(controllerSource.contains("if (!LoginHelper.isLogin())"));
|
||||
assertTrue(controllerSource.contains("return R.ok(mobileSeedService.publicHome(role))"));
|
||||
assertTrue(controllerSource.contains("!UserType.APP_USER.getUserType().equals(loginUser.getUserType())"));
|
||||
assertTrue(controllerSource.contains("throw new ServiceException(\"后台管理账号不能使用移动端员工首页\")"));
|
||||
assertEquals(
|
||||
controllerSource.indexOf("return R.ok(mobileSeedService.publicHome(role))"),
|
||||
controllerSource.lastIndexOf("return R.ok(mobileSeedService.publicHome(role))")
|
||||
);
|
||||
assertTrue(controllerSource.contains("return R.ok(mobileSeedService.home(role, resolvedIdentity))"));
|
||||
assertTrue(controllerSource.contains("mobileSeedService.requireMobileSupervisorIdentity(identity)"));
|
||||
assertTrue(controllerSource.contains("mobileSeedService.requireMobileIdentity(identity)"));
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
| 组织人员增量同步 | `POST /api/aihr/org/sync-changes` | 主动拉取上游 `/sync/changes?resource_type=employee`,即使事件没有进入 outbox、`changed_fields` 为空,也会按 `resource_id` 回源 `/employees/{id}`,并结合任职快照只替换受影响员工。主体固定使用稳定 `employee.id`;上游只返回脱敏手机号时保留本地既有有效手机号,上游显式清空手机号时同步清除本地值(对应移动端登录身份失效),手机号字段整体缺失则拒绝写入;范围字段明确不一致视为迁出,写入时删除该员工本地旧记录,范围字段缺失则保守跳过不删。任职快照失败、资源 ID 不一致或项目关系重复时拒绝写入。首次调用必须传 `sinceTime`,后续可传响应的 `nextCursor`;仍须先 `dryRun=true` 再以相同起点执行 `dryRun=false`,且只处理当前租户有效组织绑定范围。 |
|
||||
| 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;移动专用接口固定服务端默认租户,客户端不传也不能选择 `tenantId`;验证码按“默认租户 + 手机号”隔离。校验在手机号粒度的分布式锁内完成:仅匹配成功才消费,输错不会作废原验证码。移动端令牌只关联 `app_user`;若同一手机号存在任何后台/系统账号(即使同时存在 `app_user`),一律拒绝登录而不复用或并置身份;手机号完全不存在时才自动注册 `app_user`。`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`)。prod 默认关闭,试点期只有同时设置 `AIHR_SMS_DEV_FIXED_CODE` 与 `AIHR_SMS_PROD_FIXED_CODE_ENABLED=true` 才启用固定码。 |
|
||||
| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 未登录请求只返回不读取租户业务统计的公开首屏 seed;已登录移动端请求自动携带 `Authorization/clientid`,才返回员工/主管真实统计;移动端本地 fallback 保演示 |
|
||||
| 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后仅以手机号精确查询 `person_phone`(`activeByMobilePhone`,不使用组织模糊搜索);`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端。响应返回去重后的 `projects[]` 供页面按项目名称选择,不要求用户输入或记忆编码;同一人员在多个项目的快照行按 `tenant_id + project_code + ext_party_id` 保留。接口失败回退员工端。 |
|
||||
| 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后仅以手机号精确查询 `person_phone`(`activeByMobilePhone`,不使用组织模糊搜索);`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端。响应返回去重后的 `projects[]` 供页面按项目名称选择,不要求用户输入或记忆编码;同一人员在多个项目的快照行按 `tenant_id + project_code + ext_party_id` 保留。登录身份缺失、组织查询异常或没有有效在职主体时一律明确失败关闭,不再静默回退员工端。 |
|
||||
| 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/practice/mistakes`、`/profile`;训练完成后提交 `POST /api/aihr/mobile/practice/satisfaction`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;服务端固定身份和 `mode=mobile` 后才写入 `aihr_practice_session` 并进入员工/主管统计。员工历史、错题、画像、成长进度、晋升证据和满意度均只使用当前认证手机号经服务端验证得到的受控历史别名;后台内容/任务运营只通过独立 `/api/train/practice/**` 契约,不得向 `/api/aihr/mobile/**` 传入外部 ID 读取个人或主管数据。主管 `/practice/team` 的范围只含已验证外部 ID;历史训练或任务若存的是同一员工的旧手机号,只在该手机号能无碰撞规范化到团队外部 ID 时才展示/统计,避免把另一人的外部 ID 误并入团队。管理端 `mode=preview` 仅是运营调试记录,不能伪装成员工训练、满意度、复盘、成长或试点证据。满意度接口只接受本人已完成训练的 1-5 分,意见脱敏后落库,未填写不补默认值。错题本按员工本人聚合低分/红线回合,并关联已有 `retry` assignment,不伪造错题结论。主管 `/practice/team` 在同一项目权限范围内额外返回 `mistakes` 聚合,按场景/归因统计次数、影响人数、平均分和最近发生时间;普通员工返回“无主管权限”。主管接口以后端当前登录手机号先解析为在职组织外部 ID,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度。每日三题正式只对 `hire_date` 在当前日期前三个月内的在职员工派发,且只会从已发布、已启用、风险审核完成的同岗位场景题库取题;选择时优先补最低未覆盖的成长层/能力项,仅作为训练推荐,不自动调整职级或形成硬性解锁。没有入职日期时不使用训练次数推断,开发 Demo 的旧回退只有在 `dev/local` profile 且由 `aihr.practice.allow-legacy-daily-drill-fallback` 显式开启时生效,生产 profile 强制关闭。 |
|
||||
| 移动端候选人闭环 | 页面拆为 `/pages/candidate/index/index`、`/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish` 和 `GET /records`;资料 `POST/GET /api/aihr/mobile/candidate/materials`;预习 `POST /api/knowledge/search`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;首页只按当前手机号对应的真实面试记录和最新资料状态分流,不读取公开 home seed。APP 候选人身份以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;面试支持文字作答和复用 `/api/ai/asr` 的录音转文字/选音频,`/answer` 可带可选 `answerAudioOssId`,服务端只接受当前候选人名下处于 `staged/bound` 状态的音频并将 `ossId` 写入问题快照,不向客户端暴露原始 OSS URL;`/answer` 与 `/finish` 还会校验当前 APP 手机号与启动会话的候选人 ID 一致,未知或他人会话直接拒绝;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 |
|
||||
| 每日题反馈与常见难题 | `POST /api/aihr/mobile/practice/assignments/{id}/feedback`、`GET /api/aihr/mobile/practice/difficulties`、`POST /api/aihr/mobile/practice/difficulties/assign` | 反馈区分题目有用性与答案正确性;仅认证 APP 主管可按其项目范围聚合岗位、时间、次数、人数和均分,并基于难题创建专项。管理端不得调用移动主管接口;后台人工任务只走受角色保护的 `/api/train/practice/assignments/manage/**`。 |
|
||||
@@ -91,7 +91,7 @@ curl -fsS -X POST "$API_BASE/api/aihr/agent/actions/<draftId>/dismiss" \
|
||||
|
||||
当前管理端 AI 面试和案例沉淀已改为真实模型/ASR 优先;移动端首页和部分演示态数据仍保留 fallback。知识库、模型能力、文档解析、RAG、chat 按 [ruoyi-ai 能力分片迁移计划](RUOYI_AI_INCREMENTAL_MIGRATION.md) 逐片引入;知识库 DDL 与住宅类 SOP seed 在 `backend/script/sql/aihr_knowledge_mysql8.sql`,模型 DDL 在 `backend/script/sql/aihr_model_mysql8.sql`,训练记录 DDL 在 `backend/script/sql/aihr_practice_mysql8.sql`,AI 面试结果 DDL 在 `backend/script/sql/aihr_interview_result_mysql8.sql`,候选人资料 DDL 在 `backend/script/sql/aihr_candidate_material_mysql8.sql`,组织人员快照表在 `backend/script/sql/aihr_org_snapshot_mysql8.sql`,本地 reset 带 2 个住宅项目 22 人 seed;配置开放组织同步系统后用 `POST /api/aihr/org/sync` 覆盖为外部快照。
|
||||
|
||||
直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer <access_token>`;浏览器内通过已登录前端和 `/dev-api` 代理访问。移动端登录接口为 `POST /auth/mobile/sms-login`,请求 `{ phonenumber, smsCode }`,内部固定使用 app 客户端 `428a8310cd442757ae699df5d894f051` 和 `sms` grant;验证码通过后若手机号不存在,会创建 `app_user`,用户名为手机号,备注为“移动端短信自动注册”。移动端首页接口 `GET /api/aihr/mobile/home/{role}` 仍保留 `@SaIgnore` 以保证未登录首屏可用,但匿名分支只返回不读取业务统计的公开 seed;登录后的 `mobile-uni` 会自动携带移动端 token,服务端才返回真实员工/主管统计。
|
||||
直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer <access_token>`;浏览器内通过已登录前端和 `/dev-api` 代理访问。移动端登录接口为 `POST /auth/mobile/sms-login`,请求 `{ phonenumber, smsCode }`,内部固定使用 app 客户端 `428a8310cd442757ae699df5d894f051` 和 `sms` grant;验证码通过后若手机号不存在,会创建 `app_user`,用户名为手机号,备注为“移动端短信自动注册”。移动端首页接口 `GET /api/aihr/mobile/home/{role}` 仍保留 `@SaIgnore` 以保证真正未登录的首屏可用,匿名分支只返回不读取业务统计的公开数据;已登录请求必须是有效 `app_user`,并按认证手机号校验员工/主管身份后返回真实统计。已登录但身份缺失、后台账号误用、组织查询失败或客户端收到未知角色时均明确失败关闭,不得静默回到公开数据、默认员工端或旧岗位缓存。
|
||||
|
||||
阿里云短信复用 RuoYi 的 `sms.blends.config1`。本地开发把真实短信参数放根目录 `.env.local` 或外部环境变量,`scripts/dev-backend.sh` 会自动加载;`application-dev.yml` / `application-prod.yml` 只保留占位,不写真实密钥。
|
||||
|
||||
|
||||
@@ -44,7 +44,11 @@ Android 每个人工步骤完成后,可在不改变设备状态的前提下保
|
||||
|
||||
2026-07-29 又以新增的只读统一取证脚本复核同一虚拟机的当前状态:前台 Activity 为 `io.dcloud.PandoraEntryActivity`,原生隐私标题、“暂不同意”和“同意并继续”三个控件均可见,手机号登录尚不可见;相机、录音、图片和视频权限仍为 `false`,应用退出记录中未出现 Crash/ANR。截图、UI、权限、Activity、退出记录和 JSON 均落在忽略目录 `output/aug1-rc/android-acceptance/20260728-192056-privacy-before-choice/`。该次只记录选择前基线,仍未代替用户做隐私选择。
|
||||
|
||||
随后根据企业内部应用的体验决策,`0.1.12 (112)` 取消 DCloud 启动原生确认,法务同意统一放到登录页。新 APK 大小 `33,921,458` 字节,SHA-256 为 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB`;包体门禁确认 `prompt=none`、登录页两个 HTTPS 链接和未勾选拦截均已内置。在同一 Android 15/API 35 虚拟机卸载旧包后全新安装,取证结果为原生隐私层 `not-visible`、四项敏感权限未授权、进程与前台 Activity 正常且无 Crash/ANR,证据位于 `output/aug1-rc/android-acceptance/20260729-021418-no-native-privacy-fresh-install-retry/`。该 x86_64 虚拟机运行 ARM-only DCloud Weex 时出现 IPC 初始化超时,页面主体不能作为通过证据;登录页交互和认证业务仍须在 ARM64 真机完成。
|
||||
2026-07-29 当前非自定义基座内测 APK 已升级为 `0.1.13 (113)`,大小 `33,921,543` 字节,SHA-256 为 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0`。DCloud 云端证书打包成功,APK 深检通过包名、API 35、v2 测试签名、登录页法务门禁、身份失败关闭、包内资源一致性、敏感值和禁止明文 HTTP。生产固定码的首次获取/登录、退出、旧令牌失效、等待防刷窗口后的再次获取和重新登录已在非真机认证脚本中通过,两次验证码请求均确认未真实发送短信;这不替代本文件下列 Android 真机权限、系统选择器、前后台、弱网、附件和媒体链路验收。
|
||||
|
||||
随后根据企业内部应用的体验决策,`0.1.12 (112)` 取消 DCloud 启动原生确认,法务同意统一放到登录页。新 APK 大小 `33,921,458` 字节,SHA-256 为 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB`;包体门禁确认 `prompt=none`、登录页两个 HTTPS 链接和未勾选拦截均已内置。在同一 Android 15/API 35 虚拟机卸载旧包后全新安装,取证结果为原生隐私层 `not-visible`、四项敏感权限未授权、进程与前台 Activity 正常且无 Crash/ANR,证据位于 `output/aug1-rc/android-acceptance/20260729-021418-no-native-privacy-fresh-install-retry/`。当前 AVD 是 `x86_64`,而 APK 只含 `arm64-v8a/armeabi-v7a`;DCloud Weex 跨架构转译实际报 `spinWaitPeer timeout`、`sender is null` 和 IPC timeout,所以原生外壳可见但页面主体空白。该现象不证明产品页面失败,也不能作为页面通过证据;不得为此修改正式 ABI、回退 H5 壳或增加业务兜底。
|
||||
|
||||
同一 `mobile-uni` 源码的 H5 构建另在 390×844 Playwright 真浏览器补验:登录页核心表单、登录按钮和协议行均在首屏;协议默认未勾选,点击“获取验证码”或“登录”都显示“请先阅读并勾选同意”,且网络列表无业务请求;勾选后进入手机号必填校验;《服务协议》《隐私政策》分别打开 `https://peilian.njzhmj.top/legal/terms.html` 与 `https://peilian.njzhmj.top/legal/privacy.html`,页面标题和正文可读,控制台 0 错误。截图位于 `output/playwright/aug1-login-consent/`。该证据仅覆盖 Web 页面布局、链接与请求前门禁,不替代 App 运行、认证账号业务、系统录音/媒体权限或弱网验收。
|
||||
|
||||
## 已完成的 Android 基座证据
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ HBuilderX 5.22 当前实测中,自定义调试基座不支持安心打包,
|
||||
|
||||
```powershell
|
||||
& '.\mobile-uni\scripts\verify-android-apk.ps1' `
|
||||
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.12-dcloud-test.apk' `
|
||||
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.13-dcloud-test.apk' `
|
||||
-BuildKind dcloud-test
|
||||
```
|
||||
|
||||
@@ -74,13 +74,13 @@ APK 门禁还会比较包内与当前 `dist/build/app/app-service.js` 的 SHA-25
|
||||
|
||||
```powershell
|
||||
.\scripts\prepare-aug1-rc.ps1 `
|
||||
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.12-dcloud-test.apk' `
|
||||
-ApkPath '.\mobile-uni\dist\debug\bangdao-0.1.13-dcloud-test.apk' `
|
||||
-BuildKind dcloud-test
|
||||
```
|
||||
|
||||
脚本会复跑移动端测试/类型检查/H5/App 构建、管理端构建、后端定向测试和打包,验证 APK 后在被 Git 忽略的 `output/aug1-rc/` 生成标准文件名、哈希及 JSON/Markdown 证据。它不执行生产同步、数据库迁移、服务重启或登录页人工同意操作。
|
||||
|
||||
2026-07-28 最终自定义基座大小为 `21,158,193` 字节,SHA-256 为 `82D3352FF96A86F5DB4AC0C71DCB0ED6CEC98F2A785F6EAED2FE8D4FC0C5F704`。2026-07-29 当前独立内测候选包为 `bangdao-0.1.12-dcloud-test.apk`:包名 `com.yincheng.wygj`、版本 `0.1.12 (112)`、`targetSdkVersion=35`,大小 `33,921,458` 字节,SHA-256 为 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB`,签名证书 SHA-256 为 `AB06ECC0DE544DE73C385F3F0690B22966EBC12887FB8BEE1830CDBEFDEBEF5D`。APK 反编译门禁确认包内业务资源等于当前 App-Plus 编译产物、启动原生隐私提示已关闭、登录页法务链接与未勾选拦截已进入包体,并通过敏感值、`usesCleartextTraffic=false`、`networkSecurityConfig` 和系统证书信任检查。本机 Android 15/API 35 虚拟机全新安装后确认原生隐私层不可见、四项敏感权限未授权且无 Crash/ANR。统一 RC 在干净提交 `b9e08789` 上复跑移动端 199/199、`ruoyi-aihr` 698/698 及全部构建/校验门禁,证据为 `output/aug1-rc/0.1.12-112-b9e08789-frozen/`。该 x86_64 虚拟机无法可靠运行仅含 ARM ABI 的 DCloud Weex 页面,登录页和业务路径必须改由 ARM64 真机验收。该包仍使用 DCloud 云端测试证书,只能作为 8 月 1 日受控内测包;企业正式 release 还必须使用签认后的企业证书摘要。
|
||||
2026-07-28 最终自定义基座大小为 `21,158,193` 字节,SHA-256 为 `82D3352FF96A86F5DB4AC0C71DCB0ED6CEC98F2A785F6EAED2FE8D4FC0C5F704`。2026-07-29 当前独立内测候选包为 `bangdao-0.1.13-dcloud-test.apk`:包名 `com.yincheng.wygj`、版本 `0.1.13 (113)`、`targetSdkVersion=35`,大小 `33,921,543` 字节,SHA-256 为 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0`,签名证书 SHA-256 为 `AB06ECC0DE544DE73C385F3F0690B22966EBC12887FB8BEE1830CDBEFDEBEF5D`。APK 反编译门禁确认包内业务资源等于当前 App-Plus 编译产物、启动原生隐私提示已关闭、登录页法务链接与未勾选拦截及身份加载失败关闭已进入包体,并通过敏感值、`usesCleartextTraffic=false`、`networkSecurityConfig` 和系统证书信任检查。`0.1.12` 及其 RC/交付 ZIP 只保留为上一冻结基线;`0.1.13` 仍须在干净提交上完成统一 RC 才能标记为新冻结候选。当前 AVD 为 `x86_64`,APK 原生库仅含 `arm64-v8a/armeabi-v7a`;安装和原生外壳可由系统转译运行,但 DCloud Weex JS 进程实际出现 `spinWaitPeer`/IPC 超时,页面主体空白不能用于判断产品页面。浏览器验证只作补充,不替代 App 运行;不会为了模拟器改变正式 ABI、回退为 H5 壳或增加业务兜底。该包仍使用 DCloud 云端测试证书,只能作为 8 月 1 日受控内测包;企业正式 release 还必须使用签认后的企业证书摘要。
|
||||
|
||||
企业正式签名 APK 还必须用发布负责人保管的证书摘要执行 release 模式核验;该模式会额外拒绝 `debuggable=true`、明文 HTTP 和签名摘要不一致:
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
| [帮道8月1日Android内测Go-No-Go-20260728.md](帮道8月1日Android内测Go-No-Go-20260728.md) | 更新至 2026-07-29 的当前候选 APK、版本冻结、生产依赖、真机与内容门禁 Go/No-Go 结论 |
|
||||
| [生活顾问调研转化清单-20260728.md](生活顾问调研转化清单-20260728.md) | 将仓库外原始访谈脱敏映射为待业务审核的 SOP、知识、题目和对练候选池 |
|
||||
| [帮道8月1日生活顾问内容候选审核包-20260729.md](帮道8月1日生活顾问内容候选审核包-20260729.md) | 5 个脱敏场景与 30 道政策问题的待审候选包;明确 0 个正式场景、0 道正式来源标准答案及业务签认路径 |
|
||||
| [帮道8月1日业务内容与五通道签认单-20260729.md](帮道8月1日业务内容与五通道签认单-20260729.md) | 5 个场景、30 道问题和五通道真实处理人的统一业务签认入口;当前默认全部待签认 |
|
||||
| [FIGMA_DESIGN_INVENTORY.md](FIGMA_DESIGN_INVENTORY.md) | Figma 页面节点索引、设计批次范围、取消项和全文件一致性审计结果 |
|
||||
| [FIGMA_DESIGN_SYSTEM_GUIDE.md](FIGMA_DESIGN_SYSTEM_GUIDE.md) | 员工端通用视觉元素、业务元素、状态映射和 LLM 组页规则 |
|
||||
| [FIGMA_IMPLEMENTATION_MATRIX.md](FIGMA_IMPLEMENTATION_MATRIX.md) | 70 张正式画板与代码/线上实现状态、二期边界和剩余动作 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 帮道 8 月 1 日 Android 内测 Go/No-Go
|
||||
|
||||
> 评估时间:2026-07-29 10:31(CST)
|
||||
> 评估时间:2026-07-29 11:52(CST)
|
||||
>
|
||||
> 评估对象:2026-08-01 受控内部测试 APK,不代表正式发布、应用商店上架或阶段一严格试点验收。
|
||||
>
|
||||
@@ -10,46 +10,49 @@
|
||||
|
||||
**当前为 No-Go,暂不能把现有 APK 标记为 8 月 1 日可分发版本。**
|
||||
|
||||
安装包的技术出包链和干净提交自动化冻结已经成立,可继续作为验收候选包使用;阻断点集中在生产依赖、人工原生/业务验收和业务签认,而不是 HBuilderX 或 DCloud 打包本身。
|
||||
`0.1.13` DCloud 云端证书 APK 已生成并通过深检,加入认证身份失败关闭和可重复固定码重新登录门禁;还需在干净提交上完成统一 RC 冻结。阻断点集中在生产后端未更新、正式业务内容/处理人签认、发布授权和真机设备验收,而不是 HBuilderX 或 DCloud 打包本身。
|
||||
|
||||
## 已通过
|
||||
|
||||
| 检查项 | 当前结果 |
|
||||
|---|---|
|
||||
| 当前运行时冻结 | 移动端候选提交 `b9e08789265b73524e3ce2646abe2f2fc4565906` 的统一 RC 证据 `releaseEligible=true`,目录 `output/aug1-rc/0.1.12-112-b9e08789-frozen/`;前一 `dc20b920` 双提交包只保留追溯,不能再作为当前 APK |
|
||||
| 当前运行时冻结 | `0.1.13 (113)` APK 已生成并通过深检,但尚未在干净提交上完成统一 RC;因此最后一个 `releaseEligible=true` 的历史冻结仍是 `output/aug1-rc/0.1.12-112-b9e08789-frozen/`,不能把其提交或后端 JAR 哈希写成 `0.1.13` 证据 |
|
||||
| 移动端自动化 | 199 项单元测试通过;TypeScript 类型检查通过;登录页协议默认未勾选,法务链接、验证码和登录共用失败关闭门禁;五个文件选择器和九个消费页面已覆盖用户取消/平台失败分流,直通车首次弱网加载失败可原位重试并失败关闭 |
|
||||
| 构建 | `mobile-uni` H5、App-Plus、管理端生产构建通过;后端 `ruoyi-admin.jar` 重建通过 |
|
||||
| P0 后端语义 | `AihrSopSeedServiceTest` 39 项、`AihrPracticeSeedServiceTest` 102 项通过;`ruoyi-aihr` 698 项全量测试通过,0 失败、0 错误 |
|
||||
| P0 后端语义 | `AihrSopSeedServiceTest` 39 项、`AihrPracticeSeedServiceTest` 102 项通过;`ruoyi-aihr` 700 项全量测试通过,0 失败、0 错误;新增 2 项覆盖有效主管身份与空身份失败关闭 |
|
||||
| 内容候选门禁 | 已生成 5 个禁用场景候选和 30 道待审政策题,机器校验通过;正式可发布场景 `0`、正式有据标准答案 `0`,不得导入或启用 |
|
||||
| App 资源门禁 | 正式法务 URL、登录页同意门禁、原生 `prompt=none`、API 35、图标和启动图检查通过 |
|
||||
| 独立 APK | `com.yincheng.wygj`,`0.1.12 (112)`,`targetSdkVersion=35`,v2 签名,明文 HTTP 关闭且网络安全资源已进入 APK;SHA-256 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB` |
|
||||
| APK 内容门禁 | 包内业务资源等于当前 App-Plus 编译产物;原生提示关闭、登录法务链接、未勾选拦截、手机号、固定测试验证码、私钥和常见令牌扫描通过 |
|
||||
| 独立 APK | `com.yincheng.wygj`,`0.1.13 (113)`,`targetSdkVersion=35`,v2 测试签名,明文 HTTP 关闭且网络安全资源已进入 APK;大小 `33,921,543` 字节,SHA-256 `FA2D2A6DC84AE759714E6D16D146C1E1552A0F3F53C046069422012361D1D0C0` |
|
||||
| APK 内容门禁 | 包内业务资源等于当前 App-Plus 编译产物;原生提示关闭、登录法务链接、未勾选拦截、身份失败关闭、手机号、固定测试验证码、私钥和常见令牌扫描通过 |
|
||||
| 真机安装启动 | vivo V2443A(Android 15)完成 `0.1.6` 覆盖安装和冷启动;相机、录音、媒体权限保持未授权,未见 `FATAL EXCEPTION` |
|
||||
| 虚拟机安装与生命周期 | 本机 `Medium_Phone` Android 15/API 35 已全新安装 `0.1.12`;启动原生隐私层不可见、四项敏感权限为 `false`、未见 Crash/ANR。该 x86_64 虚拟机无法可靠运行 ARM-only DCloud Weex 页面,不能替代页面与业务验收 |
|
||||
| 虚拟机安装与生命周期 | 本机 `Medium_Phone` Android 15/API 35 已全新安装 `0.1.12`;启动原生隐私层不可见、四项敏感权限为 `false`、未见 Crash/ANR。AVD 为 `x86_64`、APK 仅含 ARM ABI,Weex 跨架构转译实际报 `spinWaitPeer`/IPC timeout;原生外壳证据有效,页面主体既不判失败也不判通过,不向发布包增加 x86 ABI或 H5 兜底 |
|
||||
| Android 统一取证 | 只读脚本确认 `0.1.12 (112)` 进程和前台 Activity 正常、原生隐私层 `not-visible`、四项敏感权限为 `false`、退出记录无 Crash/ANR,证据在 `output/aug1-rc/android-acceptance/20260729-021418-no-native-privacy-fresh-install-retry/` |
|
||||
| 登录页法务策略 | 启动不再弹原生确认;登录页协议默认未勾选,两个 HTTPS 正文可点击,验证码与登录在未勾选时失败关闭。包体与自动化已通过,ARM64 真机点击反例仍待完成 |
|
||||
| 登录页法务策略 | 启动不再弹原生确认,包体门禁通过;390×844 Playwright 真浏览器实测协议默认未勾选、两个 HTTPS 正文、验证码/登录请求前失败关闭和勾选后手机号校验,控制台 0 错误。该结果只作 Web 交互补充,不替代 App 认证业务验收 |
|
||||
| 生产固定码认证 | 运行时确认固定码和生产显式开关同时启用;生产应用日志确认首次和重新登录前的两次验证码请求均命中“未真实发送短信”分支。只选择已存在 `app_user` 的双向唯一在职主体,不触发自动注册,不输出手机号、固定码或令牌 |
|
||||
| 认证后非真机主路径 | `scripts/verify-aug1-authenticated-production.sh --execute` 通过身份/非空项目范围、认证首页、训练历史/画像、五通道、我的反馈、大喇叭、考试、案例/全网能力只读入口、退出、旧令牌业务码 401、防刷窗口后的再次获取、重新登录、重新读取身份和再次退出;五通道返回数为 5。未提交业务写入、附件、ASR/TTS 或 Agent 请求 |
|
||||
| 法务静态页 | `https://peilian.njzhmj.top/legal/terms.html` 与 `https://peilian.njzhmj.top/legal/privacy.html` 已部署并可访问;发布前站点备份为 `/opt/wygj/backups/www-20260728224710` |
|
||||
| 生产 API 只读契约 | 线上与候选包的 9 个关键控制器 class 哈希逐一相同,短信登录、移动首页、查/练/问、直通车和 ASR/TTS 路由均存在;可重复 GET-only 探针覆盖 20 个 APK 入口,得到公开首页 `1×200`、鉴权入口 `6×401`、仅 POST 入口 `13×405`。`answer-feedback` 因同路径另有运营 GET 归入鉴权组;脚本排除短信验证码,不登录、不上传、不发送业务 POST |
|
||||
| 生产 API 只读契约 | 对 `0.1.12` 冻结候选,线上与包内 9 个关键控制器 class 哈希逐一相同;当前 `0.1.13` 源码已修改 `AihrMobileController`,使登录身份与组织异常失败关闭,因此该“逐字节相同”结论不再适用于新后端候选。可重复 GET-only 探针仍覆盖 20 个 APK 入口;固定码认证脚本另完成认证后只读主路径 |
|
||||
| 定向发布预检 | `566f222e` 已支持只核对后端 + schema;`feece9bb` 进一步允许以冻结提交和解压后的候选 JAR 为准。两次实际只读运行均跳过管理端/H5,根站、租户、移动首页和生产禁 DDL 门禁通过;后端 + schema 模式另通过 64/64 schema。冻结物模式确认本地 JAR SHA-256 为 `8c676f2f…`,最终均仅因现网/候选模块哈希 `15f5513a…` / `c93460e8…` 不同而按预期失败 |
|
||||
| 后端发布/回滚计划 | `release-backend.sh` 固定生产 JAR 与 systemd 服务,默认只读,部署/回滚分别要求完整哈希授权串和干净工作区;备份、原子切换、发布后预检、失败恢复及并发改包保护均有回归。当前 `b9e08789` 候选 JAR 实跑只读计划通过:候选 `3f9f4506…`、现网 `46c99cff…`,服务 active、空间充足、三个公开入口、64/64 schema 和生产禁 DDL 通过,全程未备份、上传、重启或改库 |
|
||||
|
||||
## No-Go 阻断项
|
||||
|
||||
1. **生产接口兼容,但候选后端语义修复未部署。** 只读预检确认生产根站、租户接口、移动首页和 64/64 schema 门禁通过;线上与候选 `ruoyi-aihr` 都有 819 个非目录/非 Manifest 条目,0 个增删、775 个逐字节相同、44 个不同。9 个关键控制器及其路由逐字节一致,差异只落在 `AihrModelSeedService`、`AihrPracticeSeedService`、`AihrSopSeedService` 及编译生成的内部类:候选包补了“不得冒充已创建/派发工单”的兜底文案,以及场景发布前的占位内容/五维评分门禁。因此现网可以承接 APK 的既有接口,但尚不满足本次候选的业务语义红线,仍需获得授权后定向发布后端。管理端和移动 H5 入口资源也与候选不一致,但不属于独立 APK 本轮必发单元;schema 本批不变。此次仅上线法务静态页,未发布后端、H5 业务资源或数据库迁移。
|
||||
2. **原生人工反例未完成。** 全新安装、无启动原生隐私提示和权限基线已通过,媒体取消状态机已自动化修复;仍缺 ARM64 真机上的登录页协议默认未勾选、两个链接、未勾选拦截、勾选后登录、短信失败与重新登录、麦克风允许/拒绝、真实系统选择器取消、登录后前后台切换和弱网验证。
|
||||
3. **核心业务真机闭环未完成。** 仍缺认证账号下完整“查/练/问”、五个直通渠道、五个正式对练场景、ASR/TTS、附件上传下载和越权反例。
|
||||
4. **内容与业务验收不足。** 当前仅收到 1/24 份生活顾问访谈。技术侧已把调研整理为 30 道政策题候选和 5 个场景候选,但全部保持待审/禁用,正式可发布场景和正式有据标准答案仍为 0/0;五渠道真实处理人也没有业务签认证据。
|
||||
1. **生产接口兼容,但当前后端修复未部署。** 生产固定码登录和现有认证只读接口可用,根站、租户接口、移动首页和 64/64 schema 门禁此前通过;当前源码除“不得冒充已创建/派发工单”和场景发布门禁外,又让移动身份缺失、组织查询异常、无在职身份和后台账号访问员工首页失败关闭。新后端 JAR 尚未冻结、发布或完成发布后匹配,生产仍保留旧语义;获得单独授权前不得部署。管理端和移动 H5 不属于独立 APK 本轮必发单元,schema 本批不变。
|
||||
2. **剩余认证问题已收敛为真机设备能力。** 固定码首次请求、登录、退出、旧令牌失效、等待防刷窗口后的再次请求和重新登录均已通过,两次请求都由日志确认没有真实短信发送。尚未完成的麦克风允许/拒绝、真实系统选择器取消、App 前后台和真实断网恢复均属于 Android 真机门禁,不能用 H5 结果代替。
|
||||
3. **核心业务的非写入认证入口已通过,正式业务闭环仍受内容和处理人阻断。** 认证账号下身份、项目范围、首页、训练历史/画像、五通道列表、我的反馈、大喇叭、考试、案例和全网能力只读入口均已通过;正式“练”仍没有可发布的 5 个场景,五通道仍没有真实收件人,附件和 ASR/TTS 仍属真机/媒体验收。不得通过创建测试内容或绑定任意用户制造假闭环。
|
||||
4. **内容与业务验收不足。** 当前仅收到 1/24 份生活顾问访谈。技术侧已把调研整理为 30 道政策题候选和 5 个场景候选,并提供统一签认单,但全部保持待审/禁用,正式可发布场景和正式有据标准答案仍为 0/0。生产只读审计确认 `direct_president/direct_finance/direct_hr/direct_audit/direct_operations` 五个角色的有效用户绑定数均为 0,必须由业务负责人指定主处理人和替补后再验收。
|
||||
5. **发布与回滚尚未签认。** 固定目标、默认只读、显式授权、备份、原子切换、失败恢复和并发保护的后端工具已经落地,冻结候选的只读发布计划也已通过;但尚无负责人发布授权、实际发布后匹配结果或回滚演练记录。当前 APK 已关闭明文流量但仍使用 DCloud 测试证书,只能用于受控内测;企业签名和公司法务正式签认仍是正式发布门槛。
|
||||
|
||||
## 转为 Go 的最短路径
|
||||
|
||||
| 顺序 | 必须完成 | 验收证据 |
|
||||
|---|---|---|
|
||||
| 已完成 | 确定 APK、后端、管理端/H5、数据库四个发布单元并冻结当前 APK 运行时和运维审计快照 | 运行时 `b9e08789`、operations `6bf365d5`、`releaseEligible=true`;双提交 ZIP `bangdao-aug1-0.1.12-112-b9e08789-ops6bf365d5.zip` 含 14 个条目和内部 `SHA256SUMS.txt`,外层 SHA-256 `E8014A0C5BD56B66B198710E75E2B8DDEC86002D1E3BC6CAF56FB3454A199C33`;旧 `0.1.10` ZIP 只保留追溯 |
|
||||
| 1 | 获得明确授权后使用已通过只读计划的 `release-backend.sh deploy`,只发布上述 3 个服务类语义修复所在的候选后端 JAR;管理端/H5 延期,schema 本批不变;不得手工覆盖或绕过脚本失败恢复 | 候选 JAR SHA-256 `3F9F4506ED138A35F81FA84428BE4FBF5981D5D6D0F96AE2769F485D904E1CE3`;输出备份目录,生产 `ruoyi-aihr` 模块与候选匹配,64/64 schema 继续通过,兜底回答不显示已创建/已派发状态 |
|
||||
| 2 | 用户在 ARM64 Android 真机完成登录页法务门禁和权限正反例 | 默认未勾选、两个链接、未勾选拦截、勾选后登录、权限允许/拒绝;不记录手机号、验证码和个人内容 |
|
||||
| 3 | 用正式测试账号完成“查/练/问”、五直通渠道及附件/语音闭环 | 每条用例的时间、结果、失败截图或日志 |
|
||||
| 4 | 业务方审核当前 30 道题和 5 个场景候选,补齐正式来源并签认五渠道处理人 | 审核单、正式内容版本、授权矩阵;正式可发布计数从 0/0 提升到至少 5/30 |
|
||||
| 已完成 | 确定 APK、后端、管理端/H5、数据库四个发布单元并生成 `0.1.13` DCloud 云端证书 APK | `0.1.13 (113)` APK SHA-256 `FA2D2A6D…`,API 35、v2 测试签名和深检通过;`0.1.12` ZIP 只作上一冻结基线 |
|
||||
| 1 | 在干净提交上冻结 `0.1.13` APK 与后端 JAR;获得明确授权后才使用 `release-backend.sh deploy` 发布内容语义和移动身份失败关闭修复;管理端/H5 延期,schema 本批不变 | 新 RC `releaseEligible=true`、新候选 JAR 哈希、只读发布计划、备份目录、生产 `ruoyi-aihr` 模块匹配和 64/64 schema;不得手工覆盖或绕过失败恢复 |
|
||||
| 已完成 | 使用生产现有在职 APP 主体和固定码完成非真机认证、退出、重新获取、重新登录及认证后只读主路径;固定码请求不真实发送短信 | `scripts/verify-aug1-authenticated-production.sh --execute` 全部通过,五通道数为 5,旧令牌按业务码 401 拒绝,两次验证码请求均有“未真实发送”日志;执行过程不输出手机号、固定码或令牌 |
|
||||
| 2 | 业务方按统一签认单审核 30 道题和 5 个场景候选,补齐正式来源;同时指定五渠道主处理人和替补 | [业务内容与五通道签认单](帮道8月1日业务内容与五通道签认单-20260729.md)、正式内容版本、授权矩阵;正式可发布计数从 0/0 提升到至少 5/30,五个角色有效绑定均不少于 2 |
|
||||
| 3 | 绑定获批处理人后完成五通道正例、跨通道反例和一次正式回复;用正式内容完成“查/练/问”业务写入闭环 | 每条用例的时间、角色、结果和脱敏证据;不记录手机号、验证码或反馈正文 |
|
||||
| 4 | Android 真机完成麦克风、系统选择器、前后台、弱网、附件和 ASR/TTS 门禁 | 当前 APK 哈希对应的设备证据;不得用 H5 或模拟器空白 WebView 代替 |
|
||||
| 5 | 7 月 31 日复跑安装、升级、回滚和完整 Go/No-Go | RC APK 哈希、签认人、回滚点、最终结论 |
|
||||
|
||||
公司法务签认与企业签名可在负责人明确接受“仅限小范围受控内部测试”的前提下不阻断技术验收,但不能省略登录页协议门禁、权限反例、版本冻结、生产依赖匹配和核心业务闭环。
|
||||
|
||||
@@ -90,27 +90,31 @@
|
||||
|
||||
| 门禁 | 结果 | 说明 |
|
||||
|---|---|---|
|
||||
| `mobile-uni` 单元测试 | 通过 | 198/198;覆盖文件选择器取消/失败分流、九个消费页面、直通车首次弱网恢复及版本一致性,测试过程有本机 WebSocket 端口占用告警,但无失败 |
|
||||
| `mobile-uni` 单元测试 | 通过 | 199/199;覆盖身份/岗位加载失败关闭、文件选择器取消/失败分流、九个消费页面、直通车首次弱网恢复及版本一致性,测试过程有本机 WebSocket 端口占用告警,但无失败 |
|
||||
| `mobile-uni` TypeScript 检查 | 通过 | `npm --prefix mobile-uni run typecheck` |
|
||||
| H5 资源构建 | 通过 | 仅证明 H5 资源可编译 |
|
||||
| App-Plus 资源构建 | 通过 | 仅生成 HBuilderX 出包资源,不等同于 APK |
|
||||
| App 原生配置、图标与启动图校验 | 通过 | 固定包名、语义版本、API 35、双 ABI、启动权限策略、Camera/Record 模块、禁止明文 HTTP、网络安全资源、22 个图标和 14 个启动图 |
|
||||
| 独立内测 APK 结构与签名校验 | 通过 | `verify-android-apk.ps1 -BuildKind dcloud-test` 已核对包名、版本、`targetSdkVersion`、网络安全配置、包内业务资源、单签名、v2 签名和 SHA-256 |
|
||||
| `390×844` 未登录首屏与入口 | 通过 | 今日主操作、练、问、问制度快捷项、大喇叭和直通车登录门禁均在首屏可见;练、问已实际点击可达,无控制台错误 |
|
||||
| 登录后核心业务路径 | 未执行 | 当前没有可用的本地 APP 登录主体和后端会话;不得用伪造手机号或未认证预览冒充通过 |
|
||||
| 登录后非真机认证路径 | 通过 | 生产固定码模式已显式开启;脚本只读选择已存在 `app_user` 的双向唯一在职主体,首次请求验证码后登录,退出并确认旧令牌 401,等待 60 秒防刷窗口后再次请求固定码、重新登录、重新读取身份并退出。两次验证码请求均由应用日志确认未真实发送短信,不触发自动注册;身份/项目范围、认证首页、训练历史/画像、五通道、我的反馈、大喇叭、考试、案例/全网能力只读入口通过,手机号、固定码和令牌均未输出或落盘。业务写入、媒体和真机路径不由此结论替代 |
|
||||
| 管理端生产构建 | 通过 | 存在大 chunk 告警,不阻断本轮构建 |
|
||||
| 工单边界后端定向测试 | 通过 | 39 个测试,0 失败、0 错误 |
|
||||
| `ruoyi-aihr` 后端全量测试 | 通过 | 698 个测试,0 失败、0 错误;包含正式场景占位来源/回合/Rubric 禁止发布的新增回归;Windows CRLF 引发的 7 个源码契约误判已通过读取时统一 LF 修复并全量复跑 |
|
||||
| `ruoyi-aihr` 后端全量测试 | 通过 | 700 个测试,0 失败、0 错误;包含正式场景占位来源/回合/Rubric 禁止发布和移动身份空结果失败关闭回归;Windows CRLF 引发的 7 个源码契约误判已通过读取时统一 LF 修复并全量复跑 |
|
||||
| 法务配置技术链路 | 通过 | 登录协议默认不勾选;协议未配置时禁止验证码与登录;配置生成器和发布联网校验已接入,发布包拒绝开发验证码提示 |
|
||||
| 正式 App 发布配置校验 | **技术通过,法务签认阻断** | 受控内测版协议正文已通过 Caddy 公网 HTTPS 发布;启动原生提示关闭,登录页协议默认未勾选且验证码/登录失败关闭,包体与联网门禁使用同一组地址。公司法务尚未正式签认正文,不能视为正式发布通过 |
|
||||
| Android 设备、调试基座、测试签名和真机门禁 | **部分通过,仍阻断** | HBuilderX 5.22 已识别 vivo V2443A(Android 15);API 35 自定义基座和可独立安装的 DCloud 云证书内测 APK 均已生成。`0.1.12` 在 Android 15 虚拟机全新安装后确认无原生隐私提示、四项敏感权限未授权且无 Crash/ANR;媒体选择取消与直通车首次弱网恢复状态机已自动化修复。该虚拟机为 x86_64,不能完成 ARM-only DCloud 页面验收;登录页法务门禁、录音允许/拒绝、真实系统选择器取消、真实断网恢复和完整业务反例仍需 ARM64 真机 |
|
||||
| Android 设备、调试基座、测试签名和设备门禁 | **部分通过,仍阻断** | API 35 自定义基座和可独立安装的 DCloud 云证书内测 APK 均已生成。`0.1.12` 在 Android 15 虚拟机全新安装后确认无原生隐私提示、四项敏感权限未授权且无 Crash/ANR;390×844 Playwright 真浏览器已通过登录页默认未勾选、两个线上协议、验证码/登录请求前拦截及勾选后手机号校验,控制台 0 错误。AVD 的 x86_64 与 APK 的 ARM-only ABI 导致 DCloud Weex IPC 超时;浏览器结果只作补充,不替代 App 运行,也不修改发布 ABI、回退 H5 壳或增加业务兜底。录音允许/拒绝、真实系统选择器取消、真实断网恢复和认证业务反例仍待完成 |
|
||||
|
||||
统一 RC 脚本已在干净运行时提交 `dc20b92061f10a356ed73eb105ef7b00c8b8a617` 上完整复跑,证据目录为 `output/aug1-rc/0.1.10-110-dc20b920-frozen/`,结果为 `releaseEligible=true`。发布/回滚、API 探针和可识别隐私二次确认/退出态的 Android 取证工具另冻结在 operations commit `71f005c22981989c5f2b24fa31c385a1b82657b7`。当前双提交交付包为 `output/aug1-release/bangdao-aug1-0.1.10-110-dc20b920-ops71f005c2.zip`,大小 `192,926,422` 字节,SHA-256 为 `4391895420B50EBBD8CB87EA20AF71A70D5DABAD5003F5361A8B034CF7525BCA`;已完成 14 个 ZIP 条目和 13 个 `SHA256SUMS.txt` 载荷的逐条复验。旧无 operations 后缀包、`ops861fa0ea` 与 `ops414af66e` 包保留作追溯但均已被替代。上述本地自动化冻结不替代生产后端发布授权和人工验收。
|
||||
|
||||
2026-07-29 根据企业内部应用体验决策,当前候选升级为 `0.1.12 (112)`,取消启动原生隐私确认,法务同意统一由登录页协议勾选承担。干净提交 `b9e08789265b73524e3ce2646abe2f2fc4565906` 的统一 RC 证据位于 `output/aug1-rc/0.1.12-112-b9e08789-frozen/`,结果为 `releaseEligible=true`;移动端 199 项、`ruoyi-aihr` 698 项及全部构建、APK 门禁通过。APK 大小 `33,921,458` 字节,SHA-256 为 `DD24E5F20BC7C064F5059E538A1FAB7518DF46CBAB502F59A3629F0B1B1F5EBB`。以 operations commit `6bf365d5911fbe982ba059b1b4388d9692c989db` 生成的新双提交包为 `output/aug1-release/bangdao-aug1-0.1.12-112-b9e08789-ops6bf365d5.zip`,大小 `192,926,061` 字节,SHA-256 为 `E8014A0C5BD56B66B198710E75E2B8DDEC86002D1E3BC6CAF56FB3454A199C33`,14 个 ZIP 条目和内部哈希已逐条复验。包内候选后端 JAR SHA-256 为 `3F9F4506ED138A35F81FA84428BE4FBF5981D5D6D0F96AE2769F485D904E1CE3`,生产只读发布计划通过目标、服务、公开入口、64/64 schema 和生产禁 DDL 检查;现网 JAR 哈希仍不同,未执行发布。前述 `0.1.10` 双提交包仅保留追溯,不能再作为当前 APK 分发。
|
||||
|
||||
同日当前源码候选继续升级为 `0.1.13 (113)`:认证身份、组织角色和岗位加载异常改为失败关闭,不再回退普通员工、陈旧岗位或公开首页;生产固定码的首次获取/登录、退出、旧令牌拒绝、等待防刷窗口后的再次获取和重新登录完整通过,两次请求均确认未真实发送短信。移动端 199 项、`ruoyi-aihr` 700 项和 App-Plus 资源门禁通过。`0.1.13` 新 APK、后端 JAR、统一 RC 和交付 ZIP 必须另行冻结;在新证据完成前,上一 `0.1.12` 仍是最后一个已冻结安装包,不得把旧哈希写成新源码的证据。
|
||||
|
||||
生活顾问调研已转成 `docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json`:包含 5 个禁用场景候选和 30 道待审政策题,内容候选校验脚本通过。由于当前只收到 1/24 份访谈,且正式来源、业务审核和 Rubric 签认尚未完成,正式可发布场景与正式有据标准答案均为 `0`;交付包中的候选文件只供审核,不得直接导入或启用。
|
||||
|
||||
业务回填入口已统一为 `docs/帮道8月1日业务内容与五通道签认单-20260729.md`。2026-07-29 生产只读审计确认五个直通角色均存在,但有效用户绑定数分别为 0;系统功能可用不等于真实收件人已配置,技术侧不得擅自绑定人员。
|
||||
|
||||
### 3.3 Android 真机预检快照(2026-07-28)
|
||||
|
||||
- HBuilderX CLI `5.22.2026072503-alpha` 已识别唯一连接的 vivo V2443A,标准调试基座完成“编译 → 同步 → 启动”,当前 App-Plus 页面可在真机正常渲染。
|
||||
@@ -306,7 +310,7 @@
|
||||
|
||||
### 6.2 核心业务路径
|
||||
|
||||
- [ ] 手机号登录、验证码错误、登录失效重登符合预期。
|
||||
- [x] 非真机手机号固定码登录、退出、旧令牌失效和再次获取固定码重新登录符合预期;固定码请求命中“不真实发送短信”分支,执行过程不输出账号、验证码或令牌。
|
||||
- [x] 查/练/问在 `390×844` 首屏无需滚动即可进入;学、大喇叭和直通车现有入口可达。
|
||||
- [ ] 至少 5 个正式生活顾问场景可开始、完成、评分并进入主管复盘。
|
||||
- [ ] “问制度”标准问题有引用,无依据问题明确拒绝编造。
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# 帮道 8 月 1 日业务内容与五通道签认单
|
||||
|
||||
> 适用版本:2026-08-01 Android 受控内测
|
||||
>
|
||||
> 技术准备状态:`READY_FOR_BUSINESS_SIGNOFF`
|
||||
>
|
||||
> 业务签认状态:`PENDING`
|
||||
>
|
||||
> 数据边界:本文件不记录手机号、验证码、账号、访问令牌、住户个案或其他个人敏感信息。
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
技术侧已经把生活顾问调研转成可逐项审核的 5 个场景候选和 30 道问题候选,并完成五通道接口、固定码登录和认证后只读主路径验证。但业务方尚未提供正式制度来源、内容审核结论和五通道处理人,因此当前不能启用候选内容,也不能把“通道可选”解释成“有人收件”。
|
||||
|
||||
截至 2026-07-29 的签认基线:
|
||||
|
||||
| 项目 | 当前事实 | 本单完成条件 |
|
||||
|---|---:|---|
|
||||
| 已收生活顾问访谈 | 1/24 | 其余访谈可在内测后继续补,但本轮 5 个正式场景必须有独立正式依据 |
|
||||
| 场景候选 | 5 | 5/5 完成来源、红线、Rubric 和审核签认后才能发布 |
|
||||
| 正式可发布场景 | 0/5 | 不得用 seed、访谈经验或 AI 生成答案替代 |
|
||||
| 问题候选 | 30 | 30/30 绑定正式文件和预期引用/拒答行为 |
|
||||
| 正式有据标准答案 | 0/30 | 来源、版本、适用范围与审核人齐全 |
|
||||
| 直通车通道 | 5/5 可见 | 每个通道至少绑定 1 名主处理人和 1 名替补 |
|
||||
| 生产有效处理人绑定 | 5 个角色均为 0 | 绑定后完成本通道正例、跨通道反例和一次正式回复 |
|
||||
|
||||
候选事实源:
|
||||
|
||||
- [生活顾问内容候选审核包](帮道8月1日生活顾问内容候选审核包-20260729.md)
|
||||
- [`aug1-life-advisor-content-candidates-v0.1.json`](content-candidates/aug1-life-advisor-content-candidates-v0.1.json)
|
||||
- [生活顾问调研转化清单](生活顾问调研转化清单-20260728.md)
|
||||
|
||||
## 2. 五个场景签认
|
||||
|
||||
任何一行未完成时,该场景继续保持 `enabled=false`、`publishable=false`,不得进入员工端。
|
||||
|
||||
| 候选 ID | 场景 | 正式文件编号/版本/生效日 | 适用项目或业态 | 红线与升级路径已确认 | Rubric 审核 | 审核结论 | 审核人/日期 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `LC-AUG1-SC-001` | 拒绝提供定位信息时的报修受理 | 待业务填写 | 待业务填写 | 待签认 | 待签认 | `PENDING` | 待签认 |
|
||||
| `LC-AUG1-SC-002` | 物业费明细与服务争议沟通 | 待业务填写 | 待业务填写 | 待签认 | 待双人签认 | `PENDING` | 待签认 |
|
||||
| `LC-AUG1-SC-003` | 维修延期后的解释与升级跟进 | 待业务填写 | 待业务填写 | 待签认 | 待签认 | `PENDING` | 待签认 |
|
||||
| `LC-AUG1-SC-004` | 情绪化停车冲突投诉的降级处理 | 待业务填写 | 待业务填写 | 待签认 | 待双人签认 | `PENDING` | 待签认 |
|
||||
| `LC-AUG1-SC-005` | 收费沟通与服务整改并行闭环 | 待业务填写 | 待业务填写 | 待签认 | 待双人签认 | `PENDING` | 待签认 |
|
||||
|
||||
审核规则:
|
||||
|
||||
1. 正式依据必须是公司制度、已发布 SOP、有效公示或经授权的项目规则;访谈只用于发现主题。
|
||||
2. 高风险场景必须由两名不同审核人复核同一内容版本。
|
||||
3. 收费、优惠、分期、报警、隐私和承诺时限不得由模型自行补充。
|
||||
4. 场景来源、回合、五维评分说明和风险等级任何一项含占位内容时,发布门禁必须拒绝。
|
||||
5. 审核结论只允许 `APPROVED` 或 `REJECTED`;未填写一律按 `PENDING`。
|
||||
|
||||
## 3. 三十道问题签认
|
||||
|
||||
30 道问题的 ID、问题文本和预期边界行为以候选 JSON 为准,本单不复制答案。每一类均需完成 6/6,合计 30/30 后才能作为正式回归集。
|
||||
|
||||
| 类别 | 数量 | 必交证据 | 当前结论 | 审核人/日期 |
|
||||
|---|---:|---|---|---|
|
||||
| `NORMAL` | 6 | 当前有效文件、精确引用、适用范围 | `PENDING` | 待签认 |
|
||||
| `SYNONYM` | 6 | 与对应正常题相同的来源和一致结论 | `PENDING` | 待签认 |
|
||||
| `CONFLICT` | 6 | 冲突文件、版本优先级和停止自行裁决规则 | `PENDING` | 待签认 |
|
||||
| `NO_EVIDENCE` | 6 | 无依据时的明确拒答文案,不生成政策事实 | `PENDING` | 待签认 |
|
||||
| `UNAUTHORIZED` | 6 | 权限矩阵和越权命中为零的反例 | `PENDING` | 待签认 |
|
||||
|
||||
正式题单必须另附:问题 ID、标准答案、来源附件 ID、引用片段 ID、文件版本、生效日期、适用项目、审核结论和实际回归结果。不得把模型临时回答写回为标准答案。
|
||||
|
||||
## 4. 五通道处理人和权限签认
|
||||
|
||||
生产只读审计确认五个角色均已存在,但当前每个角色的有效用户绑定数都是 0。处理人身份由业务负责人指定,技术侧不得擅自把任意后台用户绑定为真实收件人。
|
||||
|
||||
| 通道 | 角色代码 | 主处理人 | 替补处理人 | 本通道列表/详情正例 | 其他四通道越权反例 | 一次正式回复 | 业务签认 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 总裁 | `direct_president` | 待指定 | 待指定 | 待验收 | 待验收 | 待验收 | `PENDING` |
|
||||
| 财务 | `direct_finance` | 待指定 | 待指定 | 待验收 | 待验收 | 待验收 | `PENDING` |
|
||||
| 人力 | `direct_hr` | 待指定 | 待指定 | 待验收 | 待验收 | 待验收 | `PENDING` |
|
||||
| 审计 | `direct_audit` | 待指定 | 待指定 | 待验收 | 待验收 | 待验收 | `PENDING` |
|
||||
| 运营 | `direct_operations` | 待指定 | 待指定 | 待验收 | 待验收 | 待验收 | `PENDING` |
|
||||
|
||||
每个通道的验收动作固定为:
|
||||
|
||||
1. 业务负责人签认主处理人和替补,由系统管理员绑定对应角色。
|
||||
2. 处理人只能看到本通道;其他通道列表、详情和回复必须拒绝。
|
||||
3. `superadmin` 仅作系统管理全局能力,不代替五个真实业务处理人。
|
||||
4. 员工匿名提交时,处理端不显示提交人;系统仍保留内部审计身份。
|
||||
5. 同一反馈只允许一次正式回复,不扩展为多轮聊天、转派或 SLA。
|
||||
6. 验收记录只写角色、时间、结果和证据编号,不写员工手机号或反馈正文。
|
||||
|
||||
## 5. 已完成的非真机技术证据
|
||||
|
||||
`scripts/verify-aug1-authenticated-production.sh --execute` 已使用生产现有 `app_user` 和双向唯一在职组织身份完成首次登录和重新登录,测试值只存在执行进程内:
|
||||
|
||||
- 首次获取验证码命中固定码分支,退出后等待 60 秒防刷窗口,再次获取固定码;应用日志确认两次均“未真实发送短信”;
|
||||
- 登录返回有效移动端会话,`/api/aihr/mobile/me` 返回有效角色和非空项目范围;
|
||||
- 认证首页、训练历史、画像、五通道列表、我的反馈、大喇叭、考试、案例能力和全网能力只读入口通过;
|
||||
- 五通道列表恰好返回 5 个通道;
|
||||
- 首次退出成功,旧令牌再次访问时按业务码 401 拒绝;第二次固定码登录、身份读取和退出再次通过;
|
||||
- 脚本只选择已存在 `app_user`,不会触发手机号自动注册,不提交反馈、训练、附件、ASR/TTS 或 Agent 业务请求。
|
||||
|
||||
这组证据完成登录与认证只读链路,不替代 Android 真机媒体权限、系统选择器、前后台和弱网验收,也不替代本单的内容与处理人签认。
|
||||
|
||||
## 6. 最终签认
|
||||
|
||||
只有以下全部完成,业务部分才可标记为 `APPROVED`:
|
||||
|
||||
- [ ] 5/5 场景完成正式来源、红线、Rubric 和审核签认;
|
||||
- [ ] 30/30 问题形成有据标准答案与权限/拒答回归证据;
|
||||
- [ ] 五个处理角色分别绑定主处理人和替补;
|
||||
- [ ] 五通道正例、跨通道反例和一次正式回复全部通过;
|
||||
- [ ] 内容运营确认候选版本、导入版本和员工可见版本一致;
|
||||
- [ ] 发布负责人确认本单对应的后端、内容和权限变更已纳入回退点。
|
||||
|
||||
| 签认角色 | 结论 | 签认人 | 日期 | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| 业务总负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||
| 知识内容负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||
| 训练内容负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||
| 五通道业务负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||
| 发布负责人 | `PENDING` | 待签认 | 待签认 | |
|
||||
@@ -19,6 +19,7 @@
|
||||
- 可导入或启用:`否`
|
||||
|
||||
机器可核验候选数据见 [`content-candidates/aug1-life-advisor-content-candidates-v0.1.json`](content-candidates/aug1-life-advisor-content-candidates-v0.1.json)。
|
||||
业务来源、审核结论和五通道处理人统一在[《帮道 8 月 1 日业务内容与五通道签认单》](帮道8月1日业务内容与五通道签认单-20260729.md)回填;签认单未完成前,本候选包继续保持不可发布。
|
||||
|
||||
## 五个候选场景
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aihr-mobile-uni",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aihr-mobile-uni",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-alpha-5020220260725001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "aihr-mobile-uni",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"appid" : "__UNI__B36D8BE",
|
||||
"description" : "帮道员工端",
|
||||
"vueVersion" : "3",
|
||||
"versionName" : "0.1.12",
|
||||
"versionCode" : "112",
|
||||
"versionName" : "0.1.13",
|
||||
"versionCode" : "113",
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
|
||||
@@ -240,6 +240,14 @@ const goAfterLogin = async () => {
|
||||
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
|
||||
};
|
||||
|
||||
const resumeAfterLogin = async () => {
|
||||
try {
|
||||
await goAfterLogin();
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '组织身份加载失败,请重试';
|
||||
}
|
||||
};
|
||||
|
||||
const sendCode = async () => {
|
||||
if (sending.value || countdown.value > 0) return;
|
||||
if (!ensureLegalConsent()) return;
|
||||
@@ -302,7 +310,7 @@ const login = async () => {
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
if (isLoggedIn()) {
|
||||
void goAfterLogin();
|
||||
void resumeAfterLogin();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { MobileIdentity, RoleKey } from '@/types/api';
|
||||
import { apiRequest, isAuthApiError } from './api';
|
||||
import {
|
||||
clearOrgPosition,
|
||||
getSelectedPosition,
|
||||
hasOrgPosition,
|
||||
isSupervisorLearnerMode,
|
||||
leaveSupervisorLearnerMode,
|
||||
@@ -43,14 +42,17 @@ export const routeForMobileRole = (role: RoleKey, requested: string) => {
|
||||
export const hydrateMobileRole = async (): Promise<RoleKey> => {
|
||||
try {
|
||||
const identity = await fetchMobileIdentity();
|
||||
const role = identity.role === 'supervisor' ? 'supervisor' : 'user';
|
||||
if (identity.role !== 'user' && identity.role !== 'supervisor') {
|
||||
throw new Error('组织角色无效,请联系管理员');
|
||||
}
|
||||
const role = identity.role;
|
||||
uni.setStorageSync(roleKey, role);
|
||||
if (role !== 'supervisor') leaveSupervisorLearnerMode();
|
||||
return role;
|
||||
} catch {
|
||||
// ponytail: role lookup failure fails closed; never reuse a stale supervisor cache.
|
||||
} catch (error) {
|
||||
uni.removeStorageSync(roleKey);
|
||||
return 'user';
|
||||
clearOrgPosition();
|
||||
throw error instanceof Error ? error : new Error('组织角色加载失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,7 +91,11 @@ export const hydratePositionFromOrg = async (): Promise<UserPosition | ''> => {
|
||||
const result = await fetchOrgPositionResult();
|
||||
if (result.status === 'matched') return result.position;
|
||||
if (result.status === 'unmatched') return '';
|
||||
return hasOrgPosition() ? getSelectedPosition() : '';
|
||||
clearOrgPosition();
|
||||
if (result.status === 'unauthorized') {
|
||||
throw new Error('登录已失效,请重新登录');
|
||||
}
|
||||
throw new Error('组织岗位加载失败,请检查网络后重试');
|
||||
};
|
||||
|
||||
export const nextUserRouteAfterLogin = async (redirect: string) => {
|
||||
|
||||
@@ -229,12 +229,22 @@ test('工作助手与查全网使用一致的来源切换头部', async () => {
|
||||
});
|
||||
|
||||
test('登录后按组织身份切换主管首页', async () => {
|
||||
const source = await pageSource('../src/services/position.ts');
|
||||
const [source, loginSource] = await Promise.all([
|
||||
pageSource('../src/services/position.ts'),
|
||||
pageSource('../src/pages/auth/login/index.vue')
|
||||
]);
|
||||
|
||||
assert.match(source, /\/api\/aihr\/mobile\/me/);
|
||||
assert.match(source, /role === 'supervisor'/);
|
||||
assert.match(source, /pages\/supervisor\/index\/index/);
|
||||
assert.match(source, /removeStorageSync\(roleKey\)/);
|
||||
assert.match(source, /identity\.role !== 'user' && identity\.role !== 'supervisor'/);
|
||||
assert.match(source, /clearOrgPosition\(\);\s*throw error/);
|
||||
assert.doesNotMatch(source, /catch\s*\{[\s\S]*return 'user'/);
|
||||
assert.doesNotMatch(source, /return hasOrgPosition\(\) \? getSelectedPosition\(\) : ''/);
|
||||
assert.match(loginSource, /const resumeAfterLogin = async \(\) => \{/);
|
||||
assert.match(loginSource, /message\.value = error instanceof Error \? error\.message : '组织身份加载失败,请重试'/);
|
||||
assert.doesNotMatch(loginSource, /void goAfterLogin\(\)/);
|
||||
});
|
||||
|
||||
test('普通员工请求主管深链时回到员工首页', async () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SCRIPT="$ROOT/scripts/verify-aug1-authenticated-production.sh"
|
||||
|
||||
contains() {
|
||||
grep -Fq -- "$2" "$1" || {
|
||||
echo "FAIL: expected '$2' in $1" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
not_contains() {
|
||||
if grep -Fq -- "$2" "$1"; then
|
||||
echo "FAIL: forbidden '$2' in $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
contains "$SCRIPT" '[[ "${1:-}" != "--execute" ]]'
|
||||
contains "$SCRIPT" 'AIHR_SMS_PROD_FIXED_CODE_ENABLED'
|
||||
contains "$SCRIPT" "u.user_type = 'app_user'"
|
||||
contains "$SCRIPT" 'HAVING COUNT(DISTINCT o.ext_party_id) = 1'
|
||||
contains "$SCRIPT" 'FIXED_SMS_NO_REAL_SEND_LOG=PASS'
|
||||
contains "$SCRIPT" 'DIRECT_CHANNEL_COUNT'
|
||||
contains "$SCRIPT" 'LOGOUT_TOKEN_REJECTED'
|
||||
contains "$SCRIPT" 'time.sleep(60)'
|
||||
contains "$SCRIPT" 'RELOGIN_FIXED_SMS_REQUEST'
|
||||
contains "$SCRIPT" '"MOBILE_RELOGIN"'
|
||||
contains "$SCRIPT" 'MOBILE_RELOGIN_ME'
|
||||
contains "$SCRIPT" 'fixed_log_before + 2'
|
||||
not_contains "$SCRIPT" 'echo "$phone"'
|
||||
not_contains "$SCRIPT" 'echo "$fixed_code"'
|
||||
not_contains "$SCRIPT" 'echo "$token"'
|
||||
not_contains "$SCRIPT" '/api/aihr/direct/feedback",'
|
||||
not_contains "$SCRIPT" '/api/aihr/agent/messages",'
|
||||
not_contains "$SCRIPT" '/api/ai/asr",'
|
||||
not_contains "$SCRIPT" '/api/ai/tts",'
|
||||
|
||||
echo "PASS: authenticated production smoke is fixed-code-only, redacted and non-business-writing"
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_SSH="${AIHR_AUG1_REMOTE_SSH:-YCWY}"
|
||||
REMOTE_SERVICE="${AIHR_AUG1_REMOTE_SERVICE:-wygj-aihr.service}"
|
||||
REMOTE_DB="${AIHR_AUG1_REMOTE_DB:-ry-vue}"
|
||||
REMOTE_BASE_URL="${AIHR_AUG1_REMOTE_BASE_URL:-https://peilian.njzhmj.top/dev-api}"
|
||||
|
||||
if [[ "${1:-}" != "--execute" ]]; then
|
||||
cat <<'USAGE'
|
||||
Usage: ./scripts/verify-aug1-authenticated-production.sh --execute
|
||||
|
||||
Runs an authenticated August 1 production smoke test. The remote service must
|
||||
have both fixed-code settings enabled. The script selects an existing active
|
||||
APP user with a unique organization identity, never prints the phone, code, or
|
||||
token, performs no registration and submits no business records.
|
||||
USAGE
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[[ "$REMOTE_DB" =~ ^[A-Za-z0-9_-]+$ ]] || {
|
||||
echo "FAIL: invalid remote database name" >&2
|
||||
exit 3
|
||||
}
|
||||
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
||||
"$REMOTE_SERVICE" "$REMOTE_DB" "$REMOTE_BASE_URL" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
service="$1"
|
||||
db="$2"
|
||||
base="$3"
|
||||
pid="$(systemctl show -p MainPID --value "$service")"
|
||||
[[ -n "$pid" && "$pid" != "0" ]] || {
|
||||
echo "FAIL: production service is not running" >&2
|
||||
exit 10
|
||||
}
|
||||
|
||||
fixed_code=""
|
||||
fixed_enabled="false"
|
||||
while IFS='=' read -r key value; do
|
||||
case "$key" in
|
||||
AIHR_SMS_DEV_FIXED_CODE) fixed_code="$value" ;;
|
||||
AIHR_SMS_PROD_FIXED_CODE_ENABLED) fixed_enabled="$value" ;;
|
||||
esac
|
||||
done < <(tr '\0' '\n' < "/proc/$pid/environ")
|
||||
|
||||
[[ -n "$fixed_code" && "$fixed_enabled" == "true" ]] || {
|
||||
echo "FAIL: production fixed-code mode is not explicitly enabled" >&2
|
||||
exit 11
|
||||
}
|
||||
|
||||
phone="$(mysql --batch --skip-column-names --connect-timeout=5 "$db" -e "
|
||||
SELECT o.person_phone
|
||||
FROM aihr_org_snapshot o
|
||||
JOIN sys_user u
|
||||
ON u.tenant_id = o.tenant_id
|
||||
AND BINARY u.phonenumber = BINARY o.person_phone
|
||||
AND u.user_type = 'app_user'
|
||||
AND u.status = '0'
|
||||
AND u.del_flag = '0'
|
||||
WHERE o.tenant_id = '000000'
|
||||
AND o.employment_status = 'active'
|
||||
AND o.person_phone REGEXP '^1[3-9][0-9]{9}$'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_user su
|
||||
WHERE su.tenant_id = o.tenant_id
|
||||
AND BINARY su.phonenumber = BINARY o.person_phone
|
||||
AND (su.user_type IS NULL OR su.user_type <> 'app_user')
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM aihr_org_snapshot x
|
||||
WHERE x.tenant_id = o.tenant_id
|
||||
AND x.employment_status = 'active'
|
||||
AND x.ext_party_id = o.ext_party_id
|
||||
AND NULLIF(x.person_phone, '') IS NOT NULL
|
||||
AND BINARY x.person_phone <> BINARY o.person_phone
|
||||
)
|
||||
GROUP BY o.person_phone
|
||||
HAVING COUNT(DISTINCT o.ext_party_id) = 1
|
||||
ORDER BY MAX(CASE WHEN o.position_name = '生活顾问' THEN 1 ELSE 0 END) DESC,
|
||||
MAX(o.snapshot_date) DESC
|
||||
LIMIT 1
|
||||
")"
|
||||
|
||||
[[ "$phone" =~ ^1[3-9][0-9]{9}$ ]] || {
|
||||
echo "FAIL: no existing active APP user has a unique organization identity" >&2
|
||||
exit 12
|
||||
}
|
||||
|
||||
count_fixed_log() {
|
||||
local total=0 root matches
|
||||
for root in /opt/wygj/logs /opt/wygj/app/logs /var/log/wygj; do
|
||||
[[ -d "$root" ]] || continue
|
||||
matches="$(grep -R -F -h --include='*.log' --include='*.out' \
|
||||
'短信验证码走固定码,未真实发送短信' "$root" 2>/dev/null | wc -l)"
|
||||
total=$((total + matches))
|
||||
done
|
||||
printf '%s\n' "$total"
|
||||
}
|
||||
|
||||
fixed_log_before="$(count_fixed_log)"
|
||||
|
||||
AIHR_TEST_BASE="$base" \
|
||||
AIHR_TEST_PHONE="$phone" \
|
||||
AIHR_TEST_CODE="$fixed_code" \
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
base = os.environ["AIHR_TEST_BASE"].rstrip("/")
|
||||
phone = os.environ["AIHR_TEST_PHONE"]
|
||||
code = os.environ["AIHR_TEST_CODE"]
|
||||
|
||||
|
||||
def call(method, path, body=None, token=None, client=None):
|
||||
data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if client:
|
||||
headers["clientid"] = client
|
||||
request = urllib.request.Request(base + path, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=45) as response:
|
||||
raw = response.read().decode("utf-8", "replace")
|
||||
return response.status, json.loads(raw or "{}")
|
||||
except urllib.error.HTTPError as error:
|
||||
raw = error.read().decode("utf-8", "replace")
|
||||
try:
|
||||
payload = json.loads(raw or "{}")
|
||||
except Exception:
|
||||
payload = {}
|
||||
return error.code, payload
|
||||
|
||||
|
||||
def require_ok(label, result):
|
||||
status, payload = result
|
||||
business_code = payload.get("code") if isinstance(payload, dict) else None
|
||||
passed = status == 200 and business_code == 200
|
||||
print(f"{label}={'PASS' if passed else 'FAIL'} http={status} biz={business_code}")
|
||||
if not passed:
|
||||
raise SystemExit(20)
|
||||
return payload.get("data")
|
||||
|
||||
|
||||
sms_path = "/resource/sms/code?phonenumber=" + urllib.parse.quote(phone)
|
||||
require_ok("FIXED_SMS_REQUEST", call("GET", sms_path))
|
||||
login = require_ok(
|
||||
"MOBILE_LOGIN",
|
||||
call("POST", "/auth/mobile/sms-login", {"phonenumber": phone, "smsCode": code}),
|
||||
)
|
||||
if not isinstance(login, dict) or not login.get("access_token") or not login.get("client_id"):
|
||||
print("MOBILE_LOGIN_TOKEN=FAIL")
|
||||
raise SystemExit(21)
|
||||
|
||||
token = login["access_token"]
|
||||
client = login["client_id"]
|
||||
identity = require_ok(
|
||||
"MOBILE_ME",
|
||||
call("GET", "/api/aihr/mobile/me", token=token, client=client),
|
||||
)
|
||||
role = identity.get("role") if isinstance(identity, dict) else None
|
||||
projects = identity.get("projects") if isinstance(identity, dict) else []
|
||||
print("MOBILE_ROLE_VALID=" + ("PASS" if role in ("user", "supervisor") else "FAIL"))
|
||||
print("MOBILE_PROJECT_SCOPE=" + ("NONEMPTY" if isinstance(projects, list) and projects else "EMPTY"))
|
||||
if role not in ("user", "supervisor") or not isinstance(projects, list) or not projects:
|
||||
raise SystemExit(22)
|
||||
|
||||
route_role = "supervisor" if role == "supervisor" else "user"
|
||||
read_checks = [
|
||||
("MOBILE_HOME_AUTHENTICATED", f"/api/aihr/mobile/home/{route_role}"),
|
||||
("PRACTICE_HISTORY_READ", "/api/aihr/mobile/practice/history"),
|
||||
("PRACTICE_PROFILE_READ", "/api/aihr/mobile/profile"),
|
||||
("DIRECT_MINE_READ", "/api/aihr/direct/mine?pageNum=1&pageSize=10"),
|
||||
("BROADCAST_READ", "/api/aihr/broadcast/messages?pageNum=1&pageSize=10&scope=all"),
|
||||
("EXAM_READ", "/api/aihr/mobile/exams"),
|
||||
("CASE_CAPABILITIES_READ", "/api/knowledge/case/capabilities"),
|
||||
("WEB_AI_CAPABILITIES_READ", "/api/aihr/web-ai/capabilities"),
|
||||
]
|
||||
for label, path in read_checks:
|
||||
require_ok(label, call("GET", path, token=token, client=client))
|
||||
|
||||
channels = require_ok(
|
||||
"DIRECT_CHANNELS_READ",
|
||||
call("GET", "/api/aihr/direct/channels", token=token, client=client),
|
||||
)
|
||||
channel_count = len(channels) if isinstance(channels, list) else -1
|
||||
print(f"DIRECT_CHANNEL_COUNT={channel_count}")
|
||||
if channel_count != 5:
|
||||
raise SystemExit(23)
|
||||
|
||||
require_ok(
|
||||
"MOBILE_LOGOUT",
|
||||
call("POST", "/auth/logout", {}, token=token, client=client),
|
||||
)
|
||||
status, payload = call("GET", "/api/aihr/mobile/me", token=token, client=client)
|
||||
business_code = payload.get("code") if isinstance(payload, dict) else None
|
||||
expired = status == 401 or business_code == 401
|
||||
print(f"LOGOUT_TOKEN_REJECTED={'PASS' if expired else 'FAIL'} http={status} biz={business_code}")
|
||||
if not expired:
|
||||
raise SystemExit(24)
|
||||
|
||||
time.sleep(60)
|
||||
require_ok("RELOGIN_FIXED_SMS_REQUEST", call("GET", sms_path))
|
||||
relogin = require_ok(
|
||||
"MOBILE_RELOGIN",
|
||||
call("POST", "/auth/mobile/sms-login", {"phonenumber": phone, "smsCode": code}),
|
||||
)
|
||||
if not isinstance(relogin, dict) or not relogin.get("access_token") or not relogin.get("client_id"):
|
||||
print("MOBILE_RELOGIN_TOKEN=FAIL")
|
||||
raise SystemExit(25)
|
||||
|
||||
relogin_token = relogin["access_token"]
|
||||
relogin_client = relogin["client_id"]
|
||||
require_ok(
|
||||
"MOBILE_RELOGIN_ME",
|
||||
call("GET", "/api/aihr/mobile/me", token=relogin_token, client=relogin_client),
|
||||
)
|
||||
require_ok(
|
||||
"MOBILE_RELOGIN_LOGOUT",
|
||||
call("POST", "/auth/logout", {}, token=relogin_token, client=relogin_client),
|
||||
)
|
||||
PY
|
||||
|
||||
fixed_log_after="$fixed_log_before"
|
||||
fixed_log_expected=$((fixed_log_before + 2))
|
||||
for _ in 1 2 3 4 5; do
|
||||
fixed_log_after="$(count_fixed_log)"
|
||||
(( fixed_log_after >= fixed_log_expected )) && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if (( fixed_log_after < fixed_log_expected )); then
|
||||
echo "FIXED_SMS_NO_REAL_SEND_LOG=FAIL"
|
||||
exit 13
|
||||
fi
|
||||
echo "FIXED_SMS_NO_REAL_SEND_LOG=PASS"
|
||||
REMOTE
|
||||
Reference in New Issue
Block a user