dev_store.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  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. VariantManualEventID string `json:"variantManualEventId"`
  22. VariantManualRelease string `json:"variantManualReleaseId"`
  23. VariantManualCardID string `json:"variantManualCardId"`
  24. CleanedSessionCount int64 `json:"cleanedSessionCount"`
  25. }
  26. func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
  27. tx, err := s.Begin(ctx)
  28. if err != nil {
  29. return nil, err
  30. }
  31. defer tx.Rollback(ctx)
  32. var tenantID string
  33. if err := tx.QueryRow(ctx, `
  34. INSERT INTO tenants (tenant_code, name, status)
  35. VALUES ('tenant_demo', 'Demo Tenant', 'active')
  36. ON CONFLICT (tenant_code) DO UPDATE SET
  37. name = EXCLUDED.name,
  38. status = EXCLUDED.status
  39. RETURNING id
  40. `).Scan(&tenantID); err != nil {
  41. return nil, fmt.Errorf("ensure demo tenant: %w", err)
  42. }
  43. var channelID string
  44. if err := tx.QueryRow(ctx, `
  45. INSERT INTO entry_channels (
  46. tenant_id, channel_code, channel_type, platform_app_id, display_name, status, is_default
  47. )
  48. VALUES ($1, 'mini-demo', 'wechat_mini', 'wx-demo-appid', 'Demo Mini Channel', 'active', true)
  49. ON CONFLICT (tenant_id, channel_code) DO UPDATE SET
  50. channel_type = EXCLUDED.channel_type,
  51. platform_app_id = EXCLUDED.platform_app_id,
  52. display_name = EXCLUDED.display_name,
  53. status = EXCLUDED.status,
  54. is_default = EXCLUDED.is_default
  55. RETURNING id
  56. `, tenantID).Scan(&channelID); err != nil {
  57. return nil, fmt.Errorf("ensure demo entry channel: %w", err)
  58. }
  59. var eventID string
  60. if err := tx.QueryRow(ctx, `
  61. INSERT INTO events (
  62. tenant_id, event_public_id, slug, display_name, summary, status
  63. )
  64. VALUES ($1, 'evt_demo_001', 'demo-city-run', 'Demo City Run', 'Launch flow demo event', 'active')
  65. ON CONFLICT (event_public_id) DO UPDATE SET
  66. tenant_id = EXCLUDED.tenant_id,
  67. slug = EXCLUDED.slug,
  68. display_name = EXCLUDED.display_name,
  69. summary = EXCLUDED.summary,
  70. status = EXCLUDED.status
  71. RETURNING id
  72. `, tenantID).Scan(&eventID); err != nil {
  73. return nil, fmt.Errorf("ensure demo event: %w", err)
  74. }
  75. var releaseRow struct {
  76. ID string
  77. PublicID string
  78. }
  79. if err := tx.QueryRow(ctx, `
  80. INSERT INTO event_releases (
  81. release_public_id,
  82. event_id,
  83. release_no,
  84. config_label,
  85. manifest_url,
  86. manifest_checksum_sha256,
  87. route_code,
  88. status
  89. )
  90. VALUES (
  91. 'rel_demo_001',
  92. $1,
  93. 1,
  94. 'Demo Config v1',
  95. 'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
  96. 'demo-checksum-001',
  97. 'route-demo-001',
  98. 'published'
  99. )
  100. ON CONFLICT (release_public_id) DO UPDATE SET
  101. event_id = EXCLUDED.event_id,
  102. config_label = EXCLUDED.config_label,
  103. manifest_url = EXCLUDED.manifest_url,
  104. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  105. route_code = EXCLUDED.route_code,
  106. status = EXCLUDED.status
  107. RETURNING id, release_public_id
  108. `, eventID).Scan(&releaseRow.ID, &releaseRow.PublicID); err != nil {
  109. return nil, fmt.Errorf("ensure demo release: %w", err)
  110. }
  111. if _, err := tx.Exec(ctx, `
  112. UPDATE events
  113. SET current_release_id = $2
  114. WHERE id = $1
  115. `, eventID, releaseRow.ID); err != nil {
  116. return nil, fmt.Errorf("attach demo release: %w", err)
  117. }
  118. sourceNotes := "demo source config imported from local event sample"
  119. source, err := s.UpsertEventConfigSource(ctx, tx, UpsertEventConfigSourceParams{
  120. EventID: eventID,
  121. SourceVersionNo: 1,
  122. SourceKind: "event_bundle",
  123. SchemaID: "event-source",
  124. SchemaVersion: "1",
  125. Status: "active",
  126. Notes: &sourceNotes,
  127. Source: map[string]any{
  128. "app": map[string]any{
  129. "id": "sample-classic-001",
  130. "title": "顺序赛示例",
  131. },
  132. "branding": map[string]any{
  133. "tenantCode": "tenant_demo",
  134. "entryChannel": "mini-demo",
  135. },
  136. "map": map[string]any{
  137. "tiles": "../map/lxcb-001/tiles/",
  138. "mapmeta": "../map/lxcb-001/tiles/meta.json",
  139. },
  140. "playfield": map[string]any{
  141. "kind": "course",
  142. "source": map[string]any{
  143. "type": "kml",
  144. "url": "../kml/lxcb-001/10/c01.kml",
  145. },
  146. },
  147. "game": map[string]any{
  148. "mode": "classic-sequential",
  149. },
  150. "content": map[string]any{
  151. "h5Template": "content-h5-test-template.html",
  152. },
  153. },
  154. })
  155. if err != nil {
  156. return nil, fmt.Errorf("ensure demo event config source: %w", err)
  157. }
  158. buildLog := "demo build generated from sample classic-sequential.json"
  159. build, err := s.UpsertEventConfigBuild(ctx, tx, UpsertEventConfigBuildParams{
  160. EventID: eventID,
  161. SourceID: source.ID,
  162. BuildNo: 1,
  163. BuildStatus: "success",
  164. BuildLog: &buildLog,
  165. Manifest: map[string]any{
  166. "schemaVersion": "1",
  167. "releaseId": "rel_demo_001",
  168. "version": "2026.04.01",
  169. "app": map[string]any{
  170. "id": "sample-classic-001",
  171. "title": "顺序赛示例",
  172. },
  173. "map": map[string]any{
  174. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  175. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  176. },
  177. "playfield": map[string]any{
  178. "kind": "course",
  179. "source": map[string]any{
  180. "type": "kml",
  181. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  182. },
  183. },
  184. "game": map[string]any{
  185. "mode": "classic-sequential",
  186. },
  187. "assets": map[string]any{
  188. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  189. },
  190. },
  191. AssetIndex: []map[string]any{
  192. {
  193. "assetType": "manifest",
  194. "assetKey": "manifest",
  195. },
  196. {
  197. "assetType": "mapmeta",
  198. "assetKey": "mapmeta",
  199. },
  200. {
  201. "assetType": "playfield",
  202. "assetKey": "playfield-kml",
  203. },
  204. {
  205. "assetType": "content_html",
  206. "assetKey": "content-html",
  207. },
  208. },
  209. })
  210. if err != nil {
  211. return nil, fmt.Errorf("ensure demo event config build: %w", err)
  212. }
  213. if err := s.AttachBuildToRelease(ctx, tx, releaseRow.ID, build.ID); err != nil {
  214. return nil, fmt.Errorf("attach demo build to release: %w", err)
  215. }
  216. tilesPath := "map/lxcb-001/tiles/"
  217. mapmetaPath := "map/lxcb-001/tiles/meta.json"
  218. playfieldPath := "kml/lxcb-001/10/c01.kml"
  219. contentPath := "event/content-h5-test-template.html"
  220. manifestChecksum := "demo-checksum-001"
  221. if err := s.ReplaceEventReleaseAssets(ctx, tx, releaseRow.ID, []UpsertEventReleaseAssetParams{
  222. {
  223. EventReleaseID: releaseRow.ID,
  224. AssetType: "manifest",
  225. AssetKey: "manifest",
  226. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json",
  227. Checksum: &manifestChecksum,
  228. Meta: map[string]any{"source": "release-manifest"},
  229. },
  230. {
  231. EventReleaseID: releaseRow.ID,
  232. AssetType: "tiles",
  233. AssetKey: "tiles-root",
  234. AssetPath: &tilesPath,
  235. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  236. Meta: map[string]any{"kind": "directory"},
  237. },
  238. {
  239. EventReleaseID: releaseRow.ID,
  240. AssetType: "mapmeta",
  241. AssetKey: "mapmeta",
  242. AssetPath: &mapmetaPath,
  243. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  244. Meta: map[string]any{"format": "json"},
  245. },
  246. {
  247. EventReleaseID: releaseRow.ID,
  248. AssetType: "playfield",
  249. AssetKey: "course-kml",
  250. AssetPath: &playfieldPath,
  251. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  252. Meta: map[string]any{"format": "kml"},
  253. },
  254. {
  255. EventReleaseID: releaseRow.ID,
  256. AssetType: "content_html",
  257. AssetKey: "content-html",
  258. AssetPath: &contentPath,
  259. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  260. Meta: map[string]any{"kind": "content-page"},
  261. },
  262. }); err != nil {
  263. return nil, fmt.Errorf("ensure demo event release assets: %w", err)
  264. }
  265. var cardPublicID string
  266. if err := tx.QueryRow(ctx, `
  267. INSERT INTO cards (
  268. card_public_id,
  269. tenant_id,
  270. entry_channel_id,
  271. card_type,
  272. title,
  273. subtitle,
  274. cover_url,
  275. event_id,
  276. display_slot,
  277. display_priority,
  278. status
  279. )
  280. VALUES (
  281. 'card_demo_001',
  282. $1,
  283. $2,
  284. 'event',
  285. 'Demo City Run',
  286. '今日推荐路线',
  287. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  288. $3,
  289. 'home_primary',
  290. 100,
  291. 'active'
  292. )
  293. ON CONFLICT (card_public_id) DO UPDATE SET
  294. tenant_id = EXCLUDED.tenant_id,
  295. entry_channel_id = EXCLUDED.entry_channel_id,
  296. card_type = EXCLUDED.card_type,
  297. title = EXCLUDED.title,
  298. subtitle = EXCLUDED.subtitle,
  299. cover_url = EXCLUDED.cover_url,
  300. event_id = EXCLUDED.event_id,
  301. display_slot = EXCLUDED.display_slot,
  302. display_priority = EXCLUDED.display_priority,
  303. status = EXCLUDED.status
  304. RETURNING card_public_id
  305. `, tenantID, channelID, eventID).Scan(&cardPublicID); err != nil {
  306. return nil, fmt.Errorf("ensure demo card: %w", err)
  307. }
  308. var placeID, placePublicID string
  309. if err := tx.QueryRow(ctx, `
  310. INSERT INTO places (
  311. place_public_id, code, name, region, status
  312. )
  313. VALUES (
  314. 'place_demo_001', 'place-demo-001', 'Demo Park', 'Shanghai', 'active'
  315. )
  316. ON CONFLICT (code) DO UPDATE SET
  317. name = EXCLUDED.name,
  318. region = EXCLUDED.region,
  319. status = EXCLUDED.status
  320. RETURNING id, place_public_id
  321. `).Scan(&placeID, &placePublicID); err != nil {
  322. return nil, fmt.Errorf("ensure demo place: %w", err)
  323. }
  324. var mapAssetID, mapAssetPublicID string
  325. if err := tx.QueryRow(ctx, `
  326. INSERT INTO map_assets (
  327. map_asset_public_id, place_id, code, name, map_type, status
  328. )
  329. VALUES (
  330. 'mapasset_demo_001', $1, 'mapasset-demo-001', 'Demo Asset Map', 'standard', 'active'
  331. )
  332. ON CONFLICT (code) DO UPDATE SET
  333. place_id = EXCLUDED.place_id,
  334. name = EXCLUDED.name,
  335. map_type = EXCLUDED.map_type,
  336. status = EXCLUDED.status
  337. RETURNING id, map_asset_public_id
  338. `, placeID).Scan(&mapAssetID, &mapAssetPublicID); err != nil {
  339. return nil, fmt.Errorf("ensure demo map asset: %w", err)
  340. }
  341. var tileReleaseID, tileReleasePublicID string
  342. if err := tx.QueryRow(ctx, `
  343. INSERT INTO tile_releases (
  344. tile_release_public_id, map_asset_id, version_code, status, tile_base_url, meta_url, published_at
  345. )
  346. VALUES (
  347. 'tile_demo_001', $1, 'v2026-04-03', 'published',
  348. 'https://example.com/tiles/demo/', 'https://example.com/tiles/demo/meta.json', NOW()
  349. )
  350. ON CONFLICT (map_asset_id, version_code) DO UPDATE SET
  351. status = EXCLUDED.status,
  352. tile_base_url = EXCLUDED.tile_base_url,
  353. meta_url = EXCLUDED.meta_url,
  354. published_at = EXCLUDED.published_at
  355. RETURNING id, tile_release_public_id
  356. `, mapAssetID).Scan(&tileReleaseID, &tileReleasePublicID); err != nil {
  357. return nil, fmt.Errorf("ensure demo tile release: %w", err)
  358. }
  359. if _, err := tx.Exec(ctx, `
  360. UPDATE map_assets
  361. SET current_tile_release_id = $2
  362. WHERE id = $1
  363. `, mapAssetID, tileReleaseID); err != nil {
  364. return nil, fmt.Errorf("attach demo tile release: %w", err)
  365. }
  366. var courseSourceID, courseSourcePublicID string
  367. if err := tx.QueryRow(ctx, `
  368. INSERT INTO course_sources (
  369. course_source_public_id, source_type, file_url, import_status
  370. )
  371. VALUES (
  372. 'csource_demo_001', 'kml', 'https://example.com/course/demo.kml', 'imported'
  373. )
  374. ON CONFLICT (course_source_public_id) DO UPDATE SET
  375. source_type = EXCLUDED.source_type,
  376. file_url = EXCLUDED.file_url,
  377. import_status = EXCLUDED.import_status
  378. RETURNING id, course_source_public_id
  379. `).Scan(&courseSourceID, &courseSourcePublicID); err != nil {
  380. return nil, fmt.Errorf("ensure demo course source: %w", err)
  381. }
  382. var courseSetID, courseSetPublicID string
  383. if err := tx.QueryRow(ctx, `
  384. INSERT INTO course_sets (
  385. course_set_public_id, place_id, map_asset_id, code, mode, name, status
  386. )
  387. VALUES (
  388. 'cset_demo_001', $1, $2, 'cset-demo-001', 'classic-sequential', 'Demo Course Set', 'active'
  389. )
  390. ON CONFLICT (code) DO UPDATE SET
  391. place_id = EXCLUDED.place_id,
  392. map_asset_id = EXCLUDED.map_asset_id,
  393. mode = EXCLUDED.mode,
  394. name = EXCLUDED.name,
  395. status = EXCLUDED.status
  396. RETURNING id, course_set_public_id
  397. `, placeID, mapAssetID).Scan(&courseSetID, &courseSetPublicID); err != nil {
  398. return nil, fmt.Errorf("ensure demo course set: %w", err)
  399. }
  400. var courseVariantID, courseVariantPublicID string
  401. if err := tx.QueryRow(ctx, `
  402. INSERT INTO course_variants (
  403. course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
  404. )
  405. VALUES (
  406. 'cvariant_demo_001', $1, $2, 'Demo Variant A', 'route-demo-a', 'classic-sequential', 8, 'active', true
  407. )
  408. ON CONFLICT (course_variant_public_id) DO UPDATE SET
  409. course_set_id = EXCLUDED.course_set_id,
  410. source_id = EXCLUDED.source_id,
  411. name = EXCLUDED.name,
  412. route_code = EXCLUDED.route_code,
  413. mode = EXCLUDED.mode,
  414. control_count = EXCLUDED.control_count,
  415. status = EXCLUDED.status,
  416. is_default = EXCLUDED.is_default
  417. RETURNING id, course_variant_public_id
  418. `, courseSetID, courseSourceID).Scan(&courseVariantID, &courseVariantPublicID); err != nil {
  419. return nil, fmt.Errorf("ensure demo course variant: %w", err)
  420. }
  421. if _, err := tx.Exec(ctx, `
  422. UPDATE course_sets
  423. SET current_variant_id = $2
  424. WHERE id = $1
  425. `, courseSetID, courseVariantID); err != nil {
  426. return nil, fmt.Errorf("attach demo course variant: %w", err)
  427. }
  428. var runtimeBindingID, runtimeBindingPublicID string
  429. if err := tx.QueryRow(ctx, `
  430. INSERT INTO map_runtime_bindings (
  431. runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
  432. )
  433. VALUES (
  434. 'runtime_demo_001', $1, $2, $3, $4, $5, $6, 'active', 'demo runtime binding'
  435. )
  436. ON CONFLICT (runtime_binding_public_id) DO UPDATE SET
  437. event_id = EXCLUDED.event_id,
  438. place_id = EXCLUDED.place_id,
  439. map_asset_id = EXCLUDED.map_asset_id,
  440. tile_release_id = EXCLUDED.tile_release_id,
  441. course_set_id = EXCLUDED.course_set_id,
  442. course_variant_id = EXCLUDED.course_variant_id,
  443. status = EXCLUDED.status,
  444. notes = EXCLUDED.notes
  445. RETURNING id, runtime_binding_public_id
  446. `, eventID, placeID, mapAssetID, tileReleaseID, courseSetID, courseVariantID).Scan(&runtimeBindingID, &runtimeBindingPublicID); err != nil {
  447. return nil, fmt.Errorf("ensure demo runtime binding: %w", err)
  448. }
  449. var manualEventID string
  450. if err := tx.QueryRow(ctx, `
  451. INSERT INTO events (
  452. tenant_id, event_public_id, slug, display_name, summary, status
  453. )
  454. VALUES ($1, 'evt_demo_variant_manual_001', 'demo-variant-manual-run', 'Demo Variant Manual Run', 'Manual 多赛道联调活动', 'active')
  455. ON CONFLICT (event_public_id) DO UPDATE SET
  456. tenant_id = EXCLUDED.tenant_id,
  457. slug = EXCLUDED.slug,
  458. display_name = EXCLUDED.display_name,
  459. summary = EXCLUDED.summary,
  460. status = EXCLUDED.status
  461. RETURNING id
  462. `, tenantID).Scan(&manualEventID); err != nil {
  463. return nil, fmt.Errorf("ensure variant manual demo event: %w", err)
  464. }
  465. var manualReleaseRow struct {
  466. ID string
  467. PublicID string
  468. }
  469. if err := tx.QueryRow(ctx, `
  470. INSERT INTO event_releases (
  471. release_public_id,
  472. event_id,
  473. release_no,
  474. config_label,
  475. manifest_url,
  476. manifest_checksum_sha256,
  477. route_code,
  478. status,
  479. payload_jsonb
  480. )
  481. VALUES (
  482. 'rel_demo_variant_manual_001',
  483. $1,
  484. 1,
  485. 'Demo Variant Manual Config v1',
  486. 'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
  487. 'demo-variant-checksum-001',
  488. 'route-variant-a',
  489. 'published',
  490. $2::jsonb
  491. )
  492. ON CONFLICT (release_public_id) DO UPDATE SET
  493. event_id = EXCLUDED.event_id,
  494. config_label = EXCLUDED.config_label,
  495. manifest_url = EXCLUDED.manifest_url,
  496. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  497. route_code = EXCLUDED.route_code,
  498. status = EXCLUDED.status,
  499. payload_jsonb = EXCLUDED.payload_jsonb
  500. RETURNING id, release_public_id
  501. `, manualEventID, `{
  502. "play": {
  503. "assignmentMode": "manual",
  504. "courseVariants": [
  505. {
  506. "id": "variant_a",
  507. "name": "A 线",
  508. "description": "短线体验版",
  509. "routeCode": "route-variant-a",
  510. "selectable": true
  511. },
  512. {
  513. "id": "variant_b",
  514. "name": "B 线",
  515. "description": "长线挑战版",
  516. "routeCode": "route-variant-b",
  517. "selectable": true
  518. }
  519. ]
  520. }
  521. }`).Scan(&manualReleaseRow.ID, &manualReleaseRow.PublicID); err != nil {
  522. return nil, fmt.Errorf("ensure variant manual demo release: %w", err)
  523. }
  524. if _, err := tx.Exec(ctx, `
  525. UPDATE events
  526. SET current_release_id = $2
  527. WHERE id = $1
  528. `, manualEventID, manualReleaseRow.ID); err != nil {
  529. return nil, fmt.Errorf("attach variant manual demo release: %w", err)
  530. }
  531. var manualCardPublicID string
  532. if err := tx.QueryRow(ctx, `
  533. INSERT INTO cards (
  534. card_public_id,
  535. tenant_id,
  536. entry_channel_id,
  537. card_type,
  538. title,
  539. subtitle,
  540. cover_url,
  541. event_id,
  542. display_slot,
  543. display_priority,
  544. status
  545. )
  546. VALUES (
  547. 'card_demo_variant_manual_001',
  548. $1,
  549. $2,
  550. 'event',
  551. 'Demo Variant Manual Run',
  552. '多赛道手动选择联调',
  553. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  554. $3,
  555. 'home_primary',
  556. 95,
  557. 'active'
  558. )
  559. ON CONFLICT (card_public_id) DO UPDATE SET
  560. tenant_id = EXCLUDED.tenant_id,
  561. entry_channel_id = EXCLUDED.entry_channel_id,
  562. card_type = EXCLUDED.card_type,
  563. title = EXCLUDED.title,
  564. subtitle = EXCLUDED.subtitle,
  565. cover_url = EXCLUDED.cover_url,
  566. event_id = EXCLUDED.event_id,
  567. display_slot = EXCLUDED.display_slot,
  568. display_priority = EXCLUDED.display_priority,
  569. status = EXCLUDED.status
  570. RETURNING card_public_id
  571. `, tenantID, channelID, manualEventID).Scan(&manualCardPublicID); err != nil {
  572. return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
  573. }
  574. var cleanedSessionCount int64
  575. if err := tx.QueryRow(ctx, `
  576. WITH cleaned AS (
  577. UPDATE game_sessions
  578. SET
  579. status = 'cancelled',
  580. ended_at = NOW(),
  581. updated_at = NOW()
  582. WHERE event_id = ANY($1::uuid[])
  583. AND status IN ('launched', 'running')
  584. RETURNING 1
  585. )
  586. SELECT COUNT(*) FROM cleaned
  587. `, []string{eventID, manualEventID}).Scan(&cleanedSessionCount); err != nil {
  588. return nil, fmt.Errorf("cleanup demo ongoing sessions: %w", err)
  589. }
  590. if err := tx.Commit(ctx); err != nil {
  591. return nil, err
  592. }
  593. return &DemoBootstrapSummary{
  594. TenantCode: "tenant_demo",
  595. ChannelCode: "mini-demo",
  596. EventID: "evt_demo_001",
  597. ReleaseID: releaseRow.PublicID,
  598. SourceID: source.ID,
  599. BuildID: build.ID,
  600. CardID: cardPublicID,
  601. PlaceID: placePublicID,
  602. MapAssetID: mapAssetPublicID,
  603. TileReleaseID: tileReleasePublicID,
  604. CourseSourceID: courseSourcePublicID,
  605. CourseSetID: courseSetPublicID,
  606. CourseVariantID: courseVariantPublicID,
  607. RuntimeBindingID: runtimeBindingPublicID,
  608. VariantManualEventID: "evt_demo_variant_manual_001",
  609. VariantManualRelease: manualReleaseRow.PublicID,
  610. VariantManualCardID: manualCardPublicID,
  611. CleanedSessionCount: cleanedSessionCount,
  612. }, nil
  613. }