diff --git a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java index 3240848a..7778fd6c 100644 --- a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java +++ b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/main/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImpl.java @@ -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; } diff --git a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java index 3bb43b19..bc113b1c 100644 --- a/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java +++ b/game-cloud/game-module-telemetry/game-module-telemetry-server/src/test/java/cn/wanxiang/game/module/telemetry/service/event/EventIngestServiceImplTest.java @@ -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 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 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 率