fix: stop snowflake ossId precision loss and daily-drill audio identity mismatch
线上现象:对练页录音转写后提交回应,报「录音不属于当前账号,请重新录音后提交」。 根因A(前端,截图直接原因):OSS 编号是雪花 ID(如 2078204068457525249), 超出 JS Number 安全整数范围,后端按 RuoYi 规范序列化为字符串,但前端 4 处 用 Number() 强转(对练 ASR、每日题 ASR、每日题恢复列表、候选人面试 ASR), 精度丢失成 …5200,后端按错误 oss_id 查归属 COUNT=0 → 误报。 生产铁证:同一暂存行,正确 id 查得 1 行,JS 强转后的 id 查得 0 行; 用损坏 id 提交 turn 复现原报错,用字符串正确 id 提交成功。 修复A:speech.ts 新增 normalizeOssId(纯字符串校验透传,禁止 Number 强转), 4 处调用点改为字符串保存,practice/candidate 服务签名与状态类型放宽为 number|string(types/api.ts 本就是 number|string,Jackson 字符串转 Long 无损)。 根因B(后端,潜伏):answerDailyDrill 用组织 extPartyId(如 1003)做录音 归属身份校验,而暂存写入的是登录手机号——每日题语音提交必失败。 修复B:新增 validateAudioOwnershipByUser,按服务端登录 owner_user_id (与暂存同源、不可伪造)校验,对练路径(身份一致)保持原校验不变。 发布与验证: - H5 已发布(备份 /opt/wygj/backups/h5-before-20260718035416) - 后端 jar 已发布重启(备份 /opt/wygj/app/ruoyi-admin.jar.bak-20260718035552), 15s 恢复,重启后对练 turn+字符串 ossId 生产复核通过 - 每日题语音路径为逻辑级+数据级验证(该账号今日无待训练每日题, 业务级复验待下一次每日题窗口) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+30
-1
@@ -1650,6 +1650,35 @@ public class AihrPracticeSeedService {
|
||||
throw new ServiceException("录音不属于当前账号,请重新录音后提交");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ownership check keyed by the login user id only. Staging always records the
|
||||
* app login (owner_user_id + phone), while daily-drill submission carries the
|
||||
* org extPartyId — comparing that against the staged phone can never match,
|
||||
* so daily answers validate by the unforgeable server-side user id instead.
|
||||
*/
|
||||
private void validateAudioOwnershipByUser(Long ossId, Long ownerUserId) {
|
||||
if (ossId == null) {
|
||||
return;
|
||||
}
|
||||
if (ownerUserId == null || ownerUserId <= 0) {
|
||||
throw new ServiceException("录音归属校验失败,请重新录音后提交");
|
||||
}
|
||||
try {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM aihr_practice_audio_upload
|
||||
WHERE tenant_id = ? AND oss_id = ? AND owner_user_id = ?
|
||||
AND status IN ('staged', 'bound')
|
||||
""", Integer.class, tenantId(), ossId, ownerUserId);
|
||||
if (count != null && count > 0) {
|
||||
return;
|
||||
}
|
||||
} catch (DataAccessException ignored) {
|
||||
// Keep the ownership failure generic; do not leak database details.
|
||||
}
|
||||
throw new ServiceException("录音不属于当前账号,请重新录音后提交");
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean markReviewed(Long id) {
|
||||
return markReviewed(id, null);
|
||||
@@ -2800,7 +2829,7 @@ public class AihrPracticeSeedService {
|
||||
String answer = maskSensitiveText(firstNonBlank(request == null ? null : request.answer(), ""));
|
||||
String audioUrl = exposedAudioUrl(request == null ? null : request.audioUrl());
|
||||
Long audioOssId = request == null ? null : request.audioOssId();
|
||||
validateAudioOwnership(audioOssId, extPartyId, ownerUserId);
|
||||
validateAudioOwnershipByUser(audioOssId, ownerUserId);
|
||||
DailyDrillScore score = scoreDailyDrill(parts.question(), answer, parts.referenceAnswer());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int updated = jdbcTemplate.update("""
|
||||
|
||||
Reference in New Issue
Block a user