dingdanquanliucheng/backend/app/api/auth.py
wsb1224 1d12d061e0 接入微信服务号模板消息,修复通知链路重复发送和静默失败
小程序订阅消息每订阅一次只能发一条,用户无法持续收到通知。改为接入微信
服务号模板消息,用户关注服务号后可无限次接收推送通知。

新增功能:
- 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>
2026-06-23 17:17:06 +08:00

148 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
认证路由模块
职责:
处理用户认证相关接口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。
请求参数LoginRequestusername、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。
请求参数WechatLoginRequestcode、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 绑定,用于微信消息通知。
请求参数BindOpenIdRequestopen_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 并绑定到当前用户。
请求参数BindOpenIdByCodeRequestcode - 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})