From 57ee96e1bf79ebfb428eca3677ddd7476d0c82f1 Mon Sep 17 00:00:00 2001 From: fengruixiang <474182370@qq.com> Date: Thu, 9 Jul 2026 16:53:45 +0800 Subject: [PATCH] =?UTF-8?q?docs(justsolutionsWebV2):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=A4=9A=E5=9C=B0=E5=8C=BA=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以路径前缀 /{region}/{lang} 隔离地区、共享 BFF 的整体方案(HTML 文档): - 地区(region)与语言(lang)维度分离:地区目录级硬隔离,语言用 i18n 字典软切换 - public/{region}/ + public/_shared/ 目录结构与请求处理流程 - BFF 保持共享,按 X-Region 做数据差异化 - 在线支付以适配器(Strategy)模式承载各地区不同支付方式 - 已锁定决策 Q1–Q5:兜底 HK(/hk/en)、语言码 BCP47、首批 jp+hk、 HK 页面为基线、纯路径前缀不用独立域名;Q6/Q7 待定 --- .../multi-region-i18n-design.html | 755 ++++++++++++++++++ 1 file changed, 755 insertions(+) create mode 100644 docs/justsolutionsWebV2/multi-region-i18n-design.html diff --git a/docs/justsolutionsWebV2/multi-region-i18n-design.html b/docs/justsolutionsWebV2/multi-region-i18n-design.html new file mode 100644 index 0000000..88f2c6d --- /dev/null +++ b/docs/justsolutionsWebV2/multi-region-i18n-design.html @@ -0,0 +1,755 @@ +justsolutionsWebV2 · 多地区多语言架构设计 + + + +
+ +
+
justsolutionsWebV2 / 架构设计
+

多地区 · 多语言架构设计

+

支持多个国家/地区站点,每地区提供「当地语言 + 英文」,通过路径前缀访问;静态前端按地区隔离BFF 层保持共享

