Files
xiaoxia-saas/packages/application/auth/wechat_bind_use_case.py
saas-backend-agent 5ca64898b7
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m58s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m51s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m9s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m34s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 18m5s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 6s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m30s
feat(#1719): 微信账号绑定/解绑三接口(GET bind/url、POST bind、DELETE bind)+ /me 返回 wechat_bound
- WechatBindUseCase:绑定到当前登录账号不建新号;openid/unionid 已绑其他账号 409;已绑同一微信幂等
- WechatUnbindUseCase:解绑前守卫——必须有已验证手机或真实已验证邮箱(随机密码 hash/占位邮箱不算兜底,口径同 binding_complete)
- /auth/me 增加 wechat_bound 字段,供设置页判断绑定状态
- state 复用 RedisStateStore CSRF 校验;22 个新单测,diff coverage 100%
2026-09-05 21:31:44 +08:00

116 lines
4.4 KiB
Python

"""
微信账号绑定/解绑 Use Case(已登录用户场景)
与 wechat_sync_use_case(登录/注册,系统级)不同:
- bind:把微信 openid/unionid 绑定到【当前登录账号】,不创建新用户;
微信身份若已绑定其他账号则冲突(409)。
- unbind:解除当前账号的微信绑定;若账号没有其他登录方式(手机/邮箱/密码),
解绑后将无法登录,因此拒绝解绑。
"""
from __future__ import annotations
from typing import Optional
from packages.domain.entities import User
class WechatBindRequest:
"""微信绑定请求"""
def __init__(self, user_id: str, openid: str, unionid: str = ""):
self.user_id = user_id
self.openid = (openid or "").strip()
self.unionid = (unionid or "").strip()
class WechatBindResult:
"""微信绑定/解绑结果"""
def __init__(self, user: User):
self.user = user
class WechatBindUseCase:
"""已登录用户绑定微信用例"""
def __init__(self, user_repository):
self.user_repository = user_repository
def bind(self, request: WechatBindRequest) -> tuple[Optional[WechatBindResult], Optional[str], int]:
"""
绑定微信到当前登录账号。
Returns:
(结果, 错误信息, http状态码) - 成功时错误信息为 None、状态码为 200;
冲突返回 409,客户端/服务端错误返回 400/404。
"""
if not request.openid:
return None, "缺少微信 openid", 400
user = self.user_repository.find_by_id(request.user_id)
if user is None:
return None, "当前用户不存在", 404
# 已绑定同一个微信:幂等成功
if user.wechat_openid == request.openid:
return WechatBindResult(user=user), None, 200
# 当前账号已绑定其他微信
if user.wechat_openid:
return None, "当前账号已绑定微信,请先解绑", 409
# openid 已被其他账号占用
existing = self.user_repository.find_by_wechat_openid(request.openid)
if existing is not None and existing.id != user.id:
return None, "该微信已绑定其他账号,请先在原账号解绑", 409
# unionid 冲突:同主体微信已绑其他账号
if request.unionid:
existing_union = self.user_repository.find_by_wechat_unionid(request.unionid)
if existing_union is not None and existing_union.id != user.id:
return None, "该微信主体已绑定其他账号,请先在原账号解绑", 409
user.wechat_openid = request.openid
if request.unionid and not user.wechat_unionid:
user.wechat_unionid = request.unionid
self.user_repository.save(user)
return WechatBindResult(user=user), None, 200
class WechatUnbindUseCase:
"""已登录用户解绑微信用例"""
def __init__(self, user_repository):
self.user_repository = user_repository
def unbind(self, user_id: str) -> tuple[Optional[WechatBindResult], Optional[str], int]:
"""
解除当前账号的微信绑定。
解绑前置条件:账号必须还有其他登录方式(密码 / 已验证手机 / 真实邮箱),
否则解绑后将永远无法登录。
"""
user = self.user_repository.find_by_id(user_id)
if user is None:
return None, "当前用户不存在", 404
if not user.wechat_openid:
return None, "当前账号未绑定微信", 400
# 守卫:解绑后账号必须仍有可实际使用的登录方式。
# 注意:微信注册用户带的是【随机密码】(用户不知道、无法用密码登录,
# 且 @wechat.local 占位邮箱收不到重置邮件),故 password_hash 不作为兜底依据,
# 口径与 /auth/me 的 binding_complete 一致。
has_phone = bool(user.phone and user.phone_verified)
has_real_email = bool(user.email and user.email_verified and "@wechat.local" not in user.email)
if not (has_phone or has_real_email):
return None, "账号需要至少一种其他登录方式(已验证手机或真实邮箱)后才能解绑微信", 400
user.wechat_openid = None
user.wechat_unionid = None
self.user_repository.save(user)
return WechatBindResult(user=user), None, 200