feat: 企微绑定重构 — 回调事件 + 支局长手动绑定/解绑

绑定流程改为 rural-optical-rectify 方案:
- 企微菜单「绑定账号」发送 click 事件(而非 OAuth 跳转)
- 回调 POST 接收事件 → 生成绑定 token → 推送绑定链接
- 前端 WecomBind.vue (/wecom-bind) 确认绑定
- 新增 POST /api/wecom/bind-confirm (JWT 保护)

支局长手动管理:
- 新增 PUT /api/users/{id}/wecom 绑定/解绑端点
- UserManage.vue 编辑对话框新增企微 UserID 字段
- 表格操作列新增「解绑」按钮
- 重复绑定检测

wecom.py 新增:
- in-memory bind token store (TTL 600s)
- send_text_card() 卡片消息方法
- access_token 属性暴露

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-26 12:10:38 +08:00
parent 1d2dacc3ab
commit 7264184315
6 changed files with 363 additions and 60 deletions
+39
View File
@@ -80,3 +80,42 @@ async def update_user_role(
"role": user.role,
"department": user.department,
}
class UpdateWecomRequest(BaseModel):
wecom_userid: Optional[str] = None # None or "" to unbind
@router.put("/{user_id}/wecom")
async def update_user_wecom(
user_id: str,
data: UpdateWecomRequest,
current_user: dict = Depends(require_director),
db: AsyncSession = Depends(get_db),
):
"""Director binds or unbinds a user's WeChat Work account."""
result = await db.execute(select(User).where(User.id == uuid.UUID(user_id)))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
new_id = (data.wecom_userid or "").strip()
# Check duplicate
if new_id:
dup = await db.execute(
select(User).where(User.wecom_userid == new_id, User.id != uuid.UUID(user_id))
)
existing = dup.scalar_one_or_none()
if existing:
raise HTTPException(status_code=400, detail=f"该企微ID已被「{existing.name}」绑定")
user.wecom_userid = new_id if new_id else None
await db.commit()
await db.refresh(user)
return {
"id": str(user.id),
"name": user.name,
"wecom_userid": user.wecom_userid,
}