"""
微信API服务
"""

import asyncio
import logging
from dataclasses import dataclass
from typing import Any, Dict

import requests
from django.conf import settings

logger = logging.getLogger(__name__)


@dataclass
class WechatUser:
    """微信用户数据"""

    openid: str
    nickname: str = ""
    avatar: str = ""
    sex: int = 0
    province: str = ""
    city: str = ""
    country: str = ""
    language: str = "zh_CN"

    @classmethod
    def from_api_response(cls, data: Dict[str, Any]) -> "WechatUser":
        """从API响应创建用户对象"""
        return cls(
            openid=data["openid"],
            nickname=data.get("nickname", ""),
            avatar=data.get("headimgurl", ""),
            sex=data.get("sex", 0),
            province=data.get("province", ""),
            city=data.get("city", ""),
            country=data.get("country", ""),
            language=data.get("language", "zh_CN"),
        )


class WechatAPIService:
    """微信API服务"""

    def __init__(self):
        self.base_url = "https://api.weixin.qq.com"
        self.app_id = settings.WECHAT_APP_ID
        self.app_secret = settings.WECHAT_APP_SECRET

    async def _request(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
        """通用请求方法"""

        def _sync_request():
            response = requests.get(url, params=params)
            data = response.json()
            if "errcode" in data:
                raise ValueError(f"微信API错误: {data.get('errmsg', '未知错误')}")
            return data

        return await asyncio.get_event_loop().run_in_executor(None, _sync_request)

    async def get_access_token(self, code: str) -> Dict[str, Any]:
        """获取访问令牌"""
        url = f"{self.base_url}/sns/oauth2/access_token"
        params = {
            "appid": self.app_id,
            "secret": self.app_secret,
            "code": code,
            "grant_type": "authorization_code",
        }
        return await self._request(url, params)

    async def get_user_info(self, access_token: str, openid: str) -> WechatUser:
        """获取用户信息"""
        url = f"{self.base_url}/sns/userinfo"
        params = {"access_token": access_token, "openid": openid, "lang": "zh_CN"}
        data = await self._request(url, params)
        return WechatUser.from_api_response(data)

    async def login_with_code(self, code: str) -> WechatUser:
        """使用授权码登录,返回令牌数据和用户信息"""
        token_data = await self.get_access_token(code)
        wechat_user = await self.get_user_info(
            token_data["access_token"], token_data["openid"]
        )
        return wechat_user


# 全局实例
wechat_api = WechatAPIService()

在这段代码中,直接拿了微信的返回值解析,会导致乱码:

data = response.json()

乱码示意

可以看到此处直接解析,拿到的是乱码。

经排查,该内容返回的 encoding 竟然是: ISO-8859-1。

解决方案:

data = response.text.encode("iso-8859-1").decode("utf8")
data = json.loads(data)

手动编码 ISO-8859-1 后,再解码为 UTF-8 即可。