feat(knowledge): ingest artifact from lingjingwanxiang/ljwx-health@71c49969d7
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0000", "chunk_index": 0, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 17, "source_start_line": 1, "text_hash": "ed6ca219331182277ce2c1be4c9e6fe1c8c8a3fe718490cf933319240e9de0fb", "token_count": 158, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0001", "chunk_index": 1, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 36, "source_start_line": 19, "text_hash": "3fc2cde638136597186100528f8c4ad3de9ac0bd6a9179c2fe6be03e156643c9", "token_count": 282, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0002", "chunk_index": 2, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 49, "source_start_line": 38, "text_hash": "0044ab5eee5b977cce3ad730bdbabec7aca117acfb7d6fed0401a664c8be4462", "token_count": 322, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0003", "chunk_index": 3, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 51, "source_start_line": 51, "text_hash": "de15131e2e4c48517dcbedd4bdf67374a19af9664e145de5abc7eb6e624850d5", "token_count": 4, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0004", "chunk_index": 4, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 66, "source_start_line": 53, "text_hash": "f56203bb5be4d69d84fa5c8365abed0014db600e3d1af72b9416351cad7474b4", "token_count": 748, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0005", "chunk_index": 5, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 90, "source_start_line": 68, "text_hash": "a98eacda34482f572adb8df3b8ebf2b80d2621416d13728b7e2bcd3ee4037916", "token_count": 300, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0006", "chunk_index": 6, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 139, "source_start_line": 92, "text_hash": "11fa6468daeddafbbcd24d88417957f3f0725880ec9de45fab162a07509b6ea8", "token_count": 281, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0007", "chunk_index": 7, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 174, "source_start_line": 141, "text_hash": "7efd0f34dd78e8d98df188e8d0f78f0739eee92a73a5a14622312504ac3fcf2a", "token_count": 268, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0008", "chunk_index": 8, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 207, "source_start_line": 176, "text_hash": "442b2a788ddad7d6f282c716ee04ac44efa2924c95160c7fd6ecc60438ed4f93", "token_count": 291, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0009", "chunk_index": 9, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 248, "source_start_line": 209, "text_hash": "a6d33fa10ad46ff9eee4efc4ab92bfd5319740dd3d2dc0b3767e8ba3ae5c5896", "token_count": 299, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0010", "chunk_index": 10, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 295, "source_start_line": 250, "text_hash": "8a45e77919a10bc133ecc9c9bbc0fc160d1018d0e37ab43e84389a3996148b16", "token_count": 283, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0011", "chunk_index": 11, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 327, "source_start_line": 297, "text_hash": "77466f73567cb0b35614f806429a1e1da01cdf18e82635308c44ecc6fb2d2739", "token_count": 240, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0012", "chunk_index": 12, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 354, "source_start_line": 329, "text_hash": "093d608d9bcadb7eb7d53cbae6436b328c2564e49be1c794f292bfe02a9650b1", "token_count": 252, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0013", "chunk_index": 13, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 385, "source_start_line": 356, "text_hash": "e491e43cd1b8a58e58eb67a50e554edf7b96b7aade05c072cfcb286f6bc91df4", "token_count": 266, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0014", "chunk_index": 14, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 418, "source_start_line": 387, "text_hash": "2de9818f8db4e9e5c4fba0e5adc18103e303e96d16019ed6af031285cc91b3f5", "token_count": 297, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0015", "chunk_index": 15, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 441, "source_start_line": 420, "text_hash": "ef8864dd35b8254e92e2e1a4d7902d73d70a5fc228c4c6dd93c5f0a30612d086", "token_count": 279, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0016", "chunk_index": 16, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 469, "source_start_line": 443, "text_hash": "aebcdaccb40e7fe532fcf854fbda9b34868a78bbdeb0696f7688b6aec8387391", "token_count": 174, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0017", "chunk_index": 17, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 490, "source_start_line": 471, "text_hash": "ccdc79b09612e809b6c19ca84e92e7a4333204af8d97db918ecce5f3052d404d", "token_count": 159, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0018", "chunk_index": 18, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 513, "source_start_line": 492, "text_hash": "2c58cf322b6231c62f4ee8f907cd6c262338345d1c2876c4a5f67ca0feceea7b", "token_count": 294, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0019", "chunk_index": 19, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 545, "source_start_line": 515, "text_hash": "e5e8b8e99e43db580814bfb46029f5b06407c98fef5a793185062d3a5442a60a", "token_count": 302, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0020", "chunk_index": 20, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 582, "source_start_line": 547, "text_hash": "b3c1d250d631e2b28521df2d18361684c9fd42ca1cf8fe6cd565cc53859af31a", "token_count": 300, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0021", "chunk_index": 21, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 608, "source_start_line": 584, "text_hash": "4ae27bb639ccdd1de74c2bba6cc7b6b0550ca809be46b6b09ca0316e6546c933", "token_count": 269, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0022", "chunk_index": 22, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 648, "source_start_line": 610, "text_hash": "cb559e40c3f71914d94223e9ef304796e83da03283d46d753db3389768145c8a", "token_count": 303, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0023", "chunk_index": 23, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 687, "source_start_line": 650, "text_hash": "a38e0d3c1ddacf420c60e7d83cdc27794f824dfe9529ef50842cb48d3d03b704", "token_count": 283, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0024", "chunk_index": 24, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 702, "source_start_line": 689, "text_hash": "dbe32d1668e4dde74ab8fdbfefc1a4d64dd5e31e47a083583b25cd1d7b794436", "token_count": 240, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0025", "chunk_index": 25, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 728, "source_start_line": 704, "text_hash": "6bc52fe8e11a9cf9ed7079bf38fe0ad7f2c80d0bfd34012d895e0191335893e8", "token_count": 267, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0026", "chunk_index": 26, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 740, "source_start_line": 730, "text_hash": "0c868bf5bc0462de652ac2ad10b9153bee2fd972acc19697240acc72c98f1bf2", "token_count": 381, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0027", "chunk_index": 27, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 756, "source_start_line": 742, "text_hash": "fc3bd4c1fe0224346027fce4fbe01cf1913316077e335de1dda15809605c2422", "token_count": 218, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0028", "chunk_index": 28, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 766, "source_start_line": 758, "text_hash": "c57f811598606c93d8f507a8f6478d5bf7a3cabc5359a3d0d5c5a633018d2943", "token_count": 220, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0029", "chunk_index": 29, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 810, "source_start_line": 768, "text_hash": "4f407409d00a0c8b3a855aa646a038ecc527a188fd40c664ffd37d7ce1cc4ffb", "token_count": 262, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0030", "chunk_index": 30, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 868, "source_start_line": 812, "text_hash": "88dc8f369429cce81aa117365ea3d321cf8c63c65588819200aba1c9d6954c60", "token_count": 288, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0031", "chunk_index": 31, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 895, "source_start_line": 870, "text_hash": "a509a5cf7b3198dc3a2ea241f98c1d672960a372c912807c12550bd51cc47d67", "token_count": 119, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0032", "chunk_index": 32, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 913, "source_start_line": 897, "text_hash": "b984b9da413bf43e367ca026d9fef63a948bf3b9d0879264e44bea6696d2cd97", "token_count": 261, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0033", "chunk_index": 33, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 917, "source_start_line": 915, "text_hash": "05da055d08de3500cc429d3254a9103562039ff290efb0b0aba61dc1945aee70", "token_count": 62, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0034", "chunk_index": 34, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 927, "source_start_line": 919, "text_hash": "057bd70c0bc76edc4b772c793d88ef65b157be88a7d831241475bb56658c199c", "token_count": 1089, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0035", "chunk_index": 35, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 931, "source_start_line": 929, "text_hash": "53827ad1fd922c278f7de4eb3d02f328e234f1479d6a1419173dca09b0f02fbc", "token_count": 66, "visibility": "internal"}
|
||||
{"acl_expression": "visibility == \"internal\"", "chunk_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md:chunk:0036", "chunk_index": 36, "customer_id": null, "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "embedding_model": null, "heading_path": [], "published_targets": [], "source_end_line": 958, "source_start_line": 933, "text_hash": "37d3d38005c910965abc33a519614053a9a1c3ffc6ad1d7f961923387ea6f4ba", "token_count": 960, "visibility": "internal"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"acl_expression": "visibility == \"internal\"", "approved_by": null, "audience": ["claude", "codex"], "checksum": "7e9060bdcd571edea19ed35ef77fb873fca0e6a61b438624b5864306eb894a4d", "customer_id": null, "doc_type": "design_spec", "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "effective_from": null, "excluded_from_retrieval": false, "expires_at": null, "owner": "team-health", "product_area": "ljwx-health", "review_status": "approved", "source_ref": "lingjingwanxiang/ljwx-health@71c49969d7482b53b923fc5ddc529a9178cda27c:docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md", "source_type": "repo_doc", "tags": ["ljwx-health", "architecture", "ssot"], "tier": "ssot", "title": "多租户身份、权限、健康资源与多终端总体架构", "version": "71c49969d7482b53b923fc5ddc529a9178cda27c", "visibility": "internal"}
|
||||
+958
@@ -0,0 +1,958 @@
|
||||
# 多租户身份、权限、健康资源与多终端总体架构
|
||||
|
||||
**状态**: Accepted
|
||||
**版本**: v1.23
|
||||
**创建日期**: 2026-07-10
|
||||
**评审日期**: 2026-07-10
|
||||
**适用范围**: `ljwx-boot` / `ljwx-admin` / `ljwx-bigscreen` / `ljwx-phone` / `ljwx-watch` 及后续手环、无人机、机器人、第三方服务
|
||||
**配套文档**: [业务数据链全景](./business-data-chains.md) / [设备抽象层](./device-abstraction-layer.md) / [身份、组织与授权术语 ADR](../engineering/decision-records/0003-identity-organization-authorization-terminology.md)
|
||||
**实施跟踪**: [GitHub Epic #383](https://github.com/lingjingwanxiang/ljwx-health/issues/383)
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论与关键决策
|
||||
|
||||
本设计补齐 `business-data-chains.md` 的横向架构:业务链文档回答“数据怎样流动”,本文回答“谁能以什么身份、在什么租户和组织范围内,对哪些资源做什么,以及不同终端怎样安全、高效地消费同一份业务能力”。
|
||||
|
||||
核心决策如下:
|
||||
|
||||
1. **角色、岗位、菜单三者解耦**:角色授予功能权限与数据范围;岗位表达人员业务属性和健康风险权重;菜单只做终端导航投影,不能成为后端授权依据。
|
||||
2. **统一授权上下文**:每次请求只解析一次 `AccessContext`,统一给 Controller、Service、Mapper、缓存键和审计日志使用;客户端传入的 `customerId/userId/orgId` 只能是查询意图,不能成为可信身份。
|
||||
3. **租户隔离、功能权限、数据权限、资源关系分层判定**:RLS 只解决租户边界,Sa-Token 权限只解决“能否执行动作”,组织/用户/设备/收件人关系由资源范围策略单独校验。
|
||||
4. **组织权限按绑定点生效**:`sys_user_org_role` 中的角色只在对应组织及配置的子树内生效;登录时可以合并“功能权限集合”,但每次资源访问必须再次按目标组织收窄。
|
||||
5. **岗位默认不授予权限**:岗位用于风险等级、健康评分、排班/工种等业务规则。确有“岗位自动带角色”的需求时,使用显式映射和审批,不在代码里把岗位名硬编码为权限。
|
||||
6. **健康事实保存事件发生时归属**:健康数据、告警、消息投递保存 `customer_id + subject_user_id + org_id_at_event + device_id/device_sn + occurred_at`。人员调动不改写历史事实。
|
||||
7. **授权看当前关系,报表看历史事实**:能否查看某个用户默认按当前组织关系判定;历史报表默认按事件发生时组织聚合。两个口径不得隐式混用。
|
||||
8. **设备是独立主体,不是用户的另一种登录名**:手表/手环使用设备凭据,机器人/无人机使用设备或服务主体;绑定关系决定它代表哪个用户、组织或任务,`deviceSn` 本身不是密钥。
|
||||
9. **领域 API 唯一,终端 BFF 负责裁剪**:Admin、Bigscreen、Phone、Watch 不复制健康/告警/消息规则,只消费统一领域服务;BFF 负责聚合、隐私、带宽、离线和交互差异。
|
||||
10. **PostgreSQL 是事实源,Redis/大屏快照是可重建投影**:高频组织/用户查询通过索引、闭包表、聚合表和可重建缓存加速,不建立第二套不可对账的事实源。
|
||||
|
||||
---
|
||||
|
||||
## 2. 代码与本地数据库核查结论
|
||||
|
||||
本节记录截至 2026-07-22 当前分支和 `ljwx-local-postgres17/ljwx_health` 的事实;生产数据库状态单独标注,不能用本地迁移结果代替生产验证。目标架构不能把“已有表”误写成“已经完整接线”。
|
||||
|
||||
### 2.1 已有基础
|
||||
|
||||
| 能力 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| 租户上下文 | `GlobalRequestInterceptor` 从 Sa-Token Session 初始化 `UserContext/TenantContext`,支持 CURRENT/TARGET/ALL | 可保留,但要让所有业务查询使用 effective tenant |
|
||||
| 数据库隔离 | 主要用户、组织、健康、设备、告警、消息事实表已启用 RLS;应用层另有 MyBatis 租户拦截器;独立 `ljwx_health_test` 已用 `NOSUPERUSER/NOBYPASSRLS` 角色验证关系表策略和连接复用 | 有双层基础;本地 owner `ljwx` 仍会绕过 RLS,因此只有受限角色测试可作为数据库边界证据,生产迁移仍需独立验证 |
|
||||
| 组织模型 | `sys_org_units + sys_org_closure`,已有祖先/后代索引 | 适合组织树与管辖范围查询,无需另造树模型 |
|
||||
| 组织管理范围 | `PrincipalBasedOrgManagerResolver` 已按 `sys_user_org.principal + sys_org_closure` 解析管理范围;`sys_org_manager_cache` 表、索引和 tenant isolation policy 已存在 | 解析器和可重建缓存可复用,但独立的 manager 事实关系尚未建立 |
|
||||
| 基础 RBAC | `sys_role/sys_permission/sys_role_permission`,Sa-Token 从登录角色加载 permission resource | 功能权限主干可复用 |
|
||||
| 管理边界 | `AuthorizationChain` 已包含 `AdminBoundaryGuard/OrgScopeGuard/RoleDelegateGuard` | 目前主要接在用户和组织写操作,尚未成为所有资源访问的统一入口 |
|
||||
| 健康事实维度 | `t_user_health_data` 已有 `customer_id/org_id/user_id/device_sn/ts`,并有 tenant-user-time、tenant-org-time 索引 | 已具备高效查询的物理基础,需要明确 `org_id` 是事件快照 |
|
||||
| 业务资源关联 | 设备、绑定、心跳、告警、消息及明细大多已有 customer/user/org/device 字段 | 字段存在但关系权威性和时间语义尚未统一 |
|
||||
| 终端入口 | Admin workbench、Bigscreen V6 BFF、Mobile API、Watch API 均已存在 | 可按现有入口渐进收敛,不需要重建全部 API |
|
||||
| 隐私呈现 | Bigscreen 已有 `private/pseudonymized/aggregate-only` | 可提升为跨终端统一字段策略 |
|
||||
|
||||
### 2.2 必须先解决的断点
|
||||
|
||||
| 断点 | 代码证据 | 风险 |
|
||||
|---|---|---|
|
||||
| 组织角色业务覆盖仍不完整 | 请求边界、Session 与导航已统一装载 `sys_user_role + sys_user_org_role` assignment;尚未纳入 IA-03/IA-08 的业务入口仍可能使用旧角色或范围算法 | 新入口已按 assignment 生效,但未审计入口仍可能把组织角色遗漏或放大 |
|
||||
| DataScope 没有形成强制查询入口 | `ScopeQueryFactory` 除自身外无业务调用;`DataScopeInterceptor.CUSTOM` 返回 `null`,而 `null` 同时表示 TENANT_ALL | 同租户跨部门、跨用户读取;CUSTOM 实际放大成全租户 |
|
||||
| 导航投影待生产收口 | 当前 IA-05 分支按 resolved assignment 分别求 role-menu 与 role-permission 交集,租户覆盖只做隐藏/别名/排序;缓存键包含 platform/tenant/user authz、navigation、blueprint version,所有现有菜单/permission 写路径在同一事务推进版本并写 authorization outbox | 代码、Docker 迁移和结构门禁已通过;仍须先在 GitOps 部署租户模板直绑迁移,再合并严格 loader 与版本化投影,避免发布窗口内导航中断 |
|
||||
| 后端 permission 与菜单并非一一对应 | 全库门禁把无同角色菜单的 permission 统计为 backend-only,不删除也不判失败;前端 permission hint 仅投影同一 assignment 同时授予的 menu-permission 交集 | 后端 API 授权保持独立;新增菜单或 permission 时必须通过结构/租户/override 一致性门禁,禁止跨 assignment 拼接 UI capability |
|
||||
| 关系表租户隔离历史缺口 | 当前分支 `V3.1.51` / 本地直连 SQL 已为 `sys_user_role/sys_role_menu/sys_role_permission` 回填并强制 `customer_id`,增加租户化唯一索引、四张授权关系表的边一致性触发器和 A/B 类 RLS policy;Docker dev/test 已验证,生产仍是迁移前状态 | 代码与本地基线已收敛,但在生产 GitOps SQL、受限角色读验证和回滚演练完成前,不能把该缺口标记为全环境关闭 |
|
||||
| 部分健康关系表未进 RLS | `t_device_heartbeat` 未启用 RLS,`R__rls_policies.sql` 也未包含 heartbeat 和 `t_device_message_department` | 设备状态或消息受众关系可能绕过统一租户边界 |
|
||||
| `principal` 语义冲突 | 业务代码把 `sys_user_org.principal` 当组织负责人;数据库唯一索引却约束 `(user_id, customer_id) WHERE principal='1'`,名为“单一主部门” | 一个字段同时承担“主组织”和“负责人”,无法正确表达多组织管理 |
|
||||
| 终端仍可传入越权维度 | Mobile `/health/data` 已改为 AccessContext 的 operator/effective tenant,并仅兼容与当前用户事实一致的 query hint;`/mobile/health/data/{id}` 与移动告警详情、AI 建议、处理入口已先按 effective tenant 定位持久化资源,再把 page/update 权限与资源当前 user 或 persisted assignee 求交,仅无 user 时才回退事件 org;Mobile 聚合状态、关键指标、风险告警和 AI 建议固定当前 tenant/user,反馈写入服务端解析的 tenant/user,缓存键包含 scope/privacy/authz version,普通员工 `COMMON` assignment 已从误配的 TENANT_ALL 收窄为 SELF;Bigscreen 的 realtime/snapshot/rebuildCache/examSummary/interventionSummary/SSE/alert action/detail/assignees 已按各自 permission 编译 subject scope,仅 TENANT_ALL 可调用租户聚合和管理入口且 query tenant 只能匹配 effective tenant;单组织 Viewer 的 snapshot/realtime/SSE replay/live 已固定 token org,健康、告警、设备、消息、体检、干预与空间聚合都在 SQL/事件层裁剪,未知事件 org fail closed,多组织 token 暂时拒绝;Viewer alert detail 已将 token org scope 与告警持久化 tenant/org 求交,Viewer 管理面也按 `bigscreen:viewer:manage` 要求 tenant-wide assignment scope;Health V2 score/profile 与 Alert V2 list/detail/handle/assign/status 已按 permission-specific scope 校验目标用户、当前组织、租户与持久化告警关系 | Mobile 健康列表/详情、告警资源及聚合状态/反馈首段,Bigscreen 租户聚合/SSE/Admin/单组织 Viewer、Health V2 score/profile、Alert V2 首段已 fail closed;多组织 Viewer 的真实聚合语义、其余 Health/Message/Device 入口仍需逐项校验同租户组织、用户与资源关系 |
|
||||
| Watch 以 SN 代替认证 | `/watch/*` 为 `@SaIgnore`,仅凭 `X-Device-SN` 查绑定用户后注入上下文 | 知道或伪造 SN 即可冒充设备上传/收消息 |
|
||||
| 当前组织与历史组织混用 | 一部分查询直接 join `sys_user.org_id`,另一部分读取事实表 `org_id` | 人员调动后历史报表漂移,不同页面统计口径不一致 |
|
||||
| 领域模型有重复与映射漂移 | system/health 下并存多套同表 Entity;`TDeviceMessage` 对 `priority_level` 有重复属性映射 | 后续权限和资源字段扩展容易只改中一套模型 |
|
||||
| 负向测试缺口 | 现有测试覆盖租户切换、事件租户隔离和大屏隐私,但未系统覆盖“同租户跨组织/跨用户/伪造设备 SN” | 安全回归难以及时发现 |
|
||||
|
||||
### 2.3 当前索引结论
|
||||
|
||||
已经适合保留的索引:
|
||||
|
||||
- `t_user_health_data(customer_id, user_id, ts DESC)`
|
||||
- `t_user_health_data(customer_id, org_id, ts DESC)`
|
||||
- `sys_org_closure(ancestor_id, customer_id)`、`(descendant_id, customer_id)` 和唯一闭包关系
|
||||
- `sys_user_org(customer_id, user_id, org_id, is_deleted)`
|
||||
- `sys_org_units(customer_id, parent_id)`、`(customer_id, status, is_deleted)`
|
||||
|
||||
当前分支已新增并在 Docker dev/test 验证的索引:
|
||||
|
||||
- `sys_user_role(customer_id, user_id, role_id)`、`sys_role_menu(customer_id, role_id, menu_id)`、`sys_role_permission(customer_id, role_id, permission_id)` 的 active-row 租户化唯一索引
|
||||
|
||||
明显缺失或不足的索引:
|
||||
|
||||
- `sys_user_position(customer_id, user_id, position_id)` 唯一索引;当前名为 customer 的索引实际只有 `user_id`
|
||||
- `t_device_info(customer_id, user_id)`、`t_device_info(customer_id, org_id, online_status, last_report_time DESC)`
|
||||
- `t_device_heartbeat(customer_id, user_id, last_heartbeat_at DESC)` 以及当前设备唯一约束
|
||||
- `t_alert_info(customer_id, user_id, occur_at DESC)`、`t_alert_info(customer_id, org_id, alert_status, occur_at DESC)`
|
||||
- `t_device_message(customer_id, user_id, sent_time DESC)`
|
||||
- `t_device_message_detail(customer_id, user_id, response_status, sent_time DESC)`
|
||||
- `t_device_message_department(customer_id, department_id, message_id)`
|
||||
|
||||
索引必须由真实查询计划决定。上述列表是首批候选,不应不经 `EXPLAIN (ANALYZE, BUFFERS)` 就全部上线。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Surface[终端与交互面]
|
||||
ADMIN[Admin]
|
||||
BIG[Bigscreen]
|
||||
PHONE[Phone]
|
||||
WEAR[Watch / Band]
|
||||
EDGE[Drone / Robot / Gateway]
|
||||
end
|
||||
|
||||
subgraph BFF[Surface BFF / API Adapter]
|
||||
NAV[Navigation + Capability API]
|
||||
ABFF[Admin BFF]
|
||||
BBFF[Bigscreen BFF]
|
||||
MBFF[Mobile BFF]
|
||||
DBFF[Device Ingress / Command API]
|
||||
end
|
||||
|
||||
subgraph Access[身份与策略平面]
|
||||
AUTHN[Human / Device / Service Authentication]
|
||||
CTX[AccessContext Resolver]
|
||||
PDP[Policy Decision: tenant + permission + data scope + relation + privacy]
|
||||
AUDIT[Decision Audit]
|
||||
end
|
||||
|
||||
subgraph Domain[领域服务]
|
||||
USER[User / Organization / Position]
|
||||
DEVICE[Device Registry / Binding]
|
||||
HEALTH[Health Query / Ingest]
|
||||
ALERT[Alert Center]
|
||||
MESSAGE[Message Center]
|
||||
end
|
||||
|
||||
subgraph Data[事实与投影]
|
||||
PG[(PostgreSQL / TimescaleDB facts)]
|
||||
OUTBOX[(Outbox)]
|
||||
SPRING[Required Spring Event publisher]
|
||||
KAFKA[Kafka publisher / transport]
|
||||
CH[(ClickHouse projection)]
|
||||
READ[(Org/User read models)]
|
||||
REDIS[(Redis rebuildable cache)]
|
||||
end
|
||||
|
||||
Surface --> BFF
|
||||
BFF --> AUTHN --> CTX --> PDP
|
||||
PDP --> Domain
|
||||
PDP --> AUDIT
|
||||
Domain --> PG
|
||||
Domain --> OUTBOX --> SPRING --> READ --> REDIS
|
||||
OUTBOX -. shadow / promoted .-> KAFKA --> CH
|
||||
KAFKA -. durable projection after promotion .-> READ
|
||||
PG -. rebuild / reconcile .-> READ
|
||||
PG -. PG↔CH reconcile .-> CH
|
||||
BFF --> READ
|
||||
BFF --> REDIS
|
||||
```
|
||||
|
||||
### 3.1 分层职责
|
||||
|
||||
| 层 | 只负责 | 不负责 |
|
||||
|---|---|---|
|
||||
| 认证 AuthN | 证明人、设备、服务是谁 | 决定能看哪些业务数据 |
|
||||
| 租户隔离 | 限制 effective customer | 代替组织/用户数据权限 |
|
||||
| 功能权限 RBAC | 判断 action/resource permission | 从菜单可见性推导权限 |
|
||||
| 数据范围 | 计算可访问组织和用户集合 | 跨租户放行 |
|
||||
| 资源关系 | 校验本人、绑定设备、告警受理人、消息收件人等关系 | 替代 RBAC |
|
||||
| 隐私策略 | 字段脱敏、聚合阈值、用途限制 | 改变事实数据 |
|
||||
| BFF | 聚合、裁剪、协议适配、缓存策略 | 重写领域规则 |
|
||||
| Read Model | 加速确定口径的查询 | 成为不可重建事实源 |
|
||||
|
||||
### 3.2 事件源与投影边界
|
||||
|
||||
`t_health_event_outbox` 是领域事务提交后的**唯一事件源**。现有 `HealthEventOutboxWorker` 读取同一条 outbox 记录,再交给 publisher adapter:
|
||||
|
||||
- `SpringEventHealthEventPublisher` 是当前 required 的进程内运行链;
|
||||
- `KafkaHealthEventPublisher` 是当前 non-required 的 shadow publish,复用同一事件标识与 payload;
|
||||
- Kafka 是传输通道,ClickHouse、组织/用户读模型和 Redis 都只是可重建投影;PostgreSQL 事实仍是对账基准。
|
||||
|
||||
禁止领域服务在“写 PG 后”再独立直发另一条 Kafka 事件,也禁止 Spring Event 与 Kafka 各自定义无法关联的事件 ID。进程内低延迟投影可以消费 required Spring Event;跨进程投影在 Kafka 完成 shadow 对账、重放和延迟门禁后再切为 durable consumer。每个消费者必须以 outbox event id 幂等,保存消费位点/版本,并提供 PG rebuild、diff 和 lag 指标。现有 Bigscreen Redis 热缓存从 PG 或 canonical read model 重建,不把未经晋级的 Kafka shadow 数据当成第二事实源。
|
||||
|
||||
---
|
||||
|
||||
## 4. 身份、组织、岗位、角色和菜单模型
|
||||
|
||||
本章术语的规范定义、旧字段兼容解释和迁移归属以
|
||||
[ADR 0003](../engineering/decision-records/0003-identity-organization-authorization-terminology.md)
|
||||
为准。特别是:principal/actor/subject 不可混用,主组织不等于组织负责人,岗位和菜单不授予后端权限,当前组织与事件时组织必须显式区分。
|
||||
|
||||
### 4.1 主体模型
|
||||
|
||||
统一使用 `Principal` 表达调用主体:
|
||||
|
||||
| principal_type | 示例 | 认证方式 | 默认数据边界 |
|
||||
|---|---|---|---|
|
||||
| `HUMAN` | 平台管理员、租户管理员、部门负责人、员工 | Sa-Token/OIDC,后续可接 MFA | 角色 + 组织范围 + 资源关系 |
|
||||
| `DEVICE` | Watch、手环、机器人、无人机 | 设备注册凭据、签名 nonce、短期 token 或 mTLS | 设备自身 + 当前有效绑定/任务 |
|
||||
| `SERVICE` | 定时任务、第三方告警源、内部 worker | workload identity/API key/mTLS | 明确 service scope,不模拟普通用户 |
|
||||
| `VIEWER` | 无人值守大屏 | 短期 viewer token | 固定租户/组织 + 固定 surface + 隐私模式 |
|
||||
|
||||
设备和服务主体不能伪造 `UserContext` 后获得完整用户权限。需要代表用户执行时,必须保留:
|
||||
|
||||
```text
|
||||
actor_principal = device/service
|
||||
subject_user_id = 被代表的用户
|
||||
delegation_source = binding/task/grant
|
||||
```
|
||||
|
||||
### 4.2 组织与人员关系
|
||||
|
||||
推荐语义:
|
||||
|
||||
```text
|
||||
sys_user [已有] 人的租户内账号
|
||||
sys_org_units [已有] 组织树
|
||||
sys_user_org [已有,待修正语义] 人员组织任职关系
|
||||
- is_primary 当前主组织,只能一个
|
||||
- valid_from / valid_to 关系有效期
|
||||
sys_org_manager [新建] 组织管理责任,不再复用 is_primary/principal
|
||||
- customer_id, org_id, user_id
|
||||
- manager_type, valid_from, valid_to
|
||||
sys_org_closure [已有] 组织祖先/后代投影
|
||||
sys_org_manager_cache [已有] 管理范围缓存,可重建
|
||||
```
|
||||
|
||||
当前 `PrincipalBasedOrgManagerResolver` 已能基于 `sys_user_org.principal` 和 closure 计算管理范围,并读取既有 `sys_org_manager_cache`。迁移时保留该解析器作为兼容适配层:先引入新的 `sys_org_manager` 事实表,再让 resolver/cache 改从新事实重建,最后停止复用 `principal`。
|
||||
|
||||
迁移期间可以保留 `sys_user.org_id/org_name` 作为当前主组织读优化字段,但它们是 `sys_user_org.is_primary=true` 的投影,不是关系事实源。
|
||||
|
||||
### 4.3 岗位语义
|
||||
|
||||
岗位回答“这个人在业务上做什么”,角色回答“这个账号在系统里能做什么”。
|
||||
|
||||
岗位可承载:
|
||||
|
||||
- 工种/职级/作业场景;
|
||||
- `risk_level/weight` 等健康风险参数;
|
||||
- 默认采集策略、体检计划、健康阈值模板;
|
||||
- 排班、在岗、暴露因素等后续业务属性。
|
||||
|
||||
岗位不能直接承载:
|
||||
|
||||
- 菜单列表;
|
||||
- API permission 字符串;
|
||||
- 跨组织数据范围;
|
||||
- 平台或租户管理员级别。
|
||||
|
||||
如果一个用户在不同组织有不同岗位,目标关系应包含 `org_id` 和有效期。`sys_user_position` 已有 `customer_id` 物理列,首期应确保它始终正确写入,再补 `org_id/valid_from/valid_to` 和 tenant-aware 唯一索引;后续再评估是否收敛成统一 assignment 表。
|
||||
|
||||
### 4.4 角色与组织绑定
|
||||
|
||||
角色分两种绑定:
|
||||
|
||||
```text
|
||||
sys_user_role
|
||||
-> 租户级或 SELF 角色
|
||||
-> 在整个租户或本人范围生效
|
||||
|
||||
sys_user_org_role
|
||||
-> 组织绑定角色
|
||||
-> 只在绑定 org_id 或其子树内生效
|
||||
```
|
||||
|
||||
登录授权快照不能只是一组 roleId,应保存带作用域的 assignment:
|
||||
|
||||
```json
|
||||
{
|
||||
"authzVersion": 42,
|
||||
"roleAssignments": [
|
||||
{"assignmentId": 1001, "roleId": 10, "roleDataScope": "SELF", "boundary": "TENANT"},
|
||||
{"assignmentId": 1002, "roleId": 21, "roleDataScope": "DEPT_AND_CHILD", "boundary": "ORG_SUBTREE", "anchorOrgId": 3001}
|
||||
],
|
||||
"permissions": ["health:record:read", "alert:handle"],
|
||||
"scopeDigest": "sha256:..."
|
||||
}
|
||||
```
|
||||
|
||||
`permissions` 可以为前端动作提示做并集;后端处理具体 `action + targetOrgId/subjectUserId` 时必须用原始 assignment 再判定,不能只看并集,也不能把 assignment A 的 permission 与 assignment B 的范围拼成一次授权。
|
||||
|
||||
### 4.5 菜单与能力投影
|
||||
|
||||
菜单生成公式:
|
||||
|
||||
```text
|
||||
EffectiveNavigation
|
||||
= PlatformBaselineMenu
|
||||
∩ TenantEntitlement
|
||||
∩ RoleMenuGrant
|
||||
∩ PermissionAvailability
|
||||
∩ SurfaceCapability
|
||||
⊖ TenantOverride.hidden
|
||||
+ TenantOverride.alias/sort
|
||||
```
|
||||
|
||||
后端授权只检查 permission,不检查菜单。建议提供:
|
||||
|
||||
- `GET /api/auth/capabilities?surface=admin|bigscreen|phone|watch`
|
||||
- `GET /api/auth/navigation?surface=admin`
|
||||
- 返回 `authzVersion/navigationVersion/tenantConfigVersion`,支持客户端和 Redis 精确失效。
|
||||
|
||||
Phone、Watch 不必复刻 Admin 菜单树;它们消费 capability 列表和固定的原生导航。未来若需要租户可配置终端模块,再引入 `sys_surface_feature`,不要把 Admin Vue route 直接下发给设备。
|
||||
|
||||
### 4.6 多租户资源生命周期
|
||||
|
||||
沿用现有“三层资源模型”,但为模板升级增加可治理的版本语义:
|
||||
|
||||
| 资源 | 所有权与策略 | 更新方式 |
|
||||
|---|---|---|
|
||||
| 菜单、permission | 平台基线,租户只读;受 license/entitlement 约束 | 平台发布;租户仅通过 override 做隐藏、别名、排序 |
|
||||
| 角色模板 | 平台 blueprint,创建租户时复制 | 租户副本可编辑;保留 `source_template_id/template_version` |
|
||||
| 岗位模板 | 平台 blueprint,租户复制后可按组织调整风险参数 | 同角色模板,禁止平台更新静默覆盖租户修改 |
|
||||
| 租户角色/岗位 | 租户自治 | 只在本租户可见,受管理员边界和委派池限制 |
|
||||
| 组织、用户、assignment | 租户事实 | 不从模板继承 |
|
||||
|
||||
模板升级流程必须是 `diff -> preview -> apply -> audit -> rollback`:
|
||||
|
||||
```text
|
||||
TenantBlueprint(version)
|
||||
-> compare source_template_id + last_synced_version
|
||||
-> classify unchanged / tenant-modified / removed / new
|
||||
-> auto-apply only unchanged items
|
||||
-> require tenant decision for conflicts
|
||||
```
|
||||
|
||||
新增 permission 可以通过 entitlement 灰度开放;删除或改名必须先检查角色、菜单、终端 capability 和 API 注解引用。模板同步不能直接复用“创建租户时无条件复制”的逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. 统一授权模型
|
||||
|
||||
### 5.1 AccessContext
|
||||
|
||||
```text
|
||||
AccessContext
|
||||
actorPrincipalId/type
|
||||
operatorUserId # HUMAN 或被授权的操作用户
|
||||
loginCustomerId
|
||||
tenantScope # CURRENT / TARGET / ALL
|
||||
effectiveCustomerId
|
||||
surface # admin / bigscreen / phone / watch / edge
|
||||
grantAssignments[] # 权限与范围不可拆开的原始授权单元
|
||||
assignmentId / roleId
|
||||
permissionRefs[]
|
||||
roleDataScope # SELF / DEPT_ONLY / DEPT_AND_CHILD / TENANT_ALL / CUSTOM
|
||||
boundary # TENANT / ORG_SUBTREE
|
||||
anchorOrgId / customScopeId
|
||||
validFrom / validTo
|
||||
permissionsHint[] # 仅供能力提示的并集,不是后端授权依据
|
||||
scopeDigest # assignments 的稳定摘要,用于缓存键与审计
|
||||
deviceId / bindingId
|
||||
privacyMode
|
||||
authzVersion # platform/tenant/user version tuple
|
||||
traceId
|
||||
```
|
||||
|
||||
所有字段由服务端 Session、设备凭据、绑定关系和租户配置解析。Header `X-Client-Platform/X-Device-Vendor/X-Source-Adapter` 只参与遥测和能力协商。`AccessContext` 不保存一个扁平的最终 `dataScope`:多组织 assignment 的范围只能结合当前 action 和目标资源求值。
|
||||
|
||||
### 5.2 判定公式
|
||||
|
||||
```text
|
||||
ALLOW = authenticated(actor)
|
||||
AND tenantVisible(effectiveTenant, resource.customerId)
|
||||
AND existsGrantAssignment(actor, action, resource)
|
||||
AND relationAllows(actor, resource, action)
|
||||
AND surfaceAllows(surface, action)
|
||||
AND privacyAllows(purpose, fields)
|
||||
AND notBreakAdminBoundary(actor, target)
|
||||
```
|
||||
|
||||
建议新增统一入口:
|
||||
|
||||
```java
|
||||
accessPolicy.authorize(Action.READ_HEALTH_RECORD, ResourceRef.health(recordId));
|
||||
scopedQuery.healthRecords(query, accessContext);
|
||||
```
|
||||
|
||||
Controller 的 `@SaCheckPermission` 继续作为第一道功能权限 PEP;Service 层的 `AccessPolicy` 是不可绕过的资源级 PEP;数据库 RLS 是最后的租户边界。账号禁用、租户冻结、管理员边界、隐私禁止和显式 deny 属于 hard deny,优先于任何 grant assignment。
|
||||
|
||||
### 5.3 数据范围编译
|
||||
|
||||
| DataScope | 查询谓词 |
|
||||
|---|---|
|
||||
| SELF | `subject_user_id = actorUserId` |
|
||||
| DEPT_ONLY | `org_id IN (角色绑定组织)` |
|
||||
| DEPT_AND_CHILD | `EXISTS sys_org_closure WHERE ancestor_id IN (managedRoots) AND descendant_id = resource.org_id` |
|
||||
| TENANT_ALL | 仅 `customer_id = effectiveCustomerId` |
|
||||
| CUSTOM | join `sys_data_scope_member`;没有成员时返回空集,绝不能退化为 TENANT_ALL |
|
||||
|
||||
每次授权按目标资源编译,而不是先把多个 assignment 压成“最宽枚举”:
|
||||
|
||||
1. 只选择当前有效且自身授予目标 permission 的 assignments;
|
||||
2. 将角色数据范围与 assignment 边界取交集:`effectiveScope = roleDataScope ∩ boundary`;除 `SELF/TENANT_ALL` 外必须有明确 anchor,租户级部门角色按当前有效组织展开成独立 assignments,不能隐式共用一组 managed roots;
|
||||
3. 每个 assignment 独立判断目标 `orgId/subjectUserId`,任一完整 assignment 覆盖目标即可放行,即同一 action 的 grant 范围取并集;
|
||||
4. `sys_user_org_role` 的最大边界始终是绑定组织/子树,即使其角色声明 `TENANT_ALL` 也不能越出该边界;
|
||||
5. `CUSTOM` 成员也必须与 assignment 边界取交集;不能把一个 assignment 的 permission 与另一个 assignment 的 scope 交叉拼接。
|
||||
|
||||
因此,一个用户在租户级只有 `SELF`,同时在组织 A 持有 `DEPT_AND_CHILD + health:record:read` 时:本人资源由前一个 assignment 放行;组织 A 子树资源由后一个 assignment 放行;组织 B 仍拒绝。`permissionsHint` 可以显示该用户具备 read 动作,但不能证明任意目标资源可读。
|
||||
|
||||
小组织可以把 org IDs 缓存在授权快照中;大组织直接用 closure `EXISTS/JOIN`,避免生成数千项 `IN (...)`。
|
||||
|
||||
建议增加:
|
||||
|
||||
```text
|
||||
sys_data_scope
|
||||
id, customer_id, name, status, version
|
||||
|
||||
sys_data_scope_member
|
||||
scope_id, member_type(ORG/USER), member_id, include_children
|
||||
```
|
||||
|
||||
### 5.4 当前关系与历史归属
|
||||
|
||||
同一查询必须显式选择两种口径之一:
|
||||
|
||||
| 口径 | 用途 | 组织来源 |
|
||||
|---|---|---|
|
||||
| `CURRENT_SUBJECT_SCOPE` | 判断管理员现在能否查看某用户 | 当前有效 `sys_user_org` + closure |
|
||||
| `EVENT_ORG_SCOPE` | 历史趋势、部门绩效、事件审计 | 事实表 `org_id_at_event` |
|
||||
|
||||
默认规则:先用当前关系完成授权,再按请求的报表口径聚合。用户离开组织后,原部门管理员是否还能看历史个人明细属于隐私策略,不通过改写历史数据实现。
|
||||
|
||||
### 5.5 缓存与失效
|
||||
|
||||
版本事实由 PostgreSQL 持有,不由某个应用实例或 Redis 自行发号。目标表可以采用:
|
||||
|
||||
```text
|
||||
sys_platform_authz_version(id=1, authz_version, updated_at)
|
||||
sys_tenant_authz_version(customer_id, authz_version, navigation_version, blueprint_version, updated_at)
|
||||
sys_user_authz_version(customer_id, user_id, authz_version, updated_at)
|
||||
```
|
||||
|
||||
对用户生效的 `authzVersion` 是 `(platformVersion, tenantVersion, userVersion)` 元组或其稳定摘要;navigation 再加入 `navigationVersion`。授权缓存键必须包含:
|
||||
|
||||
```text
|
||||
authz:{customerId}:{userId}:{platformVersion}:{tenantVersion}:{userVersion}
|
||||
navigation:{customerId}:{userId}:{surface}:{platformVersion}:{tenantVersion}:{userVersion}:{navigationVersion}
|
||||
scope:{customerId}:{userId}:{scopeDigest}:{tenantVersion}:{userVersion}
|
||||
```
|
||||
|
||||
只有 `AuthorizationVersionService` 可以递增版本;它与授权事实变更在同一数据库事务内执行原子 `UPDATE ... version = version + 1 RETURNING version`,并写入 authorization outbox。递增归属如下:
|
||||
|
||||
- 用户调动、用户角色/组织角色、设备绑定和账号状态等单用户变化递增 user version;
|
||||
- 角色定义、permission、菜单、租户 override、组织结构等影响一批用户的变化递增 tenant version,菜单类同时递增 navigation version;
|
||||
- 平台 permission/menu 基线和全局策略变化递增 platform version;
|
||||
- blueprint 发布递增 blueprint version,只有实际应用到租户时才同步递增该 tenant 的授权/navigation 版本。
|
||||
|
||||
提交后事件负责删除 Redis/L1 快照并广播新版本;Redis 只镜像最新版本,miss 时回源 PostgreSQL,多实例不得依赖本机内存计数器。订阅丢失由短 TTL + DB 回源兜底。账号禁用、角色移除、设备凭据撤销等高风险收权必须在提交后同步更新共享版本指针、撤销 Session/token;高风险请求在版本广播不可用或版本不确定时直接回源 PostgreSQL 校验,不能等待缓存 TTL,也不能继续使用旧快照。版本递增失败则授权事实事务整体回滚。
|
||||
|
||||
---
|
||||
|
||||
## 6. 健康业务资源归属模型
|
||||
|
||||
### 6.1 通用资源信封
|
||||
|
||||
所有健康领域事实和事件采用统一逻辑字段;物理列可阶段性沿用现名:
|
||||
|
||||
```text
|
||||
resource_id
|
||||
customer_id
|
||||
subject_type # USER / DEVICE / ORG / MISSION / EXTERNAL
|
||||
subject_user_id # 当前多数表物理列为 user_id
|
||||
org_id_at_event # 当前多数表物理列为 org_id
|
||||
position_id_at_event # 需要按事件时岗位解释风险时保存
|
||||
device_id / device_sn
|
||||
source_resource_type/id
|
||||
occurred_at
|
||||
ingested_at
|
||||
actor_principal_id/type
|
||||
trace_id
|
||||
schema_version
|
||||
```
|
||||
|
||||
### 6.2 资源关系
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
TENANT ||--o{ ORG : owns
|
||||
TENANT ||--o{ USER : owns
|
||||
ORG ||--o{ USER_ORG : contains
|
||||
USER ||--o{ USER_ORG : assigned
|
||||
USER ||--o{ USER_POSITION : works_as
|
||||
ROLE ||--o{ USER_ROLE_ASSIGNMENT : grants
|
||||
USER ||--o{ USER_ROLE_ASSIGNMENT : receives
|
||||
ORG ||--o{ USER_ROLE_ASSIGNMENT : scopes
|
||||
DEVICE ||--o{ DEVICE_BINDING : has
|
||||
USER ||--o{ DEVICE_BINDING : bound_to
|
||||
DEVICE_BINDING ||--o{ HEALTH_OBSERVATION : produces_for
|
||||
HEALTH_OBSERVATION ||--o{ ALERT : triggers
|
||||
ALERT ||--o{ MESSAGE : publishes
|
||||
MESSAGE ||--o{ MESSAGE_DELIVERY : fans_out
|
||||
USER ||--o{ MESSAGE_DELIVERY : receives
|
||||
```
|
||||
|
||||
### 6.3 各资源的权威关系
|
||||
|
||||
| 资源 | 权威归属 | 当前可复用字段 | 必须收敛的规则 |
|
||||
|---|---|---|---|
|
||||
| 设备注册 | tenant + device | `t_device_info.customer_id/serial_number` | `(customer_id, serial_number)` 唯一;user/org 只是当前绑定投影 |
|
||||
| 设备绑定 | tenant + device + user + org + validity | `t_device_user` | `BOUND` 的时间区间唯一;解绑保留历史,不硬删除 |
|
||||
| 心跳 | tenant + device + current binding | `t_device_heartbeat.customer_id/user_id/device_sn` | 服务端从凭据和绑定注入,不能信任 body userId |
|
||||
| 健康数据 | tenant + subject user + event org + device + time | `t_user_health_data` 已具备主要字段 | `org_id` 明确为事件快照;绑定无效时拒绝或进入隔离队列 |
|
||||
| 告警 | tenant + subject + event org + source resource | `t_alert_info.user_id/org_id/device_sn/health_id/source_*` | source 必须可追溯;查询按 subject 与 handler 两套关系区分 |
|
||||
| 消息主记录 | tenant + publisher + audience snapshot | `t_device_message.audience/target_scope/source_event_id` | 群组消息不能把主表单个 user/org 当唯一受众事实 |
|
||||
| 消息投递 | tenant + recipient user + org-at-publish + channel/client | `t_device_message_detail` | 已读/确认只能由收件人或授权操作员变更 |
|
||||
|
||||
### 6.4 设备绑定的时间语义
|
||||
|
||||
推荐的绑定约束:
|
||||
|
||||
```text
|
||||
同一 tenant + device 在任一时刻最多一个 ACTIVE/BOUND 用户
|
||||
同一用户可以同时绑定多个设备
|
||||
每个用户可以有一个 primary device,但 primary 不是唯一设备
|
||||
解绑只关闭 valid_to,不删除历史
|
||||
重新绑定必须生成新 binding_id
|
||||
健康上传按 occurred_at 匹配当时有效绑定,至少按接收时绑定做严格校验
|
||||
```
|
||||
|
||||
`sys_user.device_sn`、`t_device_info.user_id/org_id` 可保留为当前绑定投影,禁止继续作为唯一事实源。
|
||||
|
||||
### 6.5 设备初始信任与凭据生命周期
|
||||
|
||||
“绑定审批通过”与“设备已经证明自己是谁”是两件事。现有 `t_device_bind_approval` 和 `DeviceBindApprovalServiceImpl` 作为 provisioning 的业务授权锚点,但审批记录本身不是设备凭据:
|
||||
|
||||
1. 设备先使用出厂预置公钥/证书、一次性 QR 激活码或线下安全下发的 bootstrap secret 证明其库存身份;只保存 public key、credential id 或 secret hash,不保存明文长期密钥;
|
||||
2. HUMAN 用户发起绑定请求,审批流确认 `customer + device + user + org`,审批通过后才允许 provisioning;
|
||||
3. 设备完成 possession challenge 后领取短期 token,或注册设备生成的公钥并取得客户端证书;bootstrap 凭据随即标记 consumed,不能重放;
|
||||
4. token/证书中的 tenant、device、binding、capability 来自服务端事实;解绑、遗失、租户冻结或审批撤销时独立轮换/撤销;
|
||||
5. 仅凭 `deviceSn` 的旧设备进入有期限、有审计的 allowlist 兼容通道,不能自动完成首次注册。
|
||||
|
||||
短期 token、请求签名、nonce/replay 与 `/watch/**` 的 `disabled -> monitor -> enforce`
|
||||
迁移契约见
|
||||
[`device-request-authentication.md`](../06-development/backend/device/device-request-authentication.md)。
|
||||
该实现只闭合 bootstrap proof 之后的请求认证链;生产 KMS/Vault 凭据供应和
|
||||
Phone/Watch 安全存储仍是切换 `enforce` 的前置条件。
|
||||
|
||||
### 6.6 非用户设备
|
||||
|
||||
机器人、无人机等资源不应强制绑定 `user_id`:
|
||||
|
||||
- 设备可直接归属 tenant/org;
|
||||
- 一次任务通过 `mission_id/operator_user_id` 关联人员;
|
||||
- 设备遥测进入通用 telemetry/observation,再按能力映射到健康、环境或安全事件;
|
||||
- 下行指令要求独立 `device:command:*` 权限、双向认证、幂等 commandId 和审计回执;
|
||||
- 不把机器人/无人机的控制能力放进普通 Phone token。
|
||||
|
||||
---
|
||||
|
||||
## 7. 高效组织与用户查询架构
|
||||
|
||||
### 7.1 查询入口
|
||||
|
||||
新增统一 `SubjectScopeQueryService`,禁止各 Controller 自行拼 `customerId/orgId/userId`:
|
||||
|
||||
```text
|
||||
resolve AccessContext
|
||||
-> validate requested user/org belongs to effective tenant
|
||||
-> intersect requested scope with allowed scope
|
||||
-> produce AuthorizedSubjectScope
|
||||
-> domain query uses the authorized scope
|
||||
```
|
||||
|
||||
```text
|
||||
AuthorizedSubjectScope
|
||||
customerId
|
||||
mode = SELF | USER_SET | ORG_SUBTREE | TENANT
|
||||
userIds? # 小集合才物化
|
||||
rootOrgIds?
|
||||
includeChildren
|
||||
grouping = EVENT_ORG | CURRENT_ORG
|
||||
```
|
||||
|
||||
### 7.2 查询模式
|
||||
|
||||
| 场景 | 推荐 SQL 形状 | 说明 |
|
||||
|---|---|---|
|
||||
| 用户个人时序 | `customer_id + user_id + ts range` | 命中现有 user-time 索引 |
|
||||
| 单组织历史趋势 | `customer_id + org_id_at_event + ts range` | 命中现有 org-time 索引 |
|
||||
| 组织子树当前人员 | closure → `sys_user_org` → user | 不依赖逗号 ancestors 模糊匹配 |
|
||||
| 组织子树历史事实 | closure 得到 descendant org,join fact.org_id_at_event | 可使用 semi-join,避免先拉全量 userId |
|
||||
| 当前用户归属聚合 | fact.user_id join 当前 primary membership | 必须在 API 中标记 current-org 口径 |
|
||||
| 告警工作队列 | tenant + org/status/severity + occur_at keyset | 避免 offset 扫描深页 |
|
||||
| 用户收件箱 | tenant + recipient user + sent_time/response_status | 主记录与 delivery 明细分层查询 |
|
||||
| 大屏 | 组织/租户分钟级投影 + realtime event cursor | PG 可重建,Redis 只加速 |
|
||||
|
||||
### 7.3 读模型
|
||||
|
||||
建议按用途建设窄投影,不建设一个全能宽表:
|
||||
|
||||
| 投影 | 主键 | 消费端 |
|
||||
|---|---|---|
|
||||
| `rm_user_health_current` | customer + user | Phone、Admin 用户概览 |
|
||||
| `rm_org_health_hourly/daily` | customer + org + bucket | Admin、Bigscreen 趋势 |
|
||||
| `rm_device_current_state` | customer + device | Admin、Bigscreen、Phone |
|
||||
| `rm_alert_workload` | customer + org + status + severity | Admin、Bigscreen |
|
||||
| `rm_message_inbox` | customer + user + message | Phone、Watch |
|
||||
| `rm_bigscreen_snapshot` | customer + filter signature | Bigscreen |
|
||||
|
||||
投影统一消费 §3.2 定义的 outbox 事件,不再建立第二条领域事件链:
|
||||
|
||||
```text
|
||||
domain transaction
|
||||
-> PostgreSQL facts + t_health_event_outbox
|
||||
-> HealthEventOutboxWorker
|
||||
-> required Spring Event publisher -> 当前进程内消费者/低延迟投影
|
||||
-> non-required Kafka shadow publisher -> ClickHouse 与对账
|
||||
-> Kafka 晋级后 -> 跨进程 durable read-model consumers
|
||||
```
|
||||
|
||||
Kafka 晋级前,shadow 失败不能改变 outbox 已完成语义,跨进程读模型也不能假设 Kafka 是完整事实源;晋级门禁至少包含事件 ID 一致、PG↔ClickHouse 数量/字段对账、重放幂等、消费延迟和故障恢复。每张投影必须有重建任务、对账 SQL、延迟指标、版本字段、source outbox event id 和 PG fallback。现有 Bigscreen Redis 热缓存继续作为从 PG/canonical snapshot 重建的末级缓存。
|
||||
|
||||
### 7.4 分页、缓存和隐私
|
||||
|
||||
- 时序、告警、消息使用 `(time, id)` keyset pagination;管理端固定页码场景可以保留浅层 offset。
|
||||
- 缓存键必须包含 tenant、授权范围摘要、隐私模式和查询版本。
|
||||
- 不缓存未经脱敏的 Bigscreen 响应到 aggregate-only/pseudonymized key。
|
||||
- 人数低于租户配置的最小聚合阈值时,不返回组织健康统计明细,防止小样本反推个人。
|
||||
|
||||
---
|
||||
|
||||
## 8. 多终端呈现模型
|
||||
|
||||
### 8.1 终端矩阵
|
||||
|
||||
| 终端 | 主体 | 默认范围 | 主要能力 | 输出策略 |
|
||||
|---|---|---|---|---|
|
||||
| Admin | HUMAN | 角色绑定的 tenant/org/user | 配置、用户管理、健康工作台、告警处置、消息运营、设备管理 | 私有字段按 permission + purpose 控制 |
|
||||
| Bigscreen | VIEWER/HUMAN | 固定 tenant,可选受限 org | 汇总、实时态势、有限告警处置 | 默认 pseudonymized;aggregate-only 可无人值守 |
|
||||
| Phone | HUMAN | SELF | 个人健康、本人设备、本人消息/告警;管理者显式切换 managed scope | 不接受任意 user/customer 覆盖当前身份 |
|
||||
| Watch/手环 | DEVICE | 当前 binding subject | 上传、配置拉取、少量消息、回执 | 低带宽 DTO、短 token、无自由组织查询 |
|
||||
| 无人机/机器人 | DEVICE/SERVICE | tenant/org/mission | 遥测、任务、受控下行命令 | capability allowlist + 强审计 |
|
||||
| 第三方系统 | SERVICE | grant 中声明 | 事件推送、批量查询、Webhook | API scope、限流、签名、字段最小化 |
|
||||
|
||||
### 8.2 Surface Capability
|
||||
|
||||
每个终端先获取能力,再呈现 UI:
|
||||
|
||||
```json
|
||||
{
|
||||
"surface": "phone",
|
||||
"authzVersion": 42,
|
||||
"features": {
|
||||
"health.self.read": true,
|
||||
"health.org.read": false,
|
||||
"alert.self.ack": true,
|
||||
"alert.reassign": false,
|
||||
"device.bind.request": true
|
||||
},
|
||||
"privacyMode": "private",
|
||||
"limits": {"historyDays": 90, "pageSize": 50}
|
||||
}
|
||||
```
|
||||
|
||||
能力由租户授权、角色权限、组织 assignment、终端类型、设备能力和 license 共同计算。前端隐藏功能只改善体验,后端仍必须鉴权。
|
||||
|
||||
### 8.3 BFF 收敛
|
||||
|
||||
```text
|
||||
Domain Services
|
||||
HealthQueryService
|
||||
DeviceRegistryService
|
||||
AlertCenterService
|
||||
MessageCenterService
|
||||
|
|
||||
+-- AdminHealthWorkbenchController
|
||||
+-- BigscreenController/SSE
|
||||
+-- MobileApiController
|
||||
+-- DeviceIngressController
|
||||
```
|
||||
|
||||
收敛规则:
|
||||
|
||||
- Admin workbench 与 Health V2 共用 `AuthorizedSubjectScope`;
|
||||
- Bigscreen 的 `tenantId/orgId` 必须与 viewer/session scope 取交集;
|
||||
- Phone 默认强制当前 userId,只有拥有 manager capability 时才能传 managed userId;
|
||||
- Watch/手环由设备凭据和 binding 注入 customer/user/org,body 中同名字段一律覆盖或拒绝;
|
||||
- 所有端的告警动作调用同一状态机,消息响应调用同一响应服务。
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据库与本地 SQL 方案
|
||||
|
||||
### 9.1 P0 关系表修正
|
||||
|
||||
当前分支 `V3.1.51__authorization_relation_tenant_keys.sql` 与完全一致的本地直连 SQL 已为以下关系表补 `customer_id NOT NULL` 并回填:
|
||||
|
||||
- `sys_user_role`
|
||||
- `sys_role_menu`
|
||||
- `sys_role_permission`
|
||||
|
||||
这让 RLS、唯一约束、缓存失效、租户清理和查询索引使用同一租户键,不再依赖多层 join 才能证明归属。迁移先校验悬空/跨租户/重复 active edge,再回填并建立 partial unique index;`SECURITY DEFINER` 触发器以用户或角色事实重新推导关系归属,拒绝伪造 `customer_id` 和跨租户边。`sys_user_role/sys_user_org_role` 使用 A 类严格隔离,`sys_role_menu/sys_role_permission` 使用 B 类全局只读、租户可写策略。
|
||||
|
||||
Docker `ljwx_health` 已直接应用;`ljwx_health_test` 已验证迁移重复执行、数据保留回滚到原 RLS 基线后重放,以及受限角色的 tenant A / tenant B / 缺失上下文 / 越权写负向场景。生产必须通过 GitOps PreSync 直连 SQL 独立部署和核验,不能依赖运行时 Flyway。
|
||||
|
||||
同时:
|
||||
|
||||
- `sys_user_org` 增加独立 `is_primary`,迁移后停止用 `principal` 表示主组织;
|
||||
- 新建 `sys_org_manager`,或至少把 `principal` 明确定义为 manager 并修正唯一约束;
|
||||
- `sys_user_position` 保证现有 `customer_id` 正确写入,补 `org_id`、有效期和真实 tenant-aware 索引/唯一约束;
|
||||
- `t_device_heartbeat/t_device_message_department` 纳入一致的 RLS 和受限角色测试;
|
||||
- 资源关系表增加 tenant-aware foreign key/校验触发器或 service guard;
|
||||
- RLS policy 使用 `NULLIF(current_setting(...), '')` 和显式 elevation,所有策略由受限角色回归测试。
|
||||
|
||||
### 9.2 RLS 会话变量与连接池纪律
|
||||
|
||||
当前 `TenantAwareDataSource` 在连接借出时使用 `set_config('app.current_tenant', value, false)`,这是 **session-scoped**,并在代理 `Connection.close()` 时清空。若连接仍处于显式事务,close 先按连接池语义 rollback,再执行 reset 并 commit,使清理不会被 Hikari 随后的 rollback 撤销;借出注入、归还 reset 或物理 close 失败时先 JDBC `abort()` 物理连接,只有 abort 成功后才 close/release 逻辑池句柄,禁止把状态未知的 session 放回池中。重复 close 保持幂等。
|
||||
|
||||
- `SET LOCAL` 只用于已经进入显式事务的业务路径;不能在 Hikari checkout 的 auto-commit 连接上盲目替换为 `SET LOCAL`,否则语句结束后上下文立即失效;
|
||||
- 非事务或尚未完成事务化的路径继续使用 session-scoped set 时,连接归还前必须 reset;reset 失败的物理连接必须 evict/关闭,禁止只记录 warning 后回池;
|
||||
- tenant A、TARGET/ALL、tenant B 每次 checkout 都显式覆盖上下文,不能继承连接上一次的值;elevation 还必须独立校验受控角色和审计原因;
|
||||
- 异常、rollback、timeout、异步执行、嵌套事务和只读事务均进入 reset/evict 测试;不得把 tenant ThreadLocal 自动传播等同于数据库 session 已正确设置;
|
||||
- 用同一物理连接按 `tenant A -> exception -> tenant B`、`CURRENT -> ALL -> CURRENT` 顺序做回归,并以 `NOBYPASSRLS` 用户验证查询结果和 `current_setting`。
|
||||
|
||||
目标态优先让需要 RLS 的访问进入明确事务,在事务开始后执行 `SET LOCAL app.current_tenant = ...`;仍需 auto-commit 的少数入口保留严格的 set/reset-on-return 封装。两种模式都必须 fail closed:上下文缺失、非法或清理失败时不能继续执行下一租户请求。
|
||||
|
||||
### 9.3 本地执行约束
|
||||
|
||||
本仓库本地开发不依赖 Flyway:
|
||||
|
||||
1. 可复用、幂等 SQL 放到 `ljwx-boot/scripts/sql/`;
|
||||
2. 通过 `ljwx-boot/scripts/apply-local-sql.sh` 或 `docker exec ... psql` 应用;
|
||||
3. 每个结构变更提供 precheck、apply、verify、rollback/兼容说明;
|
||||
4. 验证 `flyway_schema_history` 不存在也不影响本地启动;
|
||||
5. 生产迁移与本地脚本保持同一 DDL 语义,但不要求本地伪造 Flyway 历史。
|
||||
|
||||
---
|
||||
|
||||
## 10. 分阶段落地计划
|
||||
|
||||
阶段以依赖和退出条件驱动,不以“写完代码”作为完成标准。
|
||||
|
||||
### Phase 0:安全语义止血与契约冻结(1–2 个 Sprint)
|
||||
|
||||
| 工作包 | 实施内容 | 退出条件 |
|
||||
|---|---|---|
|
||||
| IA-01 统一词汇与 ADR | 固化 primary org、org manager、position、role、menu、subject、actor、event org 语义 | 代码、DB、API 文档不再混用 principal/primary |
|
||||
| IA-02 AccessContext | 新增 resolver/filter,统一 effective tenant、user、surface、device、privacy、assignment、authzVersion tuple | Admin/Phone/Bigscreen/Watch 请求均可输出同一审计上下文;上下文中不存在可误用的扁平 dataScope |
|
||||
| IA-03 DataScope fail-closed | `CUSTOM` 空配置返回空集;按 action 逐 assignment 编译 scope;实现 `AuthorizedSubjectScope`;先接 Health V2、Admin workbench、Mobile health、Bigscreen | 同租户跨组织/跨用户负向测试全部 403/404;SELF + 多组织角色组合结果符合 §5.3 |
|
||||
| IA-04 组织角色接线 | 登录加载 `sys_user_role + sys_user_org_role` assignments;permission hint 可并集,资源访问必须由同一 assignment 同时授予 action 和 scope | 组织角色在绑定组织有效,在兄弟组织无效;不能跨 assignment 拼接 permission/scope |
|
||||
| IA-05 菜单/权限收敛 | 路由装配应用 tenant override;增加 menu-permission 一致性校验;缓存版本化 | 隐藏/别名/排序生效,API 权限不依赖菜单 |
|
||||
| IA-06 RLS 真实验证 | 修正关系表 tenant key/policy;落实事务 `SET LOCAL` 或 session set/reset/evict 纪律;用 `NOBYPASSRLS` 角色跑 CURRENT/TARGET/ALL 和连接复用测试 | 超级用户与受限角色业务结果一致;tenant A 的物理连接在异常/ALL 后不能污染 tenant B;越权查询被拒绝 |
|
||||
| IA-07 Device Auth P0 | 以 `t_device_bind_approval` 为 provisioning 授权锚点,bootstrap possession challenge 后换取短期 token;签名含 method/path/body hash/timestamp/nonce | 审批不能替代设备身份证明;只持有 SN 无法上传、拉配置或收消息;bootstrap 和业务请求重放均被拒绝 |
|
||||
| IA-08 负向契约矩阵 | tenant×org×user×surface×action 参数化测试 | CI 强制覆盖横向/纵向越权与伪造维度 |
|
||||
| IA-09 授权版本服务 | 建立 platform/tenant/user version 事实表和唯一递增服务;同事务写 authorization outbox,多实例清缓存并对高风险收权撤销 Session/token | 任一实例修改授权后其他实例立即拒绝旧高风险凭据;普通缓存失效可由版本 + TTL + DB 回源证明 |
|
||||
|
||||
Phase 0 兼容策略:旧接口保留一个灰度周期,记录客户端传入维度与服务端解析维度的差异;先告警再拒绝,设备认证例外应通过明确的临时 allowlist,而不是永久信任 SN。
|
||||
|
||||
### Phase 1:关系与资源归属收敛(2–3 个 Sprint)
|
||||
|
||||
| 工作包 | 实施内容 | 退出条件 |
|
||||
|---|---|---|
|
||||
| IA-11 人员组织模型 | 拆分 `is_primary` 与 org manager;实现有效期、调动和状态端点;发布 assignment changed event | 调动单事务完成,权限/缓存/设备/告警影响可审计 |
|
||||
| IA-12 岗位模型 | 岗位绑定组织和有效期;风险等级/权重进入统一策略服务 | 岗位变化不直接改变权限,健康规则能按事件时岗位取值 |
|
||||
| IA-13 设备绑定事实源 | `t_device_user` 变为时间化 binding;`sys_user.device_sn/t_device_info.user_id` 降级为投影 | 同设备不能同时绑定两人,用户支持多设备,历史可追溯 |
|
||||
| IA-14 资源信封 | 健康、告警、消息补齐 actor/subject/source/event-org 语义和字段 | 任一告警可追溯到原始事实和设备;消息可追溯到告警/发布人 |
|
||||
| IA-15 查询索引 | 对候选索引做真实数据 EXPLAIN;只上线证明有效的组合/部分索引 | 核心组织/用户查询 P95 达标且写放大可接受 |
|
||||
| IA-16 模型去重 | 选定每张核心表的 canonical Entity/Mapper,逐步删除重复模型和双列映射 | 同一物理列只由一个 canonical 属性映射 |
|
||||
| IA-17 租户 Blueprint | 模板版本、差异预览、冲突处理、幂等同步和回滚 | 平台模板升级不覆盖租户自定义,新增权限可按 entitlement 灰度 |
|
||||
|
||||
### Phase 2:统一查询服务与可重建投影(2–4 个 Sprint)
|
||||
|
||||
| 工作包 | 实施内容 | 退出条件 |
|
||||
|---|---|---|
|
||||
| IA-21 SubjectScopeQueryService | 统一 current/event scope、org subtree、user set、keyset pagination | Controller 不再自行拼 customer/user/org 条件 |
|
||||
| IA-22 领域查询 API | 收敛 Health/Device/Alert/Message 查询 DTO 和错误语义 | 四端同一资源得到一致权限与状态 |
|
||||
| IA-23 组织/用户读模型 | 复用同一 outbox 事件;建设 health current、org daily、device current、alert workload、message inbox;Kafka 达到 shadow 晋级门禁后再承载跨进程 durable consumer | 每个投影有 source event id、幂等、rebuild、diff、lag、PG fallback;Spring/Kafka 不产生平行事实 |
|
||||
| IA-24 Bigscreen 收敛 | 只改 realtime/snapshot/rebuildCache/examSummary/interventionSummary 等 query-first 端点,使 tenant/org 与 viewer scope 取交集;保留 `getAssignees/urgeExam/getAlertDetail` 的 `effectiveTenantSafe()` 参考实现;快照与 SSE cursor 对账 | 所有未收敛端点的 query tenantId 不能扩权,已收敛端点无重复改造,隐私模式不串缓存 |
|
||||
| IA-25 可观测性 | 授权拒绝原因、scope 大小、查询延迟、缓存命中、投影 lag | 能区分认证失败、租户拒绝、数据范围拒绝和隐私裁剪 |
|
||||
|
||||
### Phase 3:多终端能力平台(2–3 个 Sprint)
|
||||
|
||||
| 工作包 | 实施内容 | 退出条件 |
|
||||
|---|---|---|
|
||||
| IA-31 Capability API | surface/tenant/role/device capability 合并,版本化缓存 | Admin/Phone/Bigscreen 可基于同一能力事实呈现 |
|
||||
| IA-32 Viewer Principal | 大屏短期 token,固定 tenant/org/privacy/action | 无人值守大屏无普通管理员长期 token |
|
||||
| IA-33 Device Principal | 手表/手环设备凭据轮换、撤销、离线队列和命令回执 | 设备遗失后可单独撤销,不影响用户账号 |
|
||||
| IA-34 Edge Principal | 机器人/无人机/网关 capability、mission、command 权限 | 遥测与控制分权,所有命令可审计和幂等 |
|
||||
| IA-35 第三方授权 | service account、API scope、Webhook 签名、字段最小化 | 外部系统不能复用平台管理员 token |
|
||||
|
||||
### Phase 4:隐私治理与规模化
|
||||
|
||||
- 用途/同意/字段级策略和紧急 break-glass;
|
||||
- 小样本聚合保护、导出水印、敏感字段访问审计;
|
||||
- 租户模板版本、差异预览、灰度同步和回滚;
|
||||
- 策略模拟器:给定 actor/action/resource 输出允许/拒绝及每层证据;
|
||||
- 大租户按时间分区、冷热分层、归档和投影容量治理。
|
||||
|
||||
---
|
||||
|
||||
## 11. 测试与验收矩阵
|
||||
|
||||
### 11.1 必测主体
|
||||
|
||||
- 平台超管 CURRENT/TARGET/ALL;
|
||||
- 租户管理员;
|
||||
- 部门负责人;
|
||||
- 组织角色用户;
|
||||
- SELF 普通用户;
|
||||
- CUSTOM scope 用户;
|
||||
- Bigscreen viewer;
|
||||
- 已绑定/解绑/伪造/撤销设备;
|
||||
- 内部 service 与第三方 service account。
|
||||
|
||||
### 11.2 必测资源
|
||||
|
||||
- 用户列表与详情;
|
||||
- 菜单、permission、岗位和角色分配;
|
||||
- 健康明细、时序、分析、组织聚合;
|
||||
- 设备列表、绑定、心跳和同步状态;
|
||||
- 告警列表、详情、确认、关闭、转派;
|
||||
- 消息发布、收件箱、已读/确认、组织受众统计;
|
||||
- Bigscreen snapshot/realtime/SSE/action。
|
||||
|
||||
### 11.3 四类断言
|
||||
|
||||
1. **正向**:合法主体在合法范围内可读写。
|
||||
2. **横向越权**:同租户兄弟组织、其他用户、其他设备、其他收件人均拒绝。
|
||||
3. **纵向越权**:普通用户不能分配高权限角色、切换租户、执行大屏/管理员动作。
|
||||
4. **参数伪造**:body/query/header 的 customer/user/org/device 与服务端上下文冲突时拒绝并审计。
|
||||
|
||||
数据库测试必须同时运行:
|
||||
|
||||
- 超级用户,仅验证功能;
|
||||
- `NOBYPASSRLS` 应用角色,验证真实 RLS;
|
||||
- elevation/ALL 的受控事务角色,验证跨租户运维路径。
|
||||
|
||||
---
|
||||
|
||||
## 12. 性能与 SLO 建议
|
||||
|
||||
| 场景 | 目标 |
|
||||
|---|---|
|
||||
| 登录授权快照 | P95 < 200 ms,缓存命中 < 30 ms |
|
||||
| Admin 组织树/用户筛选 | P95 < 300 ms |
|
||||
| 用户健康时序 7 天 | P95 < 300 ms |
|
||||
| 组织聚合 30 天 | 投影命中 P95 < 500 ms,PG fallback < 2 s |
|
||||
| Bigscreen realtime | 新事件可见 < 5 s |
|
||||
| Bigscreen snapshot | 缓存命中 < 200 ms,PG 重建 < 3 s |
|
||||
| 告警工作队列 | P95 < 300 ms |
|
||||
| Phone 收件箱 | P95 < 300 ms |
|
||||
| 授权策略判定 | 缓存命中 P95 < 20 ms |
|
||||
|
||||
这些是首版容量目标,最终以生产数据量、租户规模和隐私策略评审后固化。
|
||||
|
||||
---
|
||||
|
||||
## 13. 实施顺序与依赖
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[IA-01 语义 ADR] --> B[IA-02 AccessContext]
|
||||
B --> C[IA-03 Scope fail-closed]
|
||||
B --> D[IA-04 组织角色]
|
||||
D --> E[IA-05 菜单/权限]
|
||||
B --> V[IA-09 授权版本]
|
||||
D --> V
|
||||
E --> V
|
||||
C --> F[IA-08 负向矩阵]
|
||||
D --> F
|
||||
E --> F
|
||||
V --> F
|
||||
B --> G[IA-07 Device Auth]
|
||||
C --> H[IA-11 人员组织关系]
|
||||
H --> I[IA-13 设备绑定]
|
||||
H --> J[IA-14 资源信封]
|
||||
I --> J
|
||||
J --> K[IA-21 统一查询]
|
||||
K --> L[IA-23 读模型]
|
||||
L --> M[IA-31 多终端能力]
|
||||
```
|
||||
|
||||
最小可交付切片不是“把所有表重构完”,而是:
|
||||
|
||||
```text
|
||||
AccessContext
|
||||
+ fail-closed DataScope
|
||||
+ 组织角色接入登录
|
||||
+ platform/tenant/user 授权版本与高风险收权
|
||||
+ Health Workbench/Mobile/Bigscreen 三个高风险查询接线
|
||||
+ 受限 RLS 与同租户越权负向测试
|
||||
```
|
||||
|
||||
完成该切片后,后续设备绑定、资源信封、读模型和新终端才能在稳定授权边界上迭代。
|
||||
|
||||
---
|
||||
|
||||
## 14. 不在本轮做的事情
|
||||
|
||||
- 不在架构第一阶段引入独立 IAM 微服务;先在现有 Spring 模块内建立清晰接口与边界。
|
||||
- 不把所有历史表一次性重命名为新字段;先统一逻辑语义和访问层,再渐进迁移物理模型。
|
||||
- 不把 Redis 变成授权或健康事实源。
|
||||
- 不让前端菜单替代后端权限。
|
||||
- 不让岗位自动拥有管理员权限。
|
||||
- 不为了支持未来设备而把 `user_id` 设为所有设备资源的必填字段。
|
||||
- 不在本地创建或修复 `flyway_schema_history`。
|
||||
|
||||
---
|
||||
|
||||
## 15. 代码审计锚点
|
||||
|
||||
- 租户/用户上下文:`GlobalRequestInterceptor`、`UserContext`、`TenantContext`
|
||||
- 登录角色与权限:`SysUserServiceImpl.saveUserToSession()`、`SysRoleMapper.xml`、`SysPermissionInterfaceImpl`
|
||||
- 菜单路由:`AuthenticationFacadeImpl.queryUserRouteWithUserId()`、`SysMenuServiceImpl`
|
||||
- 组织角色:`UserOrgRoleService`、`SysUserOrgRole`
|
||||
- 数据范围:`DataScopeInterceptor`、`ScopeQueryFactory`
|
||||
- 统一授权上下文:`AccessContext`、`GrantAssignment`、`AuthorizedSubjectScope`、`AccessContextHolder`、`RequestAccessContextResolver`
|
||||
- Assignment 授权:`DefaultGrantAssignmentLoader`、`AssignmentResourceAuthorizer`、`PermissionSubjectScopeResolver`
|
||||
- 管理边界:`AuthorizationChain`、`AdminBoundaryGuard`、`OrgScopeGuard`、`RoleDelegateGuard`
|
||||
- 组织关系:`SysUserOrgServiceImpl`、`PrincipalBasedOrgManagerResolver`
|
||||
- 健康工作台:`AdminHealthWorkbenchController`、`HealthWorkbenchServiceImpl`
|
||||
- 多终端:`BigscreenController`、`MobileApiController`、`WatchApiController`
|
||||
- 业务资源:`TUserHealthData`、`TDeviceInfo`、`TDeviceUser`、`TDeviceHeartbeat`、`TAlertInfo`、`TDeviceMessage`、`TDeviceMessageDetail`
|
||||
- RLS:`R__rls_policies.sql`
|
||||
|
||||
---
|
||||
|
||||
## 16. 实施跟踪与版本记录
|
||||
|
||||
IA 编号是稳定的工作包标识,父跟踪项为 [GitHub Epic #383](https://github.com/lingjingwanxiang/ljwx-health/issues/383)。后续按 IA 工作包创建或复用 issue;issue 标题保留 IA 编号,退出条件原样进入 acceptance criteria,依赖关系按 §13 维护。创建前必须基于实时 issue/PR 状态做去重,不能仅凭文档预分配可能冲突的 issue number。
|
||||
|
||||
### 16.1 实施台账
|
||||
|
||||
| 工作包 | 状态 | 已落地 | 尚未完成 |
|
||||
|---|---|---|---|
|
||||
| IA-01 | DONE | ADR 0003 已固化 principal/actor/operator/subject、effective tenant、primary org/org manager、position/role/menu、assignment 和 event org;明确旧 `principal/primary/org_id` 字段仅为兼容映射并分配迁移归属 | 旧字段和资源信封迁移由 IA-11/IA-14/IA-16 继续交付,不再阻塞术语决策 |
|
||||
| IA-02 | DONE | `AccessContext/GrantAssignment/AuthzVersion` 不可变契约;请求入口解析 HUMAN、platformAdmin、CURRENT/TARGET/ALL、server-bound surface、privacy、trace;每个已认证 HUMAN 请求在进入 Controller 前强制装载 assignments 与真实 platform/tenant/user authzVersion,缺失 resolver 或解析异常会清除 Access/User/Tenant 上下文并 fail closed;平台 TARGET 通过受审计 RLS elevation 形成快照,ALL 使用平台 assignment 与平台租户版本;ThreadLocal 在请求前置与 completion 双向清理 | DEVICE/VIEWER/SERVICE principal 的凭据解析分别由 IA-07/IA-32/IA-35 扩展同一契约,不重新定义上下文 |
|
||||
| IA-03 | IN_PROGRESS | `CUSTOM`、空组织列表、缺失 SELF user 改为 fail closed;`AuthorizedSubjectScope` 按 permission 合并完整 assignments;Admin Health Workbench context/records/series/analysis/actions 已裁剪可见组织树、用户选项并在领域查询前校验 resource scope;Mobile `/mobile/health/data` 从 AccessContext 固定 operator/effective tenant,健康记录详情和告警详情/AI/处理按已落库 page/update 权限对 effective tenant 内当前 user 或 persisted assignee 做资源授权,仅无 user 时回退事件 org,角色名白名单已移除,`COMMON` 模板及租户克隆从 TENANT_ALL 更正为 SELF;Mobile 聚合状态、关键指标、告警计数/摘要和 AI 建议按 action permission 固定当前 tenant/user,AI 快照显式携带 tenant,缓存键纳入 scope/privacy/authz version,建议反馈新增独立 add permission 并写入服务端解析的 tenant/user,持久化失败不再伪报成功;Bigscreen realtime/snapshot/cache rebuild/exam/intervention/SSE 租户聚合与 Admin alert action/detail/assignees 从 permission-specific scope 固定 effective tenant,非 TENANT_ALL assignment 与 query tenant 扩大均返回 HTTP 403;SSE 复用已落库的 `bigscreen:realtime:read`,不再引用不存在的 subscribe permission;单组织 Viewer create 固定有效租户内组织,snapshot/realtime/SSE replay/live 强制 token org,健康、告警、设备、消息、体检、干预与空间聚合按 org 裁剪,事件缺少可信 org 时 fail closed,设备事件可从当前用户/设备关系解析可信 org;多组织 Viewer 暂时 fail closed;Viewer alert detail 在 Facade 前按持久化告警 tenant/org 校验 token org scope;Viewer create/bootstrap/revoke/revoke-tenant 管理操作按 `bigscreen:viewer:manage` 编译 tenant-wide assignment scope;Health V2 score trend/distribution/ranking 在进入 Facade 前按 endpoint permission 校验 user/org/tenant 聚合范围并覆盖 effective tenant;Health Profile V2 comprehensive/daily/behavior/risk 取消角色白名单,改由 `t:user:health:data:page` 的同 assignment subject scope 校验目标用户;Alert V2 list 固定服务端 tenant/user/org 查询维度,detail/handle/assign/status 在读写前将 endpoint permission 与持久化告警的当前 user/org 关系求交,批量更新先授权全部资源;Device Message 读取统一使用 `t:device:message:page` 编译服务端 tenant/user/org/resource scope;新增、修改、删除、分发、确认、状态、重试、清理与缓存操作复用已落库的 add/update/delete permission,写前完成资源和目标全量授权,opaque device/target 与租户级操作只允许 tenant-wide assignment,Service 写路径显式携带服务端租户 | 多组织 Viewer 的聚合/事件并集语义、其余 Health/Device 入口的组织/用户/资源范围及真正的组织级聚合仍待逐端接线 |
|
||||
| IA-04 | DONE | `sys_user_role + sys_user_org_role` 分别装载为带 source/boundary/anchor 的 assignment;`sys_user_role.customer_id` 已进入 canonical Entity/Mapper,显式绕过 tenantLine 后严格校验关系租户、角色租户、组织锚点和 dataScope;平台 TARGET 读取使用事务内受审计 RLS elevation;仅兼容显式 `is_template=true` 的平台模板直绑;资源访问由同一 assignment 同时授予 action 与 scope;组织角色的 TENANT_ALL 仍受绑定组织子树约束;登录菜单与所有已认证 HUMAN 请求消费同一 resolved assignment snapshot | 存量平台模板直绑迁移为租户角色副本归 IA-05;其余高风险业务资源逐端接线归 IA-03/IA-08 |
|
||||
| IA-05 | IN_PROGRESS | Admin 路由按 IA-04 resolved assignments 独立求 role-menu 与 role-permission 交集,避免跨 assignment 拼接;tenant override 的隐藏、别名、排序在复制后应用;Redis 使用 tenant/user/surface + platform/tenant/user authz + navigation + blueprint version 的不可变键;菜单、permission 及角色关系写路径事务内推进版本并写 durable outbox;Docker dev/test 已验证幂等租户模板副本迁移、严格 tenant trigger、结构门禁与数据保留 rollback contract | 先将租户模板直绑迁移及 verify/rollback 门禁部署到 GitOps 生产,再合并并验证严格 assignment loader 与版本化导航镜像;生产闭环前不标记 DONE |
|
||||
| IA-06 | DONE | session-scoped RLS tenant context 在每次 checkout 显式覆盖;显式事务 close 先 rollback,再 reset+commit;失败时 abort 物理连接并安全释放池句柄;Docker `ljwx_health_test` 的真实 `NOSUPERUSER/NOBYPASSRLS` 角色与单物理连接覆盖 CURRENT/TARGET/ALL、tenant A→ALL→tenant B、rollback、timeout/aborted transaction、嵌套 savepoint、异步缺失上下文 fail closed;关系表 tenant key、唯一约束、边一致性 trigger、A/B policy 已完成 Docker 重复迁移、数据保留回滚、GitOps PreSync、生产受限角色验证和 workload 健康核验 | 后续新授权关系表继续复用同一 tenant key/RLS/连接池测试规范;不再阻塞 IA-06 退出条件 |
|
||||
| IA-07 | IN_PROGRESS | challenge 使用按设备 bootstrap proof 并单次消费;新增 15 分钟短 token、method/path/body hash/timestamp/nonce HMAC、Redis replay key、解绑撤销与 disabled/monitor/enforce 灰度模式;Redis 仅保存 token hash | 生产 KMS/Vault 按设备凭证、Phone/Watch 安全 provisioning、强制绑定签名及 monitor 到 enforce 的实证切换尚未完成 |
|
||||
|
||||
台账状态按退出条件判断,不能因类或接口已经创建就标记 DONE。2026-07-22 当前 changed slices 覆盖 AccessContext/请求解析、DataScope fail-closed、assignment loader/evaluator、Workbench/Mobile/Bigscreen/Health V2 资源 guard、assignment-aware versioned navigation 和短期设备认证;验证结果以 Epic #383 的进展记录为准。
|
||||
|
||||
### 16.2 版本记录
|
||||
|
||||
| 版本 | 日期 | 状态 | 变更 |
|
||||
|---|---|---|---|
|
||||
| v1.0 | 2026-07-10 | Proposed | 初版总体架构、事实核查和 IA-01~IA-35 落地计划 |
|
||||
| v1.1 | 2026-07-10 | Accepted | 标注组织管理现状与 Bigscreen 已收敛端点;补齐 assignment 合并语义、RLS 连接池纪律、Outbox/Kafka 单事件源、授权版本属主、设备 provisioning 和实施跟踪规则 |
|
||||
| v1.2 | 2026-07-10 | Accepted | 关联 Epic #383;记录 IA-04 assignment 装载、同 assignment 授权判定、permission-specific subject scope 与 Health Workbench 全入口接线 |
|
||||
| v1.3 | 2026-07-19 | Accepted | 记录 IA-05 assignment-aware tenant navigation 与 IA-07 短期设备请求认证的阶段交付和剩余生产门禁 |
|
||||
| v1.4 | 2026-07-19 | Accepted | 记录 IA-03 Mobile health subject scope 首段接线:服务端主体定界、兼容 hint 匹配与 HTTP 403 越权拒绝 |
|
||||
| v1.5 | 2026-07-19 | Accepted | 记录 IA-03 Bigscreen 租户聚合首段接线:permission-specific tenant-wide scope、effective tenant 改写与 query tenant 扩大拒绝 |
|
||||
| v1.6 | 2026-07-19 | Accepted | 记录 IA-03 Health V2 score 端点接线:permission-specific user/org/tenant 范围、effective tenant 改写与冲突维度拒绝 |
|
||||
| v1.7 | 2026-07-20 | Accepted | 记录 IA-03 Health Profile V2 画像读接线:移除角色白名单,以现有健康数据读取 permission 编译 user/org/tenant subject scope,并统一 HTTP 403 越权语义 |
|
||||
| v1.8 | 2026-07-20 | Accepted | 记录 IA-03 Bigscreen SSE 接线:复用 realtime read permission 编译 tenant-wide subject scope,以服务端 effective tenant 打开 replay/live channel,并拒绝跨租户 query hint |
|
||||
| v1.9 | 2026-07-20 | Accepted | 记录 IA-03 Bigscreen Admin 告警入口接线:action、detail、assignees 在调用 Facade 前按各自 permission 要求 tenant-wide subject scope |
|
||||
| v1.10 | 2026-07-20 | Accepted | 记录 IA-03 Bigscreen Viewer 告警详情资源校验:token org scope 与告警持久化 tenant/org 在 Facade 前求交,未知或越界资源统一拒绝 |
|
||||
| v1.11 | 2026-07-20 | Accepted | 记录 IA-03 Bigscreen Viewer 管理面接线:create/bootstrap/revoke/revoke-tenant 按 `bigscreen:viewer:manage` 编译 tenant-wide assignment scope,非租户级授权在进入 session service 前拒绝 |
|
||||
| v1.12 | 2026-07-20 | Accepted | 记录 IA-03 Alert V2 接线:list 固定服务端 tenant/user/org 维度,detail/handle/assign/status 在 Facade 或写服务前校验持久化告警与当前 user/org subject scope,批量更新先授权全部资源 |
|
||||
| v1.13 | 2026-07-20 | Accepted | 记录 IA-03 Device Message 读取接线:page/export 固定服务端 tenant/user/org 维度,detail/summary 校验持久化消息当前 user/org 关系,opaque device/target 与租户聚合 fail closed 到 tenant-wide assignment |
|
||||
| v1.14 | 2026-07-20 | Accepted | 记录 IA-03 Device Message 变更面接线:add/update/delete/distribution/status/cleanup/cache 复用持久化权限,资源与目标写前全量授权,opaque target 要求 tenant-wide scope,Service 写路径显式绑定服务端租户 |
|
||||
| v1.15 | 2026-07-20 | Accepted | 记录 IA-03 Mobile 资源入口接线:健康记录详情及告警详情/AI/处理按持久化 page/update 权限求交 effective tenant 内当前 user 或 assignee,无 user 时回退事件 org;移除角色名白名单,并把普通员工 COMMON assignment 从 TENANT_ALL 更正为 SELF |
|
||||
| v1.16 | 2026-07-20 | Accepted | 记录 IA-03 Mobile 聚合状态与反馈接线:健康/告警聚合及 AI 快照固定当前 tenant/user,缓存隔离 scope/privacy/authz version,反馈独立授权、租户持久化与失败透传 |
|
||||
| v1.17 | 2026-07-20 | Accepted | 记录 IA-03 单组织 Bigscreen Viewer 聚合接线:token org 强制覆盖 snapshot/realtime/SSE,健康、告警、设备、消息、体检、干预与空间按组织裁剪,未知事件组织 fail closed,多组织 token 暂不放行 |
|
||||
| v1.18 | 2026-07-20 | Accepted | 记录 IA-06 RLS 连接池纪律:session tenant 注入、归还 reset、失败 abort、不回池与 CURRENT/TARGET/ALL 复用负向测试;真实 NOBYPASSRLS 角色下的异常事务复用仍待完成 |
|
||||
| v1.19 | 2026-07-20 | Accepted | 记录 IA-06 真实连接池验证:修复事务内 reset 被回滚与 abort 后 Hikari 句柄未释放;NOBYPASSRLS 单物理连接覆盖 rollback、timeout、嵌套 savepoint、异步及 CURRENT/TARGET/ALL,剩余关系表 policy 核验与环境账号切换 |
|
||||
| v1.20 | 2026-07-20 | Accepted | 记录 IA-06 授权关系表租户键与 policy:三张关系表回填 `customer_id` 和 active-row 唯一索引,四张关系表增加租户一致性触发器,Docker dev/test 完成幂等迁移、数据保留回滚重放及 NOBYPASSRLS 正负向验证,生产 GitOps rollout 待完成 |
|
||||
| v1.21 | 2026-07-22 | Accepted | 以 ADR 0003 完成 IA-01 术语冻结:区分 principal/actor/operator/subject、主组织/组织负责人、岗位/角色/菜单、当前组织/事件时组织,明确旧字段兼容边界和 IA-11/14/16 迁移归属 |
|
||||
| v1.22 | 2026-07-22 | Accepted | IA-02/IA-04 收口:每个认证 HUMAN 请求在 Controller 前生成完整 assignment/version 快照,平台 TARGET 读取使用受审计 RLS elevation,并补齐 `sys_user_role.customer_id` canonical 映射与跨租户关系拒绝 |
|
||||
| v1.23 | 2026-07-22 | Accepted | IA-05 上线前收口:同 assignment 的 menu-permission 投影、版本化 Redis navigation cache、事务版本/outbox 失效、租户模板直绑幂等迁移、结构门禁与 data-preserving rollback contract;同时回写 IA-06 生产完成状态 |
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"artifact_id": "lingjingwanxiang__ljwx-health-71c49969d748",
|
||||
"documents": [
|
||||
{
|
||||
"approved_by": null,
|
||||
"audience": [
|
||||
"claude",
|
||||
"codex"
|
||||
],
|
||||
"checksum": "7e9060bdcd571edea19ed35ef77fb873fca0e6a61b438624b5864306eb894a4d",
|
||||
"content_path": "documents/ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md.md",
|
||||
"doc_type": "design_spec",
|
||||
"document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md",
|
||||
"effective_from": null,
|
||||
"excluded_from_retrieval": false,
|
||||
"expires_at": null,
|
||||
"op": "upsert",
|
||||
"owner": "team-health",
|
||||
"product_area": "ljwx-health",
|
||||
"reason": null,
|
||||
"review_status": "approved",
|
||||
"source_id": "ljwx-health-architecture-ssot",
|
||||
"source_path": "docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md",
|
||||
"source_ref": "lingjingwanxiang/ljwx-health@71c49969d7482b53b923fc5ddc529a9178cda27c:docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md",
|
||||
"source_type": "repo_doc",
|
||||
"tags": [
|
||||
"ljwx-health",
|
||||
"architecture",
|
||||
"ssot"
|
||||
],
|
||||
"tier": "ssot",
|
||||
"title": "多租户身份、权限、健康资源与多终端总体架构",
|
||||
"version": "71c49969d7482b53b923fc5ddc529a9178cda27c",
|
||||
"visibility": "internal"
|
||||
}
|
||||
],
|
||||
"export_mode": "changed",
|
||||
"generated_at": "2026-07-22T02:36:10.727703+00:00",
|
||||
"schema_version": "0.1.0",
|
||||
"source": {
|
||||
"commit": "71c49969d7482b53b923fc5ddc529a9178cda27c",
|
||||
"ref": "refs/heads/main",
|
||||
"repo": "lingjingwanxiang/ljwx-health",
|
||||
"run_attempt": 1,
|
||||
"run_id": 29886230228,
|
||||
"service": "ljwx-health",
|
||||
"workflow": "knowledge-export"
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"acl_expression": "visibility == \"internal\"", "approved_by": null, "audience": ["claude", "codex"], "checksum": "7e9060bdcd571edea19ed35ef77fb873fca0e6a61b438624b5864306eb894a4d", "customer_id": null, "doc_type": "design_spec", "document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md", "effective_from": null, "excluded_from_retrieval": false, "expires_at": null, "owner": "team-health", "product_area": "ljwx-health", "review_status": "approved", "source_ref": "lingjingwanxiang/ljwx-health@71c49969d7482b53b923fc5ddc529a9178cda27c:docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md", "source_type": "repo_doc", "tags": ["ljwx-health", "architecture", "ssot"], "tier": "ssot", "title": "多租户身份、权限、健康资源与多终端总体架构", "version": "71c49969d7482b53b923fc5ddc529a9178cda27c", "visibility": "internal"}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"action": "publish",
|
||||
"artifact_id": "lingjingwanxiang__ljwx-health-71c49969d748",
|
||||
"document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md",
|
||||
"event_id": "knowledge-publish-ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md-20260722T023627.966285+0000",
|
||||
"reason": "artifact_ingest",
|
||||
"source_repo": "lingjingwanxiang/ljwx-health",
|
||||
"status": "published",
|
||||
"target_ids": [
|
||||
"dify-internal"
|
||||
],
|
||||
"timestamp": "2026-07-22T02:36:27.966303+00:00"
|
||||
}
|
||||
+11
-11
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"acl_expression": "visibility == \"internal\"",
|
||||
"artifact_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-1f01e08a3018/manifest.json",
|
||||
"artifact_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-71c49969d748/manifest.json",
|
||||
"audience": [
|
||||
"claude",
|
||||
"codex"
|
||||
],
|
||||
"checksum": "a378f898875e01fc6045ebdd9506b56f812eb12391e4a910a5f0e7a596b6d1a7",
|
||||
"checksum": "7e9060bdcd571edea19ed35ef77fb873fca0e6a61b438624b5864306eb894a4d",
|
||||
"chunk_count": 37,
|
||||
"chunk_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-1f01e08a3018/chunks.jsonl",
|
||||
"classified_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-1f01e08a3018/classified-documents.jsonl",
|
||||
"content_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-1f01e08a3018/documents/ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md.md",
|
||||
"chunk_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-71c49969d748/chunks.jsonl",
|
||||
"classified_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-71c49969d748/classified-documents.jsonl",
|
||||
"content_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-71c49969d748/documents/ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md.md",
|
||||
"doc_type": "design_spec",
|
||||
"document_id": "ljwx-health-docs-02-architecture-tenant-identity-access-resource-terminal-architecture-md",
|
||||
"duplicate_of": null,
|
||||
"excluded_from_retrieval": false,
|
||||
"latest_action": "publish",
|
||||
"latest_reason": "artifact_ingest",
|
||||
"normalized_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-1f01e08a3018/normalized-documents.jsonl",
|
||||
"normalized_ref": "knowledge/artifacts/dev/lingjingwanxiang__ljwx-health/lingjingwanxiang__ljwx-health-71c49969d748/normalized-documents.jsonl",
|
||||
"owner": "team-health",
|
||||
"product_area": "ljwx-health",
|
||||
"published_at": "2026-07-22T01:25:33.450139+00:00",
|
||||
"published_at": "2026-07-22T02:36:26.462892+00:00",
|
||||
"review_status": "approved",
|
||||
"schema_version": "0.1.0",
|
||||
"source_id": "ljwx-health-architecture-ssot",
|
||||
"source_path": "docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md",
|
||||
"source_ref": "lingjingwanxiang/ljwx-health@1f01e08a30189ecaf4e0c96871d97b2277971eb9:docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md",
|
||||
"source_ref": "lingjingwanxiang/ljwx-health@71c49969d7482b53b923fc5ddc529a9178cda27c:docs/02-architecture/tenant-identity-access-resource-terminal-architecture.md",
|
||||
"source_repo": "lingjingwanxiang/ljwx-health",
|
||||
"source_type": "repo_doc",
|
||||
"status": "published",
|
||||
@@ -42,12 +42,12 @@
|
||||
"status": "published",
|
||||
"target_id": "dify-internal",
|
||||
"target_type": "dify_dataset",
|
||||
"updated_at": "2026-07-22T01:25:33.450195+00:00"
|
||||
"updated_at": "2026-07-22T02:36:26.462934+00:00"
|
||||
}
|
||||
],
|
||||
"tier": "ssot",
|
||||
"title": "多租户身份、权限、健康资源与多终端总体架构",
|
||||
"updated_at": "2026-07-22T01:25:33.450164+00:00",
|
||||
"version": "1f01e08a30189ecaf4e0c96871d97b2277971eb9",
|
||||
"updated_at": "2026-07-22T02:36:26.462913+00:00",
|
||||
"version": "71c49969d7482b53b923fc5ddc529a9178cda27c",
|
||||
"visibility": "internal"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user