Metadata-Version: 2.4
Name: uc-integration
Version: 0.1.33
Summary: GSS 用户中心 SSO / 主数据同步 Fred 模块（子系统可 pip 安装）
Author: GSS
License: Proprietary
Project-URL: Homepage, https://github.com/renzhong/gss
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: flask>=2.0
Requires-Dist: flask-smorest>=0.40
Requires-Dist: flask-jwt-extended>=4.0
Requires-Dist: flask-babelplus>=2.0
Requires-Dist: marshmallow>=3.0
Requires-Dist: sqlalchemy>=1.4
Requires-Dist: pip==26.1.2
Requires-Dist: pyyaml>=6.0
Provides-Extra: fred
Requires-Dist: fred-admin; extra == "fred"
Dynamic: requires-python

# GSS UC Integration — 用户中心 SSO / 在线主数据查询（Fred 子系统共享包）

将各子系统里同构的 `modules/uc_integration` 抽成可安装包，避免复制粘贴分叉。

## 能力

| 接口 | 说明 |
|------|------|
| `GET  /{subsystem}/uc_integration/auth/sso/config` | 登录模式 |
| `POST /{subsystem}/uc_integration/auth/sso/exchange` | UC 一次性 code → 本地 JWT（不接受 accessToken） |
| `POST /{subsystem}/uc_integration/auth/sso/refresh` | UC refreshToken 续期 |
| `POST /{subsystem}/uc_integration/auth/sso/admin/exchange` | 管理端：code → 按 username 同步本地 admin → JWT |
| `POST /{subsystem}/uc_integration/auth/sso/admin/refresh` | 管理端续期 |
| `POST /{subsystem}/uc_integration/internal/uc-sync` | UC 主数据推送（仅本机）**（已停用）**：UC 不再推送，端点仍挂载但不会被调用，子系统无需实现/验证 |
| `GET  /{subsystem}/uc_integration/internal/uc-sync` | 接收端存活探测（不查本地表）**（已停用）**：UC 不再调用，子系统无需联调 |

### 服务层（无子系统 HTTP）：按用户 id 查组织 + 门店

只读走 UC Open API `/open_api/internal/*`，依赖 `USER_CENTER_BASE_URL` / `USER_CENTER_INTERNAL_SECRET`。

```python
from uc_integration.service.UcStoreScopeService import (
    UcStoreScopeService,
    UcScopeQueryError,
)

try:
    scope = UcStoreScopeService().get_org_and_stores_by_user_id(user_id)
except UcScopeQueryError as e:
    # UC HTTP 失败 / 内部密钥未配置（含明确中文说明）
    ...
# 用户不存在 → None
# scope['departments']  — 所属组织
# scope['stores'] / scope['store_ids']  — 可访问门店
```

```python
from uc_integration.service.UcUserQueryService import UcUserQueryService
from uc_integration.service.UcDeptQueryService import UcDeptQueryService
from uc_integration.service.UcStoreQueryService import UcStoreQueryService
from uc_integration.service.UcDeptUserQueryService import UcDeptUserQueryService
from uc_integration.service.UcBusinessRoleService import UcBusinessRoleService
from uc_integration.service.UcPermissionService import UcPermissionService
from uc_integration.service.UcStorePickerService import UcStorePickerService
from uc_integration.service.UserCenterClient import UserCenterClient
from uc_integration.service.OmniChatService import OmniChatService

user = UcUserQueryService().get_user_by_id(user_id)          # 无则 None
depts = UcDeptQueryService().list_departments(dept_type=1)   # 门店型部门
tree = UcDeptQueryService().build_dept_tree()                # parent_id = 远程 dept_id
store = UcStoreQueryService().get_store_by_id(store_id)
dept_ids = UcDeptUserQueryService().list_dept_ids_by_user_id(user_id)
roles = UcBusinessRoleService().list_roles_for_user(user_id) # HTTP 失败 → []
# 业务功能权限（角色功能权限 ∪ 用户额外功能权限）；subsystem 省略则用 UC_SUBSYSTEM_CODE
perms = UcPermissionService().permissions_for_user(user_id)              # 本子系统
portal_perms = UcPermissionService().permissions_for_user(user_id, 'portal')
ok = UcPermissionService().has_permission(user_id, 'todo.task.assign')
catalog = UcStorePickerService().list_catalog(user_id)       # 选店列表 + 按门店裁剪的品牌/区域/省市
# catalog['stores'] / catalog['filters']
# 不要用 list_brand_departments / list_region_departments 做选店筛选（全量部门，仅管理端）

cfg = UserCenterClient().get_sys_config('OMNI_CHAT_URL')     # {name,value,desc} 或 None
vals = UserCenterClient().get_sys_configs(['OMNI_CHAT_URL', 'OMNI_CHAT_TOKEN', 'OMNI_CHAT_MODEL'])
text = OmniChatService().complete([{'role': 'user', 'content': 'hi'}])  # 凭证取自 UC sys_config

# 其它子系统 HTTP origin（= UC「子系统管理 → 访问地址」routePath；换域名只改 UC 登记）
finance_origin = UserCenterClient().get_subsystem_base_url('finance')  # 或 get_subsystem_catalog('finance')
# 禁止写死 http://127.0.0.1:5007

# AI 预览 / 加入模块：幂等登记为「开发中」（服务端 → UC internal，无 CORS）
draft = UserCenterClient().ensure_subsystem_draft(
    'my-mod', name='我的模块', route_path='https://host/my-mod/',
)
```