+
+ 路由 /{region}/{lang} + 站点 Express + 静态托管 + 现有 geoip · translations.js · /api/* + 状态 Q1–5 已定 · Q6/7 待定 +
+
+ + + +
+ + +
+
§00

结论 · 对提案的评价

+

提案的核心方向正确且符合业界标准做法(路径前缀隔离地区 + 共享 BFF):

+
+
    +
  • 地区用路径前缀/jp/hk)而非 IP/Cookie 隐式切换 —— 对 SEO、CDN 缓存、可分享链接都友好。
  • +
  • 静态层按地区硬隔离 —— 契合「不同地区不同排版、不同页数」的现实。
  • +
  • BFF 层共享 —— 各地区业务本质一致,靠「地区上下文」做数据差异化即可。
  • +
+
+

需在提案基础上补全一个维度:地区(region)与语言(lang)是两个正交维度,必须显式拆开——

+ +
+
+
region地区 · 目录级硬隔离
+

jp / hk / … 各自拥有独立的页面集合与排版。排版、页面数量、甚至页面种类都可能不同。

+
→ 各地区一个自包含目录
+
+
+
lang语言 · 地区内软切换
+

当地语 + 英文。同一地区两种语言通常共用排版,仅文案不同,用 i18n 字典切换。

+
→ 地区目录内换字典,不复制 HTML
+
+
+

一句话:地区决定「有哪些页、长什么样」,语言只决定「同一张页上显示什么文字」。

+
+ + +
+
§01

现状 As-Is

+
+ + + + + + + + + + +
方面现状问题
静态页public/ 扁平,所有地区共用同一批 HTML无法按地区差异化排版/页数
语言public/js/translations.js 单一全局字典 window.T = {en, tc, jp},运行时切字符串三语混在一个 2185 行文件,地区间无法独立演进
地区打补丁:/plans-jp 别名 + 日本 IP 从 /plans 302 跳转每加一个地区就要加一堆特判,不可扩展
geoipserver/geoip.js 已能 IP→国家→默认语种,并注入 __APP_CONFIG__.geo可直接复用为「裸域落地重定向」依据
BFFserver/routes/* + services/* 统一挂 /api/*,走 mock 或代理上游方向正确,仅需引入「地区上下文」
配置注入/config.js 动态生成 window.__APP_CONFIG__需补充 region / lang 字段
+
+
+ + +
+
§02

URL 与路由方案

+

URL 结构:/{region}/{lang}/{page}

+
+
+
/jp302 → /jp/ja(地区裸路径 → 该地区默认语言)
+
/jp/ja/plans 日文 · Plans 页
+
/jp/en/plans 英文 · Plans 页
+
/hk302 → /hk/zh
+
/hk/zh/plans香港 繁中 · Plans 页
+
/hk/en/plans香港 英文 · Plans 页
+
/302 → geoip 决定的 /{region}/{lang}(认不出 → 兜底 /hk/en
+
+
+
语言代码采用 BCP 47(已定 · Q2):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
*.html301 → 去 .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=enSEO 差、CDN 缓存被 Cookie 打碎、链接无法指定语言
+
+
+ + +
+
§03

目录结构(静态资源隔离)

+
+ public/ — 地区自包含,语言用字典而非目录 +
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 客户端、公共样式/图片只维护一份。
  • +
  • 语言不建目录、用字典:仅当某地区某语言排版确实要分叉时,才在该地区目录内加语言变体页(局部特例)。
  • +
+
+ + +
+
§04

请求处理流程

+
+
+
/config.js
动态注入 window.__APP_CONFIG__(含 region/lang,no-store)
+
/_shared/*
express.static(public/_shared)
+
/api/*
共享 BFF(注入 region 上下文)→ mock / 代理上游
+
/ (裸域)
detectGeo() → 302 /{region}/{lang}
+
/{region}
302 /{region}/{defaultLang}
+
/{region}/{lang}/*
region 解析中间件 +
+ ├ 校验 region 合法、lang ∈ 该地区支持语言 + ├ 剥掉 /{region}/{lang} 前缀 + ├ express.static(public/{region}/pages) + └ 页内加载 _shared/js/i18n.js + {region}/i18n/{lang}.js → 按 data-i18n 渲染 +
+
+
+
+

关键:「地区+语言」解析是一个中间件,而非为每地区/每页写特判。新增地区 = 加一个目录 + 注册表加一行。

+
+ + +
+
§05

i18n 方案(地区内语言切换)

+

沿用现有「运行时按 data-i18n 换字符串」的机制(改造成本低),但把字典按地区拆分

+
    +
  • 共享文案(导航/页脚/Cookie)→ _shared/i18n/{lang}.js
  • +
  • 地区专属文案 → public/{region}/i18n/{lang}.js
  • +
  • 页面加载:先加载共享字典,再加载地区字典(地区覆盖共享),由 _shared/js/i18n.js 统一渲染。
  • +
+

好处:各地区文案独立演进,互不影响;单文件体积可控(不再是 2185 行巨无霸);语言切换仍是纯前端行为,无需为每语言生成一套 HTML。

+
备选:若未来页数暴涨或需更强 SEO,可升级为构建期 / 服务端注入的 i18n(服务端直出已翻译 HTML)。当前规模用运行时方案性价比最高,本设计保留升级空间。
+
+ + +
+
§06

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 返回不同数据」。

+
+ + +
+
§07

在线支付:地区可插拔的支付方式

+

不同国家/地区接入不同的在线支付方式,这是「地区上下文」的又一维(与币种、Plans 目录同类),不新增架构维度 —— 用同一套共享 BFF + 适配器(Strategy 模式)承载即可。

+ +
现状:plans-plus 已有「下单 + 收银台」骨架(/api/subscribe/api/payments/create/pay-gateway → webhook → /pay-return 轮询),但硬编码了单一网关 PayPartner、货币 HK$、locale zh-HK —— 只能服务香港式单一支付。本节把它一般化。
+ +

各地区声明自己的币种与支付方式

+
+ + + + + + + +
地区币种可用支付方式(示例,非最终)
JPYPayPay · JCB/信用卡 · Konbini 便利店
香港 HKHKDFPS 转数快 · AlipayHK · 信用卡(Stripe)
其它按落地地区补充
+
+

写进 region 注册表:region → { …, currency, paymentMethods: [...] },与 availableLangs 同理。前端从 /config.jsGET /api/payments/methods 拿到当前地区可用方式,渲染收银台。

+ +

BFF 统一门面(网关无关),内部按 (region, method) 选适配器

+
+
+
GET /api/payments/methods当前地区可用支付方式(code / 名称 / 图标 / 币种)
+
POST /api/payments/create选中适配器 → 归一化 { paymentId, gateway, redirectUrl | clientParams }
+
POST /api/payments/webhook/:provider各网关各自回调/验签 → 归一化状态 + 回写后端订单
+
GET /api/payments/:pid归一化状态轮询(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)与前端收银台保持不变,具体网关全部收敛到适配器背后 —— 加国家、换支付方式都不动主干。

+
+ + +
+
§08

运行时配置注入(/config.js)

+

在现有 window.__APP_CONFIG__ 基础上补充地区/语言上下文,让前端 JS 无需自己解析 URL:

+
+ /config.js 动态生成(示意,非最终实现) +
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}
  • +
+
+ + +
+
§09

从现状迁移的路径

+

分阶段推进,每步可独立上线、可回滚:

+
    +
  1. 抽公共层:新建 public/_shared/,迁入公共 css/js/img、i18n 引擎、api.js;页面引用改 /_shared/…
  2. +
  3. 建 HK 基线地区:现有扁平页面复制进 public/hk/pages/ 作为基线(Q3/Q4),验证 region 中间件。
  4. +
  5. 拆 i18ntranslations.js 按「共享 / 地区」拆分,tc→zh-HKjp→ja 做别名。
  6. +
  7. 接入 region 中间件:实现 /{region}/{lang} 解析 + 裸地区/裸域重定向;同时移除 /plans-jp/plans 的日本特判
  8. +
  9. 克隆日本地区public/jp/,落地日文排版与 ja.js/en.js
  10. +
  11. BFF 引入地区上下文X-Region 中间件 + 让 countries/industries/plans 等按地区返回。
  12. +
  13. 收尾:旧扁平 URL(如 /plans)301 到默认地区,保留一段时间兼容外链。
  14. +
+
+ + +
+
§10

权衡取舍

+
+ + + + + + + + + + +
取舍点决策理由 / 代价
地区隔离 vs 单模板地区目录硬隔离契合「不同排版/页数」;代价是布局重复,用 _shared + 基线地区缓解
语言:字典 vs 复制 HTMLi18n 字典同地区两语共排版,复制 HTML 维护翻倍;真分叉时才加变体页
URL:显式语言段 vs Cookie显式 /{region}/{lang}利于 SEO/CDN/可分享;代价是 URL 多一段
i18n:运行时 vs 构建期先运行时改造成本最低;保留升级到服务端直出的空间
BFF:共享 vs 分地区共享 + 上下文逻辑一致,避免 N 套后端;差异靠 region 参数
语言码:jp/tc vs BCP47迁移到 BCP47规范、利于 hreflang;迁移期做别名兼容
+
+
+ + +
+
§11

SEO · 缓存 · 运维

+
    +
  • SEO:每个 {region}/{lang} 是独立可索引 URL;<head><link rel="alternate" hreflang> 串联各语言并设 x-default
  • +
  • 缓存/CDN:地区+语言进 URL → 天然可按路径缓存,不被 Cookie/geoip 打碎缓存键。
  • +
  • 重定向语义:裸域 / 与裸地区 /{region}302(临时/个性化);旧扁平 URL 收敛用 301
  • +
  • 可扩展性:新增地区 = 加一个 public/{region}/ 目录 + 注册表一行,无需改路由代码
  • +
+
+ + +
+
§12

决策记录(Q1–Q7)

+

Q1–Q5 已拍板并回填到上文相关章节;Q6–Q7 待后续确认。

+
+
Q1
裸域 / 的兜底地区geoip 认不出来(本地/内网/未覆盖国家)时落到哪?✓ 落 HK;语言按 geoip 语种,认不出 → en,即最终落 /hk/en
+
Q2
语言码规范是否规范化为 BCP47?采用 BCP47ja / zh-HK / en;旧 jp/tc 迁移期做别名兼容
+
Q3
首批上线地区仅 jp + hk;不单建 global 基线地区
+
Q4
地区差异化起点以现有 HK 页面为基线复制,再逐地区改排版
+
Q5
是否用独立域名不用独立域名,纯路径前缀;region 中间件无需预留「域名→地区」解析口
+
Q6
BFF 地区差异范围哪些接口/数据真的按地区不同(币种/Plans/合规文案/上游租户/支付方式)?据此确定 X-Region 影响哪些 service。⏳ 待定 —— 实现时按接口逐一确认
+
Q7
各地区支付方式 · 集成方每地区 provider 清单/优先级、是否用统一 PSP 聚合商减少适配器、是否与线下支付(subscribe-offline)并存?⏳ 待定
+
+
+ + +
+
§A

核心组件清单(实现对照)

+
+ + + + + + + + + + + + + +
组件位置(建议)职责
地区注册表server/regions.jsregion → { 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
+
+
+ +
+ + + +
+ +