dev_store.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964
  1. package postgres
  2. import (
  3. "context"
  4. "fmt"
  5. )
  6. type DemoBootstrapSummary struct {
  7. TenantCode string `json:"tenantCode"`
  8. ChannelCode string `json:"channelCode"`
  9. EventID string `json:"eventId"`
  10. ReleaseID string `json:"releaseId"`
  11. SourceID string `json:"sourceId"`
  12. BuildID string `json:"buildId"`
  13. CardID string `json:"cardId"`
  14. PlaceID string `json:"placeId"`
  15. MapAssetID string `json:"mapAssetId"`
  16. TileReleaseID string `json:"tileReleaseId"`
  17. CourseSourceID string `json:"courseSourceId"`
  18. CourseSetID string `json:"courseSetId"`
  19. CourseVariantID string `json:"courseVariantId"`
  20. RuntimeBindingID string `json:"runtimeBindingId"`
  21. ScoreOEventID string `json:"scoreOEventId"`
  22. ScoreOReleaseID string `json:"scoreOReleaseId"`
  23. ScoreOCardID string `json:"scoreOCardId"`
  24. ScoreOSourceID string `json:"scoreOSourceId"`
  25. ScoreOBuildID string `json:"scoreOBuildId"`
  26. ScoreOCourseSetID string `json:"scoreOCourseSetId"`
  27. ScoreOCourseVariantID string `json:"scoreOCourseVariantId"`
  28. ScoreORuntimeBindingID string `json:"scoreORuntimeBindingId"`
  29. VariantManualEventID string `json:"variantManualEventId"`
  30. VariantManualRelease string `json:"variantManualReleaseId"`
  31. VariantManualCardID string `json:"variantManualCardId"`
  32. CleanedSessionCount int64 `json:"cleanedSessionCount"`
  33. }
  34. func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
  35. tx, err := s.Begin(ctx)
  36. if err != nil {
  37. return nil, err
  38. }
  39. defer tx.Rollback(ctx)
  40. var tenantID string
  41. if err := tx.QueryRow(ctx, `
  42. INSERT INTO tenants (tenant_code, name, status)
  43. VALUES ('tenant_demo', 'Demo Tenant', 'active')
  44. ON CONFLICT (tenant_code) DO UPDATE SET
  45. name = EXCLUDED.name,
  46. status = EXCLUDED.status
  47. RETURNING id
  48. `).Scan(&tenantID); err != nil {
  49. return nil, fmt.Errorf("ensure demo tenant: %w", err)
  50. }
  51. var channelID string
  52. if err := tx.QueryRow(ctx, `
  53. INSERT INTO entry_channels (
  54. tenant_id, channel_code, channel_type, platform_app_id, display_name, status, is_default
  55. )
  56. VALUES ($1, 'mini-demo', 'wechat_mini', 'wx-demo-appid', 'Demo Mini Channel', 'active', true)
  57. ON CONFLICT (tenant_id, channel_code) DO UPDATE SET
  58. channel_type = EXCLUDED.channel_type,
  59. platform_app_id = EXCLUDED.platform_app_id,
  60. display_name = EXCLUDED.display_name,
  61. status = EXCLUDED.status,
  62. is_default = EXCLUDED.is_default
  63. RETURNING id
  64. `, tenantID).Scan(&channelID); err != nil {
  65. return nil, fmt.Errorf("ensure demo entry channel: %w", err)
  66. }
  67. var eventID string
  68. if err := tx.QueryRow(ctx, `
  69. INSERT INTO events (
  70. tenant_id, event_public_id, slug, display_name, summary, status
  71. )
  72. VALUES ($1, 'evt_demo_001', 'demo-city-run', 'Demo City Run', 'Launch flow demo event', 'active')
  73. ON CONFLICT (event_public_id) DO UPDATE SET
  74. tenant_id = EXCLUDED.tenant_id,
  75. slug = EXCLUDED.slug,
  76. display_name = EXCLUDED.display_name,
  77. summary = EXCLUDED.summary,
  78. status = EXCLUDED.status
  79. RETURNING id
  80. `, tenantID).Scan(&eventID); err != nil {
  81. return nil, fmt.Errorf("ensure demo event: %w", err)
  82. }
  83. var releaseRow struct {
  84. ID string
  85. PublicID string
  86. }
  87. if err := tx.QueryRow(ctx, `
  88. INSERT INTO event_releases (
  89. release_public_id,
  90. event_id,
  91. release_no,
  92. config_label,
  93. manifest_url,
  94. manifest_checksum_sha256,
  95. route_code,
  96. status
  97. )
  98. VALUES (
  99. 'rel_demo_001',
  100. $1,
  101. 1,
  102. 'Demo Config v1',
  103. 'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
  104. 'demo-checksum-001',
  105. 'route-demo-001',
  106. 'published'
  107. )
  108. ON CONFLICT (release_public_id) DO UPDATE SET
  109. event_id = EXCLUDED.event_id,
  110. config_label = EXCLUDED.config_label,
  111. manifest_url = EXCLUDED.manifest_url,
  112. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  113. route_code = EXCLUDED.route_code,
  114. status = EXCLUDED.status
  115. RETURNING id, release_public_id
  116. `, eventID).Scan(&releaseRow.ID, &releaseRow.PublicID); err != nil {
  117. return nil, fmt.Errorf("ensure demo release: %w", err)
  118. }
  119. if _, err := tx.Exec(ctx, `
  120. UPDATE events
  121. SET current_release_id = $2
  122. WHERE id = $1
  123. `, eventID, releaseRow.ID); err != nil {
  124. return nil, fmt.Errorf("attach demo release: %w", err)
  125. }
  126. sourceNotes := "demo source config imported from local event sample"
  127. source, err := s.UpsertEventConfigSource(ctx, tx, UpsertEventConfigSourceParams{
  128. EventID: eventID,
  129. SourceVersionNo: 1,
  130. SourceKind: "event_bundle",
  131. SchemaID: "event-source",
  132. SchemaVersion: "1",
  133. Status: "active",
  134. Notes: &sourceNotes,
  135. Source: map[string]any{
  136. "app": map[string]any{
  137. "id": "sample-classic-001",
  138. "title": "顺序赛示例",
  139. },
  140. "branding": map[string]any{
  141. "tenantCode": "tenant_demo",
  142. "entryChannel": "mini-demo",
  143. },
  144. "map": map[string]any{
  145. "tiles": "../map/lxcb-001/tiles/",
  146. "mapmeta": "../map/lxcb-001/tiles/meta.json",
  147. },
  148. "playfield": map[string]any{
  149. "kind": "course",
  150. "source": map[string]any{
  151. "type": "kml",
  152. "url": "../kml/lxcb-001/10/c01.kml",
  153. },
  154. },
  155. "game": map[string]any{
  156. "mode": "classic-sequential",
  157. },
  158. "content": map[string]any{
  159. "h5Template": "content-h5-test-template.html",
  160. },
  161. },
  162. })
  163. if err != nil {
  164. return nil, fmt.Errorf("ensure demo event config source: %w", err)
  165. }
  166. buildLog := "demo build generated from sample classic-sequential.json"
  167. build, err := s.UpsertEventConfigBuild(ctx, tx, UpsertEventConfigBuildParams{
  168. EventID: eventID,
  169. SourceID: source.ID,
  170. BuildNo: 1,
  171. BuildStatus: "success",
  172. BuildLog: &buildLog,
  173. Manifest: map[string]any{
  174. "schemaVersion": "1",
  175. "releaseId": "rel_demo_001",
  176. "version": "2026.04.01",
  177. "app": map[string]any{
  178. "id": "sample-classic-001",
  179. "title": "顺序赛示例",
  180. },
  181. "map": map[string]any{
  182. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  183. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  184. },
  185. "playfield": map[string]any{
  186. "kind": "course",
  187. "source": map[string]any{
  188. "type": "kml",
  189. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  190. },
  191. },
  192. "game": map[string]any{
  193. "mode": "classic-sequential",
  194. },
  195. "assets": map[string]any{
  196. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  197. },
  198. },
  199. AssetIndex: []map[string]any{
  200. {
  201. "assetType": "manifest",
  202. "assetKey": "manifest",
  203. },
  204. {
  205. "assetType": "mapmeta",
  206. "assetKey": "mapmeta",
  207. },
  208. {
  209. "assetType": "playfield",
  210. "assetKey": "playfield-kml",
  211. },
  212. {
  213. "assetType": "content_html",
  214. "assetKey": "content-html",
  215. },
  216. },
  217. })
  218. if err != nil {
  219. return nil, fmt.Errorf("ensure demo event config build: %w", err)
  220. }
  221. if err := s.AttachBuildToRelease(ctx, tx, releaseRow.ID, build.ID); err != nil {
  222. return nil, fmt.Errorf("attach demo build to release: %w", err)
  223. }
  224. tilesPath := "map/lxcb-001/tiles/"
  225. mapmetaPath := "map/lxcb-001/tiles/meta.json"
  226. playfieldPath := "kml/lxcb-001/10/c01.kml"
  227. contentPath := "event/content-h5-test-template.html"
  228. manifestChecksum := "demo-checksum-001"
  229. if err := s.ReplaceEventReleaseAssets(ctx, tx, releaseRow.ID, []UpsertEventReleaseAssetParams{
  230. {
  231. EventReleaseID: releaseRow.ID,
  232. AssetType: "manifest",
  233. AssetKey: "manifest",
  234. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json",
  235. Checksum: &manifestChecksum,
  236. Meta: map[string]any{"source": "release-manifest"},
  237. },
  238. {
  239. EventReleaseID: releaseRow.ID,
  240. AssetType: "tiles",
  241. AssetKey: "tiles-root",
  242. AssetPath: &tilesPath,
  243. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  244. Meta: map[string]any{"kind": "directory"},
  245. },
  246. {
  247. EventReleaseID: releaseRow.ID,
  248. AssetType: "mapmeta",
  249. AssetKey: "mapmeta",
  250. AssetPath: &mapmetaPath,
  251. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  252. Meta: map[string]any{"format": "json"},
  253. },
  254. {
  255. EventReleaseID: releaseRow.ID,
  256. AssetType: "playfield",
  257. AssetKey: "course-kml",
  258. AssetPath: &playfieldPath,
  259. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  260. Meta: map[string]any{"format": "kml"},
  261. },
  262. {
  263. EventReleaseID: releaseRow.ID,
  264. AssetType: "content_html",
  265. AssetKey: "content-html",
  266. AssetPath: &contentPath,
  267. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  268. Meta: map[string]any{"kind": "content-page"},
  269. },
  270. }); err != nil {
  271. return nil, fmt.Errorf("ensure demo event release assets: %w", err)
  272. }
  273. var cardPublicID string
  274. if err := tx.QueryRow(ctx, `
  275. INSERT INTO cards (
  276. card_public_id,
  277. tenant_id,
  278. entry_channel_id,
  279. card_type,
  280. title,
  281. subtitle,
  282. cover_url,
  283. event_id,
  284. display_slot,
  285. display_priority,
  286. status
  287. )
  288. VALUES (
  289. 'card_demo_001',
  290. $1,
  291. $2,
  292. 'event',
  293. 'Demo City Run',
  294. '今日推荐路线',
  295. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  296. $3,
  297. 'home_primary',
  298. 100,
  299. 'active'
  300. )
  301. ON CONFLICT (card_public_id) DO UPDATE SET
  302. tenant_id = EXCLUDED.tenant_id,
  303. entry_channel_id = EXCLUDED.entry_channel_id,
  304. card_type = EXCLUDED.card_type,
  305. title = EXCLUDED.title,
  306. subtitle = EXCLUDED.subtitle,
  307. cover_url = EXCLUDED.cover_url,
  308. event_id = EXCLUDED.event_id,
  309. display_slot = EXCLUDED.display_slot,
  310. display_priority = EXCLUDED.display_priority,
  311. status = EXCLUDED.status
  312. RETURNING card_public_id
  313. `, tenantID, channelID, eventID).Scan(&cardPublicID); err != nil {
  314. return nil, fmt.Errorf("ensure demo card: %w", err)
  315. }
  316. var placeID, placePublicID string
  317. if err := tx.QueryRow(ctx, `
  318. INSERT INTO places (
  319. place_public_id, code, name, region, status
  320. )
  321. VALUES (
  322. 'place_demo_001', 'place-demo-001', 'Demo Park', 'Shanghai', 'active'
  323. )
  324. ON CONFLICT (code) DO UPDATE SET
  325. name = EXCLUDED.name,
  326. region = EXCLUDED.region,
  327. status = EXCLUDED.status
  328. RETURNING id, place_public_id
  329. `).Scan(&placeID, &placePublicID); err != nil {
  330. return nil, fmt.Errorf("ensure demo place: %w", err)
  331. }
  332. var mapAssetID, mapAssetPublicID string
  333. if err := tx.QueryRow(ctx, `
  334. INSERT INTO map_assets (
  335. map_asset_public_id, place_id, code, name, map_type, status
  336. )
  337. VALUES (
  338. 'mapasset_demo_001', $1, 'mapasset-demo-001', 'Demo Asset Map', 'standard', 'active'
  339. )
  340. ON CONFLICT (code) DO UPDATE SET
  341. place_id = EXCLUDED.place_id,
  342. name = EXCLUDED.name,
  343. map_type = EXCLUDED.map_type,
  344. status = EXCLUDED.status
  345. RETURNING id, map_asset_public_id
  346. `, placeID).Scan(&mapAssetID, &mapAssetPublicID); err != nil {
  347. return nil, fmt.Errorf("ensure demo map asset: %w", err)
  348. }
  349. var tileReleaseID, tileReleasePublicID string
  350. if err := tx.QueryRow(ctx, `
  351. INSERT INTO tile_releases (
  352. tile_release_public_id, map_asset_id, version_code, status, tile_base_url, meta_url, published_at
  353. )
  354. VALUES (
  355. 'tile_demo_001', $1, 'v2026-04-03', 'published',
  356. 'https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/', 'https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json', NOW()
  357. )
  358. ON CONFLICT (map_asset_id, version_code) DO UPDATE SET
  359. status = EXCLUDED.status,
  360. tile_base_url = EXCLUDED.tile_base_url,
  361. meta_url = EXCLUDED.meta_url,
  362. published_at = EXCLUDED.published_at
  363. RETURNING id, tile_release_public_id
  364. `, mapAssetID).Scan(&tileReleaseID, &tileReleasePublicID); err != nil {
  365. return nil, fmt.Errorf("ensure demo tile release: %w", err)
  366. }
  367. if _, err := tx.Exec(ctx, `
  368. UPDATE map_assets
  369. SET current_tile_release_id = $2
  370. WHERE id = $1
  371. `, mapAssetID, tileReleaseID); err != nil {
  372. return nil, fmt.Errorf("attach demo tile release: %w", err)
  373. }
  374. var courseSourceID, courseSourcePublicID string
  375. if err := tx.QueryRow(ctx, `
  376. INSERT INTO course_sources (
  377. course_source_public_id, source_type, file_url, import_status
  378. )
  379. VALUES (
  380. 'csource_demo_001', 'kml', 'https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml', 'imported'
  381. )
  382. ON CONFLICT (course_source_public_id) DO UPDATE SET
  383. source_type = EXCLUDED.source_type,
  384. file_url = EXCLUDED.file_url,
  385. import_status = EXCLUDED.import_status
  386. RETURNING id, course_source_public_id
  387. `).Scan(&courseSourceID, &courseSourcePublicID); err != nil {
  388. return nil, fmt.Errorf("ensure demo course source: %w", err)
  389. }
  390. var courseSourceVariantBID string
  391. if err := tx.QueryRow(ctx, `
  392. INSERT INTO course_sources (
  393. course_source_public_id, source_type, file_url, import_status
  394. )
  395. VALUES (
  396. 'csource_demo_002', 'kml', 'https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c02.kml', 'imported'
  397. )
  398. ON CONFLICT (course_source_public_id) DO UPDATE SET
  399. source_type = EXCLUDED.source_type,
  400. file_url = EXCLUDED.file_url,
  401. import_status = EXCLUDED.import_status
  402. RETURNING id, course_source_public_id
  403. `).Scan(&courseSourceVariantBID, new(string)); err != nil {
  404. return nil, fmt.Errorf("ensure demo course source variant b: %w", err)
  405. }
  406. var courseSetID, courseSetPublicID string
  407. if err := tx.QueryRow(ctx, `
  408. INSERT INTO course_sets (
  409. course_set_public_id, place_id, map_asset_id, code, mode, name, status
  410. )
  411. VALUES (
  412. 'cset_demo_001', $1, $2, 'cset-demo-001', 'classic-sequential', 'Demo Course Set', 'active'
  413. )
  414. ON CONFLICT (code) DO UPDATE SET
  415. place_id = EXCLUDED.place_id,
  416. map_asset_id = EXCLUDED.map_asset_id,
  417. mode = EXCLUDED.mode,
  418. name = EXCLUDED.name,
  419. status = EXCLUDED.status
  420. RETURNING id, course_set_public_id
  421. `, placeID, mapAssetID).Scan(&courseSetID, &courseSetPublicID); err != nil {
  422. return nil, fmt.Errorf("ensure demo course set: %w", err)
  423. }
  424. var courseVariantID, courseVariantPublicID string
  425. if err := tx.QueryRow(ctx, `
  426. INSERT INTO course_variants (
  427. course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
  428. )
  429. VALUES (
  430. 'cvariant_demo_001', $1, $2, 'Demo Variant A', 'route-demo-a', 'classic-sequential', 8, 'active', true
  431. )
  432. ON CONFLICT (course_variant_public_id) DO UPDATE SET
  433. course_set_id = EXCLUDED.course_set_id,
  434. source_id = EXCLUDED.source_id,
  435. name = EXCLUDED.name,
  436. route_code = EXCLUDED.route_code,
  437. mode = EXCLUDED.mode,
  438. control_count = EXCLUDED.control_count,
  439. status = EXCLUDED.status,
  440. is_default = EXCLUDED.is_default
  441. RETURNING id, course_variant_public_id
  442. `, courseSetID, courseSourceID).Scan(&courseVariantID, &courseVariantPublicID); err != nil {
  443. return nil, fmt.Errorf("ensure demo course variant: %w", err)
  444. }
  445. var courseVariantBID string
  446. if err := tx.QueryRow(ctx, `
  447. INSERT INTO course_variants (
  448. course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
  449. )
  450. VALUES (
  451. 'cvariant_demo_002', $1, $2, 'Demo Variant B', 'route-demo-b', 'classic-sequential', 10, 'active', false
  452. )
  453. ON CONFLICT (course_variant_public_id) DO UPDATE SET
  454. course_set_id = EXCLUDED.course_set_id,
  455. source_id = EXCLUDED.source_id,
  456. name = EXCLUDED.name,
  457. route_code = EXCLUDED.route_code,
  458. mode = EXCLUDED.mode,
  459. control_count = EXCLUDED.control_count,
  460. status = EXCLUDED.status,
  461. is_default = EXCLUDED.is_default
  462. RETURNING id, course_variant_public_id
  463. `, courseSetID, courseSourceVariantBID).Scan(&courseVariantBID, new(string)); err != nil {
  464. return nil, fmt.Errorf("ensure demo course variant b: %w", err)
  465. }
  466. if _, err := tx.Exec(ctx, `
  467. UPDATE course_sets
  468. SET current_variant_id = $2
  469. WHERE id = $1
  470. `, courseSetID, courseVariantID); err != nil {
  471. return nil, fmt.Errorf("attach demo course variant: %w", err)
  472. }
  473. var runtimeBindingID, runtimeBindingPublicID string
  474. if err := tx.QueryRow(ctx, `
  475. INSERT INTO map_runtime_bindings (
  476. runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
  477. )
  478. VALUES (
  479. 'runtime_demo_001', $1, $2, $3, $4, $5, $6, 'active', 'demo runtime binding'
  480. )
  481. ON CONFLICT (runtime_binding_public_id) DO UPDATE SET
  482. event_id = EXCLUDED.event_id,
  483. place_id = EXCLUDED.place_id,
  484. map_asset_id = EXCLUDED.map_asset_id,
  485. tile_release_id = EXCLUDED.tile_release_id,
  486. course_set_id = EXCLUDED.course_set_id,
  487. course_variant_id = EXCLUDED.course_variant_id,
  488. status = EXCLUDED.status,
  489. notes = EXCLUDED.notes
  490. RETURNING id, runtime_binding_public_id
  491. `, eventID, placeID, mapAssetID, tileReleaseID, courseSetID, courseVariantID).Scan(&runtimeBindingID, &runtimeBindingPublicID); err != nil {
  492. return nil, fmt.Errorf("ensure demo runtime binding: %w", err)
  493. }
  494. var manualEventID string
  495. if err := tx.QueryRow(ctx, `
  496. INSERT INTO events (
  497. tenant_id, event_public_id, slug, display_name, summary, status
  498. )
  499. VALUES ($1, 'evt_demo_variant_manual_001', 'demo-variant-manual-run', 'Demo Variant Manual Run', 'Manual 多赛道联调活动', 'active')
  500. ON CONFLICT (event_public_id) DO UPDATE SET
  501. tenant_id = EXCLUDED.tenant_id,
  502. slug = EXCLUDED.slug,
  503. display_name = EXCLUDED.display_name,
  504. summary = EXCLUDED.summary,
  505. status = EXCLUDED.status
  506. RETURNING id
  507. `, tenantID).Scan(&manualEventID); err != nil {
  508. return nil, fmt.Errorf("ensure variant manual demo event: %w", err)
  509. }
  510. var manualReleaseRow struct {
  511. ID string
  512. PublicID string
  513. }
  514. if err := tx.QueryRow(ctx, `
  515. INSERT INTO event_releases (
  516. release_public_id,
  517. event_id,
  518. release_no,
  519. config_label,
  520. manifest_url,
  521. manifest_checksum_sha256,
  522. route_code,
  523. status,
  524. payload_jsonb
  525. )
  526. VALUES (
  527. 'rel_demo_variant_manual_001',
  528. $1,
  529. 1,
  530. 'Demo Variant Manual Config v1',
  531. 'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
  532. 'demo-variant-checksum-001',
  533. 'route-variant-a',
  534. 'published',
  535. $2::jsonb
  536. )
  537. ON CONFLICT (release_public_id) DO UPDATE SET
  538. event_id = EXCLUDED.event_id,
  539. config_label = EXCLUDED.config_label,
  540. manifest_url = EXCLUDED.manifest_url,
  541. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  542. route_code = EXCLUDED.route_code,
  543. status = EXCLUDED.status,
  544. payload_jsonb = EXCLUDED.payload_jsonb
  545. RETURNING id, release_public_id
  546. `, manualEventID, `{
  547. "play": {
  548. "assignmentMode": "manual",
  549. "courseVariants": [
  550. {
  551. "id": "variant_a",
  552. "name": "A 线",
  553. "description": "短线体验版(c01.kml)",
  554. "routeCode": "route-variant-a",
  555. "selectable": true
  556. },
  557. {
  558. "id": "variant_b",
  559. "name": "B 线",
  560. "description": "长线挑战版(c02.kml)",
  561. "routeCode": "route-variant-b",
  562. "selectable": true
  563. }
  564. ]
  565. }
  566. }`).Scan(&manualReleaseRow.ID, &manualReleaseRow.PublicID); err != nil {
  567. return nil, fmt.Errorf("ensure variant manual demo release: %w", err)
  568. }
  569. if _, err := tx.Exec(ctx, `
  570. UPDATE events
  571. SET current_release_id = $2
  572. WHERE id = $1
  573. `, manualEventID, manualReleaseRow.ID); err != nil {
  574. return nil, fmt.Errorf("attach variant manual demo release: %w", err)
  575. }
  576. var manualCardPublicID string
  577. if err := tx.QueryRow(ctx, `
  578. INSERT INTO cards (
  579. card_public_id,
  580. tenant_id,
  581. entry_channel_id,
  582. card_type,
  583. title,
  584. subtitle,
  585. cover_url,
  586. event_id,
  587. display_slot,
  588. display_priority,
  589. status
  590. )
  591. VALUES (
  592. 'card_demo_variant_manual_001',
  593. $1,
  594. $2,
  595. 'event',
  596. 'Demo Variant Manual Run',
  597. '多赛道手动选择联调',
  598. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  599. $3,
  600. 'home_primary',
  601. 95,
  602. 'active'
  603. )
  604. ON CONFLICT (card_public_id) DO UPDATE SET
  605. tenant_id = EXCLUDED.tenant_id,
  606. entry_channel_id = EXCLUDED.entry_channel_id,
  607. card_type = EXCLUDED.card_type,
  608. title = EXCLUDED.title,
  609. subtitle = EXCLUDED.subtitle,
  610. cover_url = EXCLUDED.cover_url,
  611. event_id = EXCLUDED.event_id,
  612. display_slot = EXCLUDED.display_slot,
  613. display_priority = EXCLUDED.display_priority,
  614. status = EXCLUDED.status
  615. RETURNING card_public_id
  616. `, tenantID, channelID, manualEventID).Scan(&manualCardPublicID); err != nil {
  617. return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
  618. }
  619. var scoreOEventID string
  620. if err := tx.QueryRow(ctx, `
  621. INSERT INTO events (
  622. tenant_id, event_public_id, slug, display_name, summary, status
  623. )
  624. VALUES ($1, 'evt_demo_score_o_001', 'demo-score-o-run', 'Demo Score-O Run', '积分赛联调活动', 'active')
  625. ON CONFLICT (event_public_id) DO UPDATE SET
  626. tenant_id = EXCLUDED.tenant_id,
  627. slug = EXCLUDED.slug,
  628. display_name = EXCLUDED.display_name,
  629. summary = EXCLUDED.summary,
  630. status = EXCLUDED.status
  631. RETURNING id
  632. `, tenantID).Scan(&scoreOEventID); err != nil {
  633. return nil, fmt.Errorf("ensure score-o demo event: %w", err)
  634. }
  635. var scoreOReleaseRow struct {
  636. ID string
  637. PublicID string
  638. }
  639. if err := tx.QueryRow(ctx, `
  640. INSERT INTO event_releases (
  641. release_public_id,
  642. event_id,
  643. release_no,
  644. config_label,
  645. manifest_url,
  646. manifest_checksum_sha256,
  647. route_code,
  648. status
  649. )
  650. VALUES (
  651. 'rel_demo_score_o_001',
  652. $1,
  653. 1,
  654. 'Demo Score-O Config v1',
  655. 'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json',
  656. 'demo-score-o-checksum-001',
  657. 'route-score-o-001',
  658. 'published'
  659. )
  660. ON CONFLICT (release_public_id) DO UPDATE SET
  661. event_id = EXCLUDED.event_id,
  662. config_label = EXCLUDED.config_label,
  663. manifest_url = EXCLUDED.manifest_url,
  664. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  665. route_code = EXCLUDED.route_code,
  666. status = EXCLUDED.status
  667. RETURNING id, release_public_id
  668. `, scoreOEventID).Scan(&scoreOReleaseRow.ID, &scoreOReleaseRow.PublicID); err != nil {
  669. return nil, fmt.Errorf("ensure score-o demo release: %w", err)
  670. }
  671. if _, err := tx.Exec(ctx, `
  672. UPDATE events
  673. SET current_release_id = $2
  674. WHERE id = $1
  675. `, scoreOEventID, scoreOReleaseRow.ID); err != nil {
  676. return nil, fmt.Errorf("attach score-o demo release: %w", err)
  677. }
  678. scoreOSourceNotes := "demo source config imported from local event sample score-o"
  679. scoreOSource, err := s.UpsertEventConfigSource(ctx, tx, UpsertEventConfigSourceParams{
  680. EventID: scoreOEventID,
  681. SourceVersionNo: 1,
  682. SourceKind: "event_bundle",
  683. SchemaID: "event-source",
  684. SchemaVersion: "1",
  685. Status: "active",
  686. Notes: &scoreOSourceNotes,
  687. Source: map[string]any{
  688. "schemaVersion": "1",
  689. "app": map[string]any{
  690. "id": "sample-score-o-001",
  691. "title": "积分赛示例",
  692. },
  693. "branding": map[string]any{
  694. "tenantCode": "tenant_demo",
  695. "entryChannel": "mini-demo",
  696. },
  697. "map": map[string]any{
  698. "tiles": "../map/lxcb-001/tiles/",
  699. "mapmeta": "../map/lxcb-001/tiles/meta.json",
  700. },
  701. "playfield": map[string]any{
  702. "kind": "control-set",
  703. "source": map[string]any{
  704. "type": "kml",
  705. "url": "../kml/lxcb-001/10/c01.kml",
  706. },
  707. },
  708. "game": map[string]any{
  709. "mode": "score-o",
  710. },
  711. "content": map[string]any{
  712. "h5Template": "content-h5-test-template.html",
  713. },
  714. },
  715. })
  716. if err != nil {
  717. return nil, fmt.Errorf("ensure score-o demo event config source: %w", err)
  718. }
  719. scoreOBuildLog := "demo build generated from sample score-o.json"
  720. scoreOBuild, err := s.UpsertEventConfigBuild(ctx, tx, UpsertEventConfigBuildParams{
  721. EventID: scoreOEventID,
  722. SourceID: scoreOSource.ID,
  723. BuildNo: 1,
  724. BuildStatus: "success",
  725. BuildLog: &scoreOBuildLog,
  726. Manifest: map[string]any{
  727. "schemaVersion": "1",
  728. "releaseId": "rel_demo_score_o_001",
  729. "version": "2026.04.01",
  730. "app": map[string]any{
  731. "id": "sample-score-o-001",
  732. "title": "积分赛示例",
  733. },
  734. "map": map[string]any{
  735. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  736. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  737. },
  738. "playfield": map[string]any{
  739. "kind": "control-set",
  740. "source": map[string]any{
  741. "type": "kml",
  742. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  743. },
  744. },
  745. "game": map[string]any{
  746. "mode": "score-o",
  747. },
  748. "assets": map[string]any{
  749. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  750. },
  751. },
  752. AssetIndex: []map[string]any{
  753. {"assetType": "manifest", "assetKey": "manifest"},
  754. {"assetType": "mapmeta", "assetKey": "mapmeta"},
  755. {"assetType": "playfield", "assetKey": "playfield-kml"},
  756. {"assetType": "content_html", "assetKey": "content-html"},
  757. },
  758. })
  759. if err != nil {
  760. return nil, fmt.Errorf("ensure score-o demo event config build: %w", err)
  761. }
  762. if err := s.AttachBuildToRelease(ctx, tx, scoreOReleaseRow.ID, scoreOBuild.ID); err != nil {
  763. return nil, fmt.Errorf("attach score-o demo build to release: %w", err)
  764. }
  765. var scoreOCardPublicID string
  766. if err := tx.QueryRow(ctx, `
  767. INSERT INTO cards (
  768. card_public_id,
  769. tenant_id,
  770. entry_channel_id,
  771. card_type,
  772. title,
  773. subtitle,
  774. cover_url,
  775. event_id,
  776. display_slot,
  777. display_priority,
  778. status
  779. )
  780. VALUES (
  781. 'card_demo_score_o_001',
  782. $1,
  783. $2,
  784. 'event',
  785. 'Demo Score-O Run',
  786. '积分赛联调入口',
  787. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  788. $3,
  789. 'home_primary',
  790. 98,
  791. 'active'
  792. )
  793. ON CONFLICT (card_public_id) DO UPDATE SET
  794. tenant_id = EXCLUDED.tenant_id,
  795. entry_channel_id = EXCLUDED.entry_channel_id,
  796. card_type = EXCLUDED.card_type,
  797. title = EXCLUDED.title,
  798. subtitle = EXCLUDED.subtitle,
  799. cover_url = EXCLUDED.cover_url,
  800. event_id = EXCLUDED.event_id,
  801. display_slot = EXCLUDED.display_slot,
  802. display_priority = EXCLUDED.display_priority,
  803. status = EXCLUDED.status
  804. RETURNING card_public_id
  805. `, tenantID, channelID, scoreOEventID).Scan(&scoreOCardPublicID); err != nil {
  806. return nil, fmt.Errorf("ensure score-o demo card: %w", err)
  807. }
  808. var scoreOCourseSetID, scoreOCourseSetPublicID string
  809. if err := tx.QueryRow(ctx, `
  810. INSERT INTO course_sets (
  811. course_set_public_id, place_id, map_asset_id, code, mode, name, status
  812. )
  813. VALUES (
  814. 'cset_demo_score_o_001', $1, $2, 'cset-demo-score-o-001', 'score-o', 'Demo Score-O Course Set', 'active'
  815. )
  816. ON CONFLICT (code) DO UPDATE SET
  817. place_id = EXCLUDED.place_id,
  818. map_asset_id = EXCLUDED.map_asset_id,
  819. mode = EXCLUDED.mode,
  820. name = EXCLUDED.name,
  821. status = EXCLUDED.status
  822. RETURNING id, course_set_public_id
  823. `, placeID, mapAssetID).Scan(&scoreOCourseSetID, &scoreOCourseSetPublicID); err != nil {
  824. return nil, fmt.Errorf("ensure score-o demo course set: %w", err)
  825. }
  826. var scoreOCourseVariantID, scoreOCourseVariantPublicID string
  827. if err := tx.QueryRow(ctx, `
  828. INSERT INTO course_variants (
  829. course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
  830. )
  831. VALUES (
  832. 'cvariant_demo_score_o_001', $1, $2, 'Demo Score-O Variant', 'route-score-o-001', 'score-o', 10, 'active', true
  833. )
  834. ON CONFLICT (course_variant_public_id) DO UPDATE SET
  835. course_set_id = EXCLUDED.course_set_id,
  836. source_id = EXCLUDED.source_id,
  837. name = EXCLUDED.name,
  838. route_code = EXCLUDED.route_code,
  839. mode = EXCLUDED.mode,
  840. control_count = EXCLUDED.control_count,
  841. status = EXCLUDED.status,
  842. is_default = EXCLUDED.is_default
  843. RETURNING id, course_variant_public_id
  844. `, scoreOCourseSetID, courseSourceID).Scan(&scoreOCourseVariantID, &scoreOCourseVariantPublicID); err != nil {
  845. return nil, fmt.Errorf("ensure score-o demo course variant: %w", err)
  846. }
  847. if _, err := tx.Exec(ctx, `
  848. UPDATE course_sets
  849. SET current_variant_id = $2
  850. WHERE id = $1
  851. `, scoreOCourseSetID, scoreOCourseVariantID); err != nil {
  852. return nil, fmt.Errorf("attach score-o demo course variant: %w", err)
  853. }
  854. var scoreORuntimeBindingID, scoreORuntimeBindingPublicID string
  855. if err := tx.QueryRow(ctx, `
  856. INSERT INTO map_runtime_bindings (
  857. runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
  858. )
  859. VALUES (
  860. 'runtime_demo_score_o_001', $1, $2, $3, $4, $5, $6, 'active', 'demo score-o runtime binding'
  861. )
  862. ON CONFLICT (runtime_binding_public_id) DO UPDATE SET
  863. event_id = EXCLUDED.event_id,
  864. place_id = EXCLUDED.place_id,
  865. map_asset_id = EXCLUDED.map_asset_id,
  866. tile_release_id = EXCLUDED.tile_release_id,
  867. course_set_id = EXCLUDED.course_set_id,
  868. course_variant_id = EXCLUDED.course_variant_id,
  869. status = EXCLUDED.status,
  870. notes = EXCLUDED.notes
  871. RETURNING id, runtime_binding_public_id
  872. `, scoreOEventID, placeID, mapAssetID, tileReleaseID, scoreOCourseSetID, scoreOCourseVariantID).Scan(&scoreORuntimeBindingID, &scoreORuntimeBindingPublicID); err != nil {
  873. return nil, fmt.Errorf("ensure score-o demo runtime binding: %w", err)
  874. }
  875. var cleanedSessionCount int64
  876. if err := tx.QueryRow(ctx, `
  877. WITH cleaned AS (
  878. UPDATE game_sessions
  879. SET
  880. status = 'cancelled',
  881. ended_at = NOW(),
  882. updated_at = NOW()
  883. WHERE event_id = ANY($1::uuid[])
  884. AND status IN ('launched', 'running')
  885. RETURNING 1
  886. )
  887. SELECT COUNT(*) FROM cleaned
  888. `, []string{eventID, scoreOEventID, manualEventID}).Scan(&cleanedSessionCount); err != nil {
  889. return nil, fmt.Errorf("cleanup demo ongoing sessions: %w", err)
  890. }
  891. if err := tx.Commit(ctx); err != nil {
  892. return nil, err
  893. }
  894. return &DemoBootstrapSummary{
  895. TenantCode: "tenant_demo",
  896. ChannelCode: "mini-demo",
  897. EventID: "evt_demo_001",
  898. ReleaseID: releaseRow.PublicID,
  899. SourceID: source.ID,
  900. BuildID: build.ID,
  901. CardID: cardPublicID,
  902. PlaceID: placePublicID,
  903. MapAssetID: mapAssetPublicID,
  904. TileReleaseID: tileReleasePublicID,
  905. CourseSourceID: courseSourcePublicID,
  906. CourseSetID: courseSetPublicID,
  907. CourseVariantID: courseVariantPublicID,
  908. RuntimeBindingID: runtimeBindingPublicID,
  909. ScoreOEventID: "evt_demo_score_o_001",
  910. ScoreOReleaseID: scoreOReleaseRow.PublicID,
  911. ScoreOCardID: scoreOCardPublicID,
  912. ScoreOSourceID: scoreOSource.ID,
  913. ScoreOBuildID: scoreOBuild.ID,
  914. ScoreOCourseSetID: scoreOCourseSetPublicID,
  915. ScoreOCourseVariantID: scoreOCourseVariantPublicID,
  916. ScoreORuntimeBindingID: scoreORuntimeBindingPublicID,
  917. VariantManualEventID: "evt_demo_variant_manual_001",
  918. VariantManualRelease: manualReleaseRow.PublicID,
  919. VariantManualCardID: manualCardPublicID,
  920. CleanedSessionCount: cleanedSessionCount,
  921. }, nil
  922. }