做了一些有限修正
This commit is contained in:
@@ -54,5 +54,26 @@ def custom_exception_handler(exc, context):
|
||||
|
||||
return Response(standard_response, status=response.status_code)
|
||||
|
||||
# 对于非DRF异常,这里可以根据需要处理
|
||||
# 对于非DRF异常(response为None),生产环境返回通用错误信息
|
||||
if response is None:
|
||||
from django.conf import settings
|
||||
request = context.get('request')
|
||||
user_info = '未知'
|
||||
if request:
|
||||
user_info = f"用户: {request.user.id if request.user.is_authenticated else '未登录'}"
|
||||
logger.error(
|
||||
f"未处理异常 - 路径: {request.path if request else '未知'}, "
|
||||
f"方法: {request.method if request else '未知'}, {user_info}, "
|
||||
f"错误类型: {type(exc).__name__}, 错误: {str(exc)}"
|
||||
)
|
||||
if not settings.DEBUG:
|
||||
return Response(
|
||||
{'code': 500, 'message': '服务器内部错误,请稍后重试'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
return Response(
|
||||
{'code': 500, 'message': str(exc)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -5,255 +5,6 @@ from django.conf import settings
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
import io
|
||||
|
||||
'''try:
|
||||
from qcloud_cos import CosConfig, CosS3Client
|
||||
|
||||
TENCENT_COS_AVAILABLE = True
|
||||
except ImportError:
|
||||
TENCENT_COS_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import oss2
|
||||
|
||||
ALIYUN_OSS_AVAILABLE = True
|
||||
except ImportError:
|
||||
ALIYUN_OSS_AVAILABLE = False
|
||||
|
||||
|
||||
def validate_image(file):
|
||||
"""
|
||||
验证图片文件
|
||||
使用Pillow检查图片是否有效
|
||||
"""
|
||||
try:
|
||||
# 打开图片但不保存到磁盘
|
||||
img = Image.open(file)
|
||||
img.verify() # 验证文件完整性
|
||||
|
||||
# 重置文件指针(因为PIL读取后会移动指针)
|
||||
file.seek(0)
|
||||
|
||||
# 检查图片格式
|
||||
allowed_formats = ['JPEG', 'PNG', 'GIF', 'WEBP', 'BMP']
|
||||
if img.format not in allowed_formats:
|
||||
return False, f"不支持{img.format}格式,请使用JPEG、PNG、GIF、WEBP或BMP格式"
|
||||
|
||||
# 检查图片大小(最大5MB)
|
||||
file_size = len(file.read())
|
||||
file.seek(0)
|
||||
if file_size > 5 * 1024 * 1024:
|
||||
return False, "图片大小不能超过5MB"
|
||||
|
||||
# 检查图片尺寸(最大2000x2000)
|
||||
img = Image.open(file)
|
||||
file.seek(0)
|
||||
if img.width > 3000 or img.height > 3000:
|
||||
return False, "图片尺寸不能超过2000x2000像素"
|
||||
|
||||
return True, "图片验证通过"
|
||||
|
||||
except UnidentifiedImageError:
|
||||
return False, "文件不是有效的图片"
|
||||
except Exception as e:
|
||||
return False, f"图片验证失败: {str(e)}"
|
||||
|
||||
|
||||
def get_oss_client():
|
||||
"""
|
||||
获取OSS客户端
|
||||
根据配置自动选择云厂商
|
||||
"""
|
||||
oss_type = getattr(settings, 'OSS_TYPE', 'tencent') # 默认为腾讯云
|
||||
|
||||
if oss_type == 'tencent' and TENCENT_COS_AVAILABLE:
|
||||
# 腾讯云COS
|
||||
config = CosConfig(
|
||||
Region=getattr(settings, 'COS_REGION', 'ap-shanghai'),
|
||||
SecretId=getattr(settings, 'COS_SECRET_ID', ''),
|
||||
SecretKey=getattr(settings, 'COS_SECRET_KEY', ''),
|
||||
Scheme=getattr(settings, 'COS_SCHEME', 'https')
|
||||
)
|
||||
return CosS3Client(config)
|
||||
|
||||
elif oss_type == 'aliyun' and ALIYUN_OSS_AVAILABLE:
|
||||
# 阿里云OSS
|
||||
auth = oss2.Auth(
|
||||
getattr(settings, 'ALIYUN_ACCESS_KEY_ID', ''),
|
||||
getattr(settings, 'ALIYUN_ACCESS_KEY_SECRET', '')
|
||||
)
|
||||
endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', '')
|
||||
bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET', '')
|
||||
return oss2.Bucket(auth, endpoint, bucket_name)
|
||||
|
||||
else:
|
||||
raise ImportError(f"不支持{oss_type}云存储或相关SDK未安装")
|
||||
|
||||
|
||||
def upload_to_oss(file_obj, file_path):
|
||||
"""
|
||||
通用上传文件到OSS
|
||||
file_obj: Django的UploadedFile对象或类文件对象
|
||||
file_path: 在OSS中的相对路径,如 'avatar/123/abc.jpg'
|
||||
返回: 文件访问URL
|
||||
"""
|
||||
try:
|
||||
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
|
||||
|
||||
if oss_type == 'tencent':
|
||||
return _upload_to_tencent_cos(file_obj, file_path)
|
||||
elif oss_type == 'aliyun':
|
||||
return _upload_to_aliyun_oss(file_obj, file_path)
|
||||
else:
|
||||
raise ValueError(f"不支持的OSS类型: {oss_type}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"OSS上传失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _upload_to_tencent_cos(file_obj, file_path):
|
||||
"""
|
||||
上传到腾讯云COS
|
||||
"""
|
||||
client = get_oss_client()
|
||||
|
||||
# 获取文件扩展名
|
||||
ext = file_path.split('.')[-1].lower() if '.' in file_path else ''
|
||||
content_type_map = {
|
||||
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
|
||||
'png': 'image/png', 'gif': 'image/gif',
|
||||
'webp': 'image/webp', 'bmp': 'image/bmp'
|
||||
}
|
||||
content_type = content_type_map.get(ext, 'image/jpeg')
|
||||
|
||||
# 如果是Django的UploadedFile,需要读取内容
|
||||
if hasattr(file_obj, 'read'):
|
||||
file_content = file_obj.read()
|
||||
else:
|
||||
file_content = file_obj
|
||||
|
||||
# 上传文件
|
||||
response = client.put_object(
|
||||
Bucket=getattr(settings, 'COS_BUCKET', ''),
|
||||
Body=file_content,
|
||||
Key=file_path,
|
||||
ContentType=content_type,
|
||||
ContentDisposition='inline'
|
||||
)
|
||||
|
||||
# 返回完整URL
|
||||
domain = getattr(settings, 'COS_DOMAIN', '')
|
||||
return f"{domain}/{file_path}"
|
||||
|
||||
|
||||
def _upload_to_aliyun_oss(file_obj, file_path):
|
||||
"""
|
||||
上传到阿里云OSS
|
||||
"""
|
||||
bucket = get_oss_client()
|
||||
|
||||
# 如果是Django的UploadedFile,需要读取内容
|
||||
if hasattr(file_obj, 'read'):
|
||||
file_content = file_obj.read()
|
||||
else:
|
||||
file_content = file_obj
|
||||
|
||||
# 上传文件
|
||||
result = bucket.put_object(file_path, file_content)
|
||||
|
||||
if result.status == 200:
|
||||
# 阿里云OSS返回的URL
|
||||
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
|
||||
return f"{domain}/{file_path}"
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def delete_from_oss(file_url):
|
||||
"""
|
||||
从OSS删除文件
|
||||
file_url: 可以是完整URL或相对路径
|
||||
"""
|
||||
try:
|
||||
if not file_url:
|
||||
return False
|
||||
|
||||
oss_type = getattr(settings, 'OSS_TYPE', 'tencent')
|
||||
|
||||
# 从URL中提取文件路径
|
||||
if oss_type == 'tencent':
|
||||
domain = getattr(settings, 'COS_DOMAIN', '')
|
||||
|
||||
# 判断是完整URL还是相对路径
|
||||
if file_url.startswith('http'):
|
||||
# 完整URL
|
||||
if domain not in file_url:
|
||||
return False # URL不属于我们的域名
|
||||
file_key = file_url.replace(f"{domain}/", "")
|
||||
else:
|
||||
# 相对路径,直接使用
|
||||
file_key = file_url
|
||||
|
||||
return _delete_from_tencent_cos(file_key)
|
||||
|
||||
elif oss_type == 'aliyun':
|
||||
domain = getattr(settings, 'ALIYUN_OSS_DOMAIN', '')
|
||||
|
||||
if file_url.startswith('http'):
|
||||
if domain not in file_url:
|
||||
return False
|
||||
file_key = file_url.replace(f"{domain}/", "")
|
||||
else:
|
||||
file_key = file_url
|
||||
|
||||
return _delete_from_aliyun_oss(file_key)
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"OSS删除失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _delete_from_tencent_cos(file_key):
|
||||
"""
|
||||
从腾讯云COS删除文件
|
||||
"""
|
||||
client = get_oss_client()
|
||||
|
||||
try:
|
||||
client.delete_object(
|
||||
Bucket=getattr(settings, 'COS_BUCKET', ''),
|
||||
Key=file_key
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"腾讯云COS删除失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _delete_from_aliyun_oss(file_key):
|
||||
"""
|
||||
从阿里云OSS删除文件
|
||||
"""
|
||||
bucket = get_oss_client()
|
||||
|
||||
try:
|
||||
bucket.delete_object(file_key)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"阿里云OSS删除失败: {e}")
|
||||
return False'''
|
||||
|
||||
|
||||
|
||||
# utils/oss_utils.py
|
||||
import os
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
import io
|
||||
|
||||
try:
|
||||
from qcloud_cos import CosConfig, CosS3Client
|
||||
@@ -508,4 +259,4 @@ def _delete_from_aliyun_oss(file_key):
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"阿里云OSS删除失败: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
import redis
|
||||
import uuid
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Redis客户端(延迟初始化)
|
||||
_redis_client = None
|
||||
|
||||
def _get_redis_client():
|
||||
global _redis_client
|
||||
if _redis_client is None:
|
||||
import redis
|
||||
_redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=settings.REDIS_DB_CELERY,
|
||||
decode_responses=True,
|
||||
)
|
||||
return _redis_client
|
||||
|
||||
redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=settings.REDIS_PORT,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=2,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
def acquire_lock(lock_key, timeout=10):
|
||||
"""获取锁,返回是否成功"""
|
||||
return redis_client.set(lock_key, '1', nx=True, ex=timeout)
|
||||
"""获取分布式锁,返回锁标识符,失败返回None"""
|
||||
client = _get_redis_client()
|
||||
identifier = str(uuid.uuid4())
|
||||
result = client.set(lock_key, identifier, nx=True, ex=timeout)
|
||||
if result:
|
||||
logger.debug(f"获取锁成功: {lock_key}, 标识: {identifier}")
|
||||
return identifier
|
||||
return None
|
||||
|
||||
def release_lock(lock_key):
|
||||
"""释放锁"""
|
||||
redis_client.delete(lock_key)
|
||||
|
||||
def release_lock(lock_key, identifier):
|
||||
"""释放分布式锁(仅当标识符匹配时才释放)"""
|
||||
if identifier is None:
|
||||
return False
|
||||
client = _get_redis_client()
|
||||
lua_script = """
|
||||
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
"""
|
||||
try:
|
||||
result = client.eval(lua_script, 1, lock_key, identifier)
|
||||
if result:
|
||||
logger.debug(f"释放锁成功: {lock_key}")
|
||||
else:
|
||||
logger.warning(f"释放锁失败(标识不匹配或锁已过期): {lock_key}")
|
||||
return bool(result)
|
||||
except Exception as e:
|
||||
logger.error(f"释放锁异常: {lock_key}, 错误: {e}")
|
||||
return False
|
||||
|
||||
@@ -1,112 +1,4 @@
|
||||
# utils/wechat_v3.py
|
||||
'''import time
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import rsa
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def load_private_key():
|
||||
"""加载商户私钥(PEM格式)"""
|
||||
with open(settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH'], 'rb') as f:
|
||||
key_data = f.read()
|
||||
return rsa.PrivateKey.load_pkcs1(key_data)
|
||||
|
||||
|
||||
def build_authorization(method, url, body):
|
||||
"""
|
||||
生成V3接口的Authorization头
|
||||
:param method: GET/POST
|
||||
:param url: 完整URL(含域名和路径)
|
||||
:param body: 请求体JSON字符串
|
||||
:return: Authorization字符串
|
||||
"""
|
||||
private_key = load_private_key()
|
||||
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
|
||||
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
|
||||
|
||||
# 提取URL路径(不含query)
|
||||
parsed = urlparse(url)
|
||||
path = parsed.path
|
||||
if parsed.query:
|
||||
path += '?' + parsed.query
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32]
|
||||
|
||||
# 构造签名串
|
||||
message = '\n'.join([method, path, timestamp, nonce, body]) + '\n'
|
||||
|
||||
# 使用私钥签名(SHA256)
|
||||
signature = rsa.sign(message.encode('utf-8'), private_key, 'SHA-256')
|
||||
signature_b64 = base64.b64encode(signature).decode('utf-8')
|
||||
|
||||
# 组装Authorization
|
||||
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
|
||||
return auth
|
||||
|
||||
|
||||
def verify_wechat_sign(headers, body):
|
||||
"""
|
||||
验证微信回调的签名
|
||||
需要提前从微信平台下载证书公钥,存放在 PLATFORM_CERT_DIR 下,文件名 = 序列号.pem
|
||||
:param headers: 请求头
|
||||
:param body: 原始请求体字符串
|
||||
:return: bool
|
||||
"""
|
||||
serial = headers.get('Wechatpay-Serial')
|
||||
signature = headers.get('Wechatpay-Signature')
|
||||
timestamp = headers.get('Wechatpay-Timestamp')
|
||||
nonce = headers.get('Wechatpay-Nonce')
|
||||
|
||||
if not all([serial, signature, timestamp, nonce]):
|
||||
return False
|
||||
|
||||
# 构造验签名串
|
||||
message = '\n'.join([timestamp, nonce, body]) + '\n'
|
||||
|
||||
# 读取对应序列号的平台证书公钥
|
||||
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
|
||||
try:
|
||||
with open(cert_path, 'rb') as f:
|
||||
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(f.read())
|
||||
except FileNotFoundError:
|
||||
# 如果证书不存在,可尝试下载(生产环境建议定时下载)
|
||||
return False
|
||||
|
||||
signature_bytes = base64.b64decode(signature)
|
||||
try:
|
||||
rsa.verify(message.encode('utf-8'), signature_bytes, pub_key)
|
||||
return True
|
||||
except rsa.VerificationError:
|
||||
return False
|
||||
|
||||
|
||||
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
|
||||
"""
|
||||
使用APIv3密钥解密回调中的加密数据
|
||||
:param associated_data: 附加数据
|
||||
:param nonce: 随机串
|
||||
:param ciphertext: 密文(Base64)
|
||||
:return: 解密后的JSON字符串
|
||||
"""
|
||||
api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8')
|
||||
nonce_bytes = nonce.encode('utf-8')
|
||||
ciphertext_bytes = base64.b64decode(ciphertext)
|
||||
|
||||
# AES-GCM解密
|
||||
cipher = AES.new(api_v3_key, AES.MODE_GCM, nonce=nonce_bytes)
|
||||
if associated_data:
|
||||
cipher.update(associated_data.encode('utf-8'))
|
||||
plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:])
|
||||
return plaintext.decode('utf-8')'''
|
||||
|
||||
# utils/wechat_v3.py
|
||||
import re
|
||||
import time
|
||||
import hashlib
|
||||
import base64
|
||||
@@ -178,6 +70,16 @@ def verify_wechat_sign(headers, body):
|
||||
if not all([serial, signature, timestamp, nonce]):
|
||||
return False
|
||||
|
||||
if not re.match(r'^[A-Za-z0-9-]+$', serial):
|
||||
return False
|
||||
|
||||
try:
|
||||
ts = int(headers.get('Wechatpay-Timestamp', '0'))
|
||||
if abs(time.time() - ts) > 300: # 5分钟有效期
|
||||
return False
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
message = '\n'.join([timestamp, nonce, body]) + '\n'
|
||||
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
|
||||
if not os.path.exists(cert_path):
|
||||
@@ -204,4 +106,4 @@ def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
|
||||
if associated_data:
|
||||
cipher.update(associated_data.encode('utf-8'))
|
||||
plaintext = cipher.decrypt_and_verify(ciphertext_bytes[:-16], ciphertext_bytes[-16:])
|
||||
return plaintext.decode('utf-8')
|
||||
return plaintext.decode('utf-8')
|
||||
|
||||
Reference in New Issue
Block a user