User / Dept / Store / DeptUser / Scope 只读调 UC，返回 dict（不返回 ORM）。空 IN → `[]` / `{}`，不发 HTTP。

本地 JWT 的 `sub` 含：

- 本地身份：`id` / `username` / `name` / `store_id`（当前选中，可空）/ `role`
- **`uc` 精简**：`code` / `name` / `store` / `is_store_manager`，以及 `departments` / `roles` / `roleCodes`（payload 有则带）
- **不含**全量 `store_ids` / `stores`（避免 Authorization 过大 → 431）

换票响应 body 可另含 `storeIds`（当次组织现查结果，供前端首屏），**不进 JWT**。后续门店列表应走服务端按部门现查。

## 权限目录（UC 点「同步」时拉取）

`GET /{subsystem}/uc_integration/internal/permissions/catalog`（内部密钥鉴权）。目录由两部分合成：

1. **yaml 钉子**：`common/permissions.yaml`（或 `UC_PERMISSIONS_FILE`）里登记的权限码原样输出，**永不被自动覆盖**；yaml 里登记过的接口（含 `permission: null`、`common.public / login_only`）不会再出自动码。
2. **自动派生**：代码里有、yaml 没登记的路由，按确定性规则（`codeRule: "v1"`）自动出码，条目带 `auto: true`。**没有 yaml 的子系统整份目录都是自动生成**，不再 404。

码规则：`<UC子系统码>.<路径段>.<method小写>`，动态段（`<int:x>` 等）写成 `id`，如 `GET /reports` → `finance.reports.get`，`POST /subtasks/<int:subtask_id>/submit` → `todo.subtasks.id.submit.post`。码只由 `(METHOD, 路径形状)` 决定，与路由注册顺序、重启、`PYTHONHASHSEED` 无关，所以重复同步只会「新增」，不会改动已有授权。

- `UC_PERMISSION_API_PREFIX`（Config 或环境变量，可选）：无 yaml 时业务路由前缀，缺省 `/{mount}`；须以 `/` 开头且不以 `/` 结尾。
- 显式设置了 `UC_PERMISSIONS_FILE` 但文件不存在 → 404（配置错误，不静默降级）；合成后目录为空 → 422，不会清空 UC 授权。
- 路径重构会让自动码变化：旧码进入 UC 的「待禁用」确认；要保住授权，在 yaml 里把旧码钉到新路径上。
- **「不需要鉴权」的接口不出码**：登录、退出、SSO、公开页、健康检查、验证码、埋点、当前用户画像等，按路径约定排除（`/auth/**`、`/sso/**`、`/public/**`、`/**/login|logout|dev-login`、`/**/health|healthz|ping`、`/**/captcha|img_captcha`、`/page-track`、顶层 `/profile`、`/me`；按路径段整段匹配、大小写不敏感、与 HTTP 方法无关，所以 `/health_score/...`、`/users/me` 不会被误伤）。命中的接口不进 `permissions` / `apis`，只列在响应的 `audit.excluded`（无 yaml 也列出，便于确认有没有误伤）；排除不改变其它接口的码。
- `UC_PERMISSION_EXCLUDE`（Config 或环境变量，Config 优先；列表，或逗号 / 换行分隔字符串）：追加排除规则，与默认取并集。语法：`*` 段内通配、`**` 独占一整段并匹配零个或多个段、可带大写 `METHOD ` 前缀只排除该方法（如 `POST /uploads/callback`）。没命中约定的免鉴权接口（如公开的 `/display_config`）在这里追加即可。
- 默认规则**误伤**了业务接口：在 yaml 里登记它（并给权限码）即可，yaml 已登记的接口永远按 yaml 输出，不受排除规则影响。
- 自动码**只供 UC 授权展示，不是鉴权守卫**；排除表只是路径约定，将来做守卫须按 yaml 的 `public / login_only` 豁免。
- `permission_scaffold` 派生的是语义码（如 `todo.history_report.view`），与自动码是两套体系：对已自动出现在 UC 的接口，钉 yaml 时 `permission` **必须填自动码**，否则会出现新旧两个码、授权对不上。

## 安装

```bash
# 开发：可编辑安装
pip install -e /path/to/uc-integration

# 或从 git
pip install "gss-uc-integration @ git+ssh://git@github.com/org/uc-integration.git"
```

Fred `>=1.0.17` 用 `EXTERNAL_MODULES` 从 **site-packages** 加载蓝图，**不要**再软链进 `modules/`：

```python
EXTERNAL_MODULES = ["uc_integration"]
```

旧脚本 `scripts/link_to_project.py` 仅兼容更早的 Fred，新项目不要跑。

## 宿主配置

`common/config/Config.py`（或等价配置）：

```python
EXTERNAL_MODULES = ["uc_integration"]   # 必填；不要把 uc_integration 放进 LOAD_CUSTOM_MODULES / modules/

SSO_LOGIN_MODE = 'uc'                    # 或 'local'
UC_SUBSYSTEM_CODE = 'ai_check'           # 路由前缀：/{code}/uc_integration；内部请求头 X-UC-Subsystem-Code
USER_CENTER_BASE_URL = 'http://127.0.0.1:5001'
USER_CENTER_INTERNAL_SECRET = '...'      # 与 UC OPEN_API_INTERNAL_SECRET 一致

# 可选
UC_INCLUDE_STORE_SCOPE = True            # 解析/返回门店 scope + 当前 store_id；不写全量 store_ids 进 JWT（默认 True）
UC_STORE_FALLBACK_FIRST = True           # 无匹配门店时回退第一个（默认 True；仅关闭 scope 时的选店回退）
```

**必填（UC 主数据只读 HTTP）**

| 配置 | 用途 |
|------|------|
| `USER_CENTER_BASE_URL` | UC Open API 基址 |
| `USER_CENTER_INTERNAL_SECRET` | 请求头 `X-UC-Internal-Secret`（与 UC `OPEN_API_INTERNAL_SECRET` 一致） |
| `UC_SUBSYSTEM_CODE` | 请求头 `X-UC-Subsystem-Code`；UC 侧若带此头则校验子系统已登记且 `status=1` |

UC 主数据 **reads 只走 HTTP**（`/open_api/internal/*`），**不对本地 `uc_*` 做 SELECT**。

> **说明：UC 已停用「数据同步」**——UC 不再向子系统主动推送用户/组织/门店主数据。`internal/uc-sync` 端点与 `UcDataSyncService` 仍随包挂载（代码保留），但 UC 不会调用它们，子系统无需实现、验证或联调；用户/门店/组织一律走 `UcUserQueryService` / `UcStoreQueryService` / `UcStoreScopeService` / `UcStorePickerService` 在线 HTTP 查询。本地 `uc_*` 镜像表不再由 UC 更新，请勿依赖其新鲜度。

本地库仅用于：

1. ~~**接收 UC push upsert**（`UcDataSyncService` POST 写本地 `uc_*`）~~ **（已停用：UC 不再推送）**
2. **其它业务表**（与 UC 实体无关）；SSO / 组织 / 门店 / 角色 / scope **不再读本地 UC 镜像**

`GET .../internal/uc-sync` 为接收端存活探测（`status=ok, mode=receiver`），不 COUNT 本地表（**已停用**：UC 不再调用，子系统无需验证）。

## 宿主前置条件

- 配置 `USER_CENTER_BASE_URL`、`USER_CENTER_INTERNAL_SECRET`、`UC_SUBSYSTEM_CODE`
- 本地 `uc_*` 镜像表原仅服务 push 接收（**已停用**：UC 不再推送，镜像表不再由 UC 更新，勿依赖其新鲜度）；只读查询与 SSO 换票不依赖它们
- 依赖：`fred_admin`、Flask、SQLAlchemy（由子系统环境提供；SQLAlchemy 仅 push 写入需要，**该路径已停用**）

## 目录结构（Fred 兼容）

```
uc_integration/
  __init__.py              # 导出蓝图
  backend/
    __init__.py            # Blueprint + url_prefix
    deps.py                # 惰性解析 model.model
    controller/
    service/
    schema/
```

## 从旧模块迁移

1. `pip install "uc-integration>=0.1.13"`（或 `-e /path/to/uc-integration`）
2. 删除子系统里的 `modules/uc_integration`（拷贝或软链都删）
3. `Config.py` 设 `EXTERNAL_MODULES = ["uc_integration"]`，不要把它写进 `LOAD_CUSTOM_MODULES`
4. 确认 `UC_SUBSYSTEM_CODE` 与门户子系统编码一致
5. 重启进程，验证 `GET /{code}/uc_integration/auth/sso/config`

## 版本

当前 `0.1.33`：`UserCenterClient.ensure_subsystem_draft` 参数为 `draft_owner_uc_user_id`（对应 UC ensure-draft 的 `draftOwnerUcUserId`；`0.1.32` 曾为 `draft_owner_admin_id`，与 ai-dev 不兼容，请升级）。

`0.1.32`：权限与其它能力见下文历史说明。

`0.1.31`：权限目录支持自动派生（见上文「权限目录」）；catalog v2 增加接口映射 / 运行时对账 / 空目录保护，新增 `permission_scaffold` 只读骨架工具。

`0.1.30`：`UcPermissionService.permissions_for_user` / `has_permission` 支持可选 `subsystem_code`；权限码走 Redis 共享缓存（`uc:perm:v1:{user_id}:{subsystem}`，TTL 3600s，与 UC 同契约；Redis 异常降级 OpenAPI）。用于门户等场景只认 UC 角色∪用户功能权限。
