Files
v6ole 2187be7aeb feat: 操作日志系统 + 日期修复 + 协同去重 + 主题持久化 + 计划自动完成
本次累积提交包含以下功能:

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>
2026-07-30 12:01:14 +08:00

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",
})