dev_store.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. VariantManualEventID string `json:"variantManualEventId"`
  15. VariantManualRelease string `json:"variantManualReleaseId"`
  16. VariantManualCardID string `json:"variantManualCardId"`
  17. }
  18. func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
  19. tx, err := s.Begin(ctx)
  20. if err != nil {
  21. return nil, err
  22. }
  23. defer tx.Rollback(ctx)
  24. var tenantID string
  25. if err := tx.QueryRow(ctx, `
  26. INSERT INTO tenants (tenant_code, name, status)
  27. VALUES ('tenant_demo', 'Demo Tenant', 'active')
  28. ON CONFLICT (tenant_code) DO UPDATE SET
  29. name = EXCLUDED.name,
  30. status = EXCLUDED.status
  31. RETURNING id
  32. `).Scan(&tenantID); err != nil {
  33. return nil, fmt.Errorf("ensure demo tenant: %w", err)
  34. }
  35. var channelID string
  36. if err := tx.QueryRow(ctx, `
  37. INSERT INTO entry_channels (
  38. tenant_id, channel_code, channel_type, platform_app_id, display_name, status, is_default
  39. )
  40. VALUES ($1, 'mini-demo', 'wechat_mini', 'wx-demo-appid', 'Demo Mini Channel', 'active', true)
  41. ON CONFLICT (tenant_id, channel_code) DO UPDATE SET
  42. channel_type = EXCLUDED.channel_type,
  43. platform_app_id = EXCLUDED.platform_app_id,
  44. display_name = EXCLUDED.display_name,
  45. status = EXCLUDED.status,
  46. is_default = EXCLUDED.is_default
  47. RETURNING id
  48. `, tenantID).Scan(&channelID); err != nil {
  49. return nil, fmt.Errorf("ensure demo entry channel: %w", err)
  50. }
  51. var eventID string
  52. if err := tx.QueryRow(ctx, `
  53. INSERT INTO events (
  54. tenant_id, event_public_id, slug, display_name, summary, status
  55. )
  56. VALUES ($1, 'evt_demo_001', 'demo-city-run', 'Demo City Run', 'Launch flow demo event', 'active')
  57. ON CONFLICT (event_public_id) DO UPDATE SET
  58. tenant_id = EXCLUDED.tenant_id,
  59. slug = EXCLUDED.slug,
  60. display_name = EXCLUDED.display_name,
  61. summary = EXCLUDED.summary,
  62. status = EXCLUDED.status
  63. RETURNING id
  64. `, tenantID).Scan(&eventID); err != nil {
  65. return nil, fmt.Errorf("ensure demo event: %w", err)
  66. }
  67. var releaseRow struct {
  68. ID string
  69. PublicID string
  70. }
  71. if err := tx.QueryRow(ctx, `
  72. INSERT INTO event_releases (
  73. release_public_id,
  74. event_id,
  75. release_no,
  76. config_label,
  77. manifest_url,
  78. manifest_checksum_sha256,
  79. route_code,
  80. status
  81. )
  82. VALUES (
  83. 'rel_demo_001',
  84. $1,
  85. 1,
  86. 'Demo Config v1',
  87. 'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
  88. 'demo-checksum-001',
  89. 'route-demo-001',
  90. 'published'
  91. )
  92. ON CONFLICT (release_public_id) DO UPDATE SET
  93. event_id = EXCLUDED.event_id,
  94. config_label = EXCLUDED.config_label,
  95. manifest_url = EXCLUDED.manifest_url,
  96. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  97. route_code = EXCLUDED.route_code,
  98. status = EXCLUDED.status
  99. RETURNING id, release_public_id
  100. `, eventID).Scan(&releaseRow.ID, &releaseRow.PublicID); err != nil {
  101. return nil, fmt.Errorf("ensure demo release: %w", err)
  102. }
  103. if _, err := tx.Exec(ctx, `
  104. UPDATE events
  105. SET current_release_id = $2
  106. WHERE id = $1
  107. `, eventID, releaseRow.ID); err != nil {
  108. return nil, fmt.Errorf("attach demo release: %w", err)
  109. }
  110. sourceNotes := "demo source config imported from local event sample"
  111. source, err := s.UpsertEventConfigSource(ctx, tx, UpsertEventConfigSourceParams{
  112. EventID: eventID,
  113. SourceVersionNo: 1,
  114. SourceKind: "event_bundle",
  115. SchemaID: "event-source",
  116. SchemaVersion: "1",
  117. Status: "active",
  118. Notes: &sourceNotes,
  119. Source: map[string]any{
  120. "app": map[string]any{
  121. "id": "sample-classic-001",
  122. "title": "顺序赛示例",
  123. },
  124. "branding": map[string]any{
  125. "tenantCode": "tenant_demo",
  126. "entryChannel": "mini-demo",
  127. },
  128. "map": map[string]any{
  129. "tiles": "../map/lxcb-001/tiles/",
  130. "mapmeta": "../map/lxcb-001/tiles/meta.json",
  131. },
  132. "playfield": map[string]any{
  133. "kind": "course",
  134. "source": map[string]any{
  135. "type": "kml",
  136. "url": "../kml/lxcb-001/10/c01.kml",
  137. },
  138. },
  139. "game": map[string]any{
  140. "mode": "classic-sequential",
  141. },
  142. "content": map[string]any{
  143. "h5Template": "content-h5-test-template.html",
  144. },
  145. },
  146. })
  147. if err != nil {
  148. return nil, fmt.Errorf("ensure demo event config source: %w", err)
  149. }
  150. buildLog := "demo build generated from sample classic-sequential.json"
  151. build, err := s.UpsertEventConfigBuild(ctx, tx, UpsertEventConfigBuildParams{
  152. EventID: eventID,
  153. SourceID: source.ID,
  154. BuildNo: 1,
  155. BuildStatus: "success",
  156. BuildLog: &buildLog,
  157. Manifest: map[string]any{
  158. "schemaVersion": "1",
  159. "releaseId": "rel_demo_001",
  160. "version": "2026.04.01",
  161. "app": map[string]any{
  162. "id": "sample-classic-001",
  163. "title": "顺序赛示例",
  164. },
  165. "map": map[string]any{
  166. "tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  167. "mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  168. },
  169. "playfield": map[string]any{
  170. "kind": "course",
  171. "source": map[string]any{
  172. "type": "kml",
  173. "url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  174. },
  175. },
  176. "game": map[string]any{
  177. "mode": "classic-sequential",
  178. },
  179. "assets": map[string]any{
  180. "contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  181. },
  182. },
  183. AssetIndex: []map[string]any{
  184. {
  185. "assetType": "manifest",
  186. "assetKey": "manifest",
  187. },
  188. {
  189. "assetType": "mapmeta",
  190. "assetKey": "mapmeta",
  191. },
  192. {
  193. "assetType": "playfield",
  194. "assetKey": "playfield-kml",
  195. },
  196. {
  197. "assetType": "content_html",
  198. "assetKey": "content-html",
  199. },
  200. },
  201. })
  202. if err != nil {
  203. return nil, fmt.Errorf("ensure demo event config build: %w", err)
  204. }
  205. if err := s.AttachBuildToRelease(ctx, tx, releaseRow.ID, build.ID); err != nil {
  206. return nil, fmt.Errorf("attach demo build to release: %w", err)
  207. }
  208. tilesPath := "map/lxcb-001/tiles/"
  209. mapmetaPath := "map/lxcb-001/tiles/meta.json"
  210. playfieldPath := "kml/lxcb-001/10/c01.kml"
  211. contentPath := "event/content-h5-test-template.html"
  212. manifestChecksum := "demo-checksum-001"
  213. if err := s.ReplaceEventReleaseAssets(ctx, tx, releaseRow.ID, []UpsertEventReleaseAssetParams{
  214. {
  215. EventReleaseID: releaseRow.ID,
  216. AssetType: "manifest",
  217. AssetKey: "manifest",
  218. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json",
  219. Checksum: &manifestChecksum,
  220. Meta: map[string]any{"source": "release-manifest"},
  221. },
  222. {
  223. EventReleaseID: releaseRow.ID,
  224. AssetType: "tiles",
  225. AssetKey: "tiles-root",
  226. AssetPath: &tilesPath,
  227. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
  228. Meta: map[string]any{"kind": "directory"},
  229. },
  230. {
  231. EventReleaseID: releaseRow.ID,
  232. AssetType: "mapmeta",
  233. AssetKey: "mapmeta",
  234. AssetPath: &mapmetaPath,
  235. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
  236. Meta: map[string]any{"format": "json"},
  237. },
  238. {
  239. EventReleaseID: releaseRow.ID,
  240. AssetType: "playfield",
  241. AssetKey: "course-kml",
  242. AssetPath: &playfieldPath,
  243. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
  244. Meta: map[string]any{"format": "kml"},
  245. },
  246. {
  247. EventReleaseID: releaseRow.ID,
  248. AssetType: "content_html",
  249. AssetKey: "content-html",
  250. AssetPath: &contentPath,
  251. AssetURL: "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
  252. Meta: map[string]any{"kind": "content-page"},
  253. },
  254. }); err != nil {
  255. return nil, fmt.Errorf("ensure demo event release assets: %w", err)
  256. }
  257. var cardPublicID string
  258. if err := tx.QueryRow(ctx, `
  259. INSERT INTO cards (
  260. card_public_id,
  261. tenant_id,
  262. entry_channel_id,
  263. card_type,
  264. title,
  265. subtitle,
  266. cover_url,
  267. event_id,
  268. display_slot,
  269. display_priority,
  270. status
  271. )
  272. VALUES (
  273. 'card_demo_001',
  274. $1,
  275. $2,
  276. 'event',
  277. 'Demo City Run',
  278. '今日推荐路线',
  279. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  280. $3,
  281. 'home_primary',
  282. 100,
  283. 'active'
  284. )
  285. ON CONFLICT (card_public_id) DO UPDATE SET
  286. tenant_id = EXCLUDED.tenant_id,
  287. entry_channel_id = EXCLUDED.entry_channel_id,
  288. card_type = EXCLUDED.card_type,
  289. title = EXCLUDED.title,
  290. subtitle = EXCLUDED.subtitle,
  291. cover_url = EXCLUDED.cover_url,
  292. event_id = EXCLUDED.event_id,
  293. display_slot = EXCLUDED.display_slot,
  294. display_priority = EXCLUDED.display_priority,
  295. status = EXCLUDED.status
  296. RETURNING card_public_id
  297. `, tenantID, channelID, eventID).Scan(&cardPublicID); err != nil {
  298. return nil, fmt.Errorf("ensure demo card: %w", err)
  299. }
  300. var manualEventID string
  301. if err := tx.QueryRow(ctx, `
  302. INSERT INTO events (
  303. tenant_id, event_public_id, slug, display_name, summary, status
  304. )
  305. VALUES ($1, 'evt_demo_variant_manual_001', 'demo-variant-manual-run', 'Demo Variant Manual Run', 'Manual 多赛道联调活动', 'active')
  306. ON CONFLICT (event_public_id) DO UPDATE SET
  307. tenant_id = EXCLUDED.tenant_id,
  308. slug = EXCLUDED.slug,
  309. display_name = EXCLUDED.display_name,
  310. summary = EXCLUDED.summary,
  311. status = EXCLUDED.status
  312. RETURNING id
  313. `, tenantID).Scan(&manualEventID); err != nil {
  314. return nil, fmt.Errorf("ensure variant manual demo event: %w", err)
  315. }
  316. var manualReleaseRow struct {
  317. ID string
  318. PublicID string
  319. }
  320. if err := tx.QueryRow(ctx, `
  321. INSERT INTO event_releases (
  322. release_public_id,
  323. event_id,
  324. release_no,
  325. config_label,
  326. manifest_url,
  327. manifest_checksum_sha256,
  328. route_code,
  329. status,
  330. payload_jsonb
  331. )
  332. VALUES (
  333. 'rel_demo_variant_manual_001',
  334. $1,
  335. 1,
  336. 'Demo Variant Manual Config v1',
  337. 'https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_001/rel_e7dd953743c5c0d2/manifest.json',
  338. 'demo-variant-checksum-001',
  339. 'route-variant-a',
  340. 'published',
  341. $2::jsonb
  342. )
  343. ON CONFLICT (release_public_id) DO UPDATE SET
  344. event_id = EXCLUDED.event_id,
  345. config_label = EXCLUDED.config_label,
  346. manifest_url = EXCLUDED.manifest_url,
  347. manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
  348. route_code = EXCLUDED.route_code,
  349. status = EXCLUDED.status,
  350. payload_jsonb = EXCLUDED.payload_jsonb
  351. RETURNING id, release_public_id
  352. `, manualEventID, `{
  353. "play": {
  354. "assignmentMode": "manual",
  355. "courseVariants": [
  356. {
  357. "id": "variant_a",
  358. "name": "A 线",
  359. "description": "短线体验版",
  360. "routeCode": "route-variant-a",
  361. "selectable": true
  362. },
  363. {
  364. "id": "variant_b",
  365. "name": "B 线",
  366. "description": "长线挑战版",
  367. "routeCode": "route-variant-b",
  368. "selectable": true
  369. }
  370. ]
  371. }
  372. }`).Scan(&manualReleaseRow.ID, &manualReleaseRow.PublicID); err != nil {
  373. return nil, fmt.Errorf("ensure variant manual demo release: %w", err)
  374. }
  375. if _, err := tx.Exec(ctx, `
  376. UPDATE events
  377. SET current_release_id = $2
  378. WHERE id = $1
  379. `, manualEventID, manualReleaseRow.ID); err != nil {
  380. return nil, fmt.Errorf("attach variant manual demo release: %w", err)
  381. }
  382. var manualCardPublicID string
  383. if err := tx.QueryRow(ctx, `
  384. INSERT INTO cards (
  385. card_public_id,
  386. tenant_id,
  387. entry_channel_id,
  388. card_type,
  389. title,
  390. subtitle,
  391. cover_url,
  392. event_id,
  393. display_slot,
  394. display_priority,
  395. status
  396. )
  397. VALUES (
  398. 'card_demo_variant_manual_001',
  399. $1,
  400. $2,
  401. 'event',
  402. 'Demo Variant Manual Run',
  403. '多赛道手动选择联调',
  404. 'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
  405. $3,
  406. 'home_primary',
  407. 95,
  408. 'active'
  409. )
  410. ON CONFLICT (card_public_id) DO UPDATE SET
  411. tenant_id = EXCLUDED.tenant_id,
  412. entry_channel_id = EXCLUDED.entry_channel_id,
  413. card_type = EXCLUDED.card_type,
  414. title = EXCLUDED.title,
  415. subtitle = EXCLUDED.subtitle,
  416. cover_url = EXCLUDED.cover_url,
  417. event_id = EXCLUDED.event_id,
  418. display_slot = EXCLUDED.display_slot,
  419. display_priority = EXCLUDED.display_priority,
  420. status = EXCLUDED.status
  421. RETURNING card_public_id
  422. `, tenantID, channelID, manualEventID).Scan(&manualCardPublicID); err != nil {
  423. return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
  424. }
  425. if err := tx.Commit(ctx); err != nil {
  426. return nil, err
  427. }
  428. return &DemoBootstrapSummary{
  429. TenantCode: "tenant_demo",
  430. ChannelCode: "mini-demo",
  431. EventID: "evt_demo_001",
  432. ReleaseID: releaseRow.PublicID,
  433. SourceID: source.ID,
  434. BuildID: build.ID,
  435. CardID: cardPublicID,
  436. VariantManualEventID: "evt_demo_variant_manual_001",
  437. VariantManualRelease: manualReleaseRow.PublicID,
  438. VariantManualCardID: manualCardPublicID,
  439. }, nil
  440. }