rainycode 1.0.0__tar.gz → 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rainycode-1.0.0 → rainycode-1.0.1}/PKG-INFO +2 -1
- {rainycode-1.0.0/common → rainycode-1.0.1/common_base}/exception.py +2 -2
- {rainycode-1.0.0/common_depend → rainycode-1.0.1/common_depends}/auth_depend.py +3 -3
- {rainycode-1.0.0/common_model → rainycode-1.0.1/common_models}/user_model.py +1 -1
- {rainycode-1.0.0/common_model → rainycode-1.0.1/common_models}/wechat_model.py +1 -1
- rainycode-1.0.1/common_servers/api/auth_api.py +50 -0
- rainycode-1.0.1/common_servers/api/wechat_api.py +48 -0
- rainycode-1.0.1/common_servers/router.py +9 -0
- rainycode-1.0.1/common_servers/schemas/__init__.py +0 -0
- rainycode-1.0.1/common_servers/schemas/auth_schema.py +43 -0
- rainycode-1.0.1/common_servers/service/auth_svc.py +131 -0
- rainycode-1.0.1/common_servers/service/wechat_svc.py +78 -0
- rainycode-1.0.1/common_servers/strategies/base_strategy.py +9 -0
- rainycode-1.0.1/common_servers/strategies/password_strategy.py +34 -0
- rainycode-1.0.1/common_servers/strategies/wechat_stragegy.py +141 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/common_utlis/ip_util.py +1 -2
- rainycode-1.0.1/common_utlis/wechat_util.py +95 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/core/base_config.py +6 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/core/databases/aiodb.py +1 -1
- {rainycode-1.0.0 → rainycode-1.0.1}/core/databases/aioredis.py +1 -1
- {rainycode-1.0.0 → rainycode-1.0.1}/core/middleware/http_middleware.py +1 -1
- {rainycode-1.0.0 → rainycode-1.0.1}/core/start.py +2 -2
- {rainycode-1.0.0 → rainycode-1.0.1}/pyproject.toml +2 -1
- {rainycode-1.0.0 → rainycode-1.0.1}/rainycode.egg-info/PKG-INFO +2 -1
- rainycode-1.0.1/rainycode.egg-info/SOURCES.txt +36 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/rainycode.egg-info/requires.txt +1 -0
- rainycode-1.0.1/rainycode.egg-info/top_level.txt +6 -0
- rainycode-1.0.0/README.md +0 -2
- rainycode-1.0.0/rainycode.egg-info/SOURCES.txt +0 -26
- rainycode-1.0.0/rainycode.egg-info/top_level.txt +0 -5
- {rainycode-1.0.0/common → rainycode-1.0.1/common_base}/aiorequests.py +0 -0
- {rainycode-1.0.0/common → rainycode-1.0.1/common_base}/consts.py +1 -1
- {rainycode-1.0.0/common → rainycode-1.0.1/common_base}/logging.py +0 -0
- {rainycode-1.0.0/common → rainycode-1.0.1/common_base}/response.py +0 -0
- {rainycode-1.0.0/common_model → rainycode-1.0.1/common_models}/base_model.py +0 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/common_utlis/bcrypt_util.py +0 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/common_utlis/captcha_util.py +0 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/common_utlis/jwt_util.py +0 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/common_utlis/snowflake_util.py +0 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/rainycode.egg-info/dependency_links.txt +0 -0
- {rainycode-1.0.0 → rainycode-1.0.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rainycode
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: FastAPI base modules
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Requires-Dist: build==1.4.0
|
|
@@ -10,6 +10,7 @@ Requires-Dist: passlib==1.7.4
|
|
|
10
10
|
Requires-Dist: redis==7.2.1
|
|
11
11
|
Requires-Dist: pyjwt==2.11.0
|
|
12
12
|
Requires-Dist: pillow==12.1.1
|
|
13
|
+
Requires-Dist: pycryptodome==3.23.0
|
|
13
14
|
Requires-Dist: uvicorn[standard]==0.41.0
|
|
14
15
|
Requires-Dist: tortoise-orm[asyncmy]==1.1.5
|
|
15
16
|
Requires-Dist: pydantic-settings==2.13.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from common_base.logging import logger
|
|
2
2
|
from typing import cast
|
|
3
|
-
from
|
|
3
|
+
from common_base.response import ErrorReturn
|
|
4
4
|
from fastapi import HTTPException, Request, status
|
|
5
5
|
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
|
6
6
|
from starlette.responses import JSONResponse
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from fastapi import Depends, Request
|
|
3
3
|
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from common_base.consts import REDIS_PREFIX_BLOCKLIST, REDIS_PREFIX_USER_INFO
|
|
5
|
+
from common_base.exception import CustomException
|
|
6
|
+
from common_models.user_model import User
|
|
7
7
|
from common_utlis.jwt_util import JwtUtil
|
|
8
8
|
from core.databases.aioredis import AioRedis
|
|
9
9
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from common_model.user_model import User
|
|
2
|
+
from fastapi import APIRouter, Security, Body, Depends
|
|
3
|
+
from common.response import SuccessReturn
|
|
4
|
+
from common_depend.auth_depend import login_require, oauth2_scheme
|
|
5
|
+
from common_servers.schemas.auth_schema import *
|
|
6
|
+
from common_servers.service.auth_svc import AuthService
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/auth")
|
|
9
|
+
|
|
10
|
+
@router.post("/register", summary="用户注册")
|
|
11
|
+
async def register(user_in: UserRegister):
|
|
12
|
+
user = await AuthService.register(user_in)
|
|
13
|
+
return SuccessReturn(
|
|
14
|
+
data={"id": str(user.id), "username": user.username}, msg="注册成功"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/login", response_model=TokenResponse, summary="用户密码登录")
|
|
19
|
+
async def login(user_in: UserLogin):
|
|
20
|
+
token_data = await AuthService.login(user_in)
|
|
21
|
+
return SuccessReturn(data=token_data, msg="登录成功")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/refresh", response_model=TokenResponse, summary="刷新令牌")
|
|
25
|
+
async def refresh_token(refresh_token: str = Body(..., embed=True)):
|
|
26
|
+
token_data = await AuthService.refresh_token(refresh_token)
|
|
27
|
+
return SuccessReturn(data=token_data, msg="刷新成功")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.post("/logout", summary="登出")
|
|
31
|
+
async def logout(
|
|
32
|
+
token: str = Depends(oauth2_scheme),
|
|
33
|
+
refresh_token: str = Body(..., embed=True),
|
|
34
|
+
_: User = Security(login_require),
|
|
35
|
+
):
|
|
36
|
+
await AuthService.logout(token, refresh_token)
|
|
37
|
+
return SuccessReturn(msg="登出成功")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.get("/me", response_model=UserInfo, summary="获取当前用户信息")
|
|
41
|
+
async def get_me(user: User = Security(login_require)):
|
|
42
|
+
return SuccessReturn(data=user)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.post(
|
|
46
|
+
"/login/wechat", response_model=TokenResponse, summary="微信登录(小程序/公众号)"
|
|
47
|
+
)
|
|
48
|
+
async def login_wechat(wechat_in: WechatLogin):
|
|
49
|
+
token_data = await AuthService.login_wechat(wechat_in)
|
|
50
|
+
return SuccessReturn(data=token_data, msg="登录成功")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, Query, Response
|
|
2
|
+
from common_servers.service.wechat_svc import WechatService
|
|
3
|
+
|
|
4
|
+
router = APIRouter(prefix="/wechat")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@router.get("/callback/{app_pk}", summary="微信回调验证")
|
|
8
|
+
async def verify_callback(
|
|
9
|
+
app_pk: int,
|
|
10
|
+
signature: str = Query(..., description="微信加密签名"),
|
|
11
|
+
timestamp: str = Query(..., description="时间戳"),
|
|
12
|
+
nonce: str = Query(..., description="随机数"),
|
|
13
|
+
echostr: str = Query(..., description="随机字符串"),
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
微信服务器配置验证接口
|
|
17
|
+
"""
|
|
18
|
+
is_valid = await WechatService.verify_signature(app_pk, signature, timestamp, nonce)
|
|
19
|
+
if is_valid:
|
|
20
|
+
# 必须直接返回 echostr 字符串,不能包含 json 格式
|
|
21
|
+
return Response(content=echostr, media_type="text/plain")
|
|
22
|
+
return Response(content="fail", media_type="text/plain")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.post("/callback/{app_pk}", summary="微信消息回调")
|
|
26
|
+
async def handle_callback(
|
|
27
|
+
app_pk: int,
|
|
28
|
+
request: Request,
|
|
29
|
+
signature: str = Query(..., description="微信加密签名"),
|
|
30
|
+
timestamp: str = Query(..., description="时间戳"),
|
|
31
|
+
nonce: str = Query(..., description="随机数"),
|
|
32
|
+
openid: str = Query(None, description="用户OpenID"),
|
|
33
|
+
encrypt_type: str = Query(None, description="加密类型"),
|
|
34
|
+
msg_signature: str = Query(None, description="消息体签名"),
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
接收微信推送的消息和事件
|
|
38
|
+
"""
|
|
39
|
+
# 获取原始 XML 数据
|
|
40
|
+
body = await request.body()
|
|
41
|
+
xml_data = body.decode("utf-8")
|
|
42
|
+
|
|
43
|
+
# 目前仅实现了明文模式,加密模式需要结合 msg_secret 解密
|
|
44
|
+
# 如果 encrypt_type == "aes",需要解密 (暂未实现,TODO)
|
|
45
|
+
result = await WechatService.handle_message(
|
|
46
|
+
app_pk, xml_data, signature, timestamp, nonce
|
|
47
|
+
)
|
|
48
|
+
return Response(content=result, media_type="application/xml")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
from common_servers.api.auth_api import router as auth_router
|
|
3
|
+
from common_servers.api.wechat_api import router as wechat_router
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
base_router = APIRouter()
|
|
7
|
+
|
|
8
|
+
base_router.include_router(auth_router, tags=["认证模块"])
|
|
9
|
+
base_router.include_router(wechat_router, tags=["微信回调"])
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, EmailStr
|
|
2
|
+
|
|
3
|
+
class UserLogin(BaseModel):
|
|
4
|
+
"""用户登录模型"""
|
|
5
|
+
username: str = Field(..., description="用户名")
|
|
6
|
+
password: str = Field(..., description="密码")
|
|
7
|
+
|
|
8
|
+
class UserRegister(BaseModel):
|
|
9
|
+
"""用户注册模型"""
|
|
10
|
+
username: str = Field(..., description="用户名")
|
|
11
|
+
password: str = Field(..., min_length=6, description="密码")
|
|
12
|
+
email: str | None = Field(None, description="邮箱")
|
|
13
|
+
phone: str | None = Field(None, description="手机号")
|
|
14
|
+
nickname: str | None = Field(None, description="昵称")
|
|
15
|
+
|
|
16
|
+
class TokenResponse(BaseModel):
|
|
17
|
+
"""Token响应模型"""
|
|
18
|
+
access_token: str = Field(..., description="访问令牌")
|
|
19
|
+
refresh_token: str = Field(..., description="刷新令牌")
|
|
20
|
+
token_type: str = Field("bearer", description="令牌类型")
|
|
21
|
+
expires_in: int = Field(..., description="过期时间(秒)")
|
|
22
|
+
|
|
23
|
+
class UserInfo(BaseModel):
|
|
24
|
+
"""用户信息模型"""
|
|
25
|
+
id: int | str = Field(..., description="用户ID")
|
|
26
|
+
username: str = Field(..., description="用户名")
|
|
27
|
+
email: str | None = Field(None, description="邮箱")
|
|
28
|
+
phone: str | None = Field(None, description="手机号")
|
|
29
|
+
nickname: str | None = Field(None, description="昵称")
|
|
30
|
+
avatar: str | None = Field(None, description="头像")
|
|
31
|
+
is_active: bool = Field(..., description="是否激活")
|
|
32
|
+
is_superuser: bool = Field(..., description="是否超级管理员")
|
|
33
|
+
create_time: str | None = Field(None, description="创建时间")
|
|
34
|
+
|
|
35
|
+
class Config:
|
|
36
|
+
from_attributes = True
|
|
37
|
+
|
|
38
|
+
class WechatLogin(BaseModel):
|
|
39
|
+
"""微信登录模型"""
|
|
40
|
+
code: str = Field(..., description="微信授权Code")
|
|
41
|
+
app_id: str = Field(..., description="微信AppID (如 wx...)")
|
|
42
|
+
encrypted_data: str | None = Field(default=None, description="加密数据(小程序一键登录用)")
|
|
43
|
+
iv: str | None = Field(default=None, description="加密算法的初始向量")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
import uuid
|
|
3
|
+
from common.consts import REDIS_PREFIX_BLOCKLIST, REDIS_PREFIX_REFRESH_TOKEN
|
|
4
|
+
from common.exception import CustomException
|
|
5
|
+
from common_model.user_model import User
|
|
6
|
+
from common_utlis.bcrypt_util import PwdUtil
|
|
7
|
+
from common_utlis.jwt_util import JwtUtil
|
|
8
|
+
from core.databases.aioredis import AioRedis
|
|
9
|
+
from common_servers.schemas.auth_schema import UserRegister, UserLogin, WechatLogin
|
|
10
|
+
from common_servers.strategies.wechat_stragegy import WechatAuthStrategy
|
|
11
|
+
from common_servers.strategies.password_strategy import PasswordStrategy
|
|
12
|
+
from tortoise.transactions import atomic
|
|
13
|
+
from tortoise.expressions import Q
|
|
14
|
+
from core.base_config import base_config
|
|
15
|
+
|
|
16
|
+
class AuthService:
|
|
17
|
+
"""认证服务逻辑"""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
@atomic('main')
|
|
21
|
+
async def register(cls, user_in: UserRegister) -> User:
|
|
22
|
+
"""
|
|
23
|
+
用户注册
|
|
24
|
+
"""
|
|
25
|
+
# 合并检查用户名、邮箱、手机号是否存在
|
|
26
|
+
filters = Q(username=user_in.username)
|
|
27
|
+
if user_in.email:
|
|
28
|
+
filters = filters | Q(email=user_in.email)
|
|
29
|
+
if user_in.phone:
|
|
30
|
+
filters = filters | Q(phone=user_in.phone)
|
|
31
|
+
|
|
32
|
+
existing_user = await User.filter(filters).first()
|
|
33
|
+
|
|
34
|
+
if existing_user:
|
|
35
|
+
if existing_user.username == user_in.username:
|
|
36
|
+
raise CustomException(msg="用户名已存在",)
|
|
37
|
+
if user_in.email and existing_user.email == user_in.email:
|
|
38
|
+
raise CustomException(msg="邮箱已存在")
|
|
39
|
+
if user_in.phone and existing_user.phone == user_in.phone:
|
|
40
|
+
raise CustomException(msg="手机号已存在")
|
|
41
|
+
|
|
42
|
+
# 创建用户
|
|
43
|
+
user = await User.create(
|
|
44
|
+
username=user_in.username,
|
|
45
|
+
password=PwdUtil.set_password_hash(user_in.password),
|
|
46
|
+
email=user_in.email,
|
|
47
|
+
phone=user_in.phone,
|
|
48
|
+
nickname=user_in.nickname
|
|
49
|
+
)
|
|
50
|
+
return user
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
async def login(cls, user_in: UserLogin) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
用户密码登录
|
|
56
|
+
"""
|
|
57
|
+
strategy = PasswordStrategy()
|
|
58
|
+
user = await strategy.authenticate(user_in)
|
|
59
|
+
return await cls._create_tokens(user)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
async def login_wechat(cls, wechat_in: WechatLogin) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
微信登录
|
|
65
|
+
"""
|
|
66
|
+
strategy = WechatAuthStrategy()
|
|
67
|
+
user = await strategy.authenticate(wechat_in)
|
|
68
|
+
return await cls._create_tokens(user)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
async def refresh_token(cls, refresh_token: str) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
刷新 Token
|
|
74
|
+
"""
|
|
75
|
+
user_id = await AioRedis.get(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
76
|
+
user_id = int(user_id) if user_id else None
|
|
77
|
+
if not user_id:
|
|
78
|
+
raise CustomException(msg="无效或已过期的刷新令牌")
|
|
79
|
+
|
|
80
|
+
user = await User.get_or_none(id=user_id)
|
|
81
|
+
if not user or not user.is_active:
|
|
82
|
+
raise CustomException(msg="用户不存在或已被禁用")
|
|
83
|
+
|
|
84
|
+
# Token 轮换:删除旧的,生成新的
|
|
85
|
+
await AioRedis.delete(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
86
|
+
return await cls._create_tokens(user)
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
async def logout(cls, access_token: str, refresh_token: str):
|
|
90
|
+
"""
|
|
91
|
+
登出
|
|
92
|
+
1. 将 Access Token 加入黑名单
|
|
93
|
+
2. 删除 Refresh Token
|
|
94
|
+
"""
|
|
95
|
+
# 1. 处理 Access Token 黑名单
|
|
96
|
+
payload = JwtUtil.decode_token(access_token)
|
|
97
|
+
if payload:
|
|
98
|
+
exp_timestamp = payload.get("exp")
|
|
99
|
+
if exp_timestamp:
|
|
100
|
+
now_timestamp = datetime.now().timestamp()
|
|
101
|
+
expire_seconds = int(exp_timestamp - now_timestamp)
|
|
102
|
+
if expire_seconds > 0:
|
|
103
|
+
# 添加 Token 到黑名单
|
|
104
|
+
await AioRedis.set(f"{REDIS_PREFIX_BLOCKLIST}{access_token}", "1", ex=expire_seconds)
|
|
105
|
+
|
|
106
|
+
# 2. 删除 Refresh Token
|
|
107
|
+
if refresh_token:
|
|
108
|
+
await AioRedis.delete(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}")
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
async def _create_tokens(cls, user: User) -> dict:
|
|
112
|
+
"""
|
|
113
|
+
生成双Token并缓存用户信息
|
|
114
|
+
"""
|
|
115
|
+
# Access Token
|
|
116
|
+
token_data = {"sub": str(user.id), "username": user.username}
|
|
117
|
+
# 显式传递配置中的过期时间
|
|
118
|
+
expires_delta = timedelta(minutes=base_config.access_token_expire_minutes)
|
|
119
|
+
access_token = JwtUtil.create_access_token(token_data, expires_delta=expires_delta)
|
|
120
|
+
|
|
121
|
+
# Refresh Token
|
|
122
|
+
refresh_token = str(uuid.uuid4())
|
|
123
|
+
expire_seconds = base_config.refresh_token_expire_days * 24 * 60 * 60
|
|
124
|
+
await AioRedis.set(f"{REDIS_PREFIX_REFRESH_TOKEN}{refresh_token}", str(user.id), ex=expire_seconds)
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"access_token": access_token,
|
|
128
|
+
"refresh_token": refresh_token,
|
|
129
|
+
"token_type": "bearer",
|
|
130
|
+
"expires_in": base_config.access_token_expire_minutes * 60,
|
|
131
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import hashlib
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
from common.exception import CustomException
|
|
5
|
+
from common.logging import logger
|
|
6
|
+
from common_model.wechat_model import WechatApp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WechatService:
|
|
10
|
+
@staticmethod
|
|
11
|
+
async def verify_signature(app_pk: int, signature: str, timestamp: str, nonce: str) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
验证微信签名
|
|
14
|
+
"""
|
|
15
|
+
app = await WechatApp.get_or_none(id=app_pk)
|
|
16
|
+
if not app:
|
|
17
|
+
raise CustomException(msg="微信应用不存在")
|
|
18
|
+
|
|
19
|
+
token = app.token
|
|
20
|
+
if not token:
|
|
21
|
+
logger.error(f"App {app_pk} 未配置Callback Token")
|
|
22
|
+
# 如果未配置Token,无法验证,视为失败
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
tmp_list = [token, timestamp, nonce]
|
|
27
|
+
tmp_list.sort()
|
|
28
|
+
tmp_str = "".join(tmp_list)
|
|
29
|
+
hash_str = hashlib.sha1(tmp_str.encode("utf-8")).hexdigest()
|
|
30
|
+
return hash_str == signature
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"签名验证异常: {e}")
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
async def handle_message(app_pk: int, xml_data: str, signature: str, timestamp: str, nonce: str) -> str:
|
|
37
|
+
"""
|
|
38
|
+
处理微信回调消息
|
|
39
|
+
"""
|
|
40
|
+
# 再次验证签名确保安全
|
|
41
|
+
if not await WechatService.verify_signature(app_pk, signature, timestamp, nonce):
|
|
42
|
+
logger.warning(f"消息验证签名失败:{app_pk},签名={signature},时间戳={timestamp},随机数={nonce}")
|
|
43
|
+
return "fail"
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
root = ET.fromstring(xml_data)
|
|
47
|
+
msg_type = root.findtext("MsgType")
|
|
48
|
+
to_user = root.findtext("ToUserName")
|
|
49
|
+
from_user = root.findtext("FromUserName")
|
|
50
|
+
if from_user is None or to_user is None:
|
|
51
|
+
logger.error(f"消息格式错误:缺少FromUserName或ToUserName")
|
|
52
|
+
return "fail"
|
|
53
|
+
|
|
54
|
+
# 简单的事件分发
|
|
55
|
+
if msg_type == "event":
|
|
56
|
+
event = root.findtext("Event")
|
|
57
|
+
if event == "subscribe":
|
|
58
|
+
# 可以在此处添加用户关注后的业务逻辑,如保存用户,发放优惠券等
|
|
59
|
+
logger.info(f"用户 {from_user} 关注了应用 {app_pk}")
|
|
60
|
+
return WechatService._reply_text(from_user, to_user, "欢迎关注!")
|
|
61
|
+
elif event == "unsubscribe":
|
|
62
|
+
logger.info(f"用户 {from_user} 取消关注应用 {app_pk}")
|
|
63
|
+
|
|
64
|
+
return "success"
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"处理微信消息异常: {e}")
|
|
67
|
+
return "fail"
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _reply_text(to_user: str, from_user: str, content: str) -> str:
|
|
71
|
+
create_time = int(time.time())
|
|
72
|
+
return f"""<xml>
|
|
73
|
+
<ToUserName><![CDATA[{to_user}]]></ToUserName>
|
|
74
|
+
<FromUserName><![CDATA[{from_user}]]></FromUserName>
|
|
75
|
+
<CreateTime>{create_time}</CreateTime>
|
|
76
|
+
<MsgType><![CDATA[text]]></MsgType>
|
|
77
|
+
<Content><![CDATA[{content}]]></Content>
|
|
78
|
+
</xml>"""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from common.consts import REDIS_PREFIX_USER_INFO
|
|
4
|
+
from core.databases.aioredis import AioRedis
|
|
5
|
+
from common_model.user_model import User
|
|
6
|
+
from common_utlis.bcrypt_util import PwdUtil
|
|
7
|
+
from common.exception import CustomException
|
|
8
|
+
from common_servers.schemas.auth_schema import UserLogin
|
|
9
|
+
from common_servers.strategies.base_strategy import IAuthStrategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PasswordStrategy(IAuthStrategy):
|
|
13
|
+
"""
|
|
14
|
+
用户名密码登录策略
|
|
15
|
+
"""
|
|
16
|
+
async def authenticate(self, data: UserLogin) -> User:
|
|
17
|
+
user = await User.get_or_none(username=data.username)
|
|
18
|
+
if not user:
|
|
19
|
+
# 统一错误提示,防止用户名枚举
|
|
20
|
+
raise CustomException(msg="用户名或密码错误")
|
|
21
|
+
|
|
22
|
+
if not PwdUtil.verify_password(data.password, user.password):
|
|
23
|
+
raise CustomException(msg="用户名或密码错误")
|
|
24
|
+
|
|
25
|
+
if not user.is_active:
|
|
26
|
+
raise CustomException(msg="用户已被禁用")
|
|
27
|
+
|
|
28
|
+
# 更新最后登录时间
|
|
29
|
+
user.last_login = datetime.now()
|
|
30
|
+
await user.save(update_fields=['last_login'])
|
|
31
|
+
|
|
32
|
+
# 清除缓存,确保下次获取的是最新信息
|
|
33
|
+
await AioRedis.delete(f"{REDIS_PREFIX_USER_INFO}{user.id}")
|
|
34
|
+
return user
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from common_model.user_model import User
|
|
3
|
+
from common_model.wechat_model import WechatApp, WechatUser, AppTypeEnum
|
|
4
|
+
from common_servers.schemas.auth_schema import WechatLogin
|
|
5
|
+
from common_utlis.wechat_util import WechatUtil
|
|
6
|
+
from common.exception import CustomException
|
|
7
|
+
from common_servers.strategies.base_strategy import IAuthStrategy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from tortoise.transactions import atomic
|
|
11
|
+
|
|
12
|
+
class WechatAuthStrategy(IAuthStrategy):
|
|
13
|
+
"""
|
|
14
|
+
微信登录策略 (支持小程序和公众号)
|
|
15
|
+
"""
|
|
16
|
+
async def authenticate(self, data: WechatLogin) -> User:
|
|
17
|
+
# 1. 获取应用配置
|
|
18
|
+
app_config = await WechatApp.get_or_none(app_id=data.app_id)
|
|
19
|
+
if not app_config:
|
|
20
|
+
raise CustomException(msg="无效的微信应用配置")
|
|
21
|
+
|
|
22
|
+
openid = None
|
|
23
|
+
unionid = None
|
|
24
|
+
session_key = None
|
|
25
|
+
|
|
26
|
+
# 2. 根据类型执行不同流程
|
|
27
|
+
if app_config.app_type == AppTypeEnum.MINI_PROGRAM:
|
|
28
|
+
# 小程序流程
|
|
29
|
+
if data.encrypted_data and data.iv:
|
|
30
|
+
# 一键登录流程 (解密手机号)
|
|
31
|
+
# 前端必须传 code 来换取 session_key (或者确保 redis 中有)
|
|
32
|
+
# 这里强制要求传 code,保证 session_key 是最新的
|
|
33
|
+
wx_res = await WechatUtil.code2session(app_config.app_id, app_config.app_secret, data.code)
|
|
34
|
+
session_key = wx_res.get("session_key")
|
|
35
|
+
openid = wx_res.get("openid")
|
|
36
|
+
unionid = wx_res.get("unionid")
|
|
37
|
+
|
|
38
|
+
if not session_key or not openid:
|
|
39
|
+
raise CustomException(msg="微信登录异常: 缺少必要信息")
|
|
40
|
+
|
|
41
|
+
# 解密手机号
|
|
42
|
+
phone_info = WechatUtil.decrypt_data(session_key, data.encrypted_data, data.iv, app_config.app_id)
|
|
43
|
+
phone_number = phone_info.get("phoneNumber")
|
|
44
|
+
|
|
45
|
+
# 自动注册/绑定逻辑
|
|
46
|
+
return await self._handle_bind_or_register(openid, unionid, app_config, phone=phone_number, session_key=session_key)
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
# 静默登录流程
|
|
50
|
+
wx_res = await WechatUtil.code2session(app_config.app_id, app_config.app_secret, data.code)
|
|
51
|
+
openid = wx_res.get("openid")
|
|
52
|
+
unionid = wx_res.get("unionid")
|
|
53
|
+
session_key = wx_res.get("session_key")
|
|
54
|
+
|
|
55
|
+
if not openid or not session_key:
|
|
56
|
+
raise CustomException(msg="微信登录异常: 缺少必要信息")
|
|
57
|
+
|
|
58
|
+
# 自动注册或登录 (不再强制要求手机号)
|
|
59
|
+
return await self._handle_bind_or_register(openid, unionid, app_config, session_key=session_key)
|
|
60
|
+
|
|
61
|
+
elif app_config.app_type == AppTypeEnum.OFFICIAL_ACCOUNT:
|
|
62
|
+
# 公众号流程
|
|
63
|
+
# 假设 code 是 oauth code
|
|
64
|
+
wx_res = await WechatUtil.get_oauth_access_token(app_config.app_id, app_config.app_secret, data.code)
|
|
65
|
+
openid = wx_res.get("openid")
|
|
66
|
+
unionid = wx_res.get("unionid")
|
|
67
|
+
access_token = wx_res.get("access_token")
|
|
68
|
+
|
|
69
|
+
if not openid or not access_token:
|
|
70
|
+
raise CustomException(msg="微信授权异常: 缺少必要信息")
|
|
71
|
+
|
|
72
|
+
# 检查是否已绑定
|
|
73
|
+
wechat_user = await WechatUser.get_or_none(openid=openid, wechat_app=app_config).select_related("user")
|
|
74
|
+
if wechat_user:
|
|
75
|
+
return wechat_user.user
|
|
76
|
+
|
|
77
|
+
# 未绑定,获取用户信息并自动注册/绑定
|
|
78
|
+
# 如果是 snsapi_base,可能没有 access_token (获取不到 userinfo),这里假设 snsapi_userinfo
|
|
79
|
+
# 如果 scope 是 snsapi_base,这里只能拿到 openid。
|
|
80
|
+
# 策略:如果是 snsapi_base 且未绑定,返回 428,前端再次请求(scope=snsapi_userinfo)
|
|
81
|
+
scope = wx_res.get("scope", "")
|
|
82
|
+
if "snsapi_userinfo" not in scope and not wechat_user:
|
|
83
|
+
raise CustomException(msg="需授权获取信息", data={"openid": openid})
|
|
84
|
+
|
|
85
|
+
user_info = await WechatUtil.get_user_info(access_token, openid)
|
|
86
|
+
return await self._handle_bind_or_register(openid, unionid, app_config, nickname=user_info.get("nickname"), avatar=user_info.get("headimgurl"))
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
raise CustomException(msg="不支持的应用类型")
|
|
90
|
+
|
|
91
|
+
@atomic('main')
|
|
92
|
+
async def _handle_bind_or_register(self, openid, unionid, app_config, phone=None, session_key=None, nickname=None, avatar=None) -> User:
|
|
93
|
+
# 1. 检查是否已有 WechatUser
|
|
94
|
+
wechat_user = await WechatUser.get_or_none(openid=openid, wechat_app=app_config).select_related("user")
|
|
95
|
+
if wechat_user:
|
|
96
|
+
# 已存在关联,更新信息
|
|
97
|
+
if session_key: wechat_user.session_key = session_key
|
|
98
|
+
if nickname: wechat_user.nickname = nickname
|
|
99
|
+
if avatar: wechat_user.avatar = avatar
|
|
100
|
+
await wechat_user.save()
|
|
101
|
+
return wechat_user.user
|
|
102
|
+
|
|
103
|
+
# 2. 检查 User 是否存在 (通过 phone 或 unionid)
|
|
104
|
+
user = None
|
|
105
|
+
if phone:
|
|
106
|
+
user = await User.get_or_none(phone=phone)
|
|
107
|
+
|
|
108
|
+
# 如果有 UnionID,检查其他应用下的 WechatUser 是否关联了 User
|
|
109
|
+
if not user and unionid:
|
|
110
|
+
other_wechat_user = await WechatUser.filter(unionid=unionid).exclude(wechat_app=app_config).first().select_related("user")
|
|
111
|
+
if other_wechat_user:
|
|
112
|
+
user = other_wechat_user.user
|
|
113
|
+
|
|
114
|
+
# 3. 创建 User
|
|
115
|
+
if not user:
|
|
116
|
+
# 创建新用户
|
|
117
|
+
username = phone if phone else f"wx_{openid[:8]}"
|
|
118
|
+
# 避免用户名冲突
|
|
119
|
+
while await User.exists(username=username):
|
|
120
|
+
username = f"{username}_{random.randint(1000,9999)}"
|
|
121
|
+
|
|
122
|
+
user = await User.create(
|
|
123
|
+
username=username,
|
|
124
|
+
password="", # 无密码
|
|
125
|
+
phone=phone,
|
|
126
|
+
nickname=nickname or f"用户{username[-4:]}",
|
|
127
|
+
avatar=avatar
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# 4. 创建 WechatUser 关联
|
|
131
|
+
await WechatUser.create(
|
|
132
|
+
user=user,
|
|
133
|
+
wechat_app=app_config,
|
|
134
|
+
openid=openid,
|
|
135
|
+
unionid=unionid,
|
|
136
|
+
session_key=session_key,
|
|
137
|
+
nickname=nickname,
|
|
138
|
+
avatar=avatar
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return user
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from common import aiorequests
|
|
2
|
+
from common.exception import CustomException
|
|
3
|
+
from common.logging import logger
|
|
4
|
+
from Crypto.Cipher import AES
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
class WechatUtil:
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
async def code2session(cls, appid: str, secret: str, code: str) -> dict:
|
|
12
|
+
"""
|
|
13
|
+
小程序登录凭证校验
|
|
14
|
+
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
|
|
15
|
+
"""
|
|
16
|
+
url = "https://api.weixin.qq.com/sns/jscode2session"
|
|
17
|
+
params = {
|
|
18
|
+
"appid": appid,
|
|
19
|
+
"secret": secret,
|
|
20
|
+
"js_code": code,
|
|
21
|
+
"grant_type": "authorization_code"
|
|
22
|
+
}
|
|
23
|
+
try:
|
|
24
|
+
data = await aiorequests.get(url, params=params)
|
|
25
|
+
if "errcode" in data and data["errcode"] != 0:
|
|
26
|
+
logger.error(f"Wechat code2session error: {data}")
|
|
27
|
+
raise CustomException(msg=f"微信登录失败: {data.get('errmsg')}")
|
|
28
|
+
return data
|
|
29
|
+
except Exception as e:
|
|
30
|
+
if isinstance(e, CustomException):
|
|
31
|
+
raise e
|
|
32
|
+
logger.error(f"Wechat API request error: {e}")
|
|
33
|
+
raise CustomException(msg="微信服务暂时不可用")
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
async def get_oauth_access_token(cls, appid: str, secret: str, code: str) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
公众号通过code获取access_token
|
|
39
|
+
"""
|
|
40
|
+
url = "https://api.weixin.qq.com/sns/oauth2/access_token"
|
|
41
|
+
params = {
|
|
42
|
+
"appid": appid,
|
|
43
|
+
"secret": secret,
|
|
44
|
+
"code": code,
|
|
45
|
+
"grant_type": "authorization_code"
|
|
46
|
+
}
|
|
47
|
+
data = await aiorequests.get(url, params=params)
|
|
48
|
+
if "errcode" in data and data["errcode"] != 0:
|
|
49
|
+
raise CustomException(msg=f"微信授权失败: {data.get('errmsg')}")
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
async def get_user_info(cls, access_token: str, openid: str) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
公众号获取用户信息
|
|
56
|
+
"""
|
|
57
|
+
url = "https://api.weixin.qq.com/sns/userinfo"
|
|
58
|
+
params = {
|
|
59
|
+
"access_token": access_token,
|
|
60
|
+
"openid": openid,
|
|
61
|
+
"lang": "zh_CN"
|
|
62
|
+
}
|
|
63
|
+
data = await aiorequests.get(url, params=params)
|
|
64
|
+
if "errcode" in data and data["errcode"] != 0:
|
|
65
|
+
raise CustomException(msg=f"获取微信用户信息失败: {data.get('errmsg')}")
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def decrypt_data(
|
|
70
|
+
cls, session_key: str, encrypted_data: str, iv: str, appid: str
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
小程序解密用户信息/手机号
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
session_key_b = base64.b64decode(session_key)
|
|
77
|
+
encrypted_data_b = base64.b64decode(encrypted_data)
|
|
78
|
+
iv_b = base64.b64decode(iv)
|
|
79
|
+
|
|
80
|
+
cipher = AES.new(session_key_b, AES.MODE_CBC, iv_b)
|
|
81
|
+
decrypted = cipher.decrypt(encrypted_data_b)
|
|
82
|
+
|
|
83
|
+
# 去除PKCS#7填充
|
|
84
|
+
unpad = lambda s: s[:-ord(s[len(s)-1:])]
|
|
85
|
+
decrypted_json = unpad(decrypted)
|
|
86
|
+
|
|
87
|
+
data = json.loads(decrypted_json)
|
|
88
|
+
|
|
89
|
+
if data['watermark']['appid'] != appid:
|
|
90
|
+
raise CustomException(msg="Invalid Buffer")
|
|
91
|
+
|
|
92
|
+
return data
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Decrypt wechat data failed: {e}")
|
|
95
|
+
raise CustomException(msg="微信数据解密失败")
|
|
@@ -6,6 +6,7 @@ class BaseConfig(BaseSettings):
|
|
|
6
6
|
app_title: str = ''
|
|
7
7
|
app_name: str = ''
|
|
8
8
|
|
|
9
|
+
# 数据库
|
|
9
10
|
redis_url: str = ''
|
|
10
11
|
mysql_url: str = ''
|
|
11
12
|
|
|
@@ -18,6 +19,11 @@ class BaseConfig(BaseSettings):
|
|
|
18
19
|
# jwt密钥
|
|
19
20
|
jwt_secret_key: str = "bgb0tnl9d58+6n-6h-ea&u^1#s0ccp!794=krylacjq75vzps$"
|
|
20
21
|
|
|
22
|
+
# Access Token 有效期
|
|
23
|
+
access_token_expire_minutes: int = 30
|
|
24
|
+
# Refresh Token 有效期
|
|
25
|
+
refresh_token_expire_days: int = 30
|
|
26
|
+
|
|
21
27
|
model_config = SettingsConfigDict(
|
|
22
28
|
env_file=('envs/dev.env', 'envs/pro.env'),
|
|
23
29
|
env_file_encoding="utf-8",
|
|
@@ -7,11 +7,11 @@ from tortoise.models import Model
|
|
|
7
7
|
from tortoise.contrib.fastapi import RegisterTortoise
|
|
8
8
|
from tortoise.queryset import QuerySetSingle, QuerySet
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
|
-
|
|
11
10
|
from core.base_config import base_config
|
|
12
11
|
|
|
13
12
|
db_models = [
|
|
14
13
|
'aerich.models',
|
|
14
|
+
'common_models',
|
|
15
15
|
'models'
|
|
16
16
|
]
|
|
17
17
|
|
|
@@ -5,7 +5,7 @@ from typing import Optional, Union, Callable, Mapping
|
|
|
5
5
|
from redis.asyncio import Redis
|
|
6
6
|
from redis import asyncio as aioredis
|
|
7
7
|
from redis.typing import KeyT, EncodableT, ExpiryT, ZScoreBoundT, AnyKeyT
|
|
8
|
-
from
|
|
8
|
+
from common_base.logging import log_exc
|
|
9
9
|
from core.base_config import base_config
|
|
10
10
|
|
|
11
11
|
|
|
@@ -3,7 +3,7 @@ from fastapi import Request
|
|
|
3
3
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
4
4
|
from starlette.types import ASGIApp
|
|
5
5
|
from starlette.responses import Response
|
|
6
|
-
from
|
|
6
|
+
from common_base.logging import logger
|
|
7
7
|
|
|
8
8
|
class RequestHttpMiddleware(BaseHTTPMiddleware):
|
|
9
9
|
def __init__(self, app: ASGIApp) -> None:
|
|
@@ -5,8 +5,8 @@ from fastapi import FastAPI, HTTPException
|
|
|
5
5
|
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
|
6
6
|
from starlette.middleware.cors import CORSMiddleware
|
|
7
7
|
from starlette.staticfiles import StaticFiles
|
|
8
|
-
from
|
|
9
|
-
from
|
|
8
|
+
from common_base.exception import AllExceptionHandler, CustomException, CustomExceptionHandler, HttpExceptionHandler, RequestValidationHandle, ResponseValidationHandle, ValueExceptionHandler
|
|
9
|
+
from common_base.logging import logger
|
|
10
10
|
from core.base_config import BaseConfig, base_config
|
|
11
11
|
from core.databases.aiodb import AioDb
|
|
12
12
|
from core.middleware.http_middleware import RequestHttpMiddleware
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rainycode"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "FastAPI base modules"
|
|
9
9
|
requires-python = ">=3.8"
|
|
10
10
|
|
|
@@ -16,6 +16,7 @@ dependencies = [
|
|
|
16
16
|
"redis == 7.2.1",
|
|
17
17
|
"pyjwt == 2.11.0",
|
|
18
18
|
"pillow == 12.1.1",
|
|
19
|
+
"pycryptodome == 3.23.0",
|
|
19
20
|
"uvicorn[standard] == 0.41.0",
|
|
20
21
|
"tortoise-orm[asyncmy] == 1.1.5",
|
|
21
22
|
"pydantic-settings == 2.13.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rainycode
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: FastAPI base modules
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Requires-Dist: build==1.4.0
|
|
@@ -10,6 +10,7 @@ Requires-Dist: passlib==1.7.4
|
|
|
10
10
|
Requires-Dist: redis==7.2.1
|
|
11
11
|
Requires-Dist: pyjwt==2.11.0
|
|
12
12
|
Requires-Dist: pillow==12.1.1
|
|
13
|
+
Requires-Dist: pycryptodome==3.23.0
|
|
13
14
|
Requires-Dist: uvicorn[standard]==0.41.0
|
|
14
15
|
Requires-Dist: tortoise-orm[asyncmy]==1.1.5
|
|
15
16
|
Requires-Dist: pydantic-settings==2.13.1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
common_base/aiorequests.py
|
|
3
|
+
common_base/consts.py
|
|
4
|
+
common_base/exception.py
|
|
5
|
+
common_base/logging.py
|
|
6
|
+
common_base/response.py
|
|
7
|
+
common_depends/auth_depend.py
|
|
8
|
+
common_models/base_model.py
|
|
9
|
+
common_models/user_model.py
|
|
10
|
+
common_models/wechat_model.py
|
|
11
|
+
common_servers/router.py
|
|
12
|
+
common_servers/api/auth_api.py
|
|
13
|
+
common_servers/api/wechat_api.py
|
|
14
|
+
common_servers/schemas/__init__.py
|
|
15
|
+
common_servers/schemas/auth_schema.py
|
|
16
|
+
common_servers/service/auth_svc.py
|
|
17
|
+
common_servers/service/wechat_svc.py
|
|
18
|
+
common_servers/strategies/base_strategy.py
|
|
19
|
+
common_servers/strategies/password_strategy.py
|
|
20
|
+
common_servers/strategies/wechat_stragegy.py
|
|
21
|
+
common_utlis/bcrypt_util.py
|
|
22
|
+
common_utlis/captcha_util.py
|
|
23
|
+
common_utlis/ip_util.py
|
|
24
|
+
common_utlis/jwt_util.py
|
|
25
|
+
common_utlis/snowflake_util.py
|
|
26
|
+
common_utlis/wechat_util.py
|
|
27
|
+
core/base_config.py
|
|
28
|
+
core/start.py
|
|
29
|
+
core/databases/aiodb.py
|
|
30
|
+
core/databases/aioredis.py
|
|
31
|
+
core/middleware/http_middleware.py
|
|
32
|
+
rainycode.egg-info/PKG-INFO
|
|
33
|
+
rainycode.egg-info/SOURCES.txt
|
|
34
|
+
rainycode.egg-info/dependency_links.txt
|
|
35
|
+
rainycode.egg-info/requires.txt
|
|
36
|
+
rainycode.egg-info/top_level.txt
|
rainycode-1.0.0/README.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
README.md
|
|
2
|
-
pyproject.toml
|
|
3
|
-
common/aiorequests.py
|
|
4
|
-
common/consts.py
|
|
5
|
-
common/exception.py
|
|
6
|
-
common/logging.py
|
|
7
|
-
common/response.py
|
|
8
|
-
common_depend/auth_depend.py
|
|
9
|
-
common_model/base_model.py
|
|
10
|
-
common_model/user_model.py
|
|
11
|
-
common_model/wechat_model.py
|
|
12
|
-
common_utlis/bcrypt_util.py
|
|
13
|
-
common_utlis/captcha_util.py
|
|
14
|
-
common_utlis/ip_util.py
|
|
15
|
-
common_utlis/jwt_util.py
|
|
16
|
-
common_utlis/snowflake_util.py
|
|
17
|
-
core/base_config.py
|
|
18
|
-
core/start.py
|
|
19
|
-
core/databases/aiodb.py
|
|
20
|
-
core/databases/aioredis.py
|
|
21
|
-
core/middleware/http_middleware.py
|
|
22
|
-
rainycode.egg-info/PKG-INFO
|
|
23
|
-
rainycode.egg-info/SOURCES.txt
|
|
24
|
-
rainycode.egg-info/dependency_links.txt
|
|
25
|
-
rainycode.egg-info/requires.txt
|
|
26
|
-
rainycode.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|