This commit is contained in:
2026-04-05 16:50:12 +08:00
parent 7d2a072b7d
commit 579db5e918
47 changed files with 1853 additions and 71 deletions
+64 -32
View File
@@ -5,11 +5,22 @@
</component>
<component name="ChangeListManager">
<list default="true" id="a7465ceb-8d86-4d89-899e-8242cccf605d" name="更改" comment="test">
<change afterPath="$PROJECT_DIR$/api/views/BaiduFanyiView.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/4c9da042-07f7-4b23-8e8e-8db67cdad1a8.png" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/views/info/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/views/info/baidu_fanyi_appid.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/api/views/info/baidu_lang_info.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/user/views/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/user/views/user.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/utils/RandCode.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/api/tests.py" beforeDir="false" afterPath="$PROJECT_DIR$/api/tests.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/api/urls.py" beforeDir="false" afterPath="$PROJECT_DIR$/api/urls.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/api/views/GetIPDataView.py" beforeDir="false" afterPath="$PROJECT_DIR$/api/views/GetIPDataView.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/api/views/BaiduFanyiView.py" beforeDir="false" afterPath="$PROJECT_DIR$/api/views/BaiduFanyiView.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/chunyu_project/settings.py" beforeDir="false" afterPath="$PROJECT_DIR$/chunyu_project/settings.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/user/migrations/0001_initial.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/user/models.py" beforeDir="false" afterPath="$PROJECT_DIR$/user/models.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/user/urls.py" beforeDir="false" afterPath="$PROJECT_DIR$/user/urls.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/user/views.py" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -41,40 +52,40 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Django 服务器.chunyu_project.executor": "Run",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"Notification.DisplayName-DoNotAsk-DatabaseConfigFileWatcher.found": "发现新的/更新的数据库配置",
"Notification.DoNotAsk-DatabaseConfigFileWatcher.found": "true",
"Python.tests.executor": "Run",
"RunOnceActivity.MCP Project settings loaded": "true",
"RunOnceActivity.OpenDjangoStructureViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "main",
"last_opened_file_path": "C:/Users/12914/PycharmProjects/chunyu_project/user",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Django 服务器.chunyu_project.executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;Notification.DisplayName-DoNotAsk-DatabaseConfigFileWatcher.found&quot;: &quot;发现新的/更新的数据库配置&quot;,
&quot;Notification.DoNotAsk-DatabaseConfigFileWatcher.found&quot;: &quot;true&quot;,
&quot;Python.tests.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.MCP Project settings loaded&quot;: &quot;true&quot;,
&quot;RunOnceActivity.OpenDjangoStructureViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/12914/PycharmProjects/chunyu_project/api&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
"keyToStringList": {
"DatabaseDriversLRU": [
"sqlite"
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;sqlite&quot;
]
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\12914\PycharmProjects\chunyu_project\api" />
<recent name="C:\Users\12914\PycharmProjects\chunyu_project\user" />
<recent name="C:\Users\12914\PycharmProjects\chunyu_project\chunyu_project" />
<recent name="C:\Users\12914\PycharmProjects\chunyu_project\api" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\12914\PycharmProjects\chunyu_project\user" />
@@ -134,7 +145,12 @@
<option name="presentableId" value="Default" />
<updated>1774436691939</updated>
<workItem from="1774436692980" duration="11637000" />
<workItem from="1774474993123" duration="15275000" />
<workItem from="1774474993123" duration="16653000" />
<workItem from="1774593859979" duration="23580000" />
<workItem from="1774733219728" duration="5609000" />
<workItem from="1775124800510" duration="1393000" />
<workItem from="1775290631675" duration="21356000" />
<workItem from="1775375846640" duration="2674000" />
</task>
<task id="LOCAL-00001" summary="test">
<option name="closed" value="true" />
@@ -160,7 +176,23 @@
<option name="project" value="LOCAL" />
<updated>1774565193807</updated>
</task>
<option name="localTasksCounter" value="4" />
<task id="LOCAL-00004" summary="test">
<option name="closed" value="true" />
<created>1774572888898</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1774572888898</updated>
</task>
<task id="LOCAL-00005" summary="test">
<option name="closed" value="true" />
<created>1774573186526</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1774573186526</updated>
</task>
<option name="localTasksCounter" value="6" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -171,6 +203,6 @@
<option name="LAST_COMMIT_MESSAGE" value="test" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/chunyu_project$tests.coverage" NAME="tests 覆盖结果" MODIFIED="1774462107469" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/app" />
<SUITE FILE_PATH="coverage/chunyu_project$tests.coverage" NAME="tests 覆盖结果" MODIFIED="1774621019782" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/api" />
</component>
</project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+18 -2
View File
@@ -1,3 +1,19 @@
from django.test import TestCase
# -*- coding=utf-8
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
import sys
import logging
# Create your tests here.
# 正常情况日志级别使用 INFO,需要定位时可以修改为 DEBUG,此时 SDK 会打印和服务端的通信信息
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
# 1. 设置用户属性, 包括 secret_id, secret_key, region 等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成
secret_id = 'SecretId' # 替换为用户的 SecretId,请登录访问管理控制台进行查看和管理,https://console.cloud.tencent.com/cam/capi
secret_key = 'SecretKey' # 替换为用户的 SecretKey,请登录访问管理控制台进行查看和管理,https://console.cloud.tencent.com/cam/capi
region = 'ap-beijing' # 替换为用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket
# COS支持的所有 region 列表参见 https://cloud.tencent.com/document/product/436/6224
token = None # 如果使用永久密钥不需要填入token,如果使用临时密钥需要填入,临时密钥生成和使用指引参见 https://cloud.tencent.com/document/product/436/14048
scheme = 'https' # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=scheme)
client = CosS3Client(config)
+21 -1
View File
@@ -1,8 +1,28 @@
from django.contrib import admin
from django.urls import path, include
from .views.GetIPDataView import GetIPDataView
from .views.BaiduFanyiView import BaiduFanyiView
from .views.BaiduFanyiView import BaiduFanyiView, AllLangTypeViews, AutoLangTypeViews, RecognizeLangTypeViews, PictureRecognizeViews, SpeechRecognitionView
urlpatterns = [
path("getIpData/", GetIPDataView.as_view()),
path("baiduFanyi/", BaiduFanyiView.as_view()),
path("getAllLangType/", AllLangTypeViews.as_view()),
path("getAutoLangType/", AutoLangTypeViews.as_view()),
path("recognizeLangType/", RecognizeLangTypeViews.as_view()),
path("pictureRecognize/", PictureRecognizeViews.as_view()),
path("speechRecognition/", SpeechRecognitionView.as_view()),
]
+206 -5
View File
@@ -1,4 +1,8 @@
from symtable import Class
from django.core.signing import base64_hmac
from rest_framework.decorators import permission_classes
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
@@ -13,6 +17,12 @@ import random
import json
from hashlib import md5
from twisted.scripts.htmlizer import header
from .info.baidu_lang_info import languages, auto_lang
from .info.baidu_fanyi_appid import appid, appkey, endpoint
def make_md5(s, encoding='utf-8'):
return md5(s.encode(encoding)).hexdigest()
@@ -20,21 +30,48 @@ def make_md5(s, encoding='utf-8'):
@permission_classes([AllowAny])
class BaiduFanyiView(APIView):
def post(self, request):
"""
请求参数为 q 翻译内容 文本长度限制3000字以下
from_lang 原语言
to_lang 翻译语言 (支持 auto 为自动识别)
:param request:
:return:
"""
print(request.data)
# Set your own appid/appkey.
appid = '20220718001275847'
appkey = 'CJHmvf7qGs3szQy32cMg'
from_lang = 'en'
to_lang = 'zh'
endpoint = 'http://api.fanyi.baidu.com'
from_lang = request.data.get('from_lang')
to_lang = request.data.get('to_lang')
if (from_lang not in languages) or (to_lang not in languages) or (to_lang == "auto"):
return Response(
{
"message": "翻译语种异常"
},
status=status.HTTP_400_BAD_REQUEST
)
if from_lang == to_lang:
return Response(
{
"message": "翻译数据异常"
}
)
path = '/api/trans/vip/translate'
url = endpoint + path
query = request.data['q']
if len(query) > 3000:
return Response({"请求数据长度过长已经超过3000."}, status=status.HTTP_400_BAD_REQUEST)
salt = random.randint(32768, 65536)
sign = make_md5(appid + query + str(salt) + appkey)
@@ -44,5 +81,169 @@ class BaiduFanyiView(APIView):
r = requests.post(url, params=payload, headers=headers)
result = r.json()
return Response(result, status=status.HTTP_200_OK)
@permission_classes([AllowAny])
class AutoLangTypeViews(APIView): # 获取可用自动识别的所有语言类型
def get(self, request):
return Response(auto_lang, status=status.HTTP_200_OK)
@permission_classes([AllowAny])
class AllLangTypeViews(APIView): # 获取可用自动识别的所有语言类型
def get(self, request):
return Response(languages, status=status.HTTP_200_OK)
@permission_classes([AllowAny])
class RecognizeLangTypeViews(APIView): # 文本语种识别
"""
请求参数为 q 翻译内容 文本长度限制3000字以下
:param request:
:return:
"""
def post(self, request):
path = '/api/trans/vip/language'
url = endpoint + path
query = request.data.get("q")
if len(query) > 3000:
return Response({"请求数据长度过长已经超过3000."}, status=status.HTTP_400_BAD_REQUEST)
salt = random.randint(32768, 65536)
sign = make_md5(appid + query + str(salt) + appkey)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
payload = {'appid': appid, 'q': query, 'salt': salt, 'sign': sign}
r = requests.post(url, params=payload, headers=headers)
result = r.json()
code = result.get('error_code')
if code == 0:
return Response(result, status=status.HTTP_200_OK)
elif code == 54009:
return Response({
"message": "不在课识别语种范围."
}, status=status.HTTP_400_BAD_REQUEST)
else :
return Response({
"message": "其他问题请联系管理员."
}, status=status.HTTP_400_BAD_REQUEST)
import requests
import random
from hashlib import md5
from .info.baidu_fanyi_appid import appid as app_id, appkey as app_key
from .info.baidu_lang_info import cuid, mac, p_code, p_lang_type
@permission_classes([AllowAny])
class PictureRecognizeViews(APIView): # 图片翻译
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
file_data = request.FILES['file'].read()
path = '/api/trans/sdk/picture'
url = endpoint + path
from_lang = request.GET.get("from_lang")
to_lang = request.GET.get("to_lang")
picture_type = request.GET.get("picture")
if (from_lang == to_lang) or (to_lang == "auto"):
return Response({"message": "翻译数据错误."}, status=status.HTTP_400_BAD_REQUEST)
if (from_lang not in p_lang_type.keys()) or (to_lang not in p_lang_type.keys()) :
return Response({"message": "翻译语种异常."}, status=status.HTTP_400_BAD_REQUEST)
def get_md5(string, encoding='utf-8'):
return md5(string.encode(encoding)).hexdigest()
salt = random.randint(32768, 65536)
sign = get_md5(app_id + md5(file_data).hexdigest() + str(salt) + cuid + mac + app_key)
payload = {'from': from_lang, 'to': to_lang, 'appid': app_id, 'salt': salt, 'sign': sign, 'cuid': cuid,
'mac': mac}
image = {'image': (f"p_image.{ picture_type }", file_data, "multipart/form-data")}
response = requests.post(url, params=payload, files=image)
result = response.json()
if result['error_code'] == 0:
return Response(result, status=status.HTTP_200_OK)
else:
result = p_code[result['error_code']]
return Response(result, status=status.HTTP_400_BAD_REQUEST)
from .info.baidu_lang_info import y_speech_type, pcm_y_lang_type, y_lang_type
import time
import hmac
import hashlib
import base64
@permission_classes([AllowAny])
class SpeechRecognitionView(APIView):
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
speech_type = request.GET.get("speech_type")
if speech_type not in y_speech_type:
return Response({"message": "不支持的语音类型."}, status=status.HTTP_400_BAD_REQUEST)
from_lang = request.GET.get('from_lang')
to_lang = request.GET.get('to_lang')
if (from_lang == to_lang) or (to_lang == "auto"):
return Response({"message": "翻译数据错误."}, status=status.HTTP_400_BAD_REQUEST)
if (from_lang not in (pcm_y_lang_type.keys() if speech_type == "pcm" else y_lang_type.keys())) or (to_lang not in (pcm_y_lang_type.keys() if speech_type == "pcm" else y_lang_type.keys())):
return Response({"message": "翻译语种异常."}, status=status.HTTP_400_BAD_REQUEST)
voice = request.FILES.get("voice").read()
base64_hmac_voice = base64.b64encode(voice).decode('utf-8')
timestamp = str(int(time.time()))
path = "/api/trans/v2/voicetrans"
url = endpoint + path
msg = appid + timestamp + base64_hmac_voice
sign = base64.b64encode(hmac.new(app_key.encode("utf-8"), msg.encode("utf-8"), digestmod=hashlib.sha256).digest())
print(sign)
headers = {
"Content-Type": "application/json",
"X-Appid": app_id,
"X-Timestamp": timestamp,
"X-Sign": sign,
"X-EncryptType": "hmac256"
}
data = {
"format": speech_type,
"voice": base64_hmac_voice,
"to": to_lang,
"from": from_lang,
}
response = requests.post(url, headers = headers,data = data)
result = response.json()
return Response(result, status=status.HTTP_200_OK)
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
appid = '20220718001275847'
appkey = 'CJHmvf7qGs3szQy32cMg'
endpoint = 'http://api.fanyi.baidu.com'
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+155 -11
View File
@@ -28,6 +28,11 @@ SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-j+-psq=m++l7hup73k1eg+wm-b&2_)!+_^o=6(=xc$1px*kqt@'
@@ -67,12 +72,12 @@ ALLOWED_HOSTS = ["*"]
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # 例如:您的React/Vue开发服务器
"http://127.0.0.1:3000",
"http://192.168.1.4:3000",
"https://192.168.1.4:3000",
"http://*",
"https://*"
]
# CORS设置
CORS_ALLOW_ALL_ORIGINS = False
# 允许凭据
CORS_ALLOW_CREDENTIALS = True
@@ -143,6 +148,17 @@ SIMPLE_JWT = {
'USER_ID_CLAIM': 'user_id',
}
# 邮件部分
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 使用SMTP
EMAIL_HOST = 'smtp.qq.com' # 需要真实的SMTP服务器
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_HOST_USER = 'cs10086086@qq.com' # 真实邮箱账号
EMAIL_HOST_PASSWORD = 'ldzpymytsinkfjhb' # 真实密码/授权码
EMAIL_USE_TLS = False
AUTH_USER_MODEL = 'user.FUser'
INSTALLED_APPS = [
@@ -155,6 +171,8 @@ INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
# 第三方应用
@@ -168,7 +186,122 @@ INSTALLED_APPS = [
]
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'chunyu_project', # 数据库名
'USER': 'root', # 数据库用户名
'PASSWORD': 'mysql_NpyMCS', # 数据库密码
'HOST': '192.168.1.4', # 数据库主机,留空默认为localhost
'PORT': '3306', # 数据库端口,留空默认为3306
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
'connect_timeout': 10, # 连接超时时间(秒)
},
'CONN_MAX_AGE': 3600, # 连接池最大存活时间(秒),0表示每次请求后关闭
'CONN_HEALTH_CHECKS': True, # 连接健康检查
'ATOMIC_REQUESTS': False, # 是否将每个HTTP请求包装在事务中
'AUTOCOMMIT': True, # 是否自动提交事务
'TIME_ZONE': 'Asia/Shanghai',
}
}
# Redis配置
REDIS_HOST = '192.168.1.4' # Redis服务器地址
REDIS_PORT = 6379 # Redis端口
REDIS_DB = 0 # Redis数据库编号
REDIS_PASSWORD = "+Xy2173240465" # Redis密码,没有密码则设为None
SESSION_DB = 1
CELERY_BROKER_DB = 2
CELERY_RESULT_BACKEND_DB = 2
# Django缓存配置(使用Redis作为缓存后端)
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": REDIS_PASSWORD,
"PARSER_CLASS": "redis.connection._HiredisParser", # 使用hiredis提高性能
'CONNECTION_POOL_CLASS': 'redis.BlockingConnectionPool',
'CONNECTION_POOL_CLASS_KWARGS': {
'max_connections': 50,
'timeout': 20,
},
'MAX_CONNECTIONS': 50,
'PICKLE_VERSION': -1,
'SOCKET_CONNECT_TIMEOUT': 5,
'SOCKET_TIMEOUT': 5,
'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
'SERIALIZER': 'django_redis.serializers.json.JSONSerializer',
'COMPRESS_MIN_LENGTH': 1024 * 10,
'IGNORE_EXCEPTIONS': True, # 缓存宕机时不崩溃
},
"KEY_PREFIX": "django_", # 缓存键前缀
"VERSION": 1, # 缓存版本
},
'session': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': f"redis://{REDIS_HOST}:{REDIS_PORT}/{SESSION_DB}",
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
},
'celery': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://:password@redis-celery:6379/0',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
# 会话配置
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'session'
SESSION_COOKIE_AGE = 1209600
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
# Celery配置
CELERY_BROKER_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{CELERY_BROKER_DB}'
CELERY_RESULT_BACKEND = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{CELERY_RESULT_BACKEND_DB}'
CELERY_CACHE_BACKEND = 'celery'
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
# 缓存中间件
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 60 * 15
CACHE_MIDDLEWARE_KEY_PREFIX = 'chunyu_site'
# 安全头
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# 定时任务:预热缓存
CELERY_BEAT_SCHEDULE = {
'warm-up-cache-every-hour': {
'task': 'core.tasks.warm_up_cache',
'schedule': 3600.0, # 每小时
},
'generate-daily-report': {
'task': 'reports.tasks.generate_daily_report',
'schedule': 86400.0, # 每天
},
}
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
@@ -177,8 +310,13 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.cache.FetchFromCacheMiddleware',
]
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 60 * 15 # 15分钟
CACHE_MIDDLEWARE_KEY_PREFIX = 'mysite'
ROOT_URLCONF = 'chunyu_project.urls'
TEMPLATES = [
@@ -200,16 +338,13 @@ TEMPLATES = [
ASGI_APPLICATION = 'chunyu_project.asgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# 使用Redis作为会话后端
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
# 使用Redis作为消息代理
CELERY_BROKER_URL = 'redis://localhost:6379/0'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/0'
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
@@ -246,3 +381,12 @@ USE_TZ = True
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / "static",
]
STATIC_ROOT = BASE_DIR / "staticfiles"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.2.12 on 2026-03-25 21:58
# Generated by Django 5.2.12 on 2026-04-04 09:10
import django.contrib.auth.models
import django.contrib.auth.validators
@@ -0,0 +1,19 @@
# Generated by Django 5.2.12 on 2026-04-04 13:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='fuser',
name='isSetPassword',
field=models.BooleanField(blank=True, default=False, verbose_name='是否主动设置过密码'),
preserve_default=False,
),
]
Binary file not shown.
+1
View File
@@ -8,6 +8,7 @@ class FUser(AbstractUser):
phone_number = models.CharField(max_length=15, blank=True, verbose_name='手机号')
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True, verbose_name='头像')
bio = models.TextField(max_length=500, blank=True, verbose_name='个人简介')
isSetPassword = models.BooleanField(blank=True, verbose_name='是否主动设置过密码')
# class Meta:
# # 定义在管理后台显示的名称
+6 -4
View File
@@ -6,11 +6,13 @@ from rest_framework_simplejwt.views import (
TokenVerifyView,
)
from .views.user import SendUserEmail
urlpatterns = [
# JWT 认证接口
path('user/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('user/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('user/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('register_send/', SendUserEmail.as_view(), name='send_user_email'),
]
-15
View File
@@ -1,15 +0,0 @@
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)
urlpatterns = [
# JWT 认证接口
path('user/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('user/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('user/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
View File
Binary file not shown.
Binary file not shown.
+75
View File
@@ -0,0 +1,75 @@
from rest_framework.decorators import permission_classes
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.core.mail import EmailMessage
import user.models
# import os
from utils import RandCode
from django.core.cache import caches
# 获取不同的缓存实例
default_cache = caches['default']
session_cache = caches['session']
celery_cache = caches['celery']
from ..models import FUser
class SendUserEmail(APIView):
permission_classes = [AllowAny]
def post(self, request):
to_email = request.data.get('to_email', None)
print(to_email)
if to_email is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
subject = 'test邮件'
code = RandCode.get_digit_characters_code_8()
body = f"你的验证码为 {code}, 该验证码仅用于账号注册, 十分钟内有效."
from_email = 'cs10086086@qq.com'
to_email = [to_email]
default_cache.set(f"register_{to_email[0]}", code, timeout=600)
email = EmailMessage(
subject=subject,
body=body,
from_email=from_email,
to=to_email,
)
email.send()
return Response('邮件发送成功!', status=status.HTTP_201_CREATED)
class RegisterByEmail(APIView):
permission_classes = [AllowAny]
def post(self, request):
code = request.data.get('code', None)
to_email = request.data.get('to_email', None)
if code is None or to_email is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
vcode = default_cache.get(f"register_{to_email}")
if vcode is None:
return Response(status=status.HTTP_400_BAD_REQUEST)
if code == vcode:
password = RandCode.get_alphanumeric_characters_code_8()
FUser.objects.create_user(
username=to_email,
email=to_email,
password=password
)
return Response("创建成功.",status=status.HTTP_200_OK)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
+99
View File
@@ -0,0 +1,99 @@
# 包含所有大小写字母、数字和常见可见特殊字符
all_visible_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,.<>?/~`\"'\\"
# 包含所有大小写字母和数字
alphanumeric_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
# 包含所有数字
digit_characters = "0123456789"
# 包含常见可见特殊字符(不包括空格)
special_characters = "!@#$%^&*()-_=+[]{}|;:,.<>?/~`\"'\\"
# 包含26个大写字母
uppercase_only = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# 包含26个小写字母
lowercase_only = "abcdefghijklmnopqrstuvwxyz"
# 包含26个小写字母和0-9 数字
lowercase_digits = "abcdefghijklmnopqrstuvwxyz0123456789"
# 包含26个大写字母和0-9 数字
uppercase_digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
import secrets
def get_all_visible_characters_code_8():
code = ''.join(secrets.choice(all_visible_characters) for _ in range(8))
while(code[0] == '0'):
first_digit = secrets.choice(all_visible_characters) # 1-9
code = first_digit + code[1:]
return code
def get_all_visible_characters_code_6():
code = ''.join(secrets.choice(all_visible_characters) for _ in range(6))
while(code[0] == '0'):
first_digit = secrets.choice(all_visible_characters) # 1-9
code = first_digit + code[1:]
return code
def get_all_visible_characters_code(length):
code = ''.join(secrets.choice(all_visible_characters) for _ in range(length))
while(code[0] == '0'):
first_digit = secrets.choice(all_visible_characters) # 1-9
code = first_digit + code[1:]
return code
def get_alphanumeric_characters_code(length):
code = ''.join(secrets.choice(alphanumeric_characters) for _ in range(length))
while(code[0] == '0'):
first_digit = secrets.choice(alphanumeric_characters) # 1-9
code = first_digit + code[1:]
return code
def get_alphanumeric_characters_code_6():
code = ''.join(secrets.choice(alphanumeric_characters) for _ in range(6))
while(code[0] == '0'):
first_digit = secrets.choice(alphanumeric_characters) # 1-9
code = first_digit + code[1:]
return code
def get_alphanumeric_characters_code_8():
code = ''.join(secrets.choice(alphanumeric_characters) for _ in range(8))
while(code[0] == '0'):
first_digit = secrets.choice(alphanumeric_characters) # 1-9
code = first_digit + code[1:]
return code
def get_digit_characters_code(length):
code = ''.join(secrets.choice(digit_characters) for _ in range(length))
while(code[0] == '0'):
first_digit = secrets.choice(digit_characters)
code = first_digit + code[1:]
return code
def get_digit_characters_code_6():
code = ''.join(secrets.choice(digit_characters) for _ in range(6))
while(code[0] == '0'):
first_digit = secrets.choice(digit_characters)
code = first_digit + code[1:]
return code
def get_digit_characters_code_8():
code = ''.join(secrets.choice(digit_characters) for _ in range(8))
while(code[0] == '0'):
first_digit = secrets.choice(digit_characters)
code = first_digit + code[1:]
return code
Binary file not shown.