fix(telemetry): props 落 MySQL json 列改用 JsonUtils 序列化(修 B1 潜伏 Map.toString 致 Invalid JSON 整批回滚)
根因:game_telemetry_event.props 为 MySQL json 列,buildEventDO 早期用 Map.toString() 存(产出 {k=v} 键无引号、非合法 JSON),真库 INSERT 报 Invalid JSON text → 同步写事务整批回滚。B1 未暴露:telemetry 从未真落库 + 单测 mock mapper 绕开 JSON 约束;B2 数据回路首次真落库逮到。
修复:toString() → JsonUtils.toJsonString()。新增回归守卫单测 testIngestOne_propsSerializedAsValidJson(断言 props 为合法 JSON 且不含 = 分隔符)。telemetry 模块 13 tests 全绿;staging 重部署后 B2 A/B e2e 全过。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
aa99e76026
commit
33b8f6becf
@ -11,6 +11,7 @@ import cn.wanxiang.game.module.telemetry.enums.EventProcessStatusEnum;
|
||||
import cn.wanxiang.game.module.telemetry.enums.TelemetryEventEnum;
|
||||
import cn.wanxiang.game.module.feed.api.FeedApi;
|
||||
import cn.wanxiang.game.module.feed.dto.FeedRankUpsertReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -310,8 +311,10 @@ public class EventIngestServiceImpl implements EventIngestService {
|
||||
eventDO.setChannel("");
|
||||
eventDO.setDeviceType("");
|
||||
}
|
||||
// props:DO 以字符串承载(DB JSON 列);MVP 不深解析,存原始 toString(前端按 #5 传 JSON)
|
||||
eventDO.setProps(envelope.getProps() == null ? null : envelope.getProps().toString());
|
||||
// props 落 MySQL json 列:必须写合法 JSON 文本。早期误用 Map.toString() 产出 {k=v} 形态(键无引号、= 分隔,
|
||||
// MySQL 在 position 1 即报 Invalid JSON text → 整批事务回滚);改用 Jackson 序列化为合法 JSON({"k":v})。
|
||||
// props 为 null 时透传 null(列可空);空对象序列化为 "{}"(合法 JSON)。
|
||||
eventDO.setProps(envelope.getProps() == null ? null : JsonUtils.toJsonString(envelope.getProps()));
|
||||
eventDO.setProcessStatus(EventProcessStatusEnum.PENDING.getStatus());
|
||||
return eventDO;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import cn.wanxiang.game.module.telemetry.dal.mysql.stat.GameStatMapper;
|
||||
import cn.wanxiang.game.module.feed.api.FeedApi;
|
||||
import cn.wanxiang.game.module.feed.dto.FeedRankUpsertReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@ -144,6 +145,33 @@ class EventIngestServiceImplTest extends BaseMockitoUnitTest {
|
||||
verify(telemetryEventMapper).updateById(any(TelemetryEventDO.class));
|
||||
}
|
||||
|
||||
// ============================== props JSON 序列化回归守卫(B2 实测 bug)==============================
|
||||
|
||||
@Test
|
||||
void testIngestOne_propsSerializedAsValidJson() {
|
||||
// 回归守卫:props 必须以合法 JSON 文本落库(game_telemetry_event.props 为 MySQL json 列)。
|
||||
// 早期误用 Map.toString() 产出 {completed=true, duration_ms=8000}(键无引号、= 分隔,非合法 JSON),
|
||||
// 真库 INSERT 即报 "Invalid JSON text" 整批回滚。此用例锁住「序列化为合法 JSON」不回归。
|
||||
Map<String, Object> props = new HashMap<>();
|
||||
props.put("completed", true);
|
||||
props.put("duration_ms", 8000);
|
||||
EnvelopeReqVO env = gameEnvelope("game_play_end", "t1", "1024", "2048", props);
|
||||
when(telemetryEventMapper.selectByEventId(env.getEventId())).thenReturn(null);
|
||||
when(gameStatMapper.selectByGameAndDate(eq(1024L), any())).thenReturn(null);
|
||||
|
||||
eventIngestService.ingestBatch(batchOf(env), 99L);
|
||||
|
||||
ArgumentCaptor<TelemetryEventDO> evtCaptor = ArgumentCaptor.forClass(TelemetryEventDO.class);
|
||||
verify(telemetryEventMapper).insert(evtCaptor.capture());
|
||||
String propsJson = evtCaptor.getValue().getProps();
|
||||
// 能被 Jackson 解析回 Map(合法 JSON),且不含 Map.toString() 的 = 分隔符
|
||||
Map<?, ?> parsed = JsonUtils.parseObject(propsJson, Map.class);
|
||||
assertNotNull(parsed, "props 应为合法 JSON 文本");
|
||||
assertEquals(Boolean.TRUE, parsed.get("completed"));
|
||||
assertEquals(8000, ((Number) parsed.get("duration_ms")).intValue());
|
||||
assertFalse(propsJson.contains("="), "props 不应为 Map.toString() 的 {k=v} 形态");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIngestOne_likeAccumulatesOnExistingStat() {
|
||||
// like 事件命中既有 stat(play_count=2,like_count=1)→ like_count 累加为 2;quality_score 含 like 率
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user