fix(framework): DefaultDBFieldHandler 无登录用户时 creator/updater 兜底 system,根治 scheduled 写 null 违约

问题: AI runtime 异步链(dispatcher/executor/recorder/projection)在 scheduled 线程 insert/update muse_ai_* 时 creator/updater 写 null 触发 NOT NULL 违约 → recordRequested insert 即回滚抛 → job 卡 running/queued、AI 执行恒失败(runtime_call 0 行)。

根因: BaseDO creator/updater 带 @TableField(jdbcType=VARCHAR),fill 字段即使 null 也被 MyBatis 强制入 SQL 列;DefaultDBFieldHandler 仅当登录用户存在才回填,scheduled worker 无登录上下文 → 写 null → 违约。insert+update 同病。上一版 5d0f17a 误判 insert 靠 default 兜底,只补了 updateById,被 e2e(runtime_call 0 行)证伪。

修正: insertFill/updateFill 无登录用户时 creator/updater 兜底 SYSTEM_OPERATOR("system"),一处根治所有模块 scheduled/async 写;回退 5d0f17a 局部 setUpdater(已被统一覆盖)。OAuth2TokenServiceImplTest 的 assertPojoEquals 补 creator/updater 忽略(token 创建无登录,DB 兜底 system 与内存返回对象不一致;auto-fill 字段本不应参与内存 vs DB 对比,与同测试既有忽略一致)。

验证: 框架+AI 回归 92/0;全量 381 测试类中 D 仅触发 OAuth2 2 断言(已修),余 Pay(Redis NOAUTH)/Knowledge(mock NPE)/P1r(meta 文件)为预存/环境失败、与 D 无关;e2e task3 executor 完整跑通——runtime_call 落库(creator/updater=system)、job/generation/call 全写回终态、无违约、P-C contextAssembly 固化进 source_summary。

注: 剩余 AI_NEW_API_UNAVAILABLE(infra.env 未配 MUSE_AI_NEW_API_*,adapter 走 Unavailable 兜底) + P-A 检索 0 chunks(KB 状态 disabled/draft 非 active) 为环境/数据 gap,非代码。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-24 04:28:33 -07:00
parent 5d0f17a410
commit 4dadc7bae3
5 changed files with 25 additions and 20 deletions

View File

@ -17,6 +17,15 @@ import java.util.Objects;
*/
public class DefaultDBFieldHandler implements MetaObjectHandler {
/**
* 无登录用户上下文(scheduled/async worker 线程),creator/updater 的兜底操作者标识
*
* <p>背景:BaseDO creator/updater {@code jdbcType=VARCHAR},fill 字段即使为 null 也会被
* MyBatis 强制写入 SQL ;后台任务无登录用户会写 null 触发 NOT NULL 违约(列定义 NOT NULL DEFAULT '')
* 这里统一兜底为系统标识,保证所有后台任务(AI runtime 调度事件发布重试等)写库不被框架字段卡住</p>
*/
private static final String SYSTEM_OPERATOR = "system";
@Override
@SuppressWarnings("PatternVariableCanBeUsed")
public void insertFill(MetaObject metaObject) {
@ -33,14 +42,14 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
baseDO.setUpdateTime(current);
}
// 有登录用户用其 id;scheduled/async worker 无登录上下文时兜底 SYSTEM_OPERATOR,避免 creator/updater null 违约
Long userId = SecurityFrameworkUtils.getLoginUserId();
// 当前登录用户不为空创建人为空则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(userId.toString());
String operator = Objects.nonNull(userId) ? userId.toString() : SYSTEM_OPERATOR;
if (Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(operator);
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
if (Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(operator);
}
}
}
@ -53,11 +62,11 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
// 更新人为空时回填:有登录用户用其 id;scheduled/async worker 无登录上下文时兜底 SYSTEM_OPERATOR,避免 updater null 违约
Object modifier = getFieldValByName("updater", metaObject);
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
if (Objects.isNull(modifier)) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
setFieldValByName("updater", Objects.nonNull(userId) ? userId.toString() : SYSTEM_OPERATOR, metaObject);
}
}
}

View File

@ -96,8 +96,6 @@ public class MuseAiRuntimeCallRecorder {
update.setNextRetryAt(succeeded || failure == null ? null : toLocalDateTime(failure.nextRetryAt()));
update.setFinishedAt(succeeded ? toLocalDateTime(result.completedAt())
: toLocalDateTime(failure == null ? Instant.now() : failure.failedAt()));
// scheduled worker 线程无登录用户上下文显式回填 updater(call owner)避免 auto-fill 落空触发 updater NOT NULL 违约
update.setUpdater(call.getOwnerUserId() == null ? "ai-runtime" : String.valueOf(call.getOwnerUserId()));
runtimeCallMapper.updateById(update);
return update;
}

View File

@ -74,10 +74,8 @@ public class MuseAiRuntimeJobDispatcher {
update.setErrorMessage(errorMessage);
update.setFinishedAt(LocalDateTime.now());
update.setTenantId(claimedJob.getTenantId());
// scheduled worker 线程无登录用户上下文yudao auto-fill 不会回填 updater(new DO)显式回填 owner 作操作者
// 避免 updater NOT NULL 违约导致写回失败job 永久 running
update.setUpdater(claimedJob.getOwnerUserId() == null ? "ai-runtime" : String.valueOf(claimedJob.getOwnerUserId()));
// claim 已经把 job 推到 running任何前置异常都必须关闭该 job避免 worker 留下永久 running
// updater/creator DefaultDBFieldHandler 在无登录用户(scheduled worker)时兜底 SYSTEM_OPERATOR
TenantUtils.executeIgnore(() -> jobMapper.updateById(update));
}

View File

@ -172,8 +172,6 @@ public class MuseAiRuntimeProjectionService {
MuseAiGenerationDO update = new MuseAiGenerationDO();
update.setId(generation.getId());
update.setCandidateId(suggestionId);
// scheduled worker 线程无登录用户上下文显式回填 updater(job owner)避免 auto-fill 落空触发 updater NOT NULL 违约
update.setUpdater(job.getOwnerUserId() == null ? "ai-runtime" : String.valueOf(job.getOwnerUserId()));
generationMapper.updateById(update);
return suggestionId;
}

View File

@ -78,7 +78,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
// 断言访问令牌
OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken());
// TODO @芋艿expiresTime 被屏蔽 win11 会复现建议后续修复
assertPojoEquals(accessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
// creator/updater DefaultDBFieldHandler 在无登录用户(token 创建无登录上下文)时兜底 SYSTEM_OPERATOR与内存返回对象不一致对比时忽略
assertPojoEquals(accessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater");
assertEquals(userId, accessTokenDO.getUserId());
assertEquals(userType, accessTokenDO.getUserType());
assertEquals(2, accessTokenDO.getUserInfo().size());
@ -93,7 +94,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
assertPojoEquals(accessTokenDO, redisAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
// 断言刷新令牌
OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0);
assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted");
assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater");
assertFalse(DateUtils.isExpired(refreshTokenDO.getExpiresTime()));
}
@ -181,7 +182,8 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest {
// 断言新的访问令牌
OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken());
// TODO @芋艿expiresTime 被屏蔽 win11 会复现建议后续修复
assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted");
// creator/updater DefaultDBFieldHandler 在无登录用户(token 创建无登录上下文)时兜底 SYSTEM_OPERATOR与内存返回对象不一致对比时忽略
assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted", "creator", "updater");
assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted",
"creator", "updater");
assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime()));