sim.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * iOS 包 H5 迁移 — 本地模拟器(假数据 + 场景预设)
  3. * ============================================================
  4. * 用途:后端 /api/mig/* 未就绪时,在本地/测试环境完整走通客户端全流程。
  5. * 开关:isDebugEnv()(localhost / 192.* / *.usgamewin.com)下,URL 带 ?migsim=1 开启、
  6. * ?migsim=0 关闭;状态明文存 localStorage(MIG_SIM / MIG_SIM_STATE),生产域名永远不生效。
  7. * 原则:客户端逻辑全部真实,仅 service.js 的网络层被替换成本文件的假实现。
  8. */
  9. import { isDebugEnv } from "@utils/helpers";
  10. const KEY_ON = "MIG_SIM";
  11. const KEY_STATE = "MIG_SIM_STATE";
  12. /** 场景预设:与《03-交互原型》10 屏一一对应(config 片段,叠加在 DEFAULT 上) */
  13. export const SIM_PRESETS = {
  14. "new_soft_b1": { note: "新增T0-6·B1首充banner", track: "new", stage: "soft", state: "S0", vip: 1, show_e1: 0, show_e2: 0, block_days: 12 },
  15. "soft_nopay": { note: "新增·软引导期但未首充(show_banner=false)·无banner", track: "new", stage: "soft", state: "S0", vip: 1, banner_flag: "off", show_e1: 0, show_e2: 0, block_days: 12 },
  16. "new_soft_e1": { note: "新增T0-6·E1首提软弹", track: "new", stage: "soft", state: "S0", vip: 1, show_e1: 1, show_e2: 0, block_days: 12 },
  17. "new_soft_e2": { note: "新增T0-6·E2三充次日软弹", track: "new", stage: "soft", state: "S0", vip: 1, show_e1: 0, show_e2: 1, block_days: 12 },
  18. "cd_bar_only": { note: "存量D1-3·后端关强弹(show_strong=false)·仅倒计时条", track: "legacy", stage: "countdown", state: "S0", vip: 0, strong_flag: "off", block_days: 6 },
  19. "cd_strong_day1": { note: "强弹首日·利益角度C-CD2", track: "legacy", stage: "countdown", state: "S0", vip: 1, strong_copy: "C-CD2", block_days: 3 },
  20. "cd_strong": { note: "强弹次日起·缺省派生·损失规避C-CD2b", track: "legacy", stage: "countdown", state: "S0", vip: 1, strong_copy: "C-CD2b", block_days: 3 },
  21. "cd_strong_forced": { note: "后端强开强弹(show_strong=true)", track: "legacy", stage: "countdown", state: "S0", vip: 1, strong_flag: "on", block_days: 3 },
  22. "s1_retry": { note: "S1≥48h·retry变体C-CD2r", track: "new", stage: "countdown", state: "S1", vip: 1, strong_copy: "C-CD2r", block_days: 3 },
  23. "s2_finish": { note: "S2·停强弹+Finish文案", track: "new", stage: "countdown", state: "S2", vip: 1, block_days: 3 },
  24. "hard_block": { note: "硬拦截·3次Later", track: "legacy", stage: "block", state: "S0", vip: 1, later_remaining: 3, block_days: -1 },
  25. "later_exhausted": { note: "Later用尽·锁大厅", track: "legacy", stage: "block", state: "S0", vip: 0, later_remaining: 0, block_days: -1 },
  26. "migrated_s3": { note: "S3·全覆盖成功页", track: "new", stage: "countdown", state: "S3", migrated: 1, vip: 1, block_days: 3 },
  27. "cfg_fail": { note: "config失败·无任何触点(无本地兜底)", track: "legacy", stage: "block", state: "S0", cfg_fail: 1, block_days: -1 },
  28. "channel_disabled": { note: "渠道未开放(enabled=false)", channel_off: 1 }
  29. };
  30. const DEFAULT_STATE = {
  31. preset: "new_soft_b1", track: "new", stage: "soft", state: "S0",
  32. migrated: 0, vip: 1, banner_flag: "auto", show_e1: 0, show_e2: 0,
  33. strong_flag: "auto", strong_copy: "C-CD2b", later_remaining: 3,
  34. freeze: 0, cfg_fail: 0, channel_off: 0, block_days: 12
  35. };
  36. /** URL ?migsim=1|0 开/关(仅 debug 环境);读一次即从地址栏清掉 */
  37. export const armSimFromUrl = () => {
  38. if (!isDebugEnv()) return;
  39. try {
  40. const u = new URL(window.location.href);
  41. const v = u.searchParams.get("migsim");
  42. if (v == null) return;
  43. if (v === "0") { localStorage.removeItem(KEY_ON); localStorage.removeItem(KEY_STATE); }
  44. else localStorage.setItem(KEY_ON, "1");
  45. u.searchParams.delete("migsim");
  46. window.history.replaceState(null, "", u.pathname + u.search + u.hash);
  47. } catch (e) { /* ignore */ }
  48. };
  49. export const isSimOn = () => {
  50. try { return isDebugEnv() && localStorage.getItem(KEY_ON) === "1"; } catch (e) { return false; }
  51. };
  52. /* ───────── 真机联调开关(与 sim 互补)─────────
  53. * migsim=1 :假数据、不出网络,只看 UI;
  54. * migdev=1 :走真实 /game/mig/config,面板可直接打后端 /game/mig/test/state 造状态(真链路)。 */
  55. const KEY_DEV = "MIG_DEV";
  56. /** URL ?migdev=1|0 开/关(仅 debug 环境);读一次即从地址栏清掉 */
  57. export const armDevFromUrl = () => {
  58. if (!isDebugEnv()) return;
  59. try {
  60. const u = new URL(window.location.href);
  61. const v = u.searchParams.get("migdev");
  62. if (v == null) return;
  63. if (v === "0") localStorage.removeItem(KEY_DEV);
  64. else localStorage.setItem(KEY_DEV, "1");
  65. u.searchParams.delete("migdev");
  66. window.history.replaceState(null, "", u.pathname + u.search + u.hash);
  67. } catch (e) { /* ignore */ }
  68. };
  69. export const isDevOn = () => {
  70. try { return isDebugEnv() && localStorage.getItem(KEY_DEV) === "1"; } catch (e) { return false; }
  71. };
  72. export const getSimState = () => {
  73. try {
  74. const raw = localStorage.getItem(KEY_STATE);
  75. return { ...DEFAULT_STATE, ...(raw ? JSON.parse(raw) : {}) };
  76. } catch (e) { return { ...DEFAULT_STATE }; }
  77. };
  78. export const setSimState = (patch = {}) => {
  79. const next = { ...getSimState(), ...patch };
  80. try { localStorage.setItem(KEY_STATE, JSON.stringify(next)); } catch (e) { /* ignore */ }
  81. return next;
  82. };
  83. export const applySimPreset = (name) => {
  84. const p = SIM_PRESETS[name];
  85. if (!p) return getSimState();
  86. const { note, ...fields } = p;
  87. // 预设为完整场景:未提字段回落 DEFAULT(而非残留上个场景)
  88. return setSimState({ ...DEFAULT_STATE, ...fields, preset: name });
  89. };
  90. /**
  91. * 由 sim 状态生成 /game/mig/config 假响应(结构对齐《IOS_H5_MIGRATION_API_CN》精简 config)。
  92. * 只输出后端真实下发的字段:enabled/track/stage/migrated/mig_state/platform/platform_at/
  93. * now/block_at/reward_pack_text/show_e1/show_e2/hard_block(+ 可选 strong_copy_id、show_strong、show_banner)。
  94. * banner/countdown_bar 触点由客户端按 stage 自派生;强弹为兼容形式:后端可选下发 show_strong,缺省按 stage。
  95. */
  96. export const buildSimConfig = (s = getSimState()) => {
  97. if (s.channel_off) return { enabled: false }; // 渠道未开放迁移
  98. const packText = s.vip ? `77 Free Bonus + ${global.DOLLAR || "£"}7 Cash` : "77 Free Bonus";
  99. const now = Date.now();
  100. const blockAt = new Date(now + Number(s.block_days) * 864e5 + 7 * 36e5).toISOString();
  101. // 端类型 → platform 枚举(与后端一致:0 H5 / 1 iOS / 2 Android / 3 PWA);sim 下按 mig_state 粗映射
  102. const platform = s.migrated ? 3 : (s.state === "S2" || s.state === "S1" ? 0 : 1);
  103. const conf = {
  104. enabled: true,
  105. track: s.track,
  106. stage: s.stage,
  107. migrated: !!s.migrated,
  108. mig_state: s.state,
  109. platform,
  110. platform_at: new Date(now).toISOString(),
  111. now: new Date(now).toISOString(), // 服务器当前时间:客户端倒计时以此校准(serverNow)
  112. block_at: blockAt,
  113. reward_pack_text: packText,
  114. show_e1: !!s.show_e1,
  115. show_e2: !!s.show_e2,
  116. hard_block: {
  117. enabled: s.stage === "block",
  118. later_total: 3,
  119. later_remaining: Number(s.later_remaining),
  120. later_scope: "session"
  121. }
  122. };
  123. if (s.strong_copy && s.strong_copy !== "C-CD2b") conf.strong_copy_id = s.strong_copy; // 可选覆盖强弹文案
  124. // 强弹开关:仅当模拟"后端显式下发"时才带 show_strong;"auto" 不下发,验证前端缺省按 stage 派生
  125. if (s.strong_flag === "on") conf.show_strong = true;
  126. else if (s.strong_flag === "off") conf.show_strong = false;
  127. // B1 banner 开关(首充):同上,"auto" 不下发 → 前端缺省 stage=soft 即挂;"off" 模拟"未首充"
  128. if (s.banner_flag === "on") conf.show_banner = true;
  129. else if (s.banner_flag === "off") conf.show_banner = false;
  130. return conf;
  131. };
  132. /** 假 POST /game/mig/later(用完对齐后端:code 429 + msg 数组 + data.later_remaining=0) */
  133. export const simConsumeLater = () => {
  134. const s = getSimState();
  135. if (Number(s.later_remaining) <= 0) {
  136. return { code: 429, msg: ["mig.later_exhausted", "All Later passes used"], data: { later_remaining: 0 } };
  137. }
  138. const next = setSimState({ later_remaining: Number(s.later_remaining) - 1 });
  139. return { code: 200, data: { later_remaining: next.later_remaining, play_pass: "SIM_PASS_" + Date.now(), scope: "session" } };
  140. };