diff --git a/backend/xt-server/src/main/resources/application-dev.yml b/backend/xt-server/src/main/resources/application-dev.yml index 4489077..0fa2606 100644 --- a/backend/xt-server/src/main/resources/application-dev.yml +++ b/backend/xt-server/src/main/resources/application-dev.yml @@ -138,14 +138,20 @@ fill: # 代码里还会再判一次 profile 不含 prod(双保险)。 stub-enabled: ${WX_H5_STUB:true} - # ── 租户域名(用子域前缀区分租户)────────────────────────────────────── + # ── 租户域名(用子域前缀区分租户)—— 客户 2026-09-29 决定**先不做**,默认关闭 ── # # 形态:t{tenantId}.{base},如 t1.example.com / t2.example.com。 # 前缀用的是 **tenantId**,与扫码 scene 的 t{tid} 是同一套约定(scene 早就是 t1-k17)。 # + # ⚠️ **默认关闭**:单域名部署是合法形态,而且这个能力要真正用上, + # 前提是**有正式域名 + 泛域名解析 + 泛域名证书**(`*.example.com`)—— + # 这些没定之前开它只会让人以为"域名能区分单位了",实际并不能。 + # 客户说先不做,所以这里是 false;**能力本身保留且已验过** + # (e2e-m1-tenant-domain.py 16/16,见 §一·二十五), + # 将来只需:填 base、配泛域名证书、把这里改成 true、重启。 + # # 打开之后: - # · PC 登录在 t1.xxx.com 上**不用再填租户码** —— 域名已经回答了"你是哪个单位的", - # 那句「该账号存在于多个单位,请填写租户码」在这条路径下不会再出现 + # · PC 登录在 t1.xxx.com 上**不用再填租户码** —— 域名已经回答了"你是哪个单位的" # · H5 微信授权回调仍落在主域名(微信的"授权回调页面域名"只能配一个), # 回调再按 scene 里的 t{tid} 跳回该单位的站点 # @@ -154,9 +160,8 @@ fill: # 就能读 t2 的数据,而界面看起来一切正常。 # ⚠️ 小程序端**用不了它**:微信的 request 合法域名是固定白名单,配不了 *.xxx.com, # 所以小程序继续靠 scene 里的 t{tid}。 - # ⚠️ 生产要配**泛域名解析 + 泛域名证书**(*.example.com)。 tenant-domain: - enabled: ${XT_TENANT_DOMAIN_ENABLED:true} + enabled: ${XT_TENANT_DOMAIN_ENABLED:false} base: ${XT_TENANT_DOMAIN_BASE:localhost} scheme: ${XT_TENANT_DOMAIN_SCHEME:http} diff --git a/docs/交接摘要.md b/docs/交接摘要.md index ab6568b..97835fa 100644 --- a/docs/交接摘要.md +++ b/docs/交接摘要.md @@ -2060,3 +2060,66 @@ t1 域名免租户码登录 / 与手填租户码矛盾时明确报错 / **跨租 3. 小程序端确认继续靠 scene(上面第 2 条硬约束)。 4. **补文档**:这条新需求目前只落在代码与本节,应补进 `数据信息填报系统PRD.md` 与 `03-接口设计.md`(登录段的"租户从哪来"要多一种来源)。 + + +--- + +## §一·二十六 租户域名:客户决定**先不做** —— 能力保留、默认关闭 + +> 客户原话:**先不做多租户了。** + +### 怎么处理的:关掉,但**不删** + +`fill.tenant-domain.enabled` 默认值从 `true` 改成 **`false`**(`application-dev.yml`)。 +**代码全部保留**,理由: + +- 这个能力是**隔离的**:关掉之后 `TenantDomainResolver` 一律返回 `null`, + 登录走老路径、`AuthInterceptor` 里那段校验根本不会触发 —— + **不影响任何既有行为**(下面有断言专守这一点)。 +- 它已经**验过**(§一·二十五 的 16/16),删掉等于把一份验过的、将来要用的东西扔掉。 +- 要真正用上它,前提是**有正式域名 + 泛域名解析 + 泛域名证书** —— + 这些没定之前开它只会让人以为"域名能区分单位了",实际并不能。 + 所以"先不做"是对的;等域名定了,**改一个配置 + 重启**就能用。 + +### 「关得干净」是这一轮的验收点 + +关一个功能键,最怕的是**关不干净**(悄悄留下一点新行为)。 +所以 `e2e-m1-tenant-domain.py` 改成**两种模式都能跑**,并按当前模式选断言: + +``` +[关闭态] 域名不再提供租户身份,老路径完全不变 ← 当前跑的就是这一节 + ✓ t1.localhost + 不填租户码 → 40012(域名不参与判断) + ✓ 同一域名 + 填租户码 → 照样能登 + ✓ 主域名 + 租户码 → 照样能登 + ✓ 主域名 + 不填租户码 → 仍是 40012(跨单位重名的原行为) + ✓ 关掉之后**不引入任何新校验**:带 t1 的 token 访问 t14 域名照常通过 + (这是关闭前的既有行为;开启态下会被 403 拒掉,见 run_on) +``` + +脚本自己探测模式(用 `t1.localhost` 免租户码试登录:成功=开、`40012`=关), +所以**开着的时候跑它就会跑那 16 条**,不会因为模式不对而假绿。 + +**全量 666 条断言、0 失败。** + +### 将来要开,需要四步 + +1. 定下正式主域名,填 `XT_TENANT_DOMAIN_BASE`(如 `example.com`) +2. 配**泛域名解析**与**泛域名证书** `*.example.com`(备案一个主域名即覆盖子域) +3. 把 `XT_TENANT_DOMAIN_ENABLED` 设为 `true` +4. 重启后跑 `python scripts/e2e-m1-tenant-domain.py`(它会自动切到开启态的 16 条断言) + +⚠️ 别忘了两条硬约束(详见 §一·二十五):**微信授权回调域名只能配一个** +(所以回调统一落主域名、再按 scene 跳回)、**小程序域名是固定白名单** +(所以小程序端永远靠 scene,不受这个能力影响)。 + +### 随之暂缓的待办 + +§一·二十五 里那三条待拍板(正式域名 / 要不要保留手填租户码 / 小程序确认), +**现在都不用追了** —— 它们只在开启这个能力时才有意义。 +`PRD` 与 `03-接口设计` 的补文档同理:**等真要做时再补**, +现在写进去反而会让文档里多一条"设计了但没启用"的东西。 + +### 下一步 + +回到 §一·二十二 的顺序:**第 4 步 = 审核闭环(后端)** —— +初审 / 终审 / 撤回 / 批量通过(只带无问题项)/ 驳回(逐条填原因)。 diff --git a/scripts/e2e-m1-tenant-domain.py b/scripts/e2e-m1-tenant-domain.py index 1ba9e29..0af6b21 100644 --- a/scripts/e2e-m1-tenant-domain.py +++ b/scripts/e2e-m1-tenant-domain.py @@ -94,7 +94,7 @@ def login(host: str, tenant_code: str | None = None, account: str = "admin", return call("POST", "/api/v1/auth/login", host, body=body) -def main() -> int: +def run_on() -> int: ap = argparse.ArgumentParser(description="租户域名 E2E") ap.add_argument("--password", default="Admin@123") args = ap.parse_args() @@ -174,5 +174,69 @@ def main() -> int: return 1 if FAILED else 0 +def detect_mode() -> str: + """ + 探测后端有没有启用租户域名:用 t1.localhost 且不填租户码试一次登录。 + + 为什么要探测而不是写死:客户 2026-09-29 决定"先不做",默认已关; + 但这个能力保留着(填个域名就能开)。脚本两种模式都要能跑, + 而且**两种模式的断言是不同的** —— 关闭态要验的是"行为与从前完全一致"。 + """ + r = login("t1.localhost", None) + if r.get("code") == 0: + return "ON" + if r.get("code") == 40012: + return "OFF" + raise SystemExit(f" ✗ 探测失败(既不是 0 也不是 40012):{r}") + + +def run_off() -> int: + """ + 关闭态(当前默认):验"关掉之后,行为与从前**完全一致**"。 + + 这一节看着简单,但它是"先不做"这件事的验收 —— + 关一个功能键,最怕的是关不干净(悄悄留下一点新行为)。 + """ + print("=" * 76) + print("租户域名 E2E —— 当前是**关闭态**(客户先不做,能力保留)") + print("=" * 76) + + print("\n[关闭态] 域名不再提供租户身份,老路径完全不变") + a = login("t1.localhost", None) + check("t1.localhost + 不填租户码 → 40012(域名不参与判断)", + a.get("code") == 40012, f"{a}") + b = login("t1.localhost", "demo") + check("同一域名 + 填租户码 → 照样能登(老路径不受影响)", b.get("code") == 0, f"{b}") + c = login("localhost", "demo") + check("主域名 + 租户码 → 照样能登", c.get("code") == 0, f"{c}") + d = login("localhost", None) + check("主域名 + 不填租户码 → 仍是 40012(跨单位重名的原行为)", + d.get("code") == 40012, f"{d}") + + token = (b.get("data") or {}).get("token") + e = call("GET", "/api/v1/users?pageNum=1&pageSize=1", "t14.localhost", token) + check("关掉之后**不引入任何新校验**:带着 t1 的 token 访问 t14 域名照常通过", + e.get("code") == 0, + f"{e}(这是关闭前的既有行为;开启态下会被 403 拒掉,见 run_on)") + + ok("要验开启态:以 XT_TENANT_DOMAIN_ENABLED=true 重启后端,再跑本脚本") + + print() + print("=" * 76) + print(f"通过 {len(PASSED)} 失败 {len(FAILED)} 共 {len(PASSED) + len(FAILED)}") + if FAILED: + for n, det in FAILED: + print(f" · {n}\n {det}") + print("=" * 76) + return 1 if FAILED else 0 + + +def main() -> int: + mode = detect_mode() + if mode == "ON": + return run_on() + return run_off() + + if __name__ == "__main__": raise SystemExit(main())