小程序订阅消息每订阅一次只能发一条,用户无法持续收到通知。改为接入微信 服务号模板消息,用户关注服务号后可无限次接收推送通知。 新增功能: - ServiceAccountNotificationService:服务号模板消息发送服务 - 服务号事件回调(subscribe/unsubscribe)+ 手机号验证绑定流程 - PendingServiceBinding 模型:存储关注者的 openid 待绑定记录 - 小程序 bind-service 页面:输入手机号完成绑定 - 三个详情页(审批/任务/发厂)增加"绑定微信通知"入口 - web-sales NotificationBell 增加服务号关注引导 - web-admin SystemPage/ConfigPage 增加服务号绑定状态和模板配置展示 修复问题: - EventBus 和 _notify_status_change 重复发送微信消息(业务员收到2条) - EventBus order_status_changed 事件缺少模板映射导致微信通知静默丢失 - 所有微信发送失败被 except Exception: pass 静默吞掉,增加日志 - 定时任务(欠款/沉默客户/物流超时)不触发 WebSocket 实时通知 - reminder_method 配置未生效 Co-Authored-By: Claude <noreply@anthropic.com>
148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
"""
|
||
认证路由模块
|
||
|
||
职责:
|
||
处理用户认证相关接口,URL 前缀为 /api/auth。
|
||
包括:登录、获取当前用户信息、登出、绑定微信 OpenID。
|
||
"""
|
||
import json
|
||
|
||
from fastapi import APIRouter, Depends, Header
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.orm import Session
|
||
|
||
from backend.app.api.deps import get_auth_service, get_current_user
|
||
from backend.app.db import get_db_session
|
||
from backend.app.schemas.auth import LoginRequest, WechatLoginRequest
|
||
from backend.app.schemas.common import success_payload
|
||
from backend.app.services.auth_service import AuthService
|
||
from backend.app.services.wechat_notification_service import wechat_notification_service
|
||
|
||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||
|
||
|
||
class BindOpenIdRequest(BaseModel):
|
||
"""绑定微信 OpenID 请求体"""
|
||
open_id: str
|
||
|
||
|
||
class BindOpenIdByCodeRequest(BaseModel):
|
||
"""通过 wx.login code 绑定微信 OpenID"""
|
||
code: str
|
||
|
||
|
||
@router.post("/login")
|
||
def login(
|
||
payload: LoginRequest, # 登录请求体,包含用户名、密码、角色类型
|
||
auth_service: AuthService = Depends(get_auth_service), # 注入认证服务
|
||
session: Session = Depends(get_db_session), # 注入数据库会话
|
||
) -> dict:
|
||
"""用户登录接口
|
||
|
||
用途:验证用户名和密码,返回 JWT Token。
|
||
请求参数:LoginRequest(username、password、role_type)。
|
||
返回值:登录结果,包含 token 等认证信息。
|
||
权限要求:无需认证(公开接口)。
|
||
"""
|
||
return success_payload(auth_service.login(payload.username, payload.password, payload.role_type, session))
|
||
|
||
|
||
@router.post("/wechat-login")
|
||
def wechat_login(
|
||
payload: WechatLoginRequest, # 微信手机号登录请求体
|
||
auth_service: AuthService = Depends(get_auth_service), # 注入认证服务
|
||
session: Session = Depends(get_db_session), # 注入数据库会话
|
||
) -> dict:
|
||
"""微信手机号一键登录接口
|
||
|
||
用途:通过微信手机号授权登录,自动匹配系统用户并绑定 open_id。
|
||
请求参数:WechatLoginRequest(code、phone_code)。
|
||
返回值:登录结果,包含 token 等认证信息。
|
||
权限要求:无需认证(公开接口)。
|
||
"""
|
||
return success_payload(auth_service.wechat_login(payload.code, payload.phone_code, session))
|
||
|
||
|
||
@router.get("/me")
|
||
def me(
|
||
current_user: dict = Depends(get_current_user), # 从 JWT Token 解析当前用户
|
||
) -> dict:
|
||
"""获取当前登录用户信息
|
||
|
||
用途:返回当前已登录用户的详细信息(角色、权限等)。
|
||
请求参数:无(通过请求头 Authorization 传递 Token)。
|
||
返回值:当前用户信息字典。
|
||
权限要求:必须已登录(携带有效 Token)。
|
||
"""
|
||
return success_payload(current_user)
|
||
|
||
|
||
@router.post("/logout")
|
||
def logout(
|
||
authorization: str | None = Header(default=None), # 请求头中的 Authorization 字段
|
||
auth_service: AuthService = Depends(get_auth_service), # 注入认证服务
|
||
) -> dict:
|
||
"""用户登出接口
|
||
|
||
用途:使当前 Token 失效,完成登出。
|
||
请求参数:通过请求头 Authorization 传递 Bearer Token。
|
||
返回值:登出结果。
|
||
权限要求:无需严格校验,但需传递 Token。
|
||
"""
|
||
token = (authorization or "").removeprefix("Bearer").strip()
|
||
return success_payload(auth_service.logout(token))
|
||
|
||
|
||
@router.post("/bind-openid")
|
||
def bind_openid(
|
||
payload: BindOpenIdRequest, # 绑定请求体,包含微信 OpenID
|
||
session: Session = Depends(get_db_session), # 注入数据库会话
|
||
current_user: dict = Depends(get_current_user), # 当前登录用户
|
||
) -> dict:
|
||
"""绑定微信 OpenID 接口
|
||
|
||
用途:将当前用户的账号与微信 OpenID 绑定,用于微信消息通知。
|
||
请求参数:BindOpenIdRequest(open_id - 微信用户的 OpenID)。
|
||
返回值:绑定结果 {"success": true}。
|
||
权限要求:必须已登录。
|
||
"""
|
||
wechat_notification_service.bind_open_id(session, current_user["user_id"], payload.open_id)
|
||
return success_payload({"success": True})
|
||
|
||
|
||
@router.post("/bind-openid-by-code")
|
||
def bind_openid_by_code(
|
||
payload: BindOpenIdByCodeRequest, # 通过 code 绑定
|
||
auth_service: AuthService = Depends(get_auth_service), # 注入认证服务
|
||
session: Session = Depends(get_db_session), # 注入数据库会话
|
||
current_user: dict = Depends(get_current_user), # 当前登录用户
|
||
) -> dict:
|
||
"""通过 wx.login code 绑定微信 OpenID
|
||
|
||
用途:密码登录后调用,用 code 换取 open_id 并绑定到当前用户。
|
||
请求参数:BindOpenIdByCodeRequest(code - wx.login 获取的临时凭证)。
|
||
返回值:绑定结果 {"success": true}。
|
||
权限要求:必须已登录。
|
||
"""
|
||
return success_payload(auth_service.bind_openid_by_code(current_user["user_id"], payload.code, session))
|
||
|
||
|
||
@router.get("/service-bind-status")
|
||
def service_bind_status(
|
||
session: Session = Depends(get_db_session),
|
||
current_user: dict = Depends(get_current_user),
|
||
) -> dict:
|
||
"""查询当前用户是否已绑定服务号通知
|
||
|
||
用途:小程序页面据此决定是否显示 <official-account> 组件。
|
||
返回值:{"bound": true/false}
|
||
权限要求:必须已登录。
|
||
"""
|
||
from backend.app.models.system import User
|
||
from sqlalchemy import select
|
||
user = session.execute(
|
||
select(User.service_open_id).where(User.id == current_user["user_id"])
|
||
).scalar_one_or_none()
|
||
bound = bool(user)
|
||
return success_payload({"bound": bound})
|