2187be7aeb
本次累积提交包含以下功能: 1. 修复:el-date-picker 缺失 value-format 导致日期保存偏移(8处) 2. 新增:协同拜访 visit_group_id 去重机制(完整副本 + 周报合并 + 亮灯去重) 3. 新增:计划自动完成(创建计划检测已有拜访 / 编辑拜访触发) 4. 新增:拜访表单选择客户后自动预填过期计划内容 5. 新增:移动端主题切换按钮 + 后端持久化 6. 新增:操作日志系统 (audit_logs),全覆盖 8 个模块 CRUD 7. 新增:PC端「我的数据」支局长/领导视角客户经理列 Co-Authored-By: Claude <noreply@anthropic.com>
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
from uuid import uuid4
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
import httpx
|
|
from app.config import settings
|
|
from app.models.user import User
|
|
from app.utils.security import create_access_token
|
|
|
|
|
|
async def get_or_create_user_from_casdoor(
|
|
db: AsyncSession, casdoor_id: str, name: str,
|
|
role: str = "manager", department: str = ""
|
|
) -> User:
|
|
"""Find existing user by casdoor_id, or create a new one."""
|
|
result = await db.execute(select(User).where(User.casdoor_id == casdoor_id))
|
|
user = result.scalar_one_or_none()
|
|
if user is None:
|
|
user = User(
|
|
casdoor_id=casdoor_id,
|
|
name=name,
|
|
role=role,
|
|
department=department,
|
|
)
|
|
db.add(user)
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
else:
|
|
# Update name/department if changed
|
|
if user.name != name or user.department != department:
|
|
user.name = name
|
|
user.department = department
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
return user
|
|
|
|
|
|
async def exchange_casdoor_code(code: str) -> dict | None:
|
|
"""Exchange Casdoor OIDC authorization code for user info."""
|
|
import logging
|
|
logger = logging.getLogger("uvicorn")
|
|
|
|
token_url = f"{settings.CASDOOR_ENDPOINT}/api/login/oauth/access_token"
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
resp = await client.post(token_url, data={
|
|
"grant_type": "authorization_code",
|
|
"client_id": settings.CASDOOR_CLIENT_ID,
|
|
"client_secret": settings.CASDOOR_CLIENT_SECRET,
|
|
"code": code,
|
|
}, timeout=10)
|
|
if resp.status_code != 200:
|
|
logger.error("[Casdoor] token exchange failed: status=%s", resp.status_code)
|
|
return None
|
|
token_data = resp.json()
|
|
access_token = token_data.get("access_token", "")
|
|
if not access_token:
|
|
logger.error("[Casdoor] token exchange response did not contain an access token")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"[Casdoor] token exchange exception: {e}")
|
|
return None
|
|
|
|
userinfo_url = f"{settings.CASDOOR_ENDPOINT}/api/userinfo"
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
resp = await client.get(userinfo_url, headers={
|
|
"Authorization": f"Bearer {access_token}"
|
|
}, timeout=10)
|
|
if resp.status_code != 200:
|
|
logger.error("[Casdoor] userinfo failed: status=%s", resp.status_code)
|
|
return None
|
|
return resp.json()
|
|
except Exception as e:
|
|
logger.error(f"[Casdoor] userinfo exception: {e}")
|
|
return None
|
|
return None
|
|
|
|
|
|
async def bind_wecom_user(db: AsyncSession, casdoor_id: str, wecom_userid: str) -> User | None:
|
|
"""Bind a WeChat Work userid to a Casdoor user."""
|
|
result = await db.execute(select(User).where(User.casdoor_id == casdoor_id))
|
|
user = result.scalar_one_or_none()
|
|
if user is None:
|
|
return None
|
|
user.wecom_userid = wecom_userid
|
|
await db.commit()
|
|
await db.refresh(user)
|
|
return user
|
|
|
|
|
|
async def get_user_by_wecom_id(db: AsyncSession, wecom_userid: str) -> User | None:
|
|
"""Find user by wecom_userid."""
|
|
result = await db.execute(select(User).where(User.wecom_userid == wecom_userid))
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
def build_token_for_user(user: User) -> str:
|
|
"""Build a JWT token for the given user."""
|
|
return create_access_token(data={
|
|
"sub": str(user.id),
|
|
"user_id": str(user.id),
|
|
"casdoor_id": user.casdoor_id,
|
|
"name": user.name,
|
|
"role": user.role,
|
|
"department": user.department,
|
|
"theme": user.theme or "editorial",
|
|
})
|