From ca4c0e2d584556ecce0659cda9da4c1d07725dd0 Mon Sep 17 00:00:00 2001 From: kx_guo Date: Sun, 13 Sep 2026 16:17:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=92=89=E9=92=89=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=90=8D=E7=A7=B0=E4=B8=8E=E9=83=A8=E9=97=A8=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.testing.example | 51 ++++++++++++++++++++++++ app/services/dingtalk_auth.py | 74 +++++++++++++++++++++++++++++++---- tests/test_auth.py | 7 ++++ 3 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 .env.testing.example diff --git a/.env.testing.example b/.env.testing.example new file mode 100644 index 0000000..98920a8 --- /dev/null +++ b/.env.testing.example @@ -0,0 +1,51 @@ +# 测试服务器环境变量模板 +# 使用前复制为服务器项目目录下的 .env,并填写真实密钥。 + +RISK_APP_NAME=Dieya Risk Prediction Backend +RISK_APP_VERSION=0.1.0 +RISK_ENVIRONMENT=testing +RISK_DEBUG=false +RISK_LOG_LEVEL=INFO + +# 数据库 +RISK_DATABASE_URL=postgresql+asyncpg://<数据库用户>:<数据库密码>@<数据库地址>:5432/<数据库名> + +# JWT:请使用至少 32 字节随机字符串,例如 openssl rand -hex 32 +RISK_JWT_SECRET_KEY=<请填写至少32字节随机密钥> +RISK_JWT_ALGORITHM=HS256 +RISK_JWT_EXPIRE_MINUTES=480 + +# 钉钉免登 +RISK_DINGTALK_APP_KEY=<钉钉AppKey> +RISK_DINGTALK_APP_SECRET=<钉钉AppSecret> +RISK_DINGTALK_CORP_ID=<钉钉CorpId> +RISK_DINGTALK_AGENT_ID=<钉钉AgentId> + +# 百度 OCR +BAIDU_OCR_API_KEY=<百度OCR_API_KEY> +BAIDU_OCR_SECRET_KEY=<百度OCR_SECRET_KEY> +RISK_BAIDU_OCR_IMAGE_URL=https://aip.baidubce.com/rest/2.0/ocr/v1/webimage +RISK_BAIDU_OCR_DOCUMENT_URL=https://aip.baidubce.com/rest/2.0/brain/online/v2/paddle-vl-parser/task +RISK_BAIDU_OCR_TIMEOUT_SECONDS=75 +RISK_BAIDU_OCR_POLL_INTERVAL_SECONDS=2 +RISK_OCR_UPLOAD_DIR=.runtime-uploads/ocr +RISK_OCR_MAX_FILE_MB=10 +RISK_OCR_MAX_BATCH_FILES=5 + +# LLM +RISK_LLM_API_KEY= +RISK_LLM_BASE_URL=https://api.openlux.ai/v1 +RISK_LLM_MODEL=gpt-4.1 +RISK_LLM_TIMEOUT_SECONDS=120 +RISK_LLM_TEMPERATURE=0.2 + +# 向量检索 +RISK_RAG_EMBEDDING_API_KEY= +RISK_RAG_EMBEDDING_BASE_URL=https://api.openlux.ai/v1 +RISK_RAG_EMBEDDING_MODEL=gemini-embedding-2-preview +RISK_RAG_EMBEDDING_DIMENSIONS=3072 +RISK_RAG_EMBEDDING_TIMEOUT_SECONDS=30 +RISK_RAG_TOP_K=5 + +# 测试服务器前端来源 +RISK_CORS_ALLOW_ORIGINS=https://risk.dieya.net.cn,http://risk.dieya.net.cn diff --git a/app/services/dingtalk_auth.py b/app/services/dingtalk_auth.py index 75c6f94..2258d89 100644 --- a/app/services/dingtalk_auth.py +++ b/app/services/dingtalk_auth.py @@ -17,6 +17,7 @@ from app.services.auth import AuthService TOKEN_URL = "https://oapi.dingtalk.com/gettoken" USERINFO_URL = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo" USERDETAIL_URL = "https://oapi.dingtalk.com/topapi/v2/user/get" +DEPARTMENT_DETAIL_URL = "https://oapi.dingtalk.com/topapi/v2/department/get" class DingTalkServiceError(AppException): @@ -101,7 +102,12 @@ class DingTalkAuthService: data = response.json() except httpx.HTTPError as exc: raise AppException("钉钉免登码已失效,请重新进入工作台", code=401, status_code=401) from exc - user_id = (data.get("result") or {}).get("userid") + result = data.get("result") or data.get("user") or data.get("data") or {} + user_id = ( + result.get("userid") or result.get("userId") or result.get("unionid") + if isinstance(result, dict) + else None + ) if data.get("errcode") or not user_id: raise AppException("钉钉免登码已失效,请重新进入工作台", code=401, status_code=401) return str(user_id) @@ -118,7 +124,57 @@ class DingTalkAuthService: data = response.json() except httpx.HTTPError: return {"userid": user_id} - return data.get("result") or {"userid": user_id} + detail = data.get("result") or data.get("user") or {"userid": user_id} + if not isinstance(detail, dict): + detail = {"userid": user_id} + detail.setdefault("userid", user_id) + dept_ids = self._department_ids(detail) + if dept_ids and not self._department_name(detail): + dept_name = await self._department_name_from_id(access_token, dept_ids[0]) + if dept_name: + detail["dept_name"] = dept_name + return detail + + async def _department_name_from_id(self, access_token: str, department_id: int) -> str | None: + try: + async with httpx.AsyncClient(timeout=8) as client: + response = await client.post( + DEPARTMENT_DETAIL_URL, + params={"access_token": access_token}, + json={"dept_id": department_id, "language": "zh_CN"}, + ) + response.raise_for_status() + data = response.json() + except (httpx.HTTPError, ValueError): + return None + result = data.get("result") or data.get("department") or {} + if not isinstance(result, dict): + return None + name = result.get("name") or result.get("dept_name") or result.get("deptName") + return str(name).strip()[:50] if name else None + + @staticmethod + def _department_name(detail: dict) -> str | None: + value = detail.get("dept_name") or detail.get("deptName") or detail.get("department_name") + return str(value).strip() if value else None + + @staticmethod + def _department_ids(detail: dict) -> list[int]: + raw = (detail.get("dept_id_list") or detail.get("deptIdList") or detail.get("department_ids") or detail.get("dept_id") or detail.get("main_dept_id") or detail.get("mainDeptId") or []) + if not isinstance(raw, list): + raw = [raw] + result: list[int] = [] + for value in raw: + try: + result.append(int(value)) + except (TypeError, ValueError): + continue + return result + + @staticmethod + def _display_name(detail: dict, fallback: str) -> str: + value = detail.get("name") or detail.get("nick") or detail.get("nickname") or detail.get("real_name") or detail.get("display_name") or fallback + return str(value).strip()[:50] or fallback async def _upsert_user(self, user_id: str, detail: dict) -> UserAccount: user = await self.repo.get_by_dingtalk_id(user_id) @@ -130,15 +186,15 @@ class DingTalkAuthService: if user is None: user = UserAccount( emp_no=f"dt_{user_id}"[:20], - name=str(detail.get("name") or user_id)[:50], + name=self._display_name(detail, user_id), role=role, status="启用", dingtalk_id=user_id, ) self.session.add(user) - user.name = str(detail.get("name") or user.name)[:50] - user.dept = str(detail.get("dept_name") or user.dept or "")[:50] or None - user.position = str(detail.get("title") or user.position or "")[:50] or None + user.name = self._display_name(detail, user.name or user_id) + user.dept = (self._department_name(detail) or user.dept or "")[:50] or None + user.position = str(detail.get("title") or detail.get("position") or detail.get("job_title") or user.position or "")[:50] or None user.role = role user.dingtalk_union_id = str(detail.get("unionid") or user.dingtalk_union_id or "")[:64] or None user.dingtalk_avatar = str(detail.get("avatar") or user.dingtalk_avatar or "")[:512] or None @@ -152,7 +208,7 @@ class DingTalkAuthService: except (TypeError, ValueError): user.dingtalk_dept_id = None user.dingtalk_is_dept_manager = self._is_dingtalk_manager(detail) - role_codes = detail.get("role_list") or user.dingtalk_role_codes + role_codes = detail.get("role_list") or detail.get("roleList") or user.dingtalk_role_codes if isinstance(role_codes, (list, dict)): role_codes = json.dumps(role_codes, ensure_ascii=False) user.dingtalk_role_codes = str(role_codes or "")[:512] or None @@ -176,6 +232,10 @@ class DingTalkAuthService: detail.get("admin"), detail.get("is_admin"), detail.get("isAdmin"), + detail.get("is_leader"), + detail.get("isLeader"), + detail.get("is_boss"), + detail.get("isBoss"), detail.get("is_dept_manager"), detail.get("isDeptManager"), ) diff --git a/tests/test_auth.py b/tests/test_auth.py index babf21b..04c1bd6 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -14,3 +14,10 @@ def test_dingtalk_manager_flag_variants() -> None: assert DingTalkAuthService._is_dingtalk_manager({"admin": True}) assert DingTalkAuthService._is_dingtalk_manager({"leader_in_dept": [{"leader": True}]}) assert not DingTalkAuthService._is_dingtalk_manager({"admin": False, "leader_in_dept": []}) + + +def test_dingtalk_detail_fields_are_normalized() -> None: + detail = {"nick": "张三", "dept_id_list": [123, "456"], "is_leader": True} + assert DingTalkAuthService._display_name(detail, "fallback") == "张三" + assert DingTalkAuthService._department_ids(detail) == [123, 456] + assert DingTalkAuthService._is_dingtalk_manager(detail)