From edb8c8255bbef4e051ef2339c32cbb0b3a805723 Mon Sep 17 00:00:00 2001 From: key Date: Thu, 30 Jul 2026 00:40:03 +0800 Subject: [PATCH] fix(learning): restore mobile mistakes query --- CHANGELOG.md | 1 + .../aihr/service/AihrPracticeSeedService.java | 32 +++++++++++-------- .../service/AihrPracticeSeedServiceTest.java | 9 ++++++ docs/MOBILE_APP_DEVICE_ACCEPTANCE.md | 2 ++ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a49d24fd..1674e368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Unreleased +- **2026-07-30,学习中心错题列表异常已修复,尚未发布后端**:vivo V2443A(Android 15)的线上会话进入“学习”页后显示“发生未知异常”,同页任务、知识引用等请求正常,生产日志将故障收敛到 `GET /api/aihr/mobile/practice/mistakes`;其 SQL 文本中的 JSON `LIKE '%"redFlag":true%'` 被 `String.formatted()` 误当成格式指令,触发 `UnknownFormatConversionException: Conversion = '"'`。当前改为只替换身份 `IN` 参数的专用占位符,保留两个 `LIKE` 通配符原样,并增加 SQL 生成回归测试;`AihrPracticeSeedServiceTest` 103 项、`ruoyi-aihr` 全量 707 项测试和 `ruoyi-admin` 生产包构建均通过。本轮仅完成本地代码、测试和提交,未部署、未重启服务或修改数据库,线上真机复验须在后续单独授权发布后进行。 - **2026-07-29,Android 文件选择、麦克风拒绝降级与上传路径修复,源码候选升级为 `0.1.16 (116)`,尚无签名 APK**:真机确认 `0.1.13` 在允许麦克风时可完成原生录音和生产 ASR,但拒绝后的降级误用 App 不支持的 `uni.chooseFile`。Android 非媒体文件现改用系统 `ACTION_OPEN_DOCUMENT`,通过 `ParcelFileDescriptor + FileChannel.transferFrom` 复制到应用专属外部缓存,避免 Native.js `byte[]` 参数兼容问题;图片/视频改走 App `uni.chooseMedia`,页面生命周期会保留系统选择器中的待完成任务,录音前统一检查麦克风权限。vivo Android 15 真机在 HBuilderX 5.22 标准调试基座完成取消、重复打开、`100,542` 字节 WAV 精确复制、multipart 上传和生产 ASR。首次成果投稿验证实际只进入追问态,当时并未出现“确认投稿”,此前“未点击确认投稿”的表述已纠正;继续补充时线上整理请求耗时 `94,965ms`,超过客户端 `45,000ms` 门限。当前源码改为仅首轮调用 AI,后续原话并入当前草稿后交由既有服务端本地复核,真机复验后续整理耗时 `1ms`,确认卡及“确认投稿”按钮均已显示但未点击,未形成正式投稿。移动端 206 项测试、类型检查、App 发布资源预检和 App-Plus 构建通过;调查用 `0.1.14/0.1.15` APK 已删除。DCloud 当日云打包额度耗尽,最终 `0.1.16` 签名 APK、深检和同包真机复验仍未完成,因此当前仍为 No-Go,未发布移动 H5、原生 App、后端、管理端或数据库。 - **2026-07-29,资料处理中心普通文档/图片单文件 500MB 已发布并完成生产验证**:管理端批量导入、后端异步队列、ZIP 子文件和 Tika 输入流统一允许受支持文件 `≤500MiB`,沿用生产 Spring/Undertow `500MB/520MB` 传输层配置;仍保留 ZIP 最多 1000 个子文件、解压总量 ≤2GB、禁止嵌套 ZIP 等安全边界,同步 SOP 上传和公司文件继续保持 100MB。生产实测发现首个候选虽已放宽页面和队列校验,但 Tika 内仍残留独立 `100MiB` 输入流上限,导致首批队列 `1194/1195` 上传成功后加工失败;补齐解析器上限及回归断言后,11 项定向后端测试和后端生产打包通过。管理端发布前备份整站到 `/opt/wygj/backups/www-20260729213203`,线上 `processing-BjvQ3qpr.js` 与本地产物 SHA-256 均为 `6ace59477519d6c53cbfe207a229518a7e2f7568e05961cfea28d669e1eeb305`,移动 H5 树哈希保持 `ba4aa656d64fc301141280357c204a67141d1e6b2e2a26a0be21c0dd48a25712` 未变。最终后端 JAR SHA-256 为 `be16c229cfc255e4cdf5ec32df047994b54d7d2c31b6c729dde6d949e191e53d`,旧包备份在 `/opt/wygj/backups/backend-20260729214901-5ef5dd484b5d`;整包及内嵌 `ruoyi-aihr-5.6.2.jar` 远近端哈希一致,服务、公开接口、64/64 schema 和生产禁运行时 DDL 预检均通过。已登录生产 Chrome 验收批次 `b-c2560e95-0cf7-46e0-9bdc-7690ea86ffd7` 中,`4、巡检表(事业部颗粒归仓专用).xlsx`(101.6MB,队列 `1196`、附件 `2036`)完成 5 个片段和 5 个向量,`新员工培训-社区发展中心-社区资源.pptx`(147.9MB,队列 `1197`、附件 `2037`)完成 7 个片段和 7 个向量;两项均显示“已完成/已入库”,数据库片段均含 embedding。资料总数从 1032 增至 1034,目标空间从 22 增至 24 个文件,处理中和需处理均为 0。本次只定向发布后端与管理端,未重发移动 H5、未执行 schema 迁移,不代表当前工作树与线上完整包全部匹配。 - **2026-07-29,资料处理媒体任务恢复已发布并完成生产验证**:修复历史图片/视频在视觉输入不兼容或未提取到文字时长期停在“等待解析”的问题;图片在送入视觉模型前会按方向纠正、限制像素和体积并统一编码,异步队列只有生成至少一个片段才进入完成态,零片段会保留为可重试失败。新增 `POST /api/knowledge/doc/processing-tasks/{attachmentId}/retry`,可从当前租户原 OSS 对象重新暂存并入队;正式迁移 `aihr_20260729_media_reprocess_mysql8.sql` 增加 `source_attach_id` 和 `(tenant_id, source_attach_id, id)` 索引。生产已备份 schema、后端和完整站点,发布管理端与后端 JAR `5376b67b33b171b5c85e22cd6a2f20bbb0bc527fc50a223603d0aec3f6587bac`,移动 H5 未重发;发布脚本在重启后等待公开端点真实就绪再执行预检,服务为 `active`,根站、租户和移动首页均返回 HTTP 200。原卡住任务 `1133/1175/1308/1405` 从原文件重试为队列 `1185–1188`,四项均完成、各生成一个片段且向量存在;资料处理页显示“待重试 0”、Tika `1028/1028`、可引用资料 `1028`。该证据证明本次资料恢复和定向发布成功,不代表移动 H5 与当前工作树完整匹配,也不替代 Android 真机或正式试点验收。 diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index da7e282d..cf0e8b95 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -1764,7 +1764,22 @@ public class AihrPracticeSeedService { args.add(tenantId()); args.addAll(identity.storageKeys()); args.add(safeLimit); - return jdbcTemplate.query(""" + return jdbcTemplate.query(mobileMistakesSql(identity.storageKeys().size()), (rs, rowNum) -> new MistakeResponse( + rs.getLong("id"), + rs.getString("session_id"), + rs.getString("scenario_id"), + firstNonBlank(rs.getString("scenario_name"), "场景训练"), + rs.getInt("total_score"), + firstNonBlank(rs.getString("cause"), "综合得分不足"), + rs.getObject("retry_assignment_id") == null ? null : rs.getLong("retry_assignment_id"), + firstNonBlank(rs.getString("retry_status"), "未生成"), + rs.getTimestamp("finished_time") == null ? "" : rs.getTimestamp("finished_time").toLocalDateTime().format(TIME_FORMATTER), + firstNonBlank(rs.getString("summary"), "建议回看训练对话和导师改写") + ), args.toArray()); + } + + static String mobileMistakesSql(int identityCount) { + return """ SELECT s.id, s.session_id, s.scenario_id, @@ -1797,23 +1812,12 @@ public class AihrPracticeSeedService { FROM aihr_practice_session s WHERE s.tenant_id = ? AND s.mode = 'mobile' - AND s.ext_party_id IN (%s) + AND s.ext_party_id IN (__IDENTITY_KEYS__) AND s.finished_time IS NOT NULL AND (s.total_score < 80 OR s.dialogue_json LIKE '%"redFlag":true%') ORDER BY s.finished_time DESC, s.id DESC LIMIT ? - """.formatted(inClause(identity.storageKeys().size())), (rs, rowNum) -> new MistakeResponse( - rs.getLong("id"), - rs.getString("session_id"), - rs.getString("scenario_id"), - firstNonBlank(rs.getString("scenario_name"), "场景训练"), - rs.getInt("total_score"), - firstNonBlank(rs.getString("cause"), "综合得分不足"), - rs.getObject("retry_assignment_id") == null ? null : rs.getLong("retry_assignment_id"), - firstNonBlank(rs.getString("retry_status"), "未生成"), - rs.getTimestamp("finished_time") == null ? "" : rs.getTimestamp("finished_time").toLocalDateTime().format(TIME_FORMATTER), - firstNonBlank(rs.getString("summary"), "建议回看训练对话和导师改写") - ), args.toArray()); + """.replace("__IDENTITY_KEYS__", inClause(identityCount)); } public PracticeTeamResponse practiceTeamSnapshot(String supervisorExtPartyId, int limit) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java index fef54d50..b22763ce 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java @@ -74,6 +74,15 @@ import static org.mockito.Mockito.when; @Tag("dev") public class AihrPracticeSeedServiceTest { + @Test + public void mobileMistakesSqlPreservesJsonLikeWildcardsWithoutFormatter() { + String sql = AihrPracticeSeedService.mobileMistakesSql(2); + + assertTrue(sql.contains("s.ext_party_id IN (?,?)")); + assertTrue(sql.contains("s.dialogue_json LIKE '%\"redFlag\":true%'")); + assertFalse(sql.contains("__IDENTITY_KEYS__")); + } + @Test public void pilotExportRequiresExplicitOrderedWindow() { AihrPracticeSeedService service = new AihrPracticeSeedService(new ObjectMapper(), null, null, null); diff --git a/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md b/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md index f1371c45..f0462de3 100644 --- a/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md +++ b/docs/MOBILE_APP_DEVICE_ACCEPTANCE.md @@ -56,6 +56,8 @@ Android 每个人工步骤完成后,可在不改变设备状态的前提下保 当前源码候选已升级为 `0.1.16 (116)`:Android 非媒体文件通过系统 `ACTION_OPEN_DOCUMENT` 选择,以 `ParcelFileDescriptor + FileChannel.transferFrom` 复制到应用专属外部缓存;页面生命周期会区分系统选择器暂时离开和真实离页,图片/视频改走 App 支持的 `uni.chooseMedia`,麦克风拒绝在启动录音前统一进入降级。vivo 真机使用 HBuilderX 5.22 标准调试基座完成选择器打开、取消、重复打开及选择 `100,542` 字节 WAV;复制后字节数一致,并继续完成 multipart 上传和生产 ASR。首次成果投稿验证只生成追问草稿,实际尚未显示“确认投稿”,不是已经出现按钮但未点击。复查又确认每轮补充重新调用模型会继续生成追问,且一次线上整理耗时 `94,965ms`,超过客户端 `45,000ms` 超时门限。当前源码改为首轮 AI 整理、后续原话并入当前草稿后交由既有服务端本地复核;同一真机复验后续整理耗时 `1ms`,已显示成果投稿确认卡和“确认投稿”按钮,但没有点击、没有形成正式投稿。当前 206/206 项移动端测试、类型检查、App 发布资源预检和 App-Plus 构建通过。 +2026-07-30,同一 vivo 真机的已登录线上会话进入“学习”中心时显示“发生未知异常”,但任务统计和知识引用仍能加载。生产只读日志确认只有 `GET /api/aihr/mobile/practice/mistakes` 失败:错题 SQL 使用 `String.formatted()` 填充身份参数时,把 JSON `LIKE '%"redFlag":true%'` 中的百分号误解析为格式指令,抛出 `UnknownFormatConversionException`。本地源码已改为专用占位符替换并增加回归测试;该修复不需要移动端重新出包,但本轮未获得新的后端发布授权,所以线上错误仍需后续发布后在同一真机刷新复验,不能把本地测试通过写成生产通过。 + 由于 DCloud 当日免费云打包次数已用尽,`0.1.16` 的最终签名 APK 尚未生成,标准调试基座证据不能冒充签名包验收。最后一个可重复校验的签名包仍是 `0.1.13 (113)`,但它不包含上述系统选择器修复;额度恢复或购买次数后必须重新云打包、深检并在同一真机复跑麦克风拒绝、选择器取消/选择和 ASR。当前结论因此仍为 No-Go。 随后根据企业内部应用的体验决策,`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 壳或增加业务兜底。