完善钉钉用户名称与部门信息同步
This commit is contained in:
parent
724a5a6453
commit
ca4c0e2d58
51
.env.testing.example
Normal file
51
.env.testing.example
Normal file
@ -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=<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=<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
|
||||||
@ -17,6 +17,7 @@ from app.services.auth import AuthService
|
|||||||
TOKEN_URL = "https://oapi.dingtalk.com/gettoken"
|
TOKEN_URL = "https://oapi.dingtalk.com/gettoken"
|
||||||
USERINFO_URL = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo"
|
USERINFO_URL = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo"
|
||||||
USERDETAIL_URL = "https://oapi.dingtalk.com/topapi/v2/user/get"
|
USERDETAIL_URL = "https://oapi.dingtalk.com/topapi/v2/user/get"
|
||||||
|
DEPARTMENT_DETAIL_URL = "https://oapi.dingtalk.com/topapi/v2/department/get"
|
||||||
|
|
||||||
|
|
||||||
class DingTalkServiceError(AppException):
|
class DingTalkServiceError(AppException):
|
||||||
@ -101,7 +102,12 @@ class DingTalkAuthService:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
except httpx.HTTPError as exc:
|
except httpx.HTTPError as exc:
|
||||||
raise AppException("钉钉免登码已失效,请重新进入工作台", code=401, status_code=401) from 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:
|
if data.get("errcode") or not user_id:
|
||||||
raise AppException("钉钉免登码已失效,请重新进入工作台", code=401, status_code=401)
|
raise AppException("钉钉免登码已失效,请重新进入工作台", code=401, status_code=401)
|
||||||
return str(user_id)
|
return str(user_id)
|
||||||
@ -118,7 +124,57 @@ class DingTalkAuthService:
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError:
|
||||||
return {"userid": user_id}
|
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:
|
async def _upsert_user(self, user_id: str, detail: dict) -> UserAccount:
|
||||||
user = await self.repo.get_by_dingtalk_id(user_id)
|
user = await self.repo.get_by_dingtalk_id(user_id)
|
||||||
@ -130,15 +186,15 @@ class DingTalkAuthService:
|
|||||||
if user is None:
|
if user is None:
|
||||||
user = UserAccount(
|
user = UserAccount(
|
||||||
emp_no=f"dt_{user_id}"[:20],
|
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,
|
role=role,
|
||||||
status="启用",
|
status="启用",
|
||||||
dingtalk_id=user_id,
|
dingtalk_id=user_id,
|
||||||
)
|
)
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
user.name = str(detail.get("name") or user.name)[:50]
|
user.name = self._display_name(detail, user.name or user_id)
|
||||||
user.dept = str(detail.get("dept_name") or user.dept or "")[:50] or None
|
user.dept = (self._department_name(detail) or user.dept or "")[:50] or None
|
||||||
user.position = str(detail.get("title") or user.position 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.role = role
|
||||||
user.dingtalk_union_id = str(detail.get("unionid") or user.dingtalk_union_id or "")[:64] or None
|
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
|
user.dingtalk_avatar = str(detail.get("avatar") or user.dingtalk_avatar or "")[:512] or None
|
||||||
@ -152,7 +208,7 @@ class DingTalkAuthService:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
user.dingtalk_dept_id = None
|
user.dingtalk_dept_id = None
|
||||||
user.dingtalk_is_dept_manager = self._is_dingtalk_manager(detail)
|
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)):
|
if isinstance(role_codes, (list, dict)):
|
||||||
role_codes = json.dumps(role_codes, ensure_ascii=False)
|
role_codes = json.dumps(role_codes, ensure_ascii=False)
|
||||||
user.dingtalk_role_codes = str(role_codes or "")[:512] or None
|
user.dingtalk_role_codes = str(role_codes or "")[:512] or None
|
||||||
@ -176,6 +232,10 @@ class DingTalkAuthService:
|
|||||||
detail.get("admin"),
|
detail.get("admin"),
|
||||||
detail.get("is_admin"),
|
detail.get("is_admin"),
|
||||||
detail.get("isAdmin"),
|
detail.get("isAdmin"),
|
||||||
|
detail.get("is_leader"),
|
||||||
|
detail.get("isLeader"),
|
||||||
|
detail.get("is_boss"),
|
||||||
|
detail.get("isBoss"),
|
||||||
detail.get("is_dept_manager"),
|
detail.get("is_dept_manager"),
|
||||||
detail.get("isDeptManager"),
|
detail.get("isDeptManager"),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -14,3 +14,10 @@ def test_dingtalk_manager_flag_variants() -> None:
|
|||||||
assert DingTalkAuthService._is_dingtalk_manager({"admin": True})
|
assert DingTalkAuthService._is_dingtalk_manager({"admin": True})
|
||||||
assert DingTalkAuthService._is_dingtalk_manager({"leader_in_dept": [{"leader": True}]})
|
assert DingTalkAuthService._is_dingtalk_manager({"leader_in_dept": [{"leader": True}]})
|
||||||
assert not DingTalkAuthService._is_dingtalk_manager({"admin": False, "leader_in_dept": []})
|
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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user