结论 · 对提案的评价
提案的核心方向正确且符合业界标准做法(路径前缀隔离地区 + 共享 BFF):
+-
+
- ✓地区用路径前缀(
/jp、/hk)而非 IP/Cookie 隐式切换 —— 对 SEO、CDN 缓存、可分享链接都友好。
+ - ✓静态层按地区硬隔离 —— 契合「不同地区不同排版、不同页数」的现实。 +
- ✓BFF 层共享 —— 各地区业务本质一致,靠「地区上下文」做数据差异化即可。 +
需在提案基础上补全一个维度:地区(region)与语言(lang)是两个正交维度,必须显式拆开——
+ +jp / hk / … 各自拥有独立的页面集合与排版。排版、页面数量、甚至页面种类都可能不同。
+当地语 + 英文。同一地区两种语言通常共用排版,仅文案不同,用 i18n 字典切换。
+一句话:地区决定「有哪些页、长什么样」,语言只决定「同一张页上显示什么文字」。
+现状 As-Is
| 方面 | 现状 | 问题 |
|---|---|---|
| 静态页 | public/ 扁平,所有地区共用同一批 HTML | 无法按地区差异化排版/页数 |
| 语言 | public/js/translations.js 单一全局字典 window.T = {en, tc, jp},运行时切字符串 | 三语混在一个 2185 行文件,地区间无法独立演进 |
| 地区 | 打补丁:/plans-jp 别名 + 日本 IP 从 /plans 302 跳转 | 每加一个地区就要加一堆特判,不可扩展 |
| geoip | server/geoip.js 已能 IP→国家→默认语种,并注入 __APP_CONFIG__.geo | 可直接复用为「裸域落地重定向」依据 |
| BFF | server/routes/* + services/* 统一挂 /api/*,走 mock 或代理上游 | 方向正确,仅需引入「地区上下文」 |
| 配置注入 | /config.js 动态生成 window.__APP_CONFIG__ | 需补充 region / lang 字段 |
URL 与路由方案
URL 结构:/{region}/{lang}/{page}
+/jp/ja(地区裸路径 → 该地区默认语言)/hk/zh/{region}/{lang}(认不出 → 兜底 /hk/en)ja / zh-HK / en。旧 jp/tc 迁移期做别名映射即可不断链。路由规则表
+| 请求 | 处理 | 说明 |
|---|---|---|
/ | 302 → geoip 命中的 /{region}/{lang} | 无法识别 → 回落 /hk/en(Q1) |
/{region} | 302 → /{region}/{defaultLang} | /jp → /jp/ja |
/{region}/{lang}/… | 从 public/{region}/ 提供静态内容 | 命中具体页面 |
/_shared/… | 提供跨地区共享资源 | css / js / img 公共部分 |
/api/… | 共享 BFF(带地区上下文) | 见 §06 |
/config.js | 动态注入运行时配置 | no-store,见 §08 |
*.html | 301 → 去 .html 干净 URL | 沿用现有规范化逻辑 |
为什么「地区在前、语言显式成段」
+| 方案 | 例子 | 评价 |
|---|---|---|
| A · 两段(推荐) | /jp/ja/plans · /hk/en/plans | 与提案 /jp /hk 一致;语言可缓存、可 hreflang;结构清晰 |
| B · 合并 locale 单段 | /ja-jp/plans · /en-hk/plans | 也可行,但地区/语言耦合,与「/jp 作为地区入口」不吻合 |
| C · 仅地区 + Cookie | /jp/plans?lang=en | SEO 差、CDN 缓存被 Cookie 打碎、链接无法指定语言 |
目录结构(静态资源隔离)
public/ +├── _shared/ # 跨地区共享(唯一真源) +│ ├── css/ # 基础样式(reset / 变量 / 公共组件) +│ ├── js/ +│ │ ├── i18n.js # i18n 引擎(读地区字典 + 渲 data-i18n) +│ │ ├── api.js # BFF 客户端(自动带 region/lang) +│ │ └── main.js # 公共交互 +│ ├── img/ # 公共图片(logo 等) +│ └── i18n/en.js … # 跨地区共享文案(nav/footer/cookie) +│ +├── jp/ # ── 日本地区:独立页面 + 排版 ── +│ ├── pages/ # index / plans / … 页数、页种可不同 +│ ├── css/ img/ # 地区专属覆盖(可选) +│ └── i18n/ +│ ├── ja.js # 日文文案 +│ └── en.js # 日本站的英文 +│ +├── hk/ # ── 香港地区 ── +│ ├── pages/ # 可以多几张页 +│ ├── css/ img/ +│ └── i18n/ +│ ├── zh-HK.js # 繁中文案 +│ └── en.js +│ +└── (新增地区照抄一个目录即可,零特判)+
-
+
- 一个地区 = 一个自包含目录:页面、专属样式、专属图片、i18n 字典都在里面 →「不同排版/页数」天然成立。 +
- 共享的抽到
_shared/:i18n 引擎、BFF 客户端、公共样式/图片只维护一份。
+ - 语言不建目录、用字典:仅当某地区某语言排版确实要分叉时,才在该地区目录内加语言变体页(局部特例)。 +
请求处理流程
window.__APP_CONFIG__(含 region/lang,no-store)express.static(public/_shared)detectGeo() → 302 /{region}/{lang}/{region}/{defaultLang}关键:「地区+语言」解析是一个中间件,而非为每地区/每页写特判。新增地区 = 加一个目录 + 注册表加一行。
+i18n 方案(地区内语言切换)
沿用现有「运行时按 data-i18n 换字符串」的机制(改造成本低),但把字典按地区拆分:
-
+
- 共享文案(导航/页脚/Cookie)→
_shared/i18n/{lang}.js
+ - 地区专属文案 →
public/{region}/i18n/{lang}.js
+ - 页面加载:先加载共享字典,再加载地区字典(地区覆盖共享),由
_shared/js/i18n.js统一渲染。
+
好处:各地区文案独立演进,互不影响;单文件体积可控(不再是 2185 行巨无霸);语言切换仍是纯前端行为,无需为每语言生成一套 HTML。
+BFF 层:共享 + 地区上下文
BFF(routes/* + services/*)保持单一共享,通过「地区上下文」做数据差异化,而非每地区一套后端逻辑。
-
+
- 上下文传递:前端
api.js每个/api/*请求自动带地区标识(推荐请求头X-Region: hk/X-Lang: en,取自__APP_CONFIG__);/api/*前缀保持全局。
+ - 服务端读取:轻量中间件把
X-Region解析进req.region,交给下游。
+ - 按地区变化的数据:国家/币种/合规文案(
countries/industries)、Plans/Editions 目录(plans/editions)、上游代理可按地区选不同API_BASE_URL/租户。
+ - 在线支付方式:各地区接入不同网关/支付方式(日本 PayPay·Konbini·JCB / 香港 FPS·AlipayHK·信用卡)—— 作为重点单列 §07。 +
- 不变的部分:鉴权、代理框架、mock 框架、错误处理 —— 全部复用。 +
即提案所说「BFF 层都是一样的」——成立。差异只体现在「同一套代码根据 region 返回不同数据」。
+在线支付:地区可插拔的支付方式
不同国家/地区接入不同的在线支付方式,这是「地区上下文」的又一维(与币种、Plans 目录同类),不新增架构维度 —— 用同一套共享 BFF + 适配器(Strategy 模式)承载即可。
+ +/api/subscribe → /api/payments/create → /pay-gateway → webhook → /pay-return 轮询),但硬编码了单一网关 PayPartner、货币 HK$、locale zh-HK —— 只能服务香港式单一支付。本节把它一般化。各地区声明自己的币种与支付方式
+| 地区 | 币种 | 可用支付方式(示例,非最终) |
|---|---|---|
| 日本 JP | JPY | PayPay · JCB/信用卡 · Konbini 便利店 |
| 香港 HK | HKD | FPS 转数快 · AlipayHK · 信用卡(Stripe) |
| 其它 | … | 按落地地区补充 |
写进 region 注册表:region → { …, currency, paymentMethods: [...] },与 availableLangs 同理。前端从 /config.js 或 GET /api/payments/methods 拿到当前地区可用方式,渲染收银台。
BFF 统一门面(网关无关),内部按 (region, method) 选适配器
+{ paymentId, gateway, redirectUrl | clientParams }pay-return 前端不变)-
+
- 适配器模式:
server/services/payments/一网关一文件(stripe/alipay-hk/payjp-konbini/fps…),实现统一接口createPayment · verifyWebhook · getStatus;注册表映射region → [providers]、providerCode → adapter。
+ - 新增支付方式 = 加一个适配器 + 注册表登记,不改前端、不改下单流程。 +
- 归一化支付记录:沿用现有 mock 已定义的形状(
paymentId / orderId / amount / currency / status / events 时间线),各适配器把第三方回调翻译成这个形状。
+ - 前端去硬编码:
pay-gateway.js/pay-return.js里的HK$/PayPartner/zh-HK改为读地区currency+ 方式元数据;收银台按paymentMethods渲染。
+ - 回调与地区解耦:webhook 是服务端到服务端,路径按
:provider与地区无关,通过paymentId反查地区/订单;用户可见的返回页地区化:/{region}/{lang}/pay-return。
+ - 线上/线下并存:保留现有
subscribe-offline(联络我们/推荐人),作为暂无在线支付地区的兜底通道。
+
要点:下单流程(subscribe)与前端收银台保持不变,具体网关全部收敛到适配器背后 —— 加国家、换支付方式都不动主干。
+运行时配置注入(/config.js)
在现有 window.__APP_CONFIG__ 基础上补充地区/语言上下文,让前端 JS 无需自己解析 URL:
window.__APP_CONFIG__ = {
+ appEnv, apiPrefix, apiBaseUrl, mock, // 现有
+ region: "hk", // 当前地区(路径解析得出)
+ lang: "en", // 当前语言
+ defaultLang: "zh-HK", // 该地区默认语言
+ availableLangs: ["zh-HK", "en"], // 语言切换器用
+ currency: "HKD", // 该地区币种(§07)
+ paymentMethods: ["fps", "alipay_hk", "card"], // 该地区可用支付方式(§07)
+ geo: { country, lang, … } // 现有 geoip 结果(裸域落地判断)
+}
+ -
+
/config.js保持no-store,按请求路径注入正确的region/lang。
+ - 语言切换器读
availableLangs渲染,切换即跳到/{region}/{targetLang}/{samePage}。
+
从现状迁移的路径
分阶段推进,每步可独立上线、可回滚:
+-
+
- 抽公共层:新建
public/_shared/,迁入公共 css/js/img、i18n 引擎、api.js;页面引用改/_shared/…。
+ - 建 HK 基线地区:现有扁平页面复制进
public/hk/pages/作为基线(Q3/Q4),验证 region 中间件。
+ - 拆 i18n:
translations.js按「共享 / 地区」拆分,tc→zh-HK、jp→ja做别名。
+ - 接入 region 中间件:实现
/{region}/{lang}解析 + 裸地区/裸域重定向;同时移除/plans-jp与/plans的日本特判。
+ - 克隆日本地区:
public/jp/,落地日文排版与ja.js/en.js。
+ - BFF 引入地区上下文:
X-Region中间件 + 让countries/industries/plans等按地区返回。
+ - 收尾:旧扁平 URL(如
/plans)301 到默认地区,保留一段时间兼容外链。
+
权衡取舍
| 取舍点 | 决策 | 理由 / 代价 |
|---|---|---|
| 地区隔离 vs 单模板 | 地区目录硬隔离 | 契合「不同排版/页数」;代价是布局重复,用 _shared + 基线地区缓解 |
| 语言:字典 vs 复制 HTML | i18n 字典 | 同地区两语共排版,复制 HTML 维护翻倍;真分叉时才加变体页 |
| URL:显式语言段 vs Cookie | 显式 /{region}/{lang} | 利于 SEO/CDN/可分享;代价是 URL 多一段 |
| i18n:运行时 vs 构建期 | 先运行时 | 改造成本最低;保留升级到服务端直出的空间 |
| BFF:共享 vs 分地区 | 共享 + 上下文 | 逻辑一致,避免 N 套后端;差异靠 region 参数 |
| 语言码:jp/tc vs BCP47 | 迁移到 BCP47 | 规范、利于 hreflang;迁移期做别名兼容 |
SEO · 缓存 · 运维
-
+
- SEO:每个
{region}/{lang}是独立可索引 URL;<head>加<link rel="alternate" hreflang>串联各语言并设x-default。
+ - 缓存/CDN:地区+语言进 URL → 天然可按路径缓存,不被 Cookie/geoip 打碎缓存键。 +
- 重定向语义:裸域
/与裸地区/{region}用 302(临时/个性化);旧扁平 URL 收敛用 301。
+ - 可扩展性:新增地区 = 加一个
public/{region}/目录 + 注册表一行,无需改路由代码。
+
决策记录(Q1–Q7)
Q1–Q5 已拍板并回填到上文相关章节;Q6–Q7 待后续确认。
+en,即最终落 /hk/enja / zh-HK / en;旧 jp/tc 迁移期做别名兼容X-Region 影响哪些 service。⏳ 待定 —— 实现时按接口逐一确认subscribe-offline)并存?⏳ 待定核心组件清单(实现对照)
| 组件 | 位置(建议) | 职责 |
|---|---|---|
| 地区注册表 | server/regions.js | region → { defaultLang, availableLangs, currency, paymentMethods, … } |
| region 解析中间件 | server/index.js | 解析 /{region}/{lang}、裸地区/裸域重定向、按地区托管静态 |
| geoip 落地 | server/geoip.js(复用) | 裸域 / 决定落地 region/lang(认不出 → hk/en) |
| BFF 地区上下文中间件 | server/index.js | 读 X-Region → req.region |
| 支付门面路由 | server/routes/payments.js | 统一 /api/payments/*,按 region + method 选适配器 |
| 支付适配器 | server/services/payments/*.js | 一网关一适配器:createPayment · verifyWebhook · getStatus |
| i18n 引擎 | public/_shared/js/i18n.js | 加载共享 + 地区字典,渲染 data-i18n |
| BFF 客户端 | public/_shared/js/api.js | /api/* 请求自动带 X-Region / X-Lang |
| 配置注入 | /config.js(server/index.js) | 注入 region / lang / availableLangs / currency / paymentMethods |