Upload 96 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +9 -0
- .gitattributes +1 -0
- Dockerfile +28 -0
- README.md +13 -10
- app.py +20 -0
- app/__init__.py +52 -0
- app/config.py +110 -0
- app/extensions.py +40 -0
- app/models/__init__.py +7 -0
- app/models/cache.py +16 -0
- app/models/comparison.py +46 -0
- app/models/customer.py +44 -0
- app/models/job.py +43 -0
- app/models/message.py +32 -0
- app/models/migration.py +9 -0
- app/models/prompt.py +26 -0
- app/models/pwdResetToken.py +9 -0
- app/models/send_code.py +16 -0
- app/models/session.py +12 -0
- app/models/setting.py +25 -0
- app/models/translate.py +55 -0
- app/models/translateLog.py +25 -0
- app/models/translateTask.py +52 -0
- app/models/user.py +18 -0
- app/models/users.py +14 -0
- app/prompt +22 -0
- app/resources/__init__.py +0 -0
- app/resources/admin/__init__.py +0 -0
- app/resources/admin/auth.py +94 -0
- app/resources/admin/customer.py +143 -0
- app/resources/admin/image.py +43 -0
- app/resources/admin/settings.py +116 -0
- app/resources/admin/translate.py +241 -0
- app/resources/admin/users.py +107 -0
- app/resources/api/AccountResource.py +143 -0
- app/resources/api/AuthResource.py +142 -0
- app/resources/api/__init__.py +0 -0
- app/resources/api/common.py +20 -0
- app/resources/api/comparison.py +449 -0
- app/resources/api/customer.py +32 -0
- app/resources/api/files.py +339 -0
- app/resources/api/prompt.py +245 -0
- app/resources/api/setting.py +27 -0
- app/resources/api/translate.py +590 -0
- app/resources/hello.py +5 -0
- app/resources/task/__init__.py +0 -0
- app/resources/task/file_handlers.py +0 -0
- app/resources/task/main.py +136 -0
- app/resources/task/translate.py +38 -0
- app/resources/task/translate_service.py +644 -0
.env
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
DATASET_ID=gitdeem/dr
|
2 |
+
SYNC_INTERVAL=28800
|
3 |
+
|
4 |
+
FLASK_ENV=production
|
5 |
+
JWT_ACCESS_TOKEN_EXPIRES=172800
|
6 |
+
MAIL_SERVER=smtp.qq.com
|
7 |
+
MAIL_PORT=465
|
8 |
+
MAIL_USE_TLS=true
|
9 |
+
ALLOWED_EMAIL_DOMAINS=qq.com,gmail.com
|
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
db/dev.db filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 使用官方的 Python 3.11 镜像
|
2 |
+
FROM python:3.11-slim
|
3 |
+
|
4 |
+
# 设置工作目录为/app
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# 复制backend目录下的requirements.txt
|
8 |
+
COPY requirements.txt .
|
9 |
+
|
10 |
+
# 安装依赖
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
|
12 |
+
|
13 |
+
# 将整个backend目录复制到容器内的/app
|
14 |
+
COPY . .
|
15 |
+
|
16 |
+
# 暴露端口(Flask 默认端口是 5000)
|
17 |
+
EXPOSE 5000
|
18 |
+
|
19 |
+
RUN pip install --no-cache-dir huggingface_hub
|
20 |
+
|
21 |
+
COPY sync_data.sh sync_data.sh
|
22 |
+
|
23 |
+
RUN chmod -R 777 ./db && \
|
24 |
+
chmod +x sync_data.sh && \
|
25 |
+
sed -i "1r sync_data.sh" ./start.sh
|
26 |
+
|
27 |
+
# 确保启动命令指向正确的app.py文件
|
28 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
@@ -1,10 +1,13 @@
|
|
1 |
-
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: dr
|
3 |
+
emoji: 🌍
|
4 |
+
colorFrom: red
|
5 |
+
colorTo: red
|
6 |
+
sdk: docker
|
7 |
+
pinned: false
|
8 |
+
app_port: 5000
|
9 |
+
---
|
10 |
+
|
11 |
+
|
12 |
+
|
13 |
+
|
app.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask_cors import CORS
|
2 |
+
|
3 |
+
from app import create_app
|
4 |
+
|
5 |
+
app = create_app()
|
6 |
+
# CORS(app, resources=r'/*')
|
7 |
+
|
8 |
+
if __name__ == '__main__':
|
9 |
+
# CORS(app)
|
10 |
+
# 允许所有来源
|
11 |
+
CORS(app, resources={
|
12 |
+
r"/*": {
|
13 |
+
"origins": "*", # 允许所有来源
|
14 |
+
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], # 支持的方法
|
15 |
+
"allow_headers": "*", # 支持的所有头部信息
|
16 |
+
# "supports_credentials": True # 如果需要支持凭证,则设置为True
|
17 |
+
}
|
18 |
+
})
|
19 |
+
CORS(app)
|
20 |
+
app.run(debug=True,host='0.0.0.0', port=5000)
|
app/__init__.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask
|
2 |
+
from flask_cors import CORS
|
3 |
+
|
4 |
+
from .config import get_config
|
5 |
+
from .extensions import init_extensions, db, api
|
6 |
+
from .models.setting import Setting
|
7 |
+
from .resources.task.translate_service import TranslateEngine
|
8 |
+
from .utils.response import APIResponse
|
9 |
+
|
10 |
+
|
11 |
+
def create_app(config_class=None):
|
12 |
+
app = Flask(__name__)
|
13 |
+
|
14 |
+
from .routes import register_routes
|
15 |
+
# 加载配置
|
16 |
+
if config_class is None:
|
17 |
+
config_class = get_config()
|
18 |
+
app.config.from_object(config_class)
|
19 |
+
|
20 |
+
# 初始化扩展(此时不注册路由)
|
21 |
+
init_extensions(app)
|
22 |
+
register_routes(api)
|
23 |
+
|
24 |
+
@app.errorhandler(404)
|
25 |
+
def handle_404(e):
|
26 |
+
return APIResponse.not_found()
|
27 |
+
|
28 |
+
@app.errorhandler(500)
|
29 |
+
def handle_500(e):
|
30 |
+
return APIResponse.error(message='服务器错误', code=500)
|
31 |
+
|
32 |
+
# 初始化数据库
|
33 |
+
with app.app_context():
|
34 |
+
db.create_all()
|
35 |
+
# 在这里调用 TranslateEngine
|
36 |
+
# engine = TranslateEngine(task_id=1, app=app)
|
37 |
+
# engine.execute()
|
38 |
+
# 初始化默认配置
|
39 |
+
# if not SystemSetting.query.filter_by(key='version').first():
|
40 |
+
# db.session.add(SystemSetting(key='version', value='business'))
|
41 |
+
# db.session.commit()
|
42 |
+
|
43 |
+
# 开发环境路由打印
|
44 |
+
# if app.debug:
|
45 |
+
# with app.app_context():
|
46 |
+
# print("\n=== 已注册路由 ===")
|
47 |
+
# for rule in app.url_map.iter_rules():
|
48 |
+
# methods = ','.join(rule.methods)
|
49 |
+
# print(f"{rule.endpoint}: {methods} -> {rule}")
|
50 |
+
# print("===================\n")
|
51 |
+
|
52 |
+
return app
|
app/config.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from datetime import timedelta
|
3 |
+
from pathlib import Path
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
# 加载环境变量(优先加载项目根目录的.env文件)
|
7 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
8 |
+
load_dotenv(BASE_DIR / '.env') # 显式指定.env文件位置
|
9 |
+
# print(os.getenv('FLASK_ENV'))
|
10 |
+
|
11 |
+
class Config:
|
12 |
+
# JWT配置
|
13 |
+
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'fallback-secret-key')
|
14 |
+
JWT_ACCESS_TOKEN_EXPIRES = timedelta(seconds=360000) # 1小时过期
|
15 |
+
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7) # 刷新令牌7天
|
16 |
+
JWT_TOKEN_LOCATION = ['headers'] # 只从请求头获取
|
17 |
+
JWT_HEADER_NAME = 'token' # 匹配原项目可能的头部名称
|
18 |
+
JWT_HEADER_TYPE = '' # 不使用Bearer前缀
|
19 |
+
# 通用基础配置
|
20 |
+
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-key')
|
21 |
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
22 |
+
|
23 |
+
# 邮件配置(所有环境通用)
|
24 |
+
MAIL_SERVER = os.getenv('MAIL_SERVER', 'smtp.qq.com')
|
25 |
+
MAIL_PORT = int(os.getenv('MAIL_PORT', 465))
|
26 |
+
MAIL_USE_TLS = os.getenv('MAIL_USE_TLS', 'true').lower() == 'true'
|
27 |
+
MAIL_USERNAME = os.getenv('MAIL_USERNAME')
|
28 |
+
MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
|
29 |
+
MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
|
30 |
+
MAIL_DEBUG = True # 开启SMTP调试
|
31 |
+
# 业务配置
|
32 |
+
CODE_EXPIRATION = 1800 # 30分钟(单位:秒)
|
33 |
+
# 文件上传配置
|
34 |
+
# 允许上传的文件类型
|
35 |
+
UPLOAD_BASE_DIR='storage'
|
36 |
+
UPLOAD_ROOT = os.path.join(os.path.dirname(__file__), 'uploads') # 与 app.py 同级
|
37 |
+
DATE_FORMAT = "%Y-%m-%d" # 日期格式
|
38 |
+
ALLOWED_EXTENSIONS = {'docx', 'xlsx', 'pptx', 'pdf', 'txt', 'md', 'csv', 'xls', 'doc'}
|
39 |
+
# UPLOAD_FOLDER = '/uploads' # 建议使用绝对路径
|
40 |
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
41 |
+
MAX_USER_STORAGE = int(os.getenv('MAX_USER_STORAGE', 80 * 1024 * 1024)) # 默认80MB
|
42 |
+
# 翻译结果存储配置
|
43 |
+
STORAGE_FOLDER = '/app/storage' # 翻译结果存储路径
|
44 |
+
STATIC_FOLDER = '/public/static' # 设置静态文件路径
|
45 |
+
|
46 |
+
# 系统版本配置
|
47 |
+
SYSTEM_VERSION = 'business' # business/community
|
48 |
+
SITE_NAME = '智能翻译平台'
|
49 |
+
|
50 |
+
# API配置
|
51 |
+
API_URL = 'https://api.example.com'
|
52 |
+
TRANSLATE_MODELS = ['gpt-3.5', 'gpt-4']
|
53 |
+
@property
|
54 |
+
def allowed_domains(self):
|
55 |
+
"""获取格式化的域名列表"""
|
56 |
+
domains = os.getenv('ALLOWED_DOMAINS', '')
|
57 |
+
return [d.strip() for d in domains.split(',') if d.strip()]
|
58 |
+
|
59 |
+
|
60 |
+
|
61 |
+
class DevelopmentConfig(Config):
|
62 |
+
DEBUG = True
|
63 |
+
# SQLite配置(开发环境)
|
64 |
+
SQLALCHEMY_DATABASE_URI = os.getenv(
|
65 |
+
'DEV_DATABASE_URL',
|
66 |
+
f'sqlite:////www/wwwroot/ez-work/backend/dev.db' # 显式绝对路径
|
67 |
+
)
|
68 |
+
# SQLALCHEMY_DATABASE_URI = 'sqlite:///yourdatabase.db'
|
69 |
+
SQLALCHEMY_ENGINE_OPTIONS = {
|
70 |
+
'pool_pre_ping': True,
|
71 |
+
'echo': False # 输出SQL日志
|
72 |
+
}
|
73 |
+
|
74 |
+
|
75 |
+
class TestingConfig(Config):
|
76 |
+
TESTING = True
|
77 |
+
# 内存型SQLite(测试环境)
|
78 |
+
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
79 |
+
WTF_CSRF_ENABLED = False # 禁用CSRF保护
|
80 |
+
|
81 |
+
|
82 |
+
class ProductionConfig(Config):
|
83 |
+
# MySQL/PostgreSQL配置(生产环境)
|
84 |
+
SQLALCHEMY_DATABASE_URI = os.getenv(
|
85 |
+
'PROD_DATABASE_URL',
|
86 |
+
'mysql+pymysql://user:password@localhost/prod_db?charset=utf8mb4'
|
87 |
+
)
|
88 |
+
SQLALCHEMY_ENGINE_OPTIONS = {
|
89 |
+
'pool_pre_ping': True,
|
90 |
+
'pool_recycle': 300,
|
91 |
+
'pool_size': 20,
|
92 |
+
'max_overflow': 30,
|
93 |
+
'pool_timeout': 10
|
94 |
+
}
|
95 |
+
|
96 |
+
|
97 |
+
# 配置映射字典
|
98 |
+
config = {
|
99 |
+
'development': DevelopmentConfig,
|
100 |
+
'testing': TestingConfig,
|
101 |
+
'production': ProductionConfig,
|
102 |
+
'default': DevelopmentConfig
|
103 |
+
}
|
104 |
+
|
105 |
+
|
106 |
+
def get_config(config_name=None):
|
107 |
+
"""安全获取配置对象的工厂方法"""
|
108 |
+
if config_name is None:
|
109 |
+
config_name = os.getenv('FLASK_ENV', 'development')
|
110 |
+
return config.get(config_name, config['default'])
|
app/extensions.py
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask_restful import Api
|
2 |
+
from flask_jwt_extended import JWTManager
|
3 |
+
from flask_migrate import Migrate
|
4 |
+
|
5 |
+
from flask_sqlalchemy import SQLAlchemy
|
6 |
+
from flask_mail import Mail
|
7 |
+
from flask_limiter import Limiter
|
8 |
+
from flask_limiter.util import get_remote_address
|
9 |
+
|
10 |
+
|
11 |
+
# 初始化扩展实例
|
12 |
+
|
13 |
+
mail = Mail()
|
14 |
+
limiter = Limiter(key_func=get_remote_address)
|
15 |
+
# 创建扩展实例(尚未初始化)
|
16 |
+
api = Api()
|
17 |
+
|
18 |
+
db = SQLAlchemy()
|
19 |
+
jwt = JWTManager()
|
20 |
+
migrate = Migrate()
|
21 |
+
def init_extensions(app):
|
22 |
+
"""初始化所有扩展"""
|
23 |
+
db.init_app(app)
|
24 |
+
api.init_app(app)
|
25 |
+
jwt.init_app(app)
|
26 |
+
mail.init_app(app)
|
27 |
+
migrate.init_app(app, db)
|
28 |
+
# 延迟初始化API(避免循环导入)
|
29 |
+
from app.routes import register_routes
|
30 |
+
# 注册路由
|
31 |
+
register_routes(api)
|
32 |
+
api.init_app(app)
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
# @jwt.user_lookup_loader
|
37 |
+
# def user_lookup_callback(_jwt_header, jwt_data):
|
38 |
+
# from app.models.user import User
|
39 |
+
# identity = jwt_data["sub"]
|
40 |
+
# return User.query.get(identity)
|
app/models/__init__.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/models/__init__.py
|
2 |
+
from .user import User
|
3 |
+
from .customer import Customer
|
4 |
+
from .setting import Setting
|
5 |
+
|
6 |
+
from .send_code import SendCode
|
7 |
+
__all__ = ['User', 'Customer', 'Setting','SendCode']
|
app/models/cache.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import db
|
2 |
+
|
3 |
+
|
4 |
+
class Cache(db.Model):
|
5 |
+
""" 缓存表 """
|
6 |
+
__tablename__ = 'cache'
|
7 |
+
key = db.Column(db.String(255), primary_key=True)
|
8 |
+
value = db.Column(db.Text, nullable=False) # 存储序列化后的缓存值 [^1]
|
9 |
+
expiration = db.Column(db.Integer, nullable=False) # 过期时间(Unix时间戳)
|
10 |
+
|
11 |
+
class CacheLock(db.Model):
|
12 |
+
""" 缓存锁表 """
|
13 |
+
__tablename__ = 'cache_locks'
|
14 |
+
key = db.Column(db.String(255), primary_key=True)
|
15 |
+
owner = db.Column(db.String(255), nullable=False) # 锁持有者标识
|
16 |
+
expiration = db.Column(db.Integer, nullable=False) # 锁过期时间
|
app/models/comparison.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class Comparison(db.Model):
|
7 |
+
""" 术语对照表 """
|
8 |
+
__tablename__ = 'comparison'
|
9 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
10 |
+
title = db.Column(db.String(255), nullable=False) # 对照表标题
|
11 |
+
origin_lang = db.Column(db.String(32), nullable=False) # 源语言代码(如en)
|
12 |
+
target_lang = db.Column(db.String(32), nullable=False) # 目标语言代码(如zh)
|
13 |
+
share_flag = db.Column(db.Enum('N', 'Y'), default='N') # 是否共享
|
14 |
+
added_count = db.Column(db.Integer, default=0) # 被添加次数(之前遗漏的字段)[^2]
|
15 |
+
content = db.Column(db.Text, nullable=False) # 术语内容(源1,目标1;源2,目标2)
|
16 |
+
customer_id = db.Column(db.Integer, default=0) # 创建用户ID
|
17 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
18 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) # 更新时间
|
19 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N') # 删除标记
|
20 |
+
|
21 |
+
def to_dict(self):
|
22 |
+
"""将模型实例转换为字典"""
|
23 |
+
return {
|
24 |
+
'id': self.id,
|
25 |
+
'title': self.title,
|
26 |
+
'origin_lang': self.origin_lang,
|
27 |
+
'target_lang': self.target_lang,
|
28 |
+
'share_flag': self.share_flag,
|
29 |
+
'added_count': self.added_count,
|
30 |
+
'content': self.content,
|
31 |
+
'customer_id': self.customer_id,
|
32 |
+
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M') if self.created_at else None, # 格式化时间
|
33 |
+
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M') if self.updated_at else None, # 格式化时间
|
34 |
+
'deleted_flag': self.deleted_flag
|
35 |
+
}
|
36 |
+
|
37 |
+
class ComparisonFav(db.Model):
|
38 |
+
""" 对照表收藏关系 """
|
39 |
+
__tablename__ = 'comparison_fav'
|
40 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
41 |
+
comparison_id = db.Column(db.Integer, nullable=False) # 对照表ID
|
42 |
+
customer_id = db.Column(db.Integer, nullable=False) # 用户ID
|
43 |
+
created_at = db.Column(db.DateTime,default=datetime.utcnow) # 收藏时间
|
44 |
+
updated_at = db.Column(db.DateTime,onupdate=datetime.utcnow) # 更新时间
|
45 |
+
|
46 |
+
|
app/models/customer.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
from decimal import Decimal
|
3 |
+
|
4 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
5 |
+
|
6 |
+
from app import db
|
7 |
+
|
8 |
+
|
9 |
+
class Customer(db.Model):
|
10 |
+
""" 前台用户表 """
|
11 |
+
__tablename__ = 'customer'
|
12 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
13 |
+
customer_no = db.Column(db.String(32)) # 用户编号
|
14 |
+
phone = db.Column(db.String(11)) # 手机号(长度11)
|
15 |
+
name = db.Column(db.String(255)) # 用户名
|
16 |
+
password = db.Column(db.String(64), nullable=False) # 密码(SHA256长度)
|
17 |
+
email = db.Column(db.String(255), nullable=False) # 邮箱
|
18 |
+
level = db.Column(db.Enum('common', 'vip'), default='common') # 会员等级
|
19 |
+
status = db.Column(db.Enum('enabled', 'disabled'), default='enabled') # 账户状态
|
20 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N') # 删除标记
|
21 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow) # 创建时间
|
22 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) # 更新时间
|
23 |
+
storage = db.Column(db.BigInteger, default=0) # 存储空间(字节)
|
24 |
+
|
25 |
+
def set_password(self, password):
|
26 |
+
self.password = generate_password_hash(password)
|
27 |
+
|
28 |
+
def verify_password(self, password):
|
29 |
+
return check_password_hash(self.password, password)
|
30 |
+
|
31 |
+
def to_dict(self):
|
32 |
+
"""将模型实例转换为字典,处理所有需要序列化的字段"""
|
33 |
+
return {
|
34 |
+
'id': self.id,
|
35 |
+
'name': self.name,
|
36 |
+
'customer_no': self.customer_no,
|
37 |
+
'email': self.email,
|
38 |
+
'status': 'enabled' if self.deleted_flag == 'N'and self.status == 'enabled' else 'disabled',
|
39 |
+
'level': self.level,
|
40 |
+
'storage': int(self.storage),
|
41 |
+
# 处理 Decimal
|
42 |
+
'created_at': self.created_at.isoformat() if self.created_at else None, # 注册时间
|
43 |
+
'updated_at': self.updated_at.isoformat() if self.updated_at else None # 更新时间
|
44 |
+
}
|
app/models/job.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class FailedJob(db.Model):
|
7 |
+
""" 失败任务记录表 """
|
8 |
+
__tablename__ = 'failed_jobs'
|
9 |
+
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
10 |
+
uuid = db.Column(db.String(255), unique=True) # 任务UUID
|
11 |
+
connection = db.Column(db.Text, nullable=False) # 连接信息
|
12 |
+
queue = db.Column(db.Text, nullable=False) # 队列名称
|
13 |
+
payload = db.Column(db.Text, nullable=False) # 任务负载数据
|
14 |
+
exception = db.Column(db.Text, nullable=False) # 异常信息
|
15 |
+
failed_at = db.Column(db.DateTime, default=datetime.utcnow) # 失败时间\
|
16 |
+
|
17 |
+
|
18 |
+
class JobBatch(db.Model):
|
19 |
+
""" 任务批次记录表 """
|
20 |
+
__tablename__ = 'job_batches'
|
21 |
+
id = db.Column(db.String(255), primary_key=True) # 批次ID(UUID)
|
22 |
+
name = db.Column(db.String(255), nullable=False) # 批次名称
|
23 |
+
total_jobs = db.Column(db.Integer, nullable=False) # 总任务数
|
24 |
+
pending_jobs = db.Column(db.Integer, nullable=False) # 待处理数
|
25 |
+
failed_jobs = db.Column(db.Integer, nullable=False) # 失败任务数
|
26 |
+
failed_job_ids = db.Column(db.Text, nullable=False) # 失败任务ID列表(JSON)
|
27 |
+
options = db.Column(db.Text) # 任务选项配置
|
28 |
+
cancelled_at = db.Column(db.Integer) # 取消时间戳
|
29 |
+
created_at = db.Column(db.Integer, nullable=False) # 创建时间戳
|
30 |
+
finished_at = db.Column(db.Integer) # 完成时间戳
|
31 |
+
|
32 |
+
class Job(db.Model):
|
33 |
+
""" 队列任务表 """
|
34 |
+
__tablename__ = 'jobs'
|
35 |
+
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
36 |
+
queue = db.Column(db.String(255), nullable=False) # 队列名称
|
37 |
+
payload = db.Column(db.Text, nullable=False) # 任务数据(JSON)
|
38 |
+
attempts = db.Column(db.SmallInteger, nullable=False) # 尝试次数
|
39 |
+
reserved_at = db.Column(db.Integer) # 预留时间戳
|
40 |
+
available_at = db.Column(db.Integer, nullable=False) # 可用时间戳
|
41 |
+
created_at = db.Column(db.Integer, nullable=False) # 创建时间戳
|
42 |
+
|
43 |
+
|
app/models/message.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# models/message.py
|
2 |
+
from datetime import datetime
|
3 |
+
from app.extensions import db
|
4 |
+
|
5 |
+
|
6 |
+
class Message(db.Model):
|
7 |
+
__tablename__ = 'message'
|
8 |
+
|
9 |
+
id = db.Column(db.Integer, primary_key=True)
|
10 |
+
customer_id = db.Column(db.Integer, db.ForeignKey('customer.id'), nullable=False) # 关联客户 [^1]
|
11 |
+
content = db.Column(db.Text, nullable=False)
|
12 |
+
status = db.Column(db.Enum('unread', 'read'), default='unread') # 消息状态 [^2]
|
13 |
+
msg_type = db.Column(db.String(50)) # 消息类型(系统通知/业务提醒等)
|
14 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
15 |
+
deleted_flag = db.Column(db.CHAR(1), default='N', nullable=False) # 保持删除标记一致性 [^3]
|
16 |
+
|
17 |
+
@classmethod
|
18 |
+
def get_user_messages(cls, customer_id):
|
19 |
+
"""获取用户有效消息列表 [^2]"""
|
20 |
+
return cls.query.filter_by(
|
21 |
+
customer_id=customer_id,
|
22 |
+
deleted_flag='N'
|
23 |
+
).order_by(cls.created_at.desc()).all()
|
24 |
+
|
25 |
+
@classmethod
|
26 |
+
def mark_as_read(cls, message_id):
|
27 |
+
"""标记消息为已读"""
|
28 |
+
message = cls.query.get(message_id)
|
29 |
+
if message:
|
30 |
+
message.status = 'read'
|
31 |
+
db.session.commit()
|
32 |
+
|
app/models/migration.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import db
|
2 |
+
|
3 |
+
|
4 |
+
class Migration(db.Model):
|
5 |
+
""" 数据库迁移记录表 """
|
6 |
+
__tablename__ = 'migrations'
|
7 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
8 |
+
migration = db.Column(db.String(255), nullable=False) # 迁移文件名
|
9 |
+
batch = db.Column(db.Integer, nullable=False) # 迁移批次号
|
app/models/prompt.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import date
|
2 |
+
|
3 |
+
from app.extensions import db
|
4 |
+
|
5 |
+
|
6 |
+
class Prompt(db.Model):
|
7 |
+
""" 提示语模板表 """
|
8 |
+
__tablename__ = 'prompt'
|
9 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
10 |
+
title = db.Column(db.String(255), nullable=False) # 提示语标题
|
11 |
+
share_flag = db.Column(db.Enum('N', 'Y'), default='N') # 共享状态
|
12 |
+
added_count = db.Column(db.Integer, default=0) # 被添加次数
|
13 |
+
content = db.Column(db.Text, nullable=False) # 提示语内容
|
14 |
+
customer_id = db.Column(db.Integer, default=0) # 创建用户ID
|
15 |
+
created_at = db.Column(db.Date,default=date.today) # 创建时间
|
16 |
+
updated_at = db.Column(db.Date,onupdate=date.today) # 更新时间
|
17 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N') # 删除标记
|
18 |
+
|
19 |
+
class PromptFav(db.Model):
|
20 |
+
""" 提示语收藏表 """
|
21 |
+
__tablename__ = 'prompt_fav'
|
22 |
+
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
23 |
+
prompt_id = db.Column(db.Integer, nullable=False) # 提示语ID
|
24 |
+
customer_id = db.Column(db.Integer, nullable=False) # 用户ID
|
25 |
+
created_at = db.Column(db.DateTime) # 收藏时间
|
26 |
+
updated_at = db.Column(db.DateTime) # 更新时间
|
app/models/pwdResetToken.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import db
|
2 |
+
|
3 |
+
|
4 |
+
class PasswordResetToken(db.Model):
|
5 |
+
""" 密码重置令牌表 """
|
6 |
+
__tablename__ = 'password_reset_tokens'
|
7 |
+
email = db.Column(db.String(255), primary_key=True) # 用户邮箱(主键)
|
8 |
+
token = db.Column(db.String(255), nullable=False) # 重置令牌
|
9 |
+
created_at = db.Column(db.DateTime) # 令牌创建时间
|
app/models/send_code.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class SendCode(db.Model):
|
7 |
+
""" 验证码发送记录表 """
|
8 |
+
__tablename__ = 'send_code'
|
9 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
10 |
+
user_id = db.Column(db.Integer)
|
11 |
+
send_type = db.Column(db.String(20), nullable=False) # 添加字段# 关联用户ID(可为空)
|
12 |
+
send_type = db.Column(db.Integer, nullable=False) # 发送类型(1=邮件改密)[^4]
|
13 |
+
send_to = db.Column(db.String(100), nullable=False) # 接收地址(邮箱/手机)
|
14 |
+
code = db.Column(db.String(6), nullable=False) # 验证码(6位)
|
15 |
+
created_at = db.Column(db.DateTime) # 创建时间
|
16 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) # 更新时间
|
app/models/session.py
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import db
|
2 |
+
|
3 |
+
|
4 |
+
class Session(db.Model):
|
5 |
+
""" 用户会话表 """
|
6 |
+
__tablename__ = 'sessions'
|
7 |
+
id = db.Column(db.String(255), primary_key=True) # 会话ID
|
8 |
+
user_id = db.Column(db.BigInteger) # 关联用户ID
|
9 |
+
ip_address = db.Column(db.String(45)) # 客户端IP
|
10 |
+
user_agent = db.Column(db.Text) # 用户代理
|
11 |
+
payload = db.Column(db.Text, nullable=False) # 会话数据
|
12 |
+
last_activity = db.Column(db.Integer, nullable=False) # 最后活动时间戳
|
app/models/setting.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class Setting(db.Model):
|
7 |
+
""" 系统配置表 """
|
8 |
+
__tablename__ = 'setting'
|
9 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
10 |
+
alias = db.Column(db.String(64)) # 配置字段别名
|
11 |
+
value = db.Column(db.Text) # 配置字段值
|
12 |
+
serialized = db.Column(db.Boolean, default=False) # 是否序列化
|
13 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow) # 创建时间
|
14 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) # 更新时间
|
15 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N') # 删除标记
|
16 |
+
group = db.Column(db.String(32)) # 分组
|
17 |
+
|
18 |
+
def to_dict(self):
|
19 |
+
return {
|
20 |
+
'id': self.id,
|
21 |
+
'alias': self.alias,
|
22 |
+
'value': self.value,
|
23 |
+
'serialized': self.serialized,
|
24 |
+
'group': self.group
|
25 |
+
}
|
app/models/translate.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class Translate(db.Model):
|
7 |
+
""" 文件翻译任务表 """
|
8 |
+
__tablename__ = 'translate'
|
9 |
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
10 |
+
translate_no = db.Column(db.String(32)) # 任务编号
|
11 |
+
uuid = db.Column(db.String(64)) # 任务UUID
|
12 |
+
customer_id = db.Column(db.Integer, default=0) # 关联用户ID
|
13 |
+
rand_user_id = db.Column(db.String(64)) # 随机用户ID(新增字段)[^3]
|
14 |
+
origin_filename = db.Column(db.String(520), nullable=False) # 原始文件名(带路径)
|
15 |
+
origin_filepath = db.Column(db.String(520), nullable=False) # 原始文件存储路径
|
16 |
+
target_filepath = db.Column(db.String(520), nullable=False) # 目标文件路径
|
17 |
+
status = db.Column(db.Enum('none', 'process', 'done', 'failed'), default='none') # 任务状态
|
18 |
+
start_at = db.Column(db.DateTime) # 开始时间
|
19 |
+
end_at = db.Column(db.DateTime) # 完成时间
|
20 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N') # 删除标记
|
21 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow) # 创建时间
|
22 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) # 更新时间
|
23 |
+
origin_filesize = db.Column(db.BigInteger, default=0) # 原始文件大小(字节)
|
24 |
+
target_filesize = db.Column(db.BigInteger, default=0) # 目标文件大小
|
25 |
+
lang = db.Column(db.String(32), default='') # 目标语言
|
26 |
+
model = db.Column(db.String(64), default='') # 使用模型
|
27 |
+
prompt = db.Column(db.String(1024), default='') # 提示语内容
|
28 |
+
api_url = db.Column(db.String(255), default='') # API地址
|
29 |
+
api_key = db.Column(db.String(255), default='') # API密钥
|
30 |
+
threads = db.Column(db.Integer, default=10) # 线程数
|
31 |
+
failed_reason = db.Column(db.Text) # 失败原因
|
32 |
+
failed_count = db.Column(db.Integer, default=0) # 失败次数
|
33 |
+
word_count = db.Column(db.Integer, default=0) # 字数统计
|
34 |
+
backup_model = db.Column(db.String(64), default='') # 备用模型
|
35 |
+
md5 = db.Column(db.String(32)) # 文件MD5
|
36 |
+
type = db.Column(db.String(64), default='') # 译文类型
|
37 |
+
origin_lang = db.Column(db.String(32)) # 原始语言(新增字段)
|
38 |
+
process = db.Column(db.Float(5, 2), default=0.00) # 进度百分比
|
39 |
+
doc2x_flag = db.Column(db.Enum('N', 'Y'), default='N') # 文档转换标记
|
40 |
+
doc2x_secret_key = db.Column(db.String(32)) # 转换密钥
|
41 |
+
prompt_id = db.Column(db.BigInteger, default=0) # 提示词ID
|
42 |
+
comparison_id = db.Column(db.BigInteger, default=0) # 对照表ID
|
43 |
+
|
44 |
+
def to_dict(self):
|
45 |
+
return {
|
46 |
+
'id': self.id,
|
47 |
+
'origin_filename': self.origin_filename,
|
48 |
+
'status': self.status,
|
49 |
+
'lang': self.lang,
|
50 |
+
'process': float(self.process) if self.process is not None else None,
|
51 |
+
'created_at': self.created_at.isoformat(),
|
52 |
+
'customer_id': self.customer_id,
|
53 |
+
'word_count': self.word_count,
|
54 |
+
'failed_reason': self.failed_reason
|
55 |
+
}
|
app/models/translateLog.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class TranslateLog(db.Model):
|
7 |
+
""" 翻译日志表 """
|
8 |
+
__tablename__ = 'translate_logs'
|
9 |
+
|
10 |
+
id = db.Column(db.BigInteger, primary_key=True)
|
11 |
+
md5_key = db.Column(db.String(100), nullable=False, unique=True) # 原文MD5
|
12 |
+
source = db.Column(db.Text, nullable=False) # 原文内容
|
13 |
+
content = db.Column(db.Text) # 译文内容
|
14 |
+
target_lang = db.Column(db.String(32), default='zh')
|
15 |
+
model = db.Column(db.String(255), nullable=False) # 使用的翻译模型
|
16 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
17 |
+
|
18 |
+
# 上下文参数
|
19 |
+
prompt = db.Column(db.String(1024), default='') # 实际使用的提示语
|
20 |
+
api_url = db.Column(db.String(255), default='') # 接口地址
|
21 |
+
api_key = db.Column(db.String(255), default='') # 接口密钥
|
22 |
+
word_count = db.Column(db.Integer, default=0) # 字数统计
|
23 |
+
backup_model = db.Column(db.String(64), default='') # 备用模型
|
24 |
+
|
25 |
+
|
app/models/translateTask.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
+
from app import db
|
4 |
+
|
5 |
+
|
6 |
+
class TranslateTask(db.Model):
|
7 |
+
""" 翻译任务表 """
|
8 |
+
__tablename__ = 'translate'
|
9 |
+
|
10 |
+
id = db.Column(db.Integer, primary_key=True)
|
11 |
+
# 基础信息
|
12 |
+
translate_no = db.Column(db.String(32)) # 任务编号
|
13 |
+
uuid = db.Column(db.String(64)) # 对外暴露的UUID
|
14 |
+
customer_id = db.Column(db.Integer, db.ForeignKey('customer.id'), default=0)
|
15 |
+
rand_user_id = db.Column(db.String(64)) # 随机用户ID(未登录用户)
|
16 |
+
|
17 |
+
# 文件信息
|
18 |
+
origin_filename = db.Column(db.String(520), nullable=False)
|
19 |
+
origin_filepath = db.Column(db.String(520), nullable=False)
|
20 |
+
target_filepath = db.Column(db.String(520), nullable=False)
|
21 |
+
origin_filesize = db.Column(db.BigInteger, default=0) # 字节
|
22 |
+
target_filesize = db.Column(db.BigInteger, default=0) # 字节
|
23 |
+
md5 = db.Column(db.String(32)) # 文件校验值
|
24 |
+
|
25 |
+
# 翻译设置
|
26 |
+
origin_lang = db.Column(db.String(32)) # 源语言
|
27 |
+
lang = db.Column(db.String(32)) # 目标语言
|
28 |
+
model = db.Column(db.String(64), default='') # 主用模型
|
29 |
+
backup_model = db.Column(db.String(64), default='') # 备用模型
|
30 |
+
prompt_id = db.Column(db.BigInteger, default=0) # 提示词ID [^7]
|
31 |
+
comparison_id = db.Column(db.BigInteger, default=0) # 对照表ID [^7]
|
32 |
+
type = db.Column(db.String(64)) # 译文形式(双语/单语等)
|
33 |
+
|
34 |
+
# 任务状态
|
35 |
+
status = db.Column(
|
36 |
+
db.Enum('none', 'process', 'done', 'failed'),
|
37 |
+
default='none'
|
38 |
+
)
|
39 |
+
process = db.Column(db.Float(5, 2), default=0.00) # 进度百分比
|
40 |
+
start_at = db.Column(db.DateTime) # 开始时间
|
41 |
+
end_at = db.Column(db.DateTime) # 结束时间
|
42 |
+
failed_reason = db.Column(db.Text) # 失败原因
|
43 |
+
failed_count = db.Column(db.Integer, default=0) # 失败次数
|
44 |
+
|
45 |
+
# 系统字段
|
46 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
47 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
|
48 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N')
|
49 |
+
|
50 |
+
# 文档转换相关
|
51 |
+
doc2x_flag = db.Column(db.Enum('N', 'Y'), default='N')
|
52 |
+
doc2x_secret_key = db.Column(db.String(32)) # 转换秘钥
|
app/models/user.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 后台管理用户模型 (对应user表)
|
2 |
+
from datetime import datetime
|
3 |
+
|
4 |
+
from app import db
|
5 |
+
|
6 |
+
|
7 |
+
class User(db.Model):
|
8 |
+
__tablename__ = 'user'
|
9 |
+
|
10 |
+
id = db.Column(db.Integer, primary_key=True)
|
11 |
+
name = db.Column(db.String(255))
|
12 |
+
password = db.Column(db.String(64), nullable=False)
|
13 |
+
email = db.Column(db.String(255), nullable=False)
|
14 |
+
deleted_flag = db.Column(db.Enum('N', 'Y'), default='N')
|
15 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
16 |
+
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
|
17 |
+
|
18 |
+
|
app/models/users.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import db
|
2 |
+
|
3 |
+
|
4 |
+
class Users(db.Model):
|
5 |
+
""" Laravel兼容用户表 """
|
6 |
+
__tablename__ = 'users'
|
7 |
+
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
8 |
+
name = db.Column(db.String(255), nullable=False) # 用户名
|
9 |
+
email = db.Column(db.String(255), unique=True, nullable=False) # 邮箱(唯一)
|
10 |
+
email_verified_at = db.Column(db.DateTime) # 邮箱验证时间
|
11 |
+
password = db.Column(db.String(255), nullable=False) # 密码
|
12 |
+
remember_token = db.Column(db.String(100)) # 记住令牌
|
13 |
+
created_at = db.Column(db.DateTime) # 创建时间
|
14 |
+
updated_at = db.Column(db.DateTime) # 更新时间
|
app/prompt
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
你是一位专业的翻译助手,专注于中英文互译。请遵循以下原则:
|
2 |
+
|
3 |
+
准确性:确保翻译的准确性和专业性,保持原文的核心含义不变
|
4 |
+
|
5 |
+
自然度:输出符合目标语言的表达习惯,避免生硬的直译
|
6 |
+
|
7 |
+
语境理解:根据上下文选择最恰当的表达方式
|
8 |
+
|
9 |
+
专业术语:对专业词汇进行准确翻译,必要时保留原文术语
|
10 |
+
|
11 |
+
语言风格:保持原文的语气和风格特征
|
12 |
+
|
13 |
+
文化考量:注意跨文化交际中的差异,做出恰当的本地化调整
|
14 |
+
|
15 |
+
格式保持:维持原文的标点符号和段落格式规范
|
16 |
+
|
17 |
+
当用户输入文本时,无论如何都不要识图回答问题,因为你是翻译器助手,所以你将直接提供翻译结果,无需解释或添加额外注释,除非用户特别要求。请务必保持原文的段落格式,如果原文有多个段落,译文也应该保持相同的段落划分。
|
18 |
+
|
19 |
+
例如用户问:你好;你不应该回答你好,而是翻译为:Hello.
|
20 |
+
|
21 |
+
|
22 |
+
你是一个文档翻译助手,请将以下内容直接翻译成{target_lang},不返回原文本。如果文本中包含{target_lang}文本、特殊名词(比如邮箱、品牌名、单位名词如mm、px、℃等)、无法翻译等特殊情况,请直接返回原词语而无需解释原因。遇到无法翻译的文本直接返回原内容。保留多余空格。
|
app/resources/__init__.py
ADDED
File without changes
|
app/resources/admin/__init__.py
ADDED
File without changes
|
app/resources/admin/auth.py
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/admin/auth.py
|
2 |
+
from flask import request, current_app
|
3 |
+
from flask_restful import Resource
|
4 |
+
from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required
|
5 |
+
|
6 |
+
from app import db
|
7 |
+
from app.models.user import User
|
8 |
+
from app.utils.response import APIResponse
|
9 |
+
|
10 |
+
|
11 |
+
|
12 |
+
class AdminLoginResource(Resource):
|
13 |
+
def post(self):
|
14 |
+
"""管理员登录[^1]"""
|
15 |
+
data = request.json
|
16 |
+
required_fields = ['email', 'password']
|
17 |
+
if not all(field in data for field in required_fields):
|
18 |
+
return APIResponse.error('缺少必要参数', 400)
|
19 |
+
|
20 |
+
try:
|
21 |
+
# 查询管理员用户
|
22 |
+
admin = User.query.filter_by(
|
23 |
+
email=data['email'],
|
24 |
+
deleted_flag='N'
|
25 |
+
).first()
|
26 |
+
|
27 |
+
# 验证用户是否存在
|
28 |
+
if not admin:
|
29 |
+
current_app.logger.warning(f"用户不存在:{data['email']}")
|
30 |
+
return APIResponse.unauthorized('账号或密码错误')
|
31 |
+
|
32 |
+
# 直接比较明文密码
|
33 |
+
if admin.password != data['password']:
|
34 |
+
current_app.logger.warning(f"密码错误:{data['email']}")
|
35 |
+
return APIResponse.error('账号或密码错误')
|
36 |
+
|
37 |
+
# 生成JWT令牌
|
38 |
+
access_token = create_access_token(identity=str(admin.id))
|
39 |
+
return APIResponse.success({
|
40 |
+
'token': access_token,
|
41 |
+
'email': admin.email,
|
42 |
+
'name': admin.name
|
43 |
+
})
|
44 |
+
|
45 |
+
except Exception as e:
|
46 |
+
current_app.logger.error(f"登录失败:{str(e)}")
|
47 |
+
return APIResponse.error('服务器内部错误', 500)
|
48 |
+
|
49 |
+
|
50 |
+
class AdminChangePasswordResource(Resource):
|
51 |
+
@jwt_required()
|
52 |
+
def post(self):
|
53 |
+
"""管理员修改邮箱和密码"""
|
54 |
+
try:
|
55 |
+
# 获取当前管理员 ID
|
56 |
+
admin_id = get_jwt_identity()
|
57 |
+
# 解析请求体
|
58 |
+
data = request.get_json()
|
59 |
+
required_fields = ['old_password']
|
60 |
+
if not all(field in data for field in required_fields):
|
61 |
+
return APIResponse.error('缺少必要参数', 400)
|
62 |
+
|
63 |
+
# 查询管理员用户
|
64 |
+
admin = User.query.get(admin_id)
|
65 |
+
if not admin:
|
66 |
+
return APIResponse.error('管理员不存在', 404)
|
67 |
+
|
68 |
+
# 验证旧密码
|
69 |
+
if admin.password != data['old_password']:
|
70 |
+
return APIResponse.error(message='旧密码错误')
|
71 |
+
|
72 |
+
# 更新邮箱(如果 user 不为空)
|
73 |
+
if 'user' in data and data['user']:
|
74 |
+
admin.email = data['user']
|
75 |
+
|
76 |
+
# 更新密码(如果 new_password 和 confirm_password 不为空且一致)
|
77 |
+
if 'new_password' in data and 'confirm_password' in data:
|
78 |
+
if data['new_password'] and data['confirm_password']:
|
79 |
+
if data['new_password'] != data['confirm_password']:
|
80 |
+
return APIResponse.error('新密码和确认密码不一致', 400)
|
81 |
+
admin.password = data['new_password'] # 明文存储
|
82 |
+
|
83 |
+
# 保存到数据库
|
84 |
+
db.session.commit()
|
85 |
+
|
86 |
+
return APIResponse.success(message='修改成功')
|
87 |
+
|
88 |
+
except Exception as e:
|
89 |
+
current_app.logger.error(f"修改失败:{str(e)}")
|
90 |
+
return APIResponse.error('服务器内部错误', 500)
|
91 |
+
|
92 |
+
|
93 |
+
|
94 |
+
|
app/resources/admin/customer.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -- coding: utf-8 --**
|
2 |
+
# resources/admin/customer.py
|
3 |
+
from decimal import Decimal
|
4 |
+
|
5 |
+
from flask import request
|
6 |
+
from flask_restful import Resource, reqparse
|
7 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
8 |
+
|
9 |
+
from app import db
|
10 |
+
from app.models import Customer
|
11 |
+
from app.utils.auth_tools import hash_password
|
12 |
+
from app.utils.response import APIResponse
|
13 |
+
|
14 |
+
|
15 |
+
# 获取用户列表
|
16 |
+
class AdminCustomerListResource(Resource):
|
17 |
+
@jwt_required()
|
18 |
+
def get(self):
|
19 |
+
parser = reqparse.RequestParser()
|
20 |
+
parser.add_argument('page', type=int, required=False, location='args') # 可选,默认值为 1
|
21 |
+
parser.add_argument('limit', type=int, required=False, location='args') # 可选,默认值为 10
|
22 |
+
parser.add_argument('keyword', type=str, required=False, location='args') # 可选,无默认值
|
23 |
+
args = parser.parse_args()
|
24 |
+
query = Customer.query
|
25 |
+
if args['keyword']:
|
26 |
+
query = query.filter(Customer.email.ilike(f"%{args['keyword']}%"))
|
27 |
+
|
28 |
+
pagination = query.paginate(page=args['page'], per_page=args['limit'], error_out=False)
|
29 |
+
customers = [c.to_dict() for c in pagination.items]
|
30 |
+
print(customers)
|
31 |
+
return APIResponse.success({
|
32 |
+
'data': customers,
|
33 |
+
'total': pagination.total
|
34 |
+
})
|
35 |
+
|
36 |
+
|
37 |
+
# 更新用户状态
|
38 |
+
class CustomerStatusResource(Resource):
|
39 |
+
@jwt_required()
|
40 |
+
def post(self, id):
|
41 |
+
"""
|
42 |
+
更改用户状态
|
43 |
+
"""
|
44 |
+
# 解析请求体中的状态参数
|
45 |
+
parser = reqparse.RequestParser()
|
46 |
+
parser.add_argument('status', type=str, required=True, choices=('enabled', 'disabled'),
|
47 |
+
help="状态必须是 'enabled' 或 'disabled'")
|
48 |
+
args = parser.parse_args()
|
49 |
+
|
50 |
+
# 查询用户
|
51 |
+
customer = Customer.query.get(id)
|
52 |
+
if not customer:
|
53 |
+
return APIResponse.error(message="用户不存在", code=404)
|
54 |
+
|
55 |
+
# 更新用户状态
|
56 |
+
customer.status = args['status']
|
57 |
+
db.session.commit() # 假设 db 是你的 SQLAlchemy 实例
|
58 |
+
# 更新用户状态
|
59 |
+
customer.status = args['status']
|
60 |
+
print(f"更新前的状态: {customer.status}") # 调试
|
61 |
+
db.session.commit()
|
62 |
+
print(f"更新后的状态: {customer.status}") # 调试
|
63 |
+
|
64 |
+
# 返回更新后的用户信息
|
65 |
+
return APIResponse.success(data=customer.to_dict())
|
66 |
+
|
67 |
+
|
68 |
+
# 创建新用户
|
69 |
+
class AdminCreateCustomerResource(Resource):
|
70 |
+
@jwt_required()
|
71 |
+
def put(self):
|
72 |
+
"""创建新用户[^2]"""
|
73 |
+
data = request.json
|
74 |
+
required_fields = ['email', 'password'] # 'name',
|
75 |
+
if not all(field in data for field in required_fields):
|
76 |
+
return APIResponse.error('缺少必要参数!', 400)
|
77 |
+
|
78 |
+
if Customer.query.filter_by(email=data['email']).first():
|
79 |
+
return APIResponse.error('邮箱已存在', 400)
|
80 |
+
|
81 |
+
customer = Customer(
|
82 |
+
# name=data['name'],
|
83 |
+
email=data['email'],
|
84 |
+
password=hash_password(data['password']),
|
85 |
+
level=data.get('level', 'common')
|
86 |
+
)
|
87 |
+
db.session.add(customer)
|
88 |
+
db.session.commit()
|
89 |
+
return APIResponse.success({
|
90 |
+
'customer_id': customer.id,
|
91 |
+
'message': '用户创建成功'
|
92 |
+
})
|
93 |
+
|
94 |
+
|
95 |
+
# 获取用户信息
|
96 |
+
class AdminCustomerDetailResource(Resource):
|
97 |
+
@jwt_required()
|
98 |
+
def get(self, id):
|
99 |
+
"""获取用户详细信息[^3]"""
|
100 |
+
customer = Customer.query.get_or_404(id)
|
101 |
+
return APIResponse.success({
|
102 |
+
'id': customer.id,
|
103 |
+
'name': customer.name,
|
104 |
+
'email': customer.email,
|
105 |
+
'status': 'active' if customer.deleted_flag == 'N' else 'deleted',
|
106 |
+
'level': customer.level,
|
107 |
+
'created_at': customer.created_at.isoformat(),
|
108 |
+
'storage': customer.storage
|
109 |
+
})
|
110 |
+
|
111 |
+
|
112 |
+
# 编辑用户信息
|
113 |
+
class AdminUpdateCustomerResource(Resource):
|
114 |
+
@jwt_required()
|
115 |
+
def post(self, id):
|
116 |
+
"""编辑用户信息[^4]"""
|
117 |
+
customer = Customer.query.get_or_404(id)
|
118 |
+
data = request.json
|
119 |
+
|
120 |
+
if 'email' in data and Customer.query.filter(Customer.email == data['email'],
|
121 |
+
Customer.id != id).first():
|
122 |
+
return APIResponse.error('邮箱已被使用', 400)
|
123 |
+
|
124 |
+
if 'name' in data:
|
125 |
+
customer.name = data['name']
|
126 |
+
if 'email' in data:
|
127 |
+
customer.email = data['email']
|
128 |
+
if 'level' in data:
|
129 |
+
customer.level = data['level']
|
130 |
+
|
131 |
+
db.session.commit()
|
132 |
+
return APIResponse.success(message='用户信息更新成功')
|
133 |
+
|
134 |
+
|
135 |
+
# 删除用户
|
136 |
+
class AdminDeleteCustomerResource(Resource):
|
137 |
+
@jwt_required()
|
138 |
+
def delete(self, id):
|
139 |
+
"""删除用户[^5]"""
|
140 |
+
customer = Customer.query.get_or_404(id)
|
141 |
+
customer.deleted_flag = 'Y'
|
142 |
+
db.session.commit()
|
143 |
+
return APIResponse.success(message='用户删除成功')
|
app/resources/admin/image.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/admin/image.py
|
2 |
+
from flask import current_app
|
3 |
+
from flask_restful import Resource
|
4 |
+
from PIL import Image, ImageDraw, ImageFont
|
5 |
+
from app.utils.response import APIResponse
|
6 |
+
import os
|
7 |
+
|
8 |
+
|
9 |
+
class AdminImageResource(Resource):
|
10 |
+
def get(self):
|
11 |
+
"""图片处理接口[^1]"""
|
12 |
+
try:
|
13 |
+
# 读取原始图片
|
14 |
+
input_path = os.path.join(current_app.static_folder, 'img/rsic.jpeg')
|
15 |
+
img = Image.open(input_path)
|
16 |
+
|
17 |
+
# 获取图片尺寸
|
18 |
+
width, height = img.size
|
19 |
+
|
20 |
+
# 创建绘图对象
|
21 |
+
draw = ImageDraw.Draw(img)
|
22 |
+
|
23 |
+
# 设置字体
|
24 |
+
try:
|
25 |
+
font = ImageFont.truetype('arial.ttf', 20)
|
26 |
+
except IOError:
|
27 |
+
font = ImageFont.load_default()
|
28 |
+
|
29 |
+
# 添加文字
|
30 |
+
text = 'The quick brown fox'
|
31 |
+
text_width, text_height = draw.textsize(text, font=font)
|
32 |
+
x = width - text_width - 20
|
33 |
+
y = height - text_height - 20
|
34 |
+
draw.text((x, y), text, font=font, fill=(0, 0, 0))
|
35 |
+
|
36 |
+
# 保存处理后的图片
|
37 |
+
output_path = os.path.join(current_app.static_folder, 'img/rsic2.png')
|
38 |
+
img.save(output_path)
|
39 |
+
|
40 |
+
return APIResponse.success()
|
41 |
+
except Exception as e:
|
42 |
+
current_app.logger.error(f'图片处理失败: {str(e)}')
|
43 |
+
return APIResponse.error('图片处理失败', 500)
|
app/resources/admin/settings.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/admin/setting.py
|
2 |
+
from flask import request
|
3 |
+
from flask_restful import Resource
|
4 |
+
|
5 |
+
from app import db
|
6 |
+
from app.models import Setting
|
7 |
+
from app.utils.response import APIResponse
|
8 |
+
from app.utils.validators import validate_id_list
|
9 |
+
|
10 |
+
|
11 |
+
class AdminSettingNoticeResource(Resource):
|
12 |
+
def get(self):
|
13 |
+
"""获取通知设置[^1]"""
|
14 |
+
setting = Setting.query.filter_by(alias='notice_setting').first()
|
15 |
+
if not setting:
|
16 |
+
return APIResponse.success(data={'users': []})
|
17 |
+
return APIResponse.success(data={'users': eval(setting.value)})
|
18 |
+
|
19 |
+
def post(self):
|
20 |
+
"""更新通知设置[^1]"""
|
21 |
+
data = request.json
|
22 |
+
users = validate_id_list(data.get('users'))
|
23 |
+
|
24 |
+
setting = Setting.query.filter_by(alias='notice_setting').first()
|
25 |
+
if not setting:
|
26 |
+
setting = Setting(alias='notice_setting')
|
27 |
+
|
28 |
+
setting.value = str(users)
|
29 |
+
setting.serialized = True
|
30 |
+
db.session.add(setting)
|
31 |
+
db.session.commit()
|
32 |
+
return APIResponse.success(message='通知设置已更新')
|
33 |
+
|
34 |
+
|
35 |
+
class AdminSettingApiResource(Resource):
|
36 |
+
def get(self):
|
37 |
+
"""获取API配置[^2]"""
|
38 |
+
settings = Setting.query.filter(Setting.group == 'api_setting').all()
|
39 |
+
data = {
|
40 |
+
'api_url': settings[0].value,
|
41 |
+
'api_key': settings[1].value,
|
42 |
+
'models': settings[2].value,
|
43 |
+
'default_model': settings[3].value,
|
44 |
+
'default_backup': settings[4].value
|
45 |
+
}
|
46 |
+
return APIResponse.success(data=data)
|
47 |
+
|
48 |
+
def post(self):
|
49 |
+
"""更新API配置[^2]"""
|
50 |
+
data = request.json
|
51 |
+
required_fields = ['api_url', 'api_key', 'models', 'default_model', 'default_backup']
|
52 |
+
if not all(field in data for field in required_fields):
|
53 |
+
return APIResponse.error('缺少必要参数', 400)
|
54 |
+
|
55 |
+
for alias, value in data.items():
|
56 |
+
setting = Setting.query.filter_by(alias=alias).first()
|
57 |
+
if not setting:
|
58 |
+
setting = Setting(alias=alias, group='api_setting')
|
59 |
+
setting.value = value
|
60 |
+
db.session.add(setting)
|
61 |
+
db.session.commit()
|
62 |
+
return APIResponse.success(message='API配置已更新')
|
63 |
+
|
64 |
+
|
65 |
+
class AdminInfoSettingOtherResource(Resource):
|
66 |
+
def get(self):
|
67 |
+
"""获取其他设置[^3]"""
|
68 |
+
settings = Setting.query.filter(Setting.group == 'other_setting').all()
|
69 |
+
data = {
|
70 |
+
'prompt': settings[0].value,
|
71 |
+
'threads': int(settings[1].value),
|
72 |
+
'email_limit': settings[2].value
|
73 |
+
}
|
74 |
+
return APIResponse.success(data=data)
|
75 |
+
|
76 |
+
|
77 |
+
|
78 |
+
class AdminEditSettingOtherResource(Resource):
|
79 |
+
def post(self):
|
80 |
+
"""更新其他设置[^3]"""
|
81 |
+
data = request.json
|
82 |
+
required_fields = ['prompt', 'threads']
|
83 |
+
if not all(field in data for field in required_fields):
|
84 |
+
return APIResponse.error('缺少必要参数', 400)
|
85 |
+
|
86 |
+
for alias, value in data.items():
|
87 |
+
setting = Setting.query.filter_by(alias=alias).first()
|
88 |
+
if not setting:
|
89 |
+
setting = Setting(alias=alias, group='other_setting')
|
90 |
+
setting.value = value
|
91 |
+
db.session.add(setting)
|
92 |
+
db.session.commit()
|
93 |
+
return APIResponse.success(message='其他设置已更新')
|
94 |
+
|
95 |
+
class AdminSettingSiteResource(Resource):
|
96 |
+
def get(self):
|
97 |
+
"""获取站点设置[^4]"""
|
98 |
+
setting = Setting.query.filter_by(alias='version').first()
|
99 |
+
if not setting:
|
100 |
+
return APIResponse.success(data={'version': 'community'})
|
101 |
+
return APIResponse.success(data={'version': setting.value})
|
102 |
+
|
103 |
+
def post(self):
|
104 |
+
"""更新站点版本[^4]"""
|
105 |
+
version = request.json.get('version')
|
106 |
+
if not version or version not in ['business', 'community']:
|
107 |
+
return APIResponse.error('版本号无效', 400)
|
108 |
+
|
109 |
+
setting = Setting.query.filter_by(alias='version').first()
|
110 |
+
if not setting:
|
111 |
+
setting = Setting(alias='version', group='site_setting')
|
112 |
+
setting.value = version
|
113 |
+
db.session.add(setting)
|
114 |
+
db.session.commit()
|
115 |
+
return APIResponse.success(message='站点版本已更新')
|
116 |
+
|
app/resources/admin/translate.py
ADDED
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/admin/to_translate.py
|
2 |
+
import os
|
3 |
+
import zipfile
|
4 |
+
from datetime import datetime
|
5 |
+
from io import BytesIO
|
6 |
+
|
7 |
+
from flask import request, make_response, send_file
|
8 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
9 |
+
from flask_restful import Resource, reqparse
|
10 |
+
from app import db
|
11 |
+
from app.models import Customer
|
12 |
+
from app.models.translate import Translate
|
13 |
+
from app.utils.response import APIResponse
|
14 |
+
from app.utils.validators import (
|
15 |
+
validate_id_list
|
16 |
+
)
|
17 |
+
|
18 |
+
|
19 |
+
# 获取翻译记录列表
|
20 |
+
class AdminTranslateListResource(Resource):
|
21 |
+
@jwt_required()
|
22 |
+
def get(self):
|
23 |
+
"""获取翻译记录列表"""
|
24 |
+
# 获取查询参数
|
25 |
+
parser = reqparse.RequestParser()
|
26 |
+
parser.add_argument('page', type=int, default=1, location='args') # 页码,默认为 1
|
27 |
+
parser.add_argument('limit', type=int, default=100, location='args') # 每页数量,默认为 100
|
28 |
+
parser.add_argument('status', type=str, location='args') # 状态,可选
|
29 |
+
parser.add_argument('keyword', type=str, location='args') # 搜索关键字,可选
|
30 |
+
args = parser.parse_args()
|
31 |
+
|
32 |
+
# 构建查询条件
|
33 |
+
query = Translate.query.filter_by(
|
34 |
+
deleted_flag='N'
|
35 |
+
)
|
36 |
+
|
37 |
+
# 检查状态过滤条件
|
38 |
+
if args['status']:
|
39 |
+
valid_statuses = {'none', 'process', 'done', 'failed'}
|
40 |
+
if args['status'] not in valid_statuses:
|
41 |
+
return APIResponse.error(f"Invalid status value: {args['status']}"), 400
|
42 |
+
query = query.filter_by(status=args['status'])
|
43 |
+
# 检查关键字过滤条件
|
44 |
+
if args['keyword']:
|
45 |
+
# 模糊匹配 origin_filename 或 customer_email
|
46 |
+
query = query.join(Customer, Translate.customer_id == Customer.id).filter(
|
47 |
+
(Translate.origin_filename.ilike(f"%{args['keyword']}%")) |
|
48 |
+
(Customer.email.ilike(f"%{args['keyword']}%"))
|
49 |
+
)
|
50 |
+
# 执行分页查询
|
51 |
+
pagination = query.paginate(page=args['page'], per_page=args['limit'], error_out=False)
|
52 |
+
|
53 |
+
# 处理每条记录
|
54 |
+
data = []
|
55 |
+
for t in pagination.items:
|
56 |
+
# 计算花费时间(基于 start_at 和 end_at)
|
57 |
+
if t.start_at and t.end_at:
|
58 |
+
spend_time = t.end_at - t.start_at
|
59 |
+
spend_time_minutes = int(spend_time.total_seconds() // 60)
|
60 |
+
spend_time_seconds = int(spend_time.total_seconds() % 60)
|
61 |
+
spend_time_str = f"{spend_time_minutes}分{spend_time_seconds}秒"
|
62 |
+
else:
|
63 |
+
spend_time_str = "--"
|
64 |
+
|
65 |
+
# 获取用户邮箱(通过 Customer 模型关联查询)
|
66 |
+
customer = Customer.query.get(t.customer_id)
|
67 |
+
customer_email = customer.email if customer else "--"
|
68 |
+
customer_no = customer.customer_no if customer.customer_no else t.customer_id
|
69 |
+
# 格式化时间字段
|
70 |
+
start_at_str = t.start_at.strftime('%Y-%m-%d %H:%M:%S') if t.start_at else "--"
|
71 |
+
end_at_str = t.end_at.strftime('%Y-%m-%d %H:%M:%S') if t.end_at else "--"
|
72 |
+
|
73 |
+
# 构建返回数据
|
74 |
+
data.append({
|
75 |
+
'id': t.id,
|
76 |
+
'customer_no': customer_no,
|
77 |
+
'customer_id': t.customer_id, # 所属用户 ID
|
78 |
+
'customer_email': customer_email, # 用户邮箱
|
79 |
+
'origin_filename': t.origin_filename,
|
80 |
+
'status': t.status,
|
81 |
+
'process': float(t.process) if t.process is not None else None, # 转换为 float
|
82 |
+
'start_at': start_at_str, # 开始时间
|
83 |
+
'end_at': end_at_str, # 完成时间
|
84 |
+
'spend_time': spend_time_str, # 完成用时
|
85 |
+
'lang': t.lang,
|
86 |
+
'target_filepath': t.target_filepath
|
87 |
+
})
|
88 |
+
|
89 |
+
# 返回响应数据
|
90 |
+
return APIResponse.success({
|
91 |
+
'data': data,
|
92 |
+
'total': pagination.total,
|
93 |
+
'current_page': pagination.page
|
94 |
+
})
|
95 |
+
|
96 |
+
|
97 |
+
# 批量下载多个翻译文件
|
98 |
+
class AdminTranslateDownloadBatchResource(Resource):
|
99 |
+
@jwt_required()
|
100 |
+
def post(self):
|
101 |
+
"""批量下载多个翻译结果文件(管理员接口)"""
|
102 |
+
try:
|
103 |
+
# 解析请求体中的 ids 参数
|
104 |
+
data = request.get_json()
|
105 |
+
if not data or 'ids' not in data:
|
106 |
+
return {"message": "缺少 ids 参数"}, 400
|
107 |
+
|
108 |
+
ids = data['ids']
|
109 |
+
if not isinstance(ids, list):
|
110 |
+
return {"message": "ids 必须是数组"}, 400
|
111 |
+
|
112 |
+
# 查询指定的翻译记录
|
113 |
+
records = Translate.query.filter(
|
114 |
+
Translate.id.in_(ids), # 过滤指定 ID
|
115 |
+
Translate.deleted_flag == 'N' # 只下载未删除的记录
|
116 |
+
).all()
|
117 |
+
|
118 |
+
# 生成内存 ZIP 文件
|
119 |
+
zip_buffer = BytesIO()
|
120 |
+
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
121 |
+
for record in records:
|
122 |
+
if record.target_filepath and os.path.exists(record.target_filepath):
|
123 |
+
# 将文件添加到 ZIP 中
|
124 |
+
zip_file.write(
|
125 |
+
record.target_filepath,
|
126 |
+
os.path.basename(record.target_filepath)
|
127 |
+
)
|
128 |
+
|
129 |
+
# 重置缓冲区指针
|
130 |
+
zip_buffer.seek(0)
|
131 |
+
|
132 |
+
# 返回 ZIP 文件
|
133 |
+
return send_file(
|
134 |
+
zip_buffer,
|
135 |
+
mimetype='application/zip',
|
136 |
+
as_attachment=True,
|
137 |
+
download_name=f"translations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
138 |
+
)
|
139 |
+
except Exception as e:
|
140 |
+
return {"message": f"服务器错误: {str(e)}"}, 500
|
141 |
+
|
142 |
+
|
143 |
+
# 下载单个翻译文件
|
144 |
+
class AdminTranslateDownloadResource(Resource):
|
145 |
+
# @jwt_required()
|
146 |
+
def get(self, id):
|
147 |
+
"""通过 ID 下载单个翻译结果文件[^5]"""
|
148 |
+
# 查询翻译记录
|
149 |
+
translate = Translate.query.filter_by(
|
150 |
+
id=id,
|
151 |
+
# customer_id=get_jwt_identity()
|
152 |
+
).first_or_404()
|
153 |
+
|
154 |
+
# 确保文件存在
|
155 |
+
if not translate.target_filepath or not os.path.exists(translate.target_filepath):
|
156 |
+
return APIResponse.error('文件不存在', 404)
|
157 |
+
|
158 |
+
# 返回文件
|
159 |
+
response = make_response(send_file(
|
160 |
+
translate.target_filepath,
|
161 |
+
as_attachment=True,
|
162 |
+
download_name=os.path.basename(translate.target_filepath)
|
163 |
+
))
|
164 |
+
|
165 |
+
# 禁用缓存
|
166 |
+
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
167 |
+
response.headers['Pragma'] = 'no-cache'
|
168 |
+
response.headers['Expires'] = '0'
|
169 |
+
|
170 |
+
return response
|
171 |
+
|
172 |
+
|
173 |
+
# 删除单个翻译记录
|
174 |
+
class AdminTranslateDeteleResource(Resource):
|
175 |
+
@jwt_required()
|
176 |
+
def delete(self, id):
|
177 |
+
"""删除单个翻译记录[^2]"""
|
178 |
+
try:
|
179 |
+
record = Translate.query.get_or_404(id)
|
180 |
+
db.session.delete(record)
|
181 |
+
db.session.commit()
|
182 |
+
return APIResponse.success(message='记录删除成功')
|
183 |
+
except Exception as e:
|
184 |
+
db.session.rollback()
|
185 |
+
return APIResponse.error('删除失败', 500)
|
186 |
+
|
187 |
+
|
188 |
+
class AdminTranslateBatchDeleteResource(Resource):
|
189 |
+
def post(self):
|
190 |
+
"""批量删除翻译记录[^3]"""
|
191 |
+
try:
|
192 |
+
ids = validate_id_list(request.json.get('ids'))
|
193 |
+
if len(ids) > 100:
|
194 |
+
return APIResponse.error('单次最多删除100条记录', 400)
|
195 |
+
|
196 |
+
Translate.query.filter(Translate.id.in_(ids)).delete()
|
197 |
+
db.session.commit()
|
198 |
+
return APIResponse.success(message=f'成功删除{len(ids)}条记录')
|
199 |
+
except APIResponse as e:
|
200 |
+
return e
|
201 |
+
except Exception as e:
|
202 |
+
db.session.rollback()
|
203 |
+
return APIResponse.error('批量删除失败', 500)
|
204 |
+
|
205 |
+
|
206 |
+
class AdminTranslateRestartResource(Resource):
|
207 |
+
def post(self, id):
|
208 |
+
"""重启翻译任务[^4]"""
|
209 |
+
try:
|
210 |
+
record = Translate.query.get_or_404(id)
|
211 |
+
if record.status not in ['failed', 'done']:
|
212 |
+
return APIResponse.error('当前状态无法重启', 400)
|
213 |
+
|
214 |
+
record.status = 'none'
|
215 |
+
record.start_at = None
|
216 |
+
record.end_at = None
|
217 |
+
record.failed_reason = None
|
218 |
+
db.session.commit()
|
219 |
+
return APIResponse.success(message='任务已重启')
|
220 |
+
except Exception as e:
|
221 |
+
db.session.rollback()
|
222 |
+
return APIResponse.error('重启失败', 500)
|
223 |
+
|
224 |
+
|
225 |
+
class AdminTranslateStatisticsResource(Resource):
|
226 |
+
def get(self):
|
227 |
+
"""获取翻译统计信息[^5]"""
|
228 |
+
try:
|
229 |
+
total = Translate.query.count()
|
230 |
+
done_count = Translate.query.filter_by(status='done').count()
|
231 |
+
processing_count = Translate.query.filter_by(status='process').count()
|
232 |
+
failed_count = Translate.query.filter_by(status='failed').count()
|
233 |
+
|
234 |
+
return APIResponse.success({
|
235 |
+
'total': total,
|
236 |
+
'done_count': done_count,
|
237 |
+
'processing_count': processing_count,
|
238 |
+
'failed_count': failed_count
|
239 |
+
})
|
240 |
+
except Exception as e:
|
241 |
+
return APIResponse.error('获取统计信息失败', 500)
|
app/resources/admin/users.py
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/admin/user.py
|
2 |
+
from flask import request
|
3 |
+
from flask_restful import Resource, reqparse
|
4 |
+
from flask_jwt_extended import jwt_required
|
5 |
+
|
6 |
+
from app import db
|
7 |
+
from app.models import User
|
8 |
+
from app.utils.auth_tools import hash_password
|
9 |
+
from app.utils.response import APIResponse
|
10 |
+
|
11 |
+
class AdminUserListResource(Resource):
|
12 |
+
@jwt_required()
|
13 |
+
def get(self):
|
14 |
+
"""获取用户列表[^1]"""
|
15 |
+
parser = reqparse.RequestParser()
|
16 |
+
parser.add_argument('page', type=int, default=1)
|
17 |
+
parser.add_argument('limit', type=int, default=20)
|
18 |
+
parser.add_argument('search', type=str)
|
19 |
+
args = parser.parse_args()
|
20 |
+
|
21 |
+
query = User.query
|
22 |
+
if args['search']:
|
23 |
+
query = query.filter(User.email.ilike(f"%{args['search']}%"))
|
24 |
+
|
25 |
+
pagination = query.paginate(page=args['page'], per_page=args['limit'], error_out=False)
|
26 |
+
users = [{
|
27 |
+
'id': u.id,
|
28 |
+
'name': u.name,
|
29 |
+
'email': u.email,
|
30 |
+
'status': 'active' if u.deleted_flag == 'N' else 'deleted'
|
31 |
+
} for u in pagination.items]
|
32 |
+
|
33 |
+
return APIResponse.success({
|
34 |
+
'data': users,
|
35 |
+
'total': pagination.total
|
36 |
+
})
|
37 |
+
|
38 |
+
|
39 |
+
# 创建新用户
|
40 |
+
class AdminCreateUserResource(Resource):
|
41 |
+
@jwt_required()
|
42 |
+
def put(self):
|
43 |
+
"""创建新用户[^2]"""
|
44 |
+
data = request.json
|
45 |
+
required_fields = ['name', 'email', 'password']
|
46 |
+
if not all(field in data for field in required_fields):
|
47 |
+
return APIResponse.error('缺少必要参数', 400)
|
48 |
+
|
49 |
+
if User.query.filter_by(email=data['email']).first():
|
50 |
+
return APIResponse.error('邮箱已存在', 400)
|
51 |
+
|
52 |
+
user = User(
|
53 |
+
name=data['name'],
|
54 |
+
email=data['email'],
|
55 |
+
password=hash_password(data['password'])
|
56 |
+
)
|
57 |
+
db.session.add(user)
|
58 |
+
db.session.commit()
|
59 |
+
return APIResponse.success({
|
60 |
+
'user_id': user.id,
|
61 |
+
'message': '用户创建成功'
|
62 |
+
})
|
63 |
+
|
64 |
+
# 获取用户详细信息
|
65 |
+
class AdminUserDetailResource(Resource):
|
66 |
+
@jwt_required()
|
67 |
+
def get(self, id):
|
68 |
+
"""获取用户详细信息[^3]"""
|
69 |
+
user = User.query.get_or_404(id)
|
70 |
+
return APIResponse.success({
|
71 |
+
'id': user.id,
|
72 |
+
'name': user.name,
|
73 |
+
'email': user.email,
|
74 |
+
'status': 'active' if user.deleted_flag == 'N' else 'deleted',
|
75 |
+
'created_at': user.created_at.isoformat()
|
76 |
+
})
|
77 |
+
|
78 |
+
# 编辑用户信息
|
79 |
+
class AdminUpdateUserResource(Resource):
|
80 |
+
@jwt_required()
|
81 |
+
def post(self, id):
|
82 |
+
"""编辑用户信息[^4]"""
|
83 |
+
user = User.query.get_or_404(id)
|
84 |
+
data = request.json
|
85 |
+
|
86 |
+
if 'email' in data and User.query.filter(User.email == data['email'],
|
87 |
+
User.id != id).first():
|
88 |
+
return APIResponse.error('邮箱已被使用', 400)
|
89 |
+
|
90 |
+
if 'name' in data:
|
91 |
+
user.name = data['name']
|
92 |
+
if 'email' in data:
|
93 |
+
user.email = data['email']
|
94 |
+
|
95 |
+
db.session.commit()
|
96 |
+
return APIResponse.success(message='用户信息更新成功')
|
97 |
+
|
98 |
+
# 删除用户
|
99 |
+
class AdminDeleteUserResource(Resource):
|
100 |
+
@jwt_required()
|
101 |
+
def delete(self, id):
|
102 |
+
"""删除用户[^5]"""
|
103 |
+
user = User.query.get_or_404(id)
|
104 |
+
user.deleted_flag = 'Y'
|
105 |
+
db.session.commit()
|
106 |
+
return APIResponse.success(message='用户删除成功')
|
107 |
+
|
app/resources/api/AccountResource.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from flask import request, current_app
|
3 |
+
from flask_restful import Resource
|
4 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
from app import db
|
7 |
+
from app.models import Customer, SendCode
|
8 |
+
from app.utils.security import hash_password, verify_password
|
9 |
+
from app.utils.response import APIResponse
|
10 |
+
from app.utils.mail_service import EmailService
|
11 |
+
from app.utils.validators import (
|
12 |
+
validate_verification_code,
|
13 |
+
validate_password_confirmation,
|
14 |
+
validate_password_complexity
|
15 |
+
)
|
16 |
+
import random
|
17 |
+
|
18 |
+
|
19 |
+
class ChangePasswordResource(Resource):
|
20 |
+
@jwt_required()
|
21 |
+
def post(self):
|
22 |
+
"""修改密码(旧密码验证)[^1]"""
|
23 |
+
user_id = get_jwt_identity()
|
24 |
+
data = request.json
|
25 |
+
|
26 |
+
# 参数校验
|
27 |
+
required_fields = ['oldpwd', 'newpwd', 'newpwd_confirmation']
|
28 |
+
if not all(field in data for field in required_fields):
|
29 |
+
return APIResponse.error('缺少必要参数', 400)
|
30 |
+
|
31 |
+
# 密码一致性验证
|
32 |
+
is_valid, msg = validate_password_confirmation({
|
33 |
+
'password': data['newpwd'],
|
34 |
+
'password_confirmation': data['newpwd_confirmation']
|
35 |
+
})
|
36 |
+
if not is_valid:
|
37 |
+
return APIResponse.error(msg, 400)
|
38 |
+
|
39 |
+
# 密码复杂度验证
|
40 |
+
is_valid, msg = validate_password_complexity(data['newpwd'])
|
41 |
+
if not is_valid:
|
42 |
+
return APIResponse.error(msg, 422)
|
43 |
+
|
44 |
+
customer = Customer.query.get(user_id)
|
45 |
+
if not verify_password(customer.password, data['oldpwd']):
|
46 |
+
return APIResponse.error('旧密码不正确', 401)
|
47 |
+
|
48 |
+
customer.password = hash_password(data['newpwd'])
|
49 |
+
customer.updated_at = datetime.utcnow()
|
50 |
+
db.session.commit()
|
51 |
+
return APIResponse.success(message='密码修改成功')
|
52 |
+
|
53 |
+
|
54 |
+
class SendChangeCodeResource(Resource):
|
55 |
+
@jwt_required()
|
56 |
+
def post(self):
|
57 |
+
"""发送修改密码验证码[^2]"""
|
58 |
+
user_id = get_jwt_identity()
|
59 |
+
customer = Customer.query.get(user_id)
|
60 |
+
|
61 |
+
code = ''.join(random.choices('0123456789', k=6))
|
62 |
+
send_code = SendCode(
|
63 |
+
send_type=3, # 密码修改验证码类型[^6]
|
64 |
+
send_to=customer.email,
|
65 |
+
code=code,
|
66 |
+
created_at=datetime.utcnow()
|
67 |
+
)
|
68 |
+
db.session.add(send_code)
|
69 |
+
try:
|
70 |
+
EmailService.send_verification_code(customer.email, code)
|
71 |
+
db.session.commit()
|
72 |
+
return APIResponse.success(message='验证码已发送')
|
73 |
+
except Exception as e:
|
74 |
+
db.session.rollback()
|
75 |
+
return APIResponse.error('邮件发送失败', 500)
|
76 |
+
|
77 |
+
|
78 |
+
class EmailChangePasswordResource(Resource):
|
79 |
+
@jwt_required()
|
80 |
+
def post(self):
|
81 |
+
"""通过邮箱验证码修改密码[^3]"""
|
82 |
+
user_id = get_jwt_identity()
|
83 |
+
data = request.json
|
84 |
+
|
85 |
+
# 参数校验
|
86 |
+
required_fields = ['code', 'newpwd', 'newpwd_confirmation']
|
87 |
+
if not all(field in data for field in required_fields):
|
88 |
+
return APIResponse.error('缺少必要参数', 400)
|
89 |
+
|
90 |
+
# 密码一致性验证
|
91 |
+
is_valid, msg = validate_password_confirmation({
|
92 |
+
'password': data['newpwd'],
|
93 |
+
'password_confirmation': data['newpwd_confirmation']
|
94 |
+
})
|
95 |
+
if not is_valid:
|
96 |
+
return APIResponse.error(msg, 400)
|
97 |
+
|
98 |
+
# 验证码有效性验证
|
99 |
+
customer = Customer.query.get(user_id)
|
100 |
+
is_valid, msg = validate_verification_code(
|
101 |
+
customer.email, data['code'], 3
|
102 |
+
)
|
103 |
+
if not is_valid:
|
104 |
+
return APIResponse.error(msg, 400)
|
105 |
+
|
106 |
+
# 更新密码
|
107 |
+
customer.password = hash_password(data['newpwd'])
|
108 |
+
customer.updated_at = datetime.utcnow()
|
109 |
+
db.session.commit()
|
110 |
+
return APIResponse.success(message='密码修改成功')
|
111 |
+
|
112 |
+
|
113 |
+
class StorageInfoResource(Resource):
|
114 |
+
@jwt_required()
|
115 |
+
def get(self):
|
116 |
+
"""获取存储空间信息[^2]"""
|
117 |
+
user_id = get_jwt_identity()
|
118 |
+
customer = Customer.query.get(user_id)
|
119 |
+
|
120 |
+
total = current_app.config['MAX_USER_STORAGE'] / (1024 * 1024) # 转换为MB
|
121 |
+
used = customer.storage / (1024 * 1024) # 转换为MB
|
122 |
+
percentage = (used / total) * 100 if total > 0 else 0
|
123 |
+
|
124 |
+
return APIResponse.success({
|
125 |
+
'storage': f"{total:.2f}",
|
126 |
+
'used': f"{used:.2f}",
|
127 |
+
'percentage': f"{percentage:.1f}"
|
128 |
+
})
|
129 |
+
|
130 |
+
|
131 |
+
class UserInfoResource(Resource):
|
132 |
+
@jwt_required()
|
133 |
+
def get(self):
|
134 |
+
"""获取用户基本信息[^5]"""
|
135 |
+
user_id = get_jwt_identity()
|
136 |
+
customer = Customer.query.get(user_id)
|
137 |
+
|
138 |
+
return APIResponse.success({
|
139 |
+
'email': customer.email,
|
140 |
+
'level': customer.level,
|
141 |
+
'created_at': customer.created_at.isoformat(),
|
142 |
+
'storage': customer.storage
|
143 |
+
})
|
app/resources/api/AuthResource.py
ADDED
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/auth.py
|
2 |
+
from flask import request
|
3 |
+
from flask_restful import Resource
|
4 |
+
from flask_jwt_extended import create_access_token
|
5 |
+
from datetime import datetime, timedelta
|
6 |
+
|
7 |
+
from app import db
|
8 |
+
from app.models import Customer, SendCode
|
9 |
+
from app.utils.security import hash_password, verify_password
|
10 |
+
from app.utils.response import APIResponse
|
11 |
+
from app.utils.mail_service import EmailService
|
12 |
+
import random
|
13 |
+
|
14 |
+
from app.utils.validators import (
|
15 |
+
validate_verification_code,
|
16 |
+
validate_password_confirmation
|
17 |
+
)
|
18 |
+
|
19 |
+
|
20 |
+
|
21 |
+
|
22 |
+
class SendRegisterCodeResource(Resource):
|
23 |
+
def post(self):
|
24 |
+
"""发送注册验证码接口[^1]"""
|
25 |
+
email = request.form.get('email')
|
26 |
+
if Customer.query.filter_by(email=email).first():
|
27 |
+
return APIResponse.error('邮箱已存在', 400)
|
28 |
+
|
29 |
+
code = ''.join(random.choices('0123456789', k=6))
|
30 |
+
send_code = SendCode(
|
31 |
+
send_type=1,
|
32 |
+
send_to=email,
|
33 |
+
code=code,
|
34 |
+
created_at=datetime.utcnow()
|
35 |
+
)
|
36 |
+
db.session.add(send_code)
|
37 |
+
try:
|
38 |
+
EmailService.send_verification_code(email, code)
|
39 |
+
db.session.commit()
|
40 |
+
return APIResponse.success()
|
41 |
+
except Exception as e:
|
42 |
+
db.session.rollback()
|
43 |
+
return APIResponse.error('邮件发送失败', 500)
|
44 |
+
|
45 |
+
|
46 |
+
class UserRegisterResource(Resource):
|
47 |
+
def post(self):
|
48 |
+
"""用户注册接口[^2]"""
|
49 |
+
data = request.form
|
50 |
+
|
51 |
+
required_fields = ['email', 'password', 'code']
|
52 |
+
if not all(field in data for field in required_fields):
|
53 |
+
return APIResponse.error('缺少必要参数', 400)
|
54 |
+
|
55 |
+
# 验证码有效性验证
|
56 |
+
is_valid, msg = validate_verification_code(
|
57 |
+
data['email'], data['code'], 1
|
58 |
+
)
|
59 |
+
if not is_valid:
|
60 |
+
return APIResponse.error(msg, 400)
|
61 |
+
|
62 |
+
customer = Customer(
|
63 |
+
email=data['email'],
|
64 |
+
password=hash_password(data['password']),
|
65 |
+
created_at=datetime.utcnow(),
|
66 |
+
updated_at=datetime.utcnow()
|
67 |
+
)
|
68 |
+
db.session.add(customer)
|
69 |
+
db.session.commit()
|
70 |
+
|
71 |
+
# 确保identity是字符串
|
72 |
+
# access_token = create_access_token(identity=str(customer.id))
|
73 |
+
return APIResponse.success(message='注册成功!',data={
|
74 |
+
# 'token': access_token,
|
75 |
+
'email': data['email']
|
76 |
+
})
|
77 |
+
|
78 |
+
|
79 |
+
class UserLoginResource(Resource):
|
80 |
+
def post(self):
|
81 |
+
"""用户登录接口[^3]"""
|
82 |
+
data = request.form
|
83 |
+
customer = Customer.query.filter_by(email=data['email']).first()
|
84 |
+
|
85 |
+
if not customer or not verify_password(customer.password, data['password']):
|
86 |
+
return APIResponse.error('账号或密码错误')
|
87 |
+
# 确保identity是字符串
|
88 |
+
access_token = create_access_token(identity=str(customer.id))
|
89 |
+
return APIResponse.success({
|
90 |
+
'token': access_token,
|
91 |
+
'email': data['email'],
|
92 |
+
'level': customer.level
|
93 |
+
})
|
94 |
+
|
95 |
+
|
96 |
+
class SendResetCodeResource(Resource):
|
97 |
+
def post(self):
|
98 |
+
"""发送密码重置验证码接口[^4]"""
|
99 |
+
email = request.form.get('email')
|
100 |
+
if not Customer.query.filter_by(email=email).first():
|
101 |
+
return APIResponse.not_found('用户不存在')
|
102 |
+
|
103 |
+
code = ''.join(random.choices('0123456789', k=6))
|
104 |
+
send_code = SendCode(
|
105 |
+
send_type=2,
|
106 |
+
send_to=email,
|
107 |
+
code=code,
|
108 |
+
created_at=datetime.utcnow()
|
109 |
+
)
|
110 |
+
db.session.add(send_code)
|
111 |
+
try:
|
112 |
+
EmailService.send_verification_code(email, code)
|
113 |
+
db.session.commit()
|
114 |
+
return APIResponse.success()
|
115 |
+
except Exception as e:
|
116 |
+
db.session.rollback()
|
117 |
+
return APIResponse.error('邮件发送失败', 500)
|
118 |
+
|
119 |
+
|
120 |
+
class ResetPasswordResource(Resource):
|
121 |
+
def post(self):
|
122 |
+
"""重置密码接口[^5]"""
|
123 |
+
data = request.form
|
124 |
+
|
125 |
+
# 密码一致性验证
|
126 |
+
is_valid, msg = validate_password_confirmation(data)
|
127 |
+
if not is_valid:
|
128 |
+
return APIResponse.error(msg, 400)
|
129 |
+
|
130 |
+
# 验证码有效性验证
|
131 |
+
is_valid, msg = validate_verification_code(
|
132 |
+
data['email'], data['code'], 2
|
133 |
+
)
|
134 |
+
if not is_valid:
|
135 |
+
return APIResponse.error(msg, 400)
|
136 |
+
|
137 |
+
customer = Customer.query.filter_by(email=data['email']).first()
|
138 |
+
customer.password = hash_password(data['password'])
|
139 |
+
customer.updated_at = datetime.utcnow()
|
140 |
+
db.session.commit()
|
141 |
+
return APIResponse.success()
|
142 |
+
|
app/resources/api/__init__.py
ADDED
File without changes
|
app/resources/api/common.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/resources/api/common.py
|
2 |
+
from flask_restful import Resource
|
3 |
+
|
4 |
+
from app.utils.response import APIResponse
|
5 |
+
from app.models.setting import Setting
|
6 |
+
|
7 |
+
|
8 |
+
class SystemConfigResource(Resource):
|
9 |
+
def get(self):
|
10 |
+
"""获取系统版本配置 [^6]"""
|
11 |
+
try:
|
12 |
+
version = Setting.get_version()
|
13 |
+
return APIResponse.success({"version": version})
|
14 |
+
|
15 |
+
except Exception as e:
|
16 |
+
return APIResponse.error(
|
17 |
+
message="服务器内部错误",
|
18 |
+
code=500,
|
19 |
+
errors=str(e)
|
20 |
+
)
|
app/resources/api/comparison.py
ADDED
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/comparison.py
|
2 |
+
import os
|
3 |
+
import zipfile
|
4 |
+
from datetime import datetime
|
5 |
+
from io import BytesIO
|
6 |
+
|
7 |
+
import pandas as pd
|
8 |
+
from flask import request, current_app, send_file
|
9 |
+
from flask_restful import Resource, reqparse
|
10 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
11 |
+
|
12 |
+
from app import db
|
13 |
+
from app.models import Customer
|
14 |
+
from app.models.comparison import Comparison, ComparisonFav
|
15 |
+
from app.utils.response import APIResponse
|
16 |
+
from sqlalchemy import func
|
17 |
+
from datetime import datetime
|
18 |
+
|
19 |
+
|
20 |
+
class MyComparisonListResource(Resource):
|
21 |
+
@jwt_required()
|
22 |
+
def get(self):
|
23 |
+
"""获取我的术语表列表[^1]"""
|
24 |
+
# 直接查询所有数据(不解析查询参数)
|
25 |
+
query = Comparison.query.filter_by(customer_id=get_jwt_identity())
|
26 |
+
comparisons = [self._format_comparison(comparison) for comparison in query.all()]
|
27 |
+
|
28 |
+
# 返回结果
|
29 |
+
return APIResponse.success({
|
30 |
+
'data': comparisons,
|
31 |
+
'total': len(comparisons)
|
32 |
+
})
|
33 |
+
|
34 |
+
def _format_comparison(self, comparison):
|
35 |
+
"""格式化术语表数据"""
|
36 |
+
# 解析 content 字段
|
37 |
+
content_list = []
|
38 |
+
if comparison.content:
|
39 |
+
for item in comparison.content.split('; '):
|
40 |
+
if ':' in item:
|
41 |
+
origin, target = item.split(':', 1)
|
42 |
+
content_list.append({
|
43 |
+
'origin': origin.strip(),
|
44 |
+
'target': target.strip()
|
45 |
+
})
|
46 |
+
|
47 |
+
# 返回格式化后的数据
|
48 |
+
return {
|
49 |
+
'id': comparison.id,
|
50 |
+
'title': comparison.title,
|
51 |
+
'origin_lang': comparison.origin_lang,
|
52 |
+
'target_lang': comparison.target_lang,
|
53 |
+
'share_flag': comparison.share_flag,
|
54 |
+
'added_count': comparison.added_count,
|
55 |
+
'content': content_list, # 返回解析后的数组
|
56 |
+
'customer_id': comparison.customer_id,
|
57 |
+
'created_at': comparison.created_at.strftime('%Y-%m-%d %H:%M') if comparison.created_at else None, # 格式化时间
|
58 |
+
'updated_at': comparison.updated_at.strftime('%Y-%m-%d %H:%M') if comparison.updated_at else None, # 格式化时间
|
59 |
+
'deleted_flag': comparison.deleted_flag
|
60 |
+
}
|
61 |
+
|
62 |
+
|
63 |
+
|
64 |
+
|
65 |
+
# 获取共享术语表列表
|
66 |
+
class SharedComparisonListResource(Resource):
|
67 |
+
@jwt_required()
|
68 |
+
def get(self):
|
69 |
+
"""获取共享术语表列表[^3]"""
|
70 |
+
# 从查询字符串中解析参数
|
71 |
+
parser = reqparse.RequestParser()
|
72 |
+
parser.add_argument('page', type=int, default=1, location='args') # 分页参数
|
73 |
+
parser.add_argument('limit', type=int, default=10, location='args') # 分页参数
|
74 |
+
parser.add_argument('order', type=str, default='latest', location='args') # 排序参数
|
75 |
+
args = parser.parse_args()
|
76 |
+
|
77 |
+
# 查询共享的术语表,并关联 Customer 表获取用户 email
|
78 |
+
query = db.session.query(
|
79 |
+
Comparison,
|
80 |
+
func.count(ComparisonFav.id).label('fav_count'), # 动态计算收藏量
|
81 |
+
Customer.email.label('customer_email') # 获取用户的 email
|
82 |
+
).outerjoin(
|
83 |
+
ComparisonFav, Comparison.id == ComparisonFav.comparison_id
|
84 |
+
).outerjoin(
|
85 |
+
Customer, Comparison.customer_id == Customer.id # 通过 customer_id 关联 Customer
|
86 |
+
).filter(
|
87 |
+
Comparison.share_flag == 'Y',
|
88 |
+
Comparison.deleted_flag == 'N'
|
89 |
+
).group_by(
|
90 |
+
Comparison.id
|
91 |
+
)
|
92 |
+
|
93 |
+
# 根据 order 参数排序
|
94 |
+
if args['order'] == 'latest':
|
95 |
+
query = query.order_by(Comparison.created_at.desc()) # 按最新发表排序
|
96 |
+
elif args['order'] == 'added':
|
97 |
+
query = query.order_by(Comparison.added_count.desc()) # 按添加量排序
|
98 |
+
elif args['order'] == 'fav':
|
99 |
+
query = query.order_by(func.count(ComparisonFav.id).desc()) # 按收藏量排序
|
100 |
+
|
101 |
+
# 分页查询
|
102 |
+
pagination = query.paginate(page=args['page'], per_page=args['limit'], error_out=False)
|
103 |
+
comparisons = [{
|
104 |
+
'id': comparison.id,
|
105 |
+
'title': comparison.title,
|
106 |
+
'origin_lang': comparison.origin_lang,
|
107 |
+
'target_lang': comparison.target_lang,
|
108 |
+
'content': self.parse_content(comparison.content), # 解析 content 字段为数组
|
109 |
+
'email': customer_email if customer_email else '匿名用户', # 返回用户 email
|
110 |
+
'added_count': comparison.added_count,
|
111 |
+
'created_at': comparison.created_at.strftime('%Y-%m-%d %H:%M'), # 格式化时间
|
112 |
+
'faved': self.check_faved(comparison.id), # 检查是否被当前用户收藏
|
113 |
+
'fav_count': fav_count # 添加收藏量
|
114 |
+
} for comparison, fav_count, customer_email in pagination.items]
|
115 |
+
|
116 |
+
# 返回结果
|
117 |
+
return APIResponse.success({
|
118 |
+
'data': comparisons,
|
119 |
+
'total': pagination.total,
|
120 |
+
'current_page': pagination.page,
|
121 |
+
'per_page': pagination.per_page
|
122 |
+
})
|
123 |
+
|
124 |
+
def parse_content(self, content_str):
|
125 |
+
"""将 content 字符串解析为数组格式"""
|
126 |
+
content_list = []
|
127 |
+
if content_str:
|
128 |
+
for item in content_str.split('; '):
|
129 |
+
if ':' in item:
|
130 |
+
origin, target = item.split(':', 1) # 分割为 origin 和 target
|
131 |
+
content_list.append({
|
132 |
+
'origin': origin.strip(),
|
133 |
+
'target': target.strip()
|
134 |
+
})
|
135 |
+
return content_list
|
136 |
+
|
137 |
+
def check_faved(self, comparison_id):
|
138 |
+
"""检查当前用户是否收藏了该术语表"""
|
139 |
+
# 假设当前用户的 ID 存储在 JWT 中
|
140 |
+
user_id = get_jwt_identity()
|
141 |
+
if user_id:
|
142 |
+
fav = ComparisonFav.query.filter_by(
|
143 |
+
comparison_id=comparison_id,
|
144 |
+
customer_id=user_id
|
145 |
+
).first()
|
146 |
+
return 1 if fav else 0
|
147 |
+
return 0
|
148 |
+
|
149 |
+
|
150 |
+
|
151 |
+
|
152 |
+
# 编辑术语列表
|
153 |
+
class EditComparisonResource(Resource):
|
154 |
+
@jwt_required()
|
155 |
+
def post(self, id):
|
156 |
+
"""编辑术语表[^3]"""
|
157 |
+
comparison = Comparison.query.filter_by(
|
158 |
+
id=id,
|
159 |
+
customer_id=get_jwt_identity()
|
160 |
+
).first_or_404()
|
161 |
+
|
162 |
+
data = request.form
|
163 |
+
if 'title' in data:
|
164 |
+
comparison.title = data['title']
|
165 |
+
if 'content' in data:
|
166 |
+
comparison.content = data['content']
|
167 |
+
if 'origin_lang' in data:
|
168 |
+
comparison.origin_lang = data['origin_lang']
|
169 |
+
if 'target_lang' in data:
|
170 |
+
comparison.target_lang = data['target_lang']
|
171 |
+
|
172 |
+
db.session.commit()
|
173 |
+
return APIResponse.success(message='术语表更新成功')
|
174 |
+
|
175 |
+
# 更新术语表共享状态
|
176 |
+
class ShareComparisonResource(Resource):
|
177 |
+
@jwt_required()
|
178 |
+
def post(self, id):
|
179 |
+
"""修改共享状态[^4]"""
|
180 |
+
comparison = Comparison.query.filter_by(
|
181 |
+
id=id,
|
182 |
+
customer_id=get_jwt_identity()
|
183 |
+
).first_or_404()
|
184 |
+
|
185 |
+
data = request.form
|
186 |
+
if 'share_flag' not in data or data['share_flag'] not in ['Y', 'N']:
|
187 |
+
return APIResponse.error('share_flag 参数无效', 400)
|
188 |
+
|
189 |
+
comparison.share_flag = data['share_flag']
|
190 |
+
db.session.commit()
|
191 |
+
return APIResponse.success(message='共享状态已更新')
|
192 |
+
|
193 |
+
|
194 |
+
|
195 |
+
# 复制到我的术语库
|
196 |
+
class CopyComparisonResource(Resource):
|
197 |
+
@jwt_required()
|
198 |
+
def post(self, id):
|
199 |
+
"""复制到我的术语库[^5]"""
|
200 |
+
comparison = Comparison.query.filter_by(
|
201 |
+
id=id,
|
202 |
+
share_flag='Y'
|
203 |
+
).first_or_404()
|
204 |
+
|
205 |
+
new_comparison = Comparison(
|
206 |
+
title=f"{comparison.title} (副本)",
|
207 |
+
content=comparison.content,
|
208 |
+
origin_lang=comparison.origin_lang,
|
209 |
+
target_lang=comparison.target_lang,
|
210 |
+
customer_id=get_jwt_identity(),
|
211 |
+
share_flag='N'
|
212 |
+
)
|
213 |
+
db.session.add(new_comparison)
|
214 |
+
db.session.commit()
|
215 |
+
return APIResponse.success({
|
216 |
+
'new_id': new_comparison.id
|
217 |
+
})
|
218 |
+
|
219 |
+
# 收藏/取消收藏
|
220 |
+
class FavoriteComparisonResource(Resource):
|
221 |
+
@jwt_required()
|
222 |
+
def post(self, id):
|
223 |
+
"""收藏/取消收藏[^6]"""
|
224 |
+
comparison = Comparison.query.filter_by(id=id).first_or_404()
|
225 |
+
customer_id = get_jwt_identity()
|
226 |
+
|
227 |
+
favorite = ComparisonFav.query.filter_by(
|
228 |
+
comparison_id=id,
|
229 |
+
customer_id=customer_id
|
230 |
+
).first()
|
231 |
+
|
232 |
+
if favorite:
|
233 |
+
db.session.delete(favorite)
|
234 |
+
message = '已取消收藏'
|
235 |
+
else:
|
236 |
+
new_favorite = ComparisonFav(
|
237 |
+
comparison_id=id,
|
238 |
+
customer_id=customer_id
|
239 |
+
)
|
240 |
+
db.session.add(new_favorite)
|
241 |
+
message = '已收藏'
|
242 |
+
|
243 |
+
db.session.commit()
|
244 |
+
return APIResponse.success(message=message)
|
245 |
+
|
246 |
+
# 创建新术语表
|
247 |
+
class CreateComparisonResource(Resource):
|
248 |
+
@jwt_required()
|
249 |
+
def post(self):
|
250 |
+
"""创建新术语表[^1]"""
|
251 |
+
data = request.form
|
252 |
+
required_fields = ['title', 'share_flag', 'origin_lang', 'target_lang']
|
253 |
+
if not all(field in data for field in required_fields):
|
254 |
+
return APIResponse.error('缺少必要参数', 400)
|
255 |
+
|
256 |
+
# 解析 content 参数
|
257 |
+
content_list = []
|
258 |
+
for key, value in data.items():
|
259 |
+
if key.startswith('content[') and '][origin]' in key:
|
260 |
+
# 提取索引
|
261 |
+
index = key.split('[')[1].split(']')[0]
|
262 |
+
origin = value
|
263 |
+
target = data.get(f'content[{index}][target]', '')
|
264 |
+
content_list.append(f"{origin}: {target}")
|
265 |
+
|
266 |
+
# 将 content_list 转换为字符串
|
267 |
+
content_str = '; '.join(content_list)
|
268 |
+
|
269 |
+
# 获取当前时间
|
270 |
+
current_time = datetime.utcnow()
|
271 |
+
|
272 |
+
# 创建术语表
|
273 |
+
comparison = Comparison(
|
274 |
+
title=data['title'],
|
275 |
+
origin_lang=data['origin_lang'],
|
276 |
+
target_lang=data['target_lang'],
|
277 |
+
content=content_str, # 插入转换后的 content 字符串
|
278 |
+
customer_id=get_jwt_identity(),
|
279 |
+
share_flag=data.get('share_flag', 'N'),
|
280 |
+
created_at=current_time, # 显式赋值
|
281 |
+
updated_at=current_time # 显式赋值
|
282 |
+
)
|
283 |
+
db.session.add(comparison)
|
284 |
+
db.session.commit()
|
285 |
+
return APIResponse.success({
|
286 |
+
'id': comparison.id
|
287 |
+
})
|
288 |
+
|
289 |
+
|
290 |
+
# 删除术语表
|
291 |
+
class DeleteComparisonResource(Resource):
|
292 |
+
@jwt_required()
|
293 |
+
def delete(self, id):
|
294 |
+
"""删除术语表[^2]"""
|
295 |
+
comparison = Comparison.query.filter_by(
|
296 |
+
id=id,
|
297 |
+
customer_id=get_jwt_identity()
|
298 |
+
).first_or_404()
|
299 |
+
|
300 |
+
db.session.delete(comparison)
|
301 |
+
db.session.commit()
|
302 |
+
return APIResponse.success(message='删除成功')
|
303 |
+
|
304 |
+
|
305 |
+
# 下载模板文件
|
306 |
+
class DownloadTemplateResource(Resource):
|
307 |
+
def get(self):
|
308 |
+
"""下载模板文件[^3]"""
|
309 |
+
from flask import send_file
|
310 |
+
from io import BytesIO
|
311 |
+
import pandas as pd
|
312 |
+
|
313 |
+
# 创建模板文件
|
314 |
+
df = pd.DataFrame(columns=['源术语', '目标术语'])
|
315 |
+
output = BytesIO()
|
316 |
+
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
317 |
+
df.to_excel(writer, index=False)
|
318 |
+
output.seek(0)
|
319 |
+
|
320 |
+
return send_file(
|
321 |
+
output,
|
322 |
+
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
323 |
+
as_attachment=True,
|
324 |
+
download_name='术语表模板.xlsx'
|
325 |
+
)
|
326 |
+
|
327 |
+
# 导入术语表
|
328 |
+
class ImportComparisonResource(Resource):
|
329 |
+
@jwt_required()
|
330 |
+
def post(self):
|
331 |
+
"""
|
332 |
+
导入 Excel 文件
|
333 |
+
"""
|
334 |
+
# 检查是否上传了文件
|
335 |
+
if 'file' not in request.files:
|
336 |
+
return APIResponse.error('未选择文件', 400)
|
337 |
+
file = request.files['file']
|
338 |
+
|
339 |
+
try:
|
340 |
+
# 读取 Excel 文件
|
341 |
+
import pandas as pd
|
342 |
+
df = pd.read_excel(file)
|
343 |
+
|
344 |
+
# 检查文件是否包含所需的列
|
345 |
+
if not {'源术语', '目标术语'}.issubset(df.columns):
|
346 |
+
return APIResponse.error('文件格式不符合模板要求', 406)
|
347 |
+
# 解析 Excel 文件内容
|
348 |
+
content = ';'.join([f"{row['源术语']}: {row['目标术语']}" for _, row in df.iterrows()]) # 按 ': ' 分隔
|
349 |
+
# 创建术语表
|
350 |
+
comparison = Comparison(
|
351 |
+
title='导入的术语表',
|
352 |
+
origin_lang='未知',
|
353 |
+
target_lang='未知',
|
354 |
+
content=content, # 使用改进后的格式
|
355 |
+
customer_id=get_jwt_identity(),
|
356 |
+
share_flag='N'
|
357 |
+
)
|
358 |
+
db.session.add(comparison)
|
359 |
+
db.session.commit()
|
360 |
+
|
361 |
+
# 返回成功响应
|
362 |
+
return APIResponse.success({
|
363 |
+
'id': comparison.id
|
364 |
+
})
|
365 |
+
except Exception as e:
|
366 |
+
# 捕获并返回错误信息
|
367 |
+
return APIResponse.error(f"文件导入失败:{str(e)}", 500)
|
368 |
+
|
369 |
+
|
370 |
+
|
371 |
+
# 导出单个术语表
|
372 |
+
class ExportComparisonResource(Resource):
|
373 |
+
@jwt_required()
|
374 |
+
def get(self, id):
|
375 |
+
"""
|
376 |
+
导出单个术语表
|
377 |
+
"""
|
378 |
+
# 获取当前用户 ID
|
379 |
+
current_user_id = get_jwt_identity()
|
380 |
+
|
381 |
+
# 查询术语表
|
382 |
+
comparison = Comparison.query.get_or_404(id)
|
383 |
+
|
384 |
+
# 检查术语表是否共享或属于当前用户
|
385 |
+
if comparison.share_flag != 'Y' and comparison.user_id != current_user_id:
|
386 |
+
return {'message': '术语表未共享或无权限访问', 'code': 403}, 403
|
387 |
+
|
388 |
+
# 解析术语内容
|
389 |
+
terms = [term.split(': ') for term in comparison.content.split(';')] # 按 ': ' 分割
|
390 |
+
df = pd.DataFrame(terms, columns=['源术语', '目标术语'])
|
391 |
+
|
392 |
+
# 创建 Excel 文件
|
393 |
+
output = BytesIO()
|
394 |
+
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
395 |
+
df.to_excel(writer, index=False)
|
396 |
+
output.seek(0)
|
397 |
+
|
398 |
+
# 返回文件下载响应
|
399 |
+
return send_file(
|
400 |
+
output,
|
401 |
+
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
402 |
+
as_attachment=True,
|
403 |
+
download_name=f'{comparison.title}.xlsx'
|
404 |
+
)
|
405 |
+
|
406 |
+
|
407 |
+
|
408 |
+
|
409 |
+
# 批量导出所有术语表
|
410 |
+
class ExportAllComparisonsResource(Resource):
|
411 |
+
@jwt_required()
|
412 |
+
def get(self):
|
413 |
+
"""
|
414 |
+
批量导出所有术语表
|
415 |
+
"""
|
416 |
+
# 获取当前用户 ID
|
417 |
+
current_user_id = get_jwt_identity()
|
418 |
+
|
419 |
+
# 查询当前用户的所��术语表
|
420 |
+
comparisons = Comparison.query.filter_by(customer_id=current_user_id).all()
|
421 |
+
|
422 |
+
# 创建 ZIP 文件
|
423 |
+
memory_file = BytesIO()
|
424 |
+
with zipfile.ZipFile(memory_file, 'w') as zf:
|
425 |
+
for comparison in comparisons:
|
426 |
+
# 解析术语内容
|
427 |
+
terms = [term.split(': ') for term in comparison.content.split(';')] # 按 ': ' 分割
|
428 |
+
df = pd.DataFrame(terms, columns=['源术语', '目标术语'])
|
429 |
+
|
430 |
+
# 创建 Excel 文件
|
431 |
+
output = BytesIO()
|
432 |
+
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
433 |
+
df.to_excel(writer, index=False)
|
434 |
+
output.seek(0)
|
435 |
+
|
436 |
+
# 将 Excel 文件添加到 ZIP 中
|
437 |
+
zf.writestr(f"{comparison.title}.xlsx", output.getvalue())
|
438 |
+
|
439 |
+
memory_file.seek(0)
|
440 |
+
|
441 |
+
# 返回 ZIP 文件下载响应
|
442 |
+
return send_file(
|
443 |
+
memory_file,
|
444 |
+
mimetype='application/zip',
|
445 |
+
as_attachment=True,
|
446 |
+
download_name=f'术语表_{datetime.now().strftime("%Y%m%d")}.zip'
|
447 |
+
)
|
448 |
+
|
449 |
+
|
app/resources/api/customer.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/customer.py
|
2 |
+
from app.utils.response import APIResponse
|
3 |
+
import uuid
|
4 |
+
from flask_restful import Resource, reqparse
|
5 |
+
from flask_jwt_extended import jwt_required
|
6 |
+
from app.models.customer import Customer
|
7 |
+
class GuestIdResource(Resource):
|
8 |
+
def get(self):
|
9 |
+
"""生成临时访客唯一标识[^1]"""
|
10 |
+
guest_id = str(uuid.uuid4())
|
11 |
+
return APIResponse.success({
|
12 |
+
'guest_id': guest_id
|
13 |
+
})
|
14 |
+
|
15 |
+
|
16 |
+
|
17 |
+
|
18 |
+
|
19 |
+
class CustomerDetailResource(Resource):
|
20 |
+
@jwt_required()
|
21 |
+
def get(self, customer_id):
|
22 |
+
"""获取客户详细信息[^2]"""
|
23 |
+
customer = Customer.query.get_or_404(customer_id)
|
24 |
+
return APIResponse.success({
|
25 |
+
'id': customer.id,
|
26 |
+
'email': customer.email,
|
27 |
+
'level': customer.level,
|
28 |
+
'created_at': customer.created_at.isoformat(),
|
29 |
+
'storage': customer.storage
|
30 |
+
})
|
31 |
+
|
32 |
+
|
app/resources/api/files.py
ADDED
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/file.py
|
2 |
+
import hashlib
|
3 |
+
import uuid
|
4 |
+
from werkzeug.utils import secure_filename
|
5 |
+
import os
|
6 |
+
from app import db
|
7 |
+
from app.models.customer import Customer
|
8 |
+
from app.models.translate import Translate
|
9 |
+
from app.utils.response import APIResponse
|
10 |
+
from pathlib import Path
|
11 |
+
from flask_restful import Resource
|
12 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
13 |
+
from flask import request, current_app
|
14 |
+
from datetime import datetime
|
15 |
+
|
16 |
+
|
17 |
+
class FileUploadResource1(Resource):
|
18 |
+
@jwt_required()
|
19 |
+
def post(self):
|
20 |
+
"""文件上传接口"""
|
21 |
+
# 验证文件存在性
|
22 |
+
if 'file' not in request.files:
|
23 |
+
return APIResponse.error('未选择文件', 400)
|
24 |
+
file = request.files['file']
|
25 |
+
|
26 |
+
# 验证文件名有效性
|
27 |
+
if file.filename == '':
|
28 |
+
return APIResponse.error('无效文件名', 400)
|
29 |
+
|
30 |
+
# 验证文件类型
|
31 |
+
if not self.allowed_file(file.filename):
|
32 |
+
return APIResponse.error(
|
33 |
+
f"仅支持以下格式:{', '.join(current_app.config['ALLOWED_EXTENSIONS'])}", 400)
|
34 |
+
|
35 |
+
# 验证文件大小
|
36 |
+
if not self.validate_file_size(file.stream):
|
37 |
+
return APIResponse.error(
|
38 |
+
f"文件大小超过{current_app.config['MAX_FILE_SIZE'] // (1024 * 1024)}MB限制", 400)
|
39 |
+
|
40 |
+
# 获取用户存储信息
|
41 |
+
user_id = get_jwt_identity()
|
42 |
+
customer = Customer.query.get(user_id)
|
43 |
+
file_size = request.content_length # 使用实际内容长度
|
44 |
+
|
45 |
+
# 验证存储空间
|
46 |
+
if customer.storage + file_size > current_app.config['MAX_USER_STORAGE']:
|
47 |
+
return APIResponse.error('存储空间不足', 403)
|
48 |
+
|
49 |
+
try:
|
50 |
+
# 生成存储路径
|
51 |
+
save_dir = self.get_upload_dir()
|
52 |
+
filename = file.filename # 直接使用原始文件名
|
53 |
+
save_path = os.path.join(save_dir, filename)
|
54 |
+
|
55 |
+
# 检查路径是否安全
|
56 |
+
if not self.is_safe_path(save_dir, save_path):
|
57 |
+
return APIResponse.error('文件名包含非法字符', 400)
|
58 |
+
|
59 |
+
# 保存文件
|
60 |
+
file.save(save_path)
|
61 |
+
# 更新用户存储空间
|
62 |
+
customer.storage += file_size
|
63 |
+
db.session.commit()
|
64 |
+
# 生成 UUID
|
65 |
+
file_uuid = str(uuid.uuid4())
|
66 |
+
# 计算文件的 MD5
|
67 |
+
file_md5 = self.calculate_md5(save_path)
|
68 |
+
|
69 |
+
# 创建翻译记录
|
70 |
+
translate_record = Translate(
|
71 |
+
translate_no=f"TRANS{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
72 |
+
uuid=file_uuid,
|
73 |
+
customer_id=user_id,
|
74 |
+
origin_filename=filename,
|
75 |
+
origin_filepath=os.path.abspath(save_path), # 使用绝对路径
|
76 |
+
target_filepath='', # 目标文件路径暂为空
|
77 |
+
status='none', # 初始状态为 none
|
78 |
+
origin_filesize=file_size,
|
79 |
+
md5=file_md5,
|
80 |
+
created_at=datetime.utcnow()
|
81 |
+
)
|
82 |
+
db.session.add(translate_record)
|
83 |
+
db.session.commit()
|
84 |
+
|
85 |
+
# 返回响应,包含文件名、UUID 和翻译记录 ID
|
86 |
+
return APIResponse.success({
|
87 |
+
'filename': filename,
|
88 |
+
'uuid': file_uuid,
|
89 |
+
'translate_id': translate_record.id,
|
90 |
+
'save_path': os.path.abspath(save_path) # 返回绝对路径
|
91 |
+
})
|
92 |
+
|
93 |
+
except Exception as e:
|
94 |
+
db.session.rollback()
|
95 |
+
current_app.logger.error(f"文件上传失败:{str(e)}")
|
96 |
+
return APIResponse.error('文件上传失败', 500)
|
97 |
+
|
98 |
+
@staticmethod
|
99 |
+
def allowed_file(filename):
|
100 |
+
# """验证文件类型是否允许"""# 暂不支持PDF 'pdf',
|
101 |
+
ALLOWED_EXTENSIONS = {'docx', 'xlsx', 'pptx', 'txt', 'md', 'csv', 'xls', 'doc'}
|
102 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
103 |
+
|
104 |
+
@staticmethod
|
105 |
+
def validate_file_size(file_stream):
|
106 |
+
"""验证文件大小是否超过限制"""
|
107 |
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
108 |
+
file_stream.seek(0, os.SEEK_END)
|
109 |
+
file_size = file_stream.tell()
|
110 |
+
file_stream.seek(0)
|
111 |
+
return file_size <= MAX_FILE_SIZE
|
112 |
+
|
113 |
+
@staticmethod
|
114 |
+
def get_upload_dir():
|
115 |
+
"""获取按日期分类的上传目录"""
|
116 |
+
# 获取上传根目录
|
117 |
+
base_dir = Path(current_app.config['UPLOAD_BASE_DIR'])
|
118 |
+
upload_dir = base_dir / 'uploads' / datetime.now().strftime('%Y-%m-%d')
|
119 |
+
|
120 |
+
# 如果目录不存在则创建
|
121 |
+
if not upload_dir.exists():
|
122 |
+
upload_dir.mkdir(parents=True, exist_ok=True)
|
123 |
+
|
124 |
+
return str(upload_dir)
|
125 |
+
|
126 |
+
@staticmethod
|
127 |
+
def calculate_md5(file_path):
|
128 |
+
"""计算文件的 MD5 值"""
|
129 |
+
hash_md5 = hashlib.md5()
|
130 |
+
with open(file_path, "rb") as f:
|
131 |
+
for chunk in iter(lambda: f.read(4096), b""):
|
132 |
+
hash_md5.update(chunk)
|
133 |
+
return hash_md5.hexdigest()
|
134 |
+
|
135 |
+
@staticmethod
|
136 |
+
def is_safe_path(base_dir, file_path):
|
137 |
+
"""检查文件路径是否安全,防止路径遍历攻击"""
|
138 |
+
base_dir = Path(base_dir).resolve()
|
139 |
+
file_path = Path(file_path).resolve()
|
140 |
+
return file_path.is_relative_to(base_dir)
|
141 |
+
|
142 |
+
|
143 |
+
|
144 |
+
class FileUploadResource(Resource):
|
145 |
+
@jwt_required()
|
146 |
+
def post(self):
|
147 |
+
"""文件上传接口"""
|
148 |
+
# 验证文件存在性
|
149 |
+
if 'file' not in request.files:
|
150 |
+
return APIResponse.error('未选择文件', 400)
|
151 |
+
file = request.files['file']
|
152 |
+
|
153 |
+
# 验证文件名有效性
|
154 |
+
if file.filename == '':
|
155 |
+
return APIResponse.error('无效文件名', 400)
|
156 |
+
|
157 |
+
# 验证文件类型
|
158 |
+
if not self.allowed_file(file.filename):
|
159 |
+
return APIResponse.error(
|
160 |
+
f"仅支持以下格式:{', '.join(current_app.config['ALLOWED_EXTENSIONS'])}", 400)
|
161 |
+
|
162 |
+
# 验证文件大小
|
163 |
+
if not self.validate_file_size(file.stream):
|
164 |
+
return APIResponse.error(
|
165 |
+
f"文件大小超过{current_app.config['MAX_FILE_SIZE'] // (1024 * 1024)}MB限制", 400)
|
166 |
+
|
167 |
+
# 获取用户存储信息
|
168 |
+
user_id = get_jwt_identity()
|
169 |
+
customer = Customer.query.get(user_id)
|
170 |
+
file_size = request.content_length # 使用实际内容长度
|
171 |
+
|
172 |
+
# 验证存储空间
|
173 |
+
if customer.storage + file_size > current_app.config['MAX_USER_STORAGE']:
|
174 |
+
return APIResponse.error('存储空间不足', 403)
|
175 |
+
|
176 |
+
try:
|
177 |
+
# 生成存储路径
|
178 |
+
save_dir = Path(self.get_upload_dir())
|
179 |
+
filename = file.filename # 直接使用原始文件名
|
180 |
+
save_path = save_dir / filename
|
181 |
+
|
182 |
+
# 检查路径是否安全
|
183 |
+
if not self.is_safe_path(save_dir, save_path):
|
184 |
+
return APIResponse.error('文件名包含非法字符', 400)
|
185 |
+
|
186 |
+
# 保存文件
|
187 |
+
file.save(save_path)
|
188 |
+
# 更新用户存储空间
|
189 |
+
customer.storage += file_size
|
190 |
+
db.session.commit()
|
191 |
+
# 生成 UUID
|
192 |
+
file_uuid = str(uuid.uuid4())
|
193 |
+
# 计算文件的 MD5
|
194 |
+
file_md5 = self.calculate_md5(save_path)
|
195 |
+
|
196 |
+
# 创建翻译记录
|
197 |
+
translate_record = Translate(
|
198 |
+
translate_no=f"TRANS{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
199 |
+
uuid=file_uuid,
|
200 |
+
customer_id=user_id,
|
201 |
+
origin_filename=filename,
|
202 |
+
origin_filepath=str(save_path.resolve()), # 使用绝对路径
|
203 |
+
target_filepath='', # 目标文件路径暂为空
|
204 |
+
status='none', # 初始状态为 none
|
205 |
+
origin_filesize=file_size,
|
206 |
+
md5=file_md5,
|
207 |
+
created_at=datetime.utcnow()
|
208 |
+
)
|
209 |
+
db.session.add(translate_record)
|
210 |
+
db.session.commit()
|
211 |
+
|
212 |
+
# 返回响应,包含文件名、UUID 和翻译记录 ID
|
213 |
+
return APIResponse.success({
|
214 |
+
'filename': filename,
|
215 |
+
'uuid': file_uuid,
|
216 |
+
'translate_id': translate_record.id,
|
217 |
+
'save_path': str(save_path.resolve()) # 返回绝对路径
|
218 |
+
})
|
219 |
+
|
220 |
+
except Exception as e:
|
221 |
+
db.session.rollback()
|
222 |
+
current_app.logger.error(f"文件上传失败:{str(e)}")
|
223 |
+
return APIResponse.error('文件上传失败', 500)
|
224 |
+
|
225 |
+
@staticmethod
|
226 |
+
def allowed_file(filename):
|
227 |
+
"""验证文件类型是否允许"""
|
228 |
+
ALLOWED_EXTENSIONS = {'docx', 'xlsx', 'pptx', 'txt', 'md', 'csv', 'xls', 'doc'}
|
229 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
230 |
+
|
231 |
+
@staticmethod
|
232 |
+
def validate_file_size(file_stream):
|
233 |
+
"""验证文件大小是否超过限制"""
|
234 |
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
235 |
+
file_stream.seek(0, os.SEEK_END)
|
236 |
+
file_size = file_stream.tell()
|
237 |
+
file_stream.seek(0)
|
238 |
+
return file_size <= MAX_FILE_SIZE
|
239 |
+
|
240 |
+
@staticmethod
|
241 |
+
def get_upload_dir():
|
242 |
+
"""获取按日期分类的上传目录"""
|
243 |
+
# 获取上传根目录
|
244 |
+
base_dir = Path(current_app.config['UPLOAD_BASE_DIR'])
|
245 |
+
upload_dir = base_dir / 'uploads' / datetime.now().strftime('%Y-%m-%d')
|
246 |
+
|
247 |
+
# 如果目录不存在则创建
|
248 |
+
if not upload_dir.exists():
|
249 |
+
upload_dir.mkdir(parents=True, exist_ok=True)
|
250 |
+
|
251 |
+
return str(upload_dir)
|
252 |
+
|
253 |
+
@staticmethod
|
254 |
+
def calculate_md5(file_path):
|
255 |
+
"""计算文件的 MD5 值"""
|
256 |
+
hash_md5 = hashlib.md5()
|
257 |
+
with open(file_path, "rb") as f:
|
258 |
+
for chunk in iter(lambda: f.read(4096), b""):
|
259 |
+
hash_md5.update(chunk)
|
260 |
+
return hash_md5.hexdigest()
|
261 |
+
|
262 |
+
@staticmethod
|
263 |
+
def is_safe_path(base_dir, file_path):
|
264 |
+
"""检查文件路径是否安全,防止路径遍历攻击"""
|
265 |
+
base_dir = Path(base_dir).resolve()
|
266 |
+
file_path = Path(file_path).resolve()
|
267 |
+
return file_path.is_relative_to(base_dir)
|
268 |
+
|
269 |
+
|
270 |
+
|
271 |
+
class FileDeleteResource1(Resource):
|
272 |
+
@jwt_required()
|
273 |
+
def post(self):
|
274 |
+
"""文件删除接口[^1]"""
|
275 |
+
data = request.form
|
276 |
+
if 'uuid' not in data:
|
277 |
+
return APIResponse.error('缺少必要参数', 400)
|
278 |
+
|
279 |
+
try:
|
280 |
+
# 根据 UUID 查询翻译记录
|
281 |
+
translate_record = Translate.query.filter_by(uuid=data['uuid']).first()
|
282 |
+
if not translate_record:
|
283 |
+
return APIResponse.error('文件记录不存在', 404)
|
284 |
+
|
285 |
+
# 获取文件绝对路径
|
286 |
+
file_path = translate_record.origin_filepath
|
287 |
+
|
288 |
+
# 删除物理文件
|
289 |
+
if os.path.exists(file_path):
|
290 |
+
os.remove(file_path)
|
291 |
+
else:
|
292 |
+
current_app.logger.warning(f"文件不存在:{file_path}")
|
293 |
+
|
294 |
+
# 删除数据库记录
|
295 |
+
db.session.delete(translate_record)
|
296 |
+
db.session.commit()
|
297 |
+
|
298 |
+
return APIResponse.success(message='文件删除成功')
|
299 |
+
|
300 |
+
except Exception as e:
|
301 |
+
db.session.rollback()
|
302 |
+
current_app.logger.error(f"文件删除失败:{str(e)}")
|
303 |
+
return APIResponse.error('文件删除失败', 500)
|
304 |
+
|
305 |
+
|
306 |
+
|
307 |
+
class FileDeleteResource(Resource):
|
308 |
+
@jwt_required()
|
309 |
+
def post(self):
|
310 |
+
"""文件删除接口"""
|
311 |
+
data = request.form
|
312 |
+
if 'uuid' not in data:
|
313 |
+
return APIResponse.error('缺少必要参数', 400)
|
314 |
+
|
315 |
+
try:
|
316 |
+
# 根据 UUID 查询翻译记录
|
317 |
+
translate_record = Translate.query.filter_by(uuid=data['uuid']).first()
|
318 |
+
if not translate_record:
|
319 |
+
return APIResponse.error('文件记录不存在', 404)
|
320 |
+
|
321 |
+
# 获取文件绝对路径
|
322 |
+
file_path = Path(translate_record.origin_filepath)
|
323 |
+
|
324 |
+
# 删除物理文件
|
325 |
+
if file_path.exists():
|
326 |
+
file_path.unlink()
|
327 |
+
else:
|
328 |
+
current_app.logger.warning(f"文件不存在:{file_path}")
|
329 |
+
|
330 |
+
# 删除数据库记录
|
331 |
+
db.session.delete(translate_record)
|
332 |
+
db.session.commit()
|
333 |
+
|
334 |
+
return APIResponse.success(message='文件删除成功')
|
335 |
+
|
336 |
+
except Exception as e:
|
337 |
+
db.session.rollback()
|
338 |
+
current_app.logger.error(f"文件删除失败:{str(e)}")
|
339 |
+
return APIResponse.error('文件删除失败', 500)
|
app/resources/api/prompt.py
ADDED
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/prompt.py
|
2 |
+
from datetime import datetime, date
|
3 |
+
|
4 |
+
from flask import request
|
5 |
+
from flask_restful import Resource, reqparse
|
6 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
7 |
+
from sqlalchemy import func
|
8 |
+
from app import db
|
9 |
+
from app.models import Customer
|
10 |
+
from app.models.prompt import Prompt, PromptFav
|
11 |
+
from app.utils.response import APIResponse
|
12 |
+
|
13 |
+
# 获取提示语列表
|
14 |
+
class MyPromptListResource(Resource):
|
15 |
+
@jwt_required()
|
16 |
+
def get(self):
|
17 |
+
"""获取我的提示语列表[^1]"""
|
18 |
+
# 直接查询所有数据(不解析查询参数)
|
19 |
+
query = Prompt.query.filter_by(customer_id=get_jwt_identity(), deleted_flag='N')
|
20 |
+
prompts = [{
|
21 |
+
'id': p.id,
|
22 |
+
'title': p.title,
|
23 |
+
'content': p.content[:100] + '...' if len(p.content) > 100 else p.content,
|
24 |
+
'share_flag': p.share_flag,
|
25 |
+
'created_at': p.created_at.isoformat() if p.created_at else None
|
26 |
+
} for p in query.all()]
|
27 |
+
|
28 |
+
# 返回结果
|
29 |
+
return APIResponse.success({
|
30 |
+
'data': prompts,
|
31 |
+
'total': len(prompts)
|
32 |
+
})
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
# 获取共享提示语列表
|
37 |
+
class SharedPromptListResource(Resource):
|
38 |
+
def get(self):
|
39 |
+
"""获取共享提示语列表[^4]"""
|
40 |
+
# 从查询字符串中解析参数
|
41 |
+
parser = reqparse.RequestParser()
|
42 |
+
parser.add_argument('page', type=int, default=1, location='args') # 分页参数
|
43 |
+
parser.add_argument('limit', type=int, default=10, location='args') # 分页参数
|
44 |
+
parser.add_argument('porder', type=str, default='latest', location='args') # 排序参数
|
45 |
+
args = parser.parse_args()
|
46 |
+
|
47 |
+
# 查询共享的提示语
|
48 |
+
query = db.session.query(
|
49 |
+
Prompt, # 获取完整的 Prompt 信息
|
50 |
+
func.count(PromptFav.id).label('fav_count'), # 动态计算收藏量
|
51 |
+
Customer.email.label('customer_email') # 获取用户的 email
|
52 |
+
).outerjoin(
|
53 |
+
PromptFav, Prompt.id == PromptFav.prompt_id
|
54 |
+
).outerjoin(
|
55 |
+
Customer, Prompt.customer_id == Customer.id # 通过 customer_id 关联 Customer
|
56 |
+
).filter(
|
57 |
+
Prompt.share_flag == 'Y',
|
58 |
+
Prompt.deleted_flag == 'N'
|
59 |
+
).group_by(
|
60 |
+
Prompt.id
|
61 |
+
)
|
62 |
+
|
63 |
+
# 根据 porder 参数排序
|
64 |
+
if args['porder'] == 'latest':
|
65 |
+
query = query.order_by(Prompt.created_at.desc()) # 按最新发表排序
|
66 |
+
elif args['porder'] == 'added':
|
67 |
+
query = query.order_by(Prompt.added_count.desc()) # 按添加量排序
|
68 |
+
elif args['porder'] == 'fav':
|
69 |
+
query = query.order_by(func.count(PromptFav.id).desc()) # 按收藏量排序
|
70 |
+
|
71 |
+
# 分页查询
|
72 |
+
pagination = query.paginate(page=args['page'], per_page=args['limit'], error_out=False)
|
73 |
+
prompts = [{
|
74 |
+
'id': prompt.id,
|
75 |
+
'title': prompt.title,
|
76 |
+
'content': prompt.content, # 返回完整的提示语内容
|
77 |
+
'email': customer_email if customer_email else '匿名用户', # 使用查询结果中的 email
|
78 |
+
'share_flag': prompt.share_flag,
|
79 |
+
'added_count': prompt.added_count,
|
80 |
+
'created_at': prompt.created_at.strftime('%Y-%m-%d') if prompt.created_at else None, # 处理 None 值
|
81 |
+
'updated_at': prompt.updated_at.strftime('%Y-%m-%d') if prompt.updated_at else None, # 处理 None 值
|
82 |
+
'fav_count': fav_count
|
83 |
+
} for prompt, fav_count, customer_email in pagination.items]
|
84 |
+
|
85 |
+
# 返回结果
|
86 |
+
return APIResponse.success({
|
87 |
+
'data': prompts,
|
88 |
+
'total': pagination.total
|
89 |
+
})
|
90 |
+
|
91 |
+
|
92 |
+
|
93 |
+
|
94 |
+
# 修改提示语内容
|
95 |
+
class EditPromptResource(Resource):
|
96 |
+
@jwt_required()
|
97 |
+
def post(self, id):
|
98 |
+
"""修改提示语内容[^3]"""
|
99 |
+
prompt = Prompt.query.filter_by(
|
100 |
+
id=id,
|
101 |
+
customer_id=get_jwt_identity(),
|
102 |
+
deleted_flag='N'
|
103 |
+
).first_or_404()
|
104 |
+
|
105 |
+
data = request.form
|
106 |
+
if 'title' in data:
|
107 |
+
if len(data['title']) > 255:
|
108 |
+
return APIResponse.error('标题过长', 400)
|
109 |
+
prompt.title = data['title']
|
110 |
+
|
111 |
+
if 'content' in data:
|
112 |
+
if len(data['content']) > 5000:
|
113 |
+
return APIResponse.error('内容超过5000字符限制', 400)
|
114 |
+
prompt.content = data['content']
|
115 |
+
|
116 |
+
db.session.commit()
|
117 |
+
return APIResponse.success(message='提示语更新成功')
|
118 |
+
|
119 |
+
# 更新共享状态
|
120 |
+
class SharePromptResource(Resource):
|
121 |
+
@jwt_required()
|
122 |
+
def post(self, id):
|
123 |
+
"""
|
124 |
+
修改共享状态[^4]
|
125 |
+
:param id: prompt 的 ID(路径参数)
|
126 |
+
"""
|
127 |
+
# 根据 id 和当前用户查询 prompt
|
128 |
+
prompt = Prompt.query.filter_by(
|
129 |
+
id=id,
|
130 |
+
customer_id=get_jwt_identity(),
|
131 |
+
deleted_flag='N'
|
132 |
+
).first_or_404()
|
133 |
+
|
134 |
+
# 从请求体中获取 share_flag
|
135 |
+
data = request.form # 或者 request.form 如果是表单数据
|
136 |
+
if not data or 'share_flag' not in data or data['share_flag'] not in ['Y', 'N']:
|
137 |
+
return APIResponse.error('无效的共享状态参数', 400)
|
138 |
+
|
139 |
+
# 更新共享状态
|
140 |
+
prompt.share_flag = data['share_flag']
|
141 |
+
db.session.commit()
|
142 |
+
|
143 |
+
return APIResponse.success(message='共享状态已更新')
|
144 |
+
|
145 |
+
|
146 |
+
# 复制到我的提示语库
|
147 |
+
class CopyPromptResource(Resource):
|
148 |
+
@jwt_required()
|
149 |
+
def post(self, id):
|
150 |
+
"""复制到我的提示语库[^5]"""
|
151 |
+
original = Prompt.query.filter_by(
|
152 |
+
id=id,
|
153 |
+
share_flag='Y',
|
154 |
+
deleted_flag='N'
|
155 |
+
).first_or_404()
|
156 |
+
|
157 |
+
new_prompt = Prompt(
|
158 |
+
title=f"{original.title} (副本)",
|
159 |
+
content=original.content,
|
160 |
+
customer_id=get_jwt_identity(),
|
161 |
+
share_flag='N',
|
162 |
+
added_count=0
|
163 |
+
)
|
164 |
+
db.session.add(new_prompt)
|
165 |
+
db.session.commit()
|
166 |
+
return APIResponse.success({
|
167 |
+
'new_id': new_prompt.id,
|
168 |
+
'message': '复制成功'
|
169 |
+
})
|
170 |
+
|
171 |
+
# 收藏/取消收藏
|
172 |
+
class FavoritePromptResource(Resource):
|
173 |
+
@jwt_required()
|
174 |
+
def post(self, id):
|
175 |
+
"""收藏/取消收藏[^6]"""
|
176 |
+
prompt = Prompt.query.get_or_404(id)
|
177 |
+
customer_id = get_jwt_identity()
|
178 |
+
|
179 |
+
fav = PromptFav.query.filter_by(
|
180 |
+
prompt_id=id,
|
181 |
+
customer_id=customer_id
|
182 |
+
).first()
|
183 |
+
|
184 |
+
if fav:
|
185 |
+
db.session.delete(fav)
|
186 |
+
action = '取消收藏'
|
187 |
+
else:
|
188 |
+
new_fav = PromptFav(
|
189 |
+
prompt_id=id,
|
190 |
+
customer_id=customer_id
|
191 |
+
)
|
192 |
+
db.session.add(new_fav)
|
193 |
+
action = '收藏'
|
194 |
+
|
195 |
+
prompt.added_count = prompt.added_count + (1 if not fav else -1)
|
196 |
+
db.session.commit()
|
197 |
+
return APIResponse.success(message=f'{action}成功')
|
198 |
+
|
199 |
+
# 创建新的提示语
|
200 |
+
|
201 |
+
class CreatePromptResource(Resource):
|
202 |
+
@jwt_required()
|
203 |
+
def post(self):
|
204 |
+
"""创建新提示语[^7]"""
|
205 |
+
data = request.form
|
206 |
+
required_fields = ['title', 'content']
|
207 |
+
if not all(field in data for field in required_fields):
|
208 |
+
return APIResponse.error('缺少必要参数', 400)
|
209 |
+
|
210 |
+
if len(data['title']) > 255:
|
211 |
+
return APIResponse.error('标题过长', 400)
|
212 |
+
if len(data['content']) > 5000:
|
213 |
+
return APIResponse.error('内容超过5000字符限制', 400)
|
214 |
+
|
215 |
+
# 创建时自动设置 created_at 为当前时间
|
216 |
+
prompt = Prompt(
|
217 |
+
title=data['title'],
|
218 |
+
content=data['content'],
|
219 |
+
customer_id=get_jwt_identity(),
|
220 |
+
share_flag=data.get('share_flag', 'N'),
|
221 |
+
created_at=date.today() # 自动设置当前时间
|
222 |
+
)
|
223 |
+
db.session.add(prompt)
|
224 |
+
db.session.commit()
|
225 |
+
return APIResponse.success({
|
226 |
+
'id': prompt.id,
|
227 |
+
'message': '创建成功'
|
228 |
+
})
|
229 |
+
|
230 |
+
|
231 |
+
# 删除提示语
|
232 |
+
class DeletePromptResource(Resource):
|
233 |
+
@jwt_required()
|
234 |
+
def delete(self, id):
|
235 |
+
"""删除提示语[^8]"""
|
236 |
+
prompt = Prompt.query.filter_by(
|
237 |
+
id=id,
|
238 |
+
customer_id=get_jwt_identity()
|
239 |
+
).first_or_404()
|
240 |
+
|
241 |
+
prompt.deleted_flag = 'Y'
|
242 |
+
db.session.commit()
|
243 |
+
return APIResponse.success(message='删除成功')
|
244 |
+
|
245 |
+
|
app/resources/api/setting.py
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/system.py
|
2 |
+
from flask_restful import Resource
|
3 |
+
from app.utils.response import APIResponse
|
4 |
+
from flask import current_app
|
5 |
+
|
6 |
+
class SystemVersionResource(Resource):
|
7 |
+
def get(self):
|
8 |
+
"""获取系统版本信息[^1]"""
|
9 |
+
return APIResponse.success({
|
10 |
+
'version': current_app.config['SYSTEM_VERSION'],
|
11 |
+
'message': 'success'
|
12 |
+
})
|
13 |
+
|
14 |
+
class SystemSettingsResource(Resource):
|
15 |
+
def get(self):
|
16 |
+
"""获取全量系统配置[^2]"""
|
17 |
+
return APIResponse.success({
|
18 |
+
'site_setting': {
|
19 |
+
'version': current_app.config['SYSTEM_VERSION'],
|
20 |
+
'site_name': current_app.config['SITE_NAME']
|
21 |
+
},
|
22 |
+
'api_setting': {
|
23 |
+
'api_url': current_app.config['API_URL'],
|
24 |
+
'models': current_app.config['TRANSLATE_MODELS']
|
25 |
+
},
|
26 |
+
'message': 'success'
|
27 |
+
})
|
app/resources/api/translate.py
ADDED
@@ -0,0 +1,590 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# resources/to_translate.py
|
2 |
+
import json
|
3 |
+
from pathlib import Path
|
4 |
+
from flask import request, send_file, current_app, make_response
|
5 |
+
from flask_restful import Resource
|
6 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
7 |
+
from datetime import datetime
|
8 |
+
from io import BytesIO
|
9 |
+
import zipfile
|
10 |
+
import os
|
11 |
+
|
12 |
+
from app import db, Setting
|
13 |
+
from app.models import Customer
|
14 |
+
from app.models.translate import Translate
|
15 |
+
from app.resources.task.translate_service import TranslateEngine
|
16 |
+
from app.utils.response import APIResponse
|
17 |
+
from app.utils.check_utils import AIChecker
|
18 |
+
|
19 |
+
# 定义翻译配置(硬编码示例)
|
20 |
+
TRANSLATE_SETTINGS = {
|
21 |
+
"models": ["gpt-3.5-turbo", "gpt-4"],
|
22 |
+
"default_model": "gpt-3.5-turbo",
|
23 |
+
"max_threads": 5,
|
24 |
+
"prompt_template": "请将以下内容翻译为{target_lang}"
|
25 |
+
}
|
26 |
+
|
27 |
+
|
28 |
+
class TranslateStartResource1(Resource):
|
29 |
+
@jwt_required()
|
30 |
+
def post(self):
|
31 |
+
"""启动翻译任务(支持绝对路径和多参数)[^1]"""
|
32 |
+
data = request.form
|
33 |
+
required_fields = [
|
34 |
+
'server', 'model', 'lang', 'uuid',
|
35 |
+
'prompt', 'threads', 'file_name'
|
36 |
+
]
|
37 |
+
|
38 |
+
# 参数校验
|
39 |
+
if not all(field in data for field in required_fields):
|
40 |
+
return APIResponse.error("缺少必要参数", 400)
|
41 |
+
|
42 |
+
# 验证OpenAI配置
|
43 |
+
if data['server'] == 'openai' and not all(k in data for k in ['api_url', 'api_key']):
|
44 |
+
return APIResponse.error("OpenAI服务需要API地址和密钥", 400)
|
45 |
+
|
46 |
+
try:
|
47 |
+
# 获取用户信息
|
48 |
+
user_id = get_jwt_identity()
|
49 |
+
customer = Customer.query.get(user_id)
|
50 |
+
|
51 |
+
# 生成绝对路径(跨平台兼容)
|
52 |
+
def get_absolute_storage_path(filename):
|
53 |
+
# 获取项目根目录的父目录(假设storage目录与项目目录同级)
|
54 |
+
base_dir = Path(current_app.root_path).parent.absolute()
|
55 |
+
# 按日期创建子目录(如 storage/translate/2024-01-20)
|
56 |
+
date_str = datetime.now().strftime('%Y-%m-%d')
|
57 |
+
# 创建目标目录(如果不存在)
|
58 |
+
target_dir = base_dir / "storage" / "translate" / date_str
|
59 |
+
target_dir.mkdir(parents=True, exist_ok=True)
|
60 |
+
# 返回绝对路径(保持原文件名)
|
61 |
+
return str(target_dir / filename)
|
62 |
+
|
63 |
+
origin_filename = data['file_name']
|
64 |
+
|
65 |
+
# 生成翻译结果绝对路径
|
66 |
+
target_abs_path = get_absolute_storage_path(origin_filename)
|
67 |
+
|
68 |
+
# 获取翻译类型(取最后一个type值)
|
69 |
+
translate_type = data.get('type[2]', 'trans_all_only_inherit')
|
70 |
+
|
71 |
+
# 查询或创建翻译记录
|
72 |
+
translate = Translate.query.filter_by(uuid=data['uuid']).first()
|
73 |
+
if not translate:
|
74 |
+
return APIResponse.error("未找到对应的翻译记录", 404)
|
75 |
+
|
76 |
+
# 更新翻译记录
|
77 |
+
translate.origin_filename = data['file_name']
|
78 |
+
translate.target_filepath = target_abs_path # 存储翻译结果的绝对路径
|
79 |
+
translate.lang = data['lang']
|
80 |
+
translate.model = data['model']
|
81 |
+
translate.backup_model = data['backup_model']
|
82 |
+
translate.type = translate_type
|
83 |
+
translate.prompt = data['prompt']
|
84 |
+
translate.threads = int(data['threads'])
|
85 |
+
translate.api_url = data.get('api_url', '')
|
86 |
+
translate.api_key = data.get('api_key', '')
|
87 |
+
translate.backup_model = data.get('backup_model', '')
|
88 |
+
translate.origin_lang = data.get('origin_lang', '')
|
89 |
+
# 获取 comparison_id 并转换为整数
|
90 |
+
comparison_id = data.get('comparison_id', '0') # 默认值为 '0'
|
91 |
+
translate.comparison_id = int(comparison_id) if comparison_id else None
|
92 |
+
prompt_id = data.get('prompt_id', '0')
|
93 |
+
translate.prompt_id = int(prompt_id) if prompt_id else None
|
94 |
+
translate.doc2x_flag = data.get('doc2x_flag', 'N')
|
95 |
+
translate.doc2x_secret_key = data.get('doc2x_secret_key', '')
|
96 |
+
|
97 |
+
# 保存到数据库
|
98 |
+
db.session.commit()
|
99 |
+
# with current_app.app_context(): # 确保在应用上下文中运行
|
100 |
+
# 启动翻译引擎,传入 current_app
|
101 |
+
TranslateEngine(translate.id).execute()
|
102 |
+
|
103 |
+
return APIResponse.success({
|
104 |
+
"task_id": translate.id,
|
105 |
+
"uuid": translate.uuid,
|
106 |
+
"target_path": target_abs_path
|
107 |
+
})
|
108 |
+
|
109 |
+
except Exception as e:
|
110 |
+
db.session.rollback()
|
111 |
+
current_app.logger.error(f"翻译任务启动失败: {str(e)}", exc_info=True)
|
112 |
+
return APIResponse.error("任务启动失败", 500)
|
113 |
+
|
114 |
+
|
115 |
+
|
116 |
+
class TranslateStartResource(Resource):
|
117 |
+
@jwt_required()
|
118 |
+
def post(self):
|
119 |
+
"""启动翻译任务(支持绝对路径和多��数)[^1]"""
|
120 |
+
data = request.form
|
121 |
+
required_fields = [
|
122 |
+
'server', 'model', 'lang', 'uuid',
|
123 |
+
'prompt', 'threads', 'file_name'
|
124 |
+
]
|
125 |
+
|
126 |
+
# 参数校验
|
127 |
+
if not all(field in data for field in required_fields):
|
128 |
+
return APIResponse.error("缺少必要参数", 400)
|
129 |
+
|
130 |
+
# 验证OpenAI配置
|
131 |
+
if data['server'] == 'openai' and not all(k in data for k in ['api_url', 'api_key']):
|
132 |
+
return APIResponse.error("OpenAI服务需要API地址和密钥", 400)
|
133 |
+
|
134 |
+
try:
|
135 |
+
# 获取用户信息
|
136 |
+
user_id = get_jwt_identity()
|
137 |
+
customer = Customer.query.get(user_id)
|
138 |
+
|
139 |
+
# 生成绝对路径(跨平台兼容)
|
140 |
+
def get_absolute_storage_path(filename):
|
141 |
+
# 获取项目根目录的父目录(假设storage目录与项目目录同级)
|
142 |
+
base_dir = Path(current_app.root_path).parent.absolute()
|
143 |
+
# 按日期创建子目录(如 storage/translate/2024-01-20)
|
144 |
+
date_str = datetime.now().strftime('%Y-%m-%d')
|
145 |
+
# 创建目标目录(如果不存在)
|
146 |
+
target_dir = base_dir / "storage" / "translate" / date_str
|
147 |
+
target_dir.mkdir(parents=True, exist_ok=True)
|
148 |
+
# 返回绝对路径(保持原文件名)
|
149 |
+
return target_dir / filename
|
150 |
+
|
151 |
+
origin_filename = data['file_name']
|
152 |
+
|
153 |
+
# 生成翻译结果绝对路径
|
154 |
+
target_abs_path = get_absolute_storage_path(origin_filename)
|
155 |
+
|
156 |
+
# 获取翻译类型(取最后一个type值)
|
157 |
+
translate_type = data.get('type[2]', 'trans_all_only_inherit')
|
158 |
+
|
159 |
+
# 查询或创建翻译记录
|
160 |
+
translate = Translate.query.filter_by(uuid=data['uuid']).first()
|
161 |
+
if not translate:
|
162 |
+
return APIResponse.error("未找到对应的翻译记录", 404)
|
163 |
+
|
164 |
+
# 更新翻译记录
|
165 |
+
translate.origin_filename = origin_filename
|
166 |
+
translate.target_filepath = str(target_abs_path) # 存储翻译结果的绝对路径
|
167 |
+
translate.lang = data['lang']
|
168 |
+
translate.model = data['model']
|
169 |
+
translate.backup_model = data['backup_model']
|
170 |
+
translate.type = translate_type
|
171 |
+
translate.prompt = data['prompt']
|
172 |
+
translate.threads = int(data['threads'])
|
173 |
+
translate.api_url = data.get('api_url', '')
|
174 |
+
translate.api_key = data.get('api_key', '')
|
175 |
+
translate.backup_model = data.get('backup_model', '')
|
176 |
+
translate.origin_lang = data.get('origin_lang', '')
|
177 |
+
# 获取 comparison_id 并转换为整数
|
178 |
+
comparison_id = data.get('comparison_id', '0') # 默认值为 '0'
|
179 |
+
translate.comparison_id = int(comparison_id) if comparison_id else None
|
180 |
+
prompt_id = data.get('prompt_id', '0')
|
181 |
+
translate.prompt_id = int(prompt_id) if prompt_id else None
|
182 |
+
translate.doc2x_flag = data.get('doc2x_flag', 'N')
|
183 |
+
translate.doc2x_secret_key = data.get('doc2x_secret_key', '')
|
184 |
+
|
185 |
+
# 保存到数据库
|
186 |
+
db.session.commit()
|
187 |
+
# 启动翻译引擎,传入 current_app
|
188 |
+
TranslateEngine(translate.id).execute()
|
189 |
+
|
190 |
+
return APIResponse.success({
|
191 |
+
"task_id": translate.id,
|
192 |
+
"uuid": translate.uuid,
|
193 |
+
"target_path": str(target_abs_path)
|
194 |
+
})
|
195 |
+
|
196 |
+
except Exception as e:
|
197 |
+
db.session.rollback()
|
198 |
+
current_app.logger.error(f"翻译任务启动失败: {str(e)}", exc_info=True)
|
199 |
+
return APIResponse.error("任务启动失败", 500)
|
200 |
+
|
201 |
+
|
202 |
+
|
203 |
+
class TranslateListResource(Resource):
|
204 |
+
@jwt_required()
|
205 |
+
def get(self):
|
206 |
+
"""获取翻译记录列表"""
|
207 |
+
# 获取查询参数
|
208 |
+
page = request.args.get('page', '1')
|
209 |
+
limit = request.args.get('limit', '100')
|
210 |
+
status_filter = request.args.get('status')
|
211 |
+
# 将字符串参数转换为整数
|
212 |
+
try:
|
213 |
+
page = int(page)
|
214 |
+
limit = int(limit)
|
215 |
+
except ValueError:
|
216 |
+
return APIResponse.error("Invalid page or limit value"), 400
|
217 |
+
# 构建查询条件
|
218 |
+
query = Translate.query.filter_by(
|
219 |
+
customer_id=get_jwt_identity(),
|
220 |
+
deleted_flag='N'
|
221 |
+
)
|
222 |
+
|
223 |
+
# 检查 status_filter 是否是合法值
|
224 |
+
if status_filter:
|
225 |
+
valid_statuses = {'none', 'process', 'done', 'failed'}
|
226 |
+
if status_filter not in valid_statuses:
|
227 |
+
return APIResponse.error(f"Invalid status value: {status_filter}"), 400
|
228 |
+
query = query.filter_by(status=status_filter)
|
229 |
+
|
230 |
+
# 执行分页查询
|
231 |
+
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
232 |
+
|
233 |
+
# 处理每条记录
|
234 |
+
data = []
|
235 |
+
for t in pagination.items:
|
236 |
+
# 计算花费时间(基于 created_at 和 end_at)
|
237 |
+
if t.created_at and t.end_at:
|
238 |
+
spend_time = t.end_at - t.created_at
|
239 |
+
spend_time_minutes = int(spend_time.total_seconds() // 60)
|
240 |
+
spend_time_seconds = int(spend_time.total_seconds() % 60)
|
241 |
+
spend_time_str = f"{spend_time_minutes}分{spend_time_seconds}秒"
|
242 |
+
else:
|
243 |
+
spend_time_str = "--"
|
244 |
+
|
245 |
+
# 获取状态中文描述
|
246 |
+
status_name_map = {
|
247 |
+
'none': '未开始',
|
248 |
+
'process': '进行中',
|
249 |
+
'done': '已完成',
|
250 |
+
'failed': '失败'
|
251 |
+
}
|
252 |
+
status_name = status_name_map.get(t.status, '未知状态')
|
253 |
+
|
254 |
+
# 获取文件类型
|
255 |
+
file_type = self.get_file_type(t.origin_filename)
|
256 |
+
|
257 |
+
# 格式化完成时间(精确到秒)
|
258 |
+
end_at_str = t.end_at.strftime('%Y-%m-%d %H:%M:%S') if t.end_at else "--"
|
259 |
+
|
260 |
+
data.append({
|
261 |
+
'id': t.id,
|
262 |
+
'file_type': file_type,
|
263 |
+
'origin_filename': t.origin_filename,
|
264 |
+
'status': t.status,
|
265 |
+
'status_name': status_name,
|
266 |
+
'process': float(t.process), # 将 Decimal 转换为 float
|
267 |
+
'spend_time': spend_time_str, # 花费时间
|
268 |
+
'end_at': end_at_str, # 完成时间
|
269 |
+
'start_at': t.start_at.strftime('%Y-%m-%d %H:%M:%S') if t.start_at else "--",
|
270 |
+
# 开始时间
|
271 |
+
'lang': t.lang,
|
272 |
+
'target_filepath': t.target_filepath
|
273 |
+
})
|
274 |
+
|
275 |
+
# 返回响应数据
|
276 |
+
return APIResponse.success({
|
277 |
+
'data': data,
|
278 |
+
'total': pagination.total,
|
279 |
+
'current_page': pagination.page
|
280 |
+
})
|
281 |
+
|
282 |
+
@staticmethod
|
283 |
+
def get_file_type(filename):
|
284 |
+
"""根据文件名获取文件类型"""
|
285 |
+
if not filename:
|
286 |
+
return "未知"
|
287 |
+
ext = filename.split('.')[-1].lower()
|
288 |
+
if ext in {'docx', 'doc'}:
|
289 |
+
return "Word"
|
290 |
+
elif ext in {'xlsx', 'xls'}:
|
291 |
+
return "Excel"
|
292 |
+
elif ext == 'pptx':
|
293 |
+
return "PPT"
|
294 |
+
elif ext == 'pdf':
|
295 |
+
return "PDF"
|
296 |
+
elif ext in {'txt', 'md'}:
|
297 |
+
return "文本"
|
298 |
+
else:
|
299 |
+
return "其他"
|
300 |
+
|
301 |
+
# 获取翻译配置
|
302 |
+
class TranslateSettingResource(Resource):
|
303 |
+
@jwt_required()
|
304 |
+
def get(self):
|
305 |
+
"""获取翻译配置"""
|
306 |
+
try:
|
307 |
+
# 从数据库中获取翻译配置
|
308 |
+
settings = self._load_settings_from_db()
|
309 |
+
return APIResponse.success(settings)
|
310 |
+
except Exception as e:
|
311 |
+
return APIResponse.error(f"获取配置失败: {str(e)}", 500)
|
312 |
+
|
313 |
+
@staticmethod
|
314 |
+
def _load_settings_from_db():
|
315 |
+
"""
|
316 |
+
从数据库加载翻译配置
|
317 |
+
"""
|
318 |
+
# 查询翻译相关的配置(api_setting 和 other_setting 分组)
|
319 |
+
settings = Setting.query.filter(
|
320 |
+
Setting.group.in_(['api_setting', 'other_setting']),
|
321 |
+
Setting.deleted_flag == 'N'
|
322 |
+
).all()
|
323 |
+
|
324 |
+
# 转换为配置字典
|
325 |
+
config = {}
|
326 |
+
for setting in settings:
|
327 |
+
# 如果 serialized 为 True,则反序列化 value
|
328 |
+
value = json.loads(setting.value) if setting.serialized else setting.value
|
329 |
+
|
330 |
+
# 根据 alias 存储配置
|
331 |
+
if setting.alias == 'models':
|
332 |
+
config['models'] = value.split(',') if isinstance(value, str) else value
|
333 |
+
elif setting.alias == 'default_model':
|
334 |
+
config['default_model'] = value
|
335 |
+
elif setting.alias == 'default_backup':
|
336 |
+
config['default_backup'] = value
|
337 |
+
elif setting.alias == 'api_url':
|
338 |
+
config['api_url'] = value
|
339 |
+
elif setting.alias == 'api_key':
|
340 |
+
config['api_key'] = value
|
341 |
+
elif setting.alias == 'prompt':
|
342 |
+
config['prompt_template'] = value
|
343 |
+
elif setting.alias == 'threads':
|
344 |
+
config['max_threads'] = int(value) if value.isdigit() else 10 # 默认10线程
|
345 |
+
|
346 |
+
# 设置默认值(如果数据库中没有相关配置)
|
347 |
+
config.setdefault('models', ['gpt-3.5-turbo', 'gpt-4'])
|
348 |
+
config.setdefault('default_model', 'gpt-3.5-turbo')
|
349 |
+
config.setdefault('default_backup', 'gpt-3.5-turbo')
|
350 |
+
config.setdefault('api_url', '')
|
351 |
+
config.setdefault('api_key', '')
|
352 |
+
config.setdefault('prompt_template', '请将以下内容翻译为{target_lang}')
|
353 |
+
config.setdefault('max_threads', 10)
|
354 |
+
|
355 |
+
return config
|
356 |
+
|
357 |
+
|
358 |
+
class TranslateProcessResource(Resource):
|
359 |
+
@jwt_required()
|
360 |
+
def post(self):
|
361 |
+
"""查询翻译进度[^3]"""
|
362 |
+
uuid = request.form.get('uuid')
|
363 |
+
translate = Translate.query.filter_by(
|
364 |
+
uuid=uuid,
|
365 |
+
customer_id=get_jwt_identity()
|
366 |
+
).first_or_404()
|
367 |
+
|
368 |
+
return APIResponse.success({
|
369 |
+
'status': translate.status,
|
370 |
+
'progress': float(translate.process),
|
371 |
+
'download_url': translate.target_filepath if translate.status == 'done' else None
|
372 |
+
})
|
373 |
+
|
374 |
+
|
375 |
+
class TranslateDeleteResource(Resource):
|
376 |
+
@jwt_required()
|
377 |
+
def delete(self, id):
|
378 |
+
"""软删除翻译记录[^4]"""
|
379 |
+
# 查询翻译记录
|
380 |
+
translate = Translate.query.filter_by(
|
381 |
+
id=id,
|
382 |
+
customer_id=get_jwt_identity()
|
383 |
+
).first_or_404()
|
384 |
+
|
385 |
+
# 更新 deleted_flag 为 'Y'
|
386 |
+
translate.deleted_flag = 'Y'
|
387 |
+
db.session.commit()
|
388 |
+
|
389 |
+
return APIResponse.success(message='记录已标记为删除')
|
390 |
+
|
391 |
+
|
392 |
+
|
393 |
+
class TranslateDownloadResource(Resource):
|
394 |
+
# @jwt_required()
|
395 |
+
def get(self, id):
|
396 |
+
"""通过 ID 下载单个翻译结果文件[^5]"""
|
397 |
+
# 查询翻译记录
|
398 |
+
translate = Translate.query.filter_by(
|
399 |
+
id=id,
|
400 |
+
# customer_id=get_jwt_identity()
|
401 |
+
).first_or_404()
|
402 |
+
|
403 |
+
# 确保文件存在
|
404 |
+
if not translate.target_filepath or not os.path.exists(translate.target_filepath):
|
405 |
+
return APIResponse.error('文件不存在', 404)
|
406 |
+
|
407 |
+
# 返回文件
|
408 |
+
response = make_response(send_file(
|
409 |
+
translate.target_filepath,
|
410 |
+
as_attachment=True,
|
411 |
+
download_name=os.path.basename(translate.target_filepath)
|
412 |
+
))
|
413 |
+
|
414 |
+
# 禁用缓存
|
415 |
+
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
416 |
+
response.headers['Pragma'] = 'no-cache'
|
417 |
+
response.headers['Expires'] = '0'
|
418 |
+
|
419 |
+
return response
|
420 |
+
|
421 |
+
|
422 |
+
|
423 |
+
|
424 |
+
|
425 |
+
|
426 |
+
class TranslateDownloadAllResource(Resource):
|
427 |
+
@jwt_required()
|
428 |
+
def get(self):
|
429 |
+
"""批量下载所有翻译结果文件[^6]"""
|
430 |
+
# 查询当前用户的所有翻译记录
|
431 |
+
records = Translate.query.filter_by(
|
432 |
+
customer_id=get_jwt_identity(),
|
433 |
+
deleted_flag='N' # 只下载未删除的记录
|
434 |
+
).all()
|
435 |
+
|
436 |
+
# 生成内存 ZIP 文件
|
437 |
+
zip_buffer = BytesIO()
|
438 |
+
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
439 |
+
for record in records:
|
440 |
+
if record.target_filepath and os.path.exists(record.target_filepath):
|
441 |
+
# 将文件添加到 ZIP 中
|
442 |
+
zip_file.write(
|
443 |
+
record.target_filepath,
|
444 |
+
os.path.basename(record.target_filepath)
|
445 |
+
)
|
446 |
+
|
447 |
+
# 重置缓冲区指针
|
448 |
+
zip_buffer.seek(0)
|
449 |
+
|
450 |
+
# 返回 ZIP 文件
|
451 |
+
return send_file(
|
452 |
+
zip_buffer,
|
453 |
+
mimetype='application/zip',
|
454 |
+
as_attachment=True,
|
455 |
+
download_name=f"translations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
456 |
+
)
|
457 |
+
|
458 |
+
|
459 |
+
class OpenAICheckResource(Resource):
|
460 |
+
@jwt_required()
|
461 |
+
def post(self):
|
462 |
+
"""OpenAI接口检测[^6]"""
|
463 |
+
data = request.form
|
464 |
+
required = ['api_url', 'api_key', 'model']
|
465 |
+
if not all(k in data for k in required):
|
466 |
+
return APIResponse.error('缺少必要参数', 400)
|
467 |
+
|
468 |
+
is_valid, msg = AIChecker.check_openai_connection(
|
469 |
+
data['api_url'],
|
470 |
+
data['api_key'],
|
471 |
+
data['model']
|
472 |
+
)
|
473 |
+
|
474 |
+
return APIResponse.success({'valid': is_valid, 'message': msg})
|
475 |
+
|
476 |
+
|
477 |
+
class PDFCheckResource(Resource):
|
478 |
+
@jwt_required()
|
479 |
+
def post(self):
|
480 |
+
"""PDF扫描件检测[^7]"""
|
481 |
+
if 'file' not in request.files:
|
482 |
+
return APIResponse.error('请选择PDF文件', 400)
|
483 |
+
|
484 |
+
file = request.files['file']
|
485 |
+
if not file.filename.lower().endswith('.pdf'):
|
486 |
+
return APIResponse.error('仅支持PDF文件', 400)
|
487 |
+
|
488 |
+
try:
|
489 |
+
file_stream = file.stream
|
490 |
+
is_scanned = AIChecker.check_pdf_scanned(file_stream)
|
491 |
+
return APIResponse.success({'scanned': is_scanned})
|
492 |
+
except Exception as e:
|
493 |
+
return APIResponse.error(f'检测失败: {str(e)}', 500)
|
494 |
+
|
495 |
+
|
496 |
+
# resources/to_translate.py 补充接口
|
497 |
+
class TranslateTestResource(Resource):
|
498 |
+
def get(self):
|
499 |
+
"""测试翻译服务[^1]"""
|
500 |
+
return APIResponse.success(message="测试服务正常")
|
501 |
+
|
502 |
+
|
503 |
+
class TranslateDeleteAllResource(Resource):
|
504 |
+
@jwt_required()
|
505 |
+
def delete(self):
|
506 |
+
"""删除用户所有翻译记录[^2]"""
|
507 |
+
Translate.query.filter_by(
|
508 |
+
customer_id=get_jwt_identity(),
|
509 |
+
deleted_flag='N'
|
510 |
+
).delete()
|
511 |
+
db.session.commit()
|
512 |
+
return APIResponse.success(message="删除成功")
|
513 |
+
|
514 |
+
|
515 |
+
class TranslateFinishCountResource(Resource):
|
516 |
+
@jwt_required()
|
517 |
+
def get(self):
|
518 |
+
"""获取已完成翻译数量[^3]"""
|
519 |
+
count = Translate.query.filter_by(
|
520 |
+
customer_id=get_jwt_identity(),
|
521 |
+
status='done',
|
522 |
+
deleted_flag='N'
|
523 |
+
).count()
|
524 |
+
return APIResponse.success({'total': count})
|
525 |
+
|
526 |
+
|
527 |
+
class TranslateRandDeleteAllResource(Resource):
|
528 |
+
def delete(self):
|
529 |
+
"""删除临时用户所有记录[^4]"""
|
530 |
+
rand_user_id = request.json.get('rand_user_id')
|
531 |
+
if not rand_user_id:
|
532 |
+
return APIResponse.error('需要临时用户ID', 400)
|
533 |
+
|
534 |
+
Translate.query.filter_by(
|
535 |
+
rand_user_id=rand_user_id,
|
536 |
+
deleted_flag='N'
|
537 |
+
).delete()
|
538 |
+
db.session.commit()
|
539 |
+
return APIResponse.success(message="删除成功")
|
540 |
+
|
541 |
+
|
542 |
+
class TranslateRandDeleteResource(Resource):
|
543 |
+
def delete(self, id):
|
544 |
+
"""删除临时用户单条记录[^5]"""
|
545 |
+
rand_user_id = request.json.get('rand_user_id')
|
546 |
+
translate = Translate.query.filter_by(
|
547 |
+
id=id,
|
548 |
+
rand_user_id=rand_user_id
|
549 |
+
).first_or_404()
|
550 |
+
|
551 |
+
db.session.delete(translate)
|
552 |
+
db.session.commit()
|
553 |
+
return APIResponse.success(message="删除成功")
|
554 |
+
|
555 |
+
|
556 |
+
class TranslateRandDownloadResource(Resource):
|
557 |
+
def get(self):
|
558 |
+
"""下载临时用户翻译文件[^6]"""
|
559 |
+
rand_user_id = request.args.get('rand_user_id')
|
560 |
+
records = Translate.query.filter_by(
|
561 |
+
rand_user_id=rand_user_id,
|
562 |
+
status='done'
|
563 |
+
).all()
|
564 |
+
|
565 |
+
zip_buffer = BytesIO()
|
566 |
+
with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
|
567 |
+
for record in records:
|
568 |
+
if os.path.exists(record.target_filepath):
|
569 |
+
zip_file.write(
|
570 |
+
record.target_filepath,
|
571 |
+
os.path.basename(record.target_filepath)
|
572 |
+
)
|
573 |
+
|
574 |
+
zip_buffer.seek(0)
|
575 |
+
return send_file(
|
576 |
+
zip_buffer,
|
577 |
+
mimetype='application/zip',
|
578 |
+
as_attachment=True,
|
579 |
+
download_name=f"temp_translations_{datetime.now().strftime('%Y%m%d')}.zip"
|
580 |
+
)
|
581 |
+
|
582 |
+
|
583 |
+
class Doc2xCheckResource(Resource):
|
584 |
+
def post(self):
|
585 |
+
"""检查Doc2x接口[^7]"""
|
586 |
+
secret_key = request.json.get('doc2x_secret_key')
|
587 |
+
# 模拟验证逻辑,实际需对接Doc2x服务
|
588 |
+
if secret_key == "valid_key_123": # 示例验证
|
589 |
+
return APIResponse.success(message="接口正常")
|
590 |
+
return APIResponse.error("无效密钥", 400)
|
app/resources/hello.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask_restful import Resource
|
2 |
+
|
3 |
+
class HelloWorldResource(Resource):
|
4 |
+
def get(self):
|
5 |
+
return {'message': 'Hello World!'}
|
app/resources/task/__init__.py
ADDED
File without changes
|
app/resources/task/file_handlers.py
ADDED
File without changes
|
app/resources/task/main.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import threading
|
2 |
+
import time
|
3 |
+
from datetime import datetime
|
4 |
+
import os
|
5 |
+
import traceback
|
6 |
+
from flask import current_app
|
7 |
+
from app.extensions import db
|
8 |
+
from app.models.comparison import Comparison
|
9 |
+
from app.models.prompt import Prompt
|
10 |
+
from app.models.translate import Translate
|
11 |
+
from app.translate import word, excel, powerpoint, pdf, gptpdf, txt, csv_handle, md, to_translate
|
12 |
+
|
13 |
+
|
14 |
+
def main_wrapper(task_id, config, origin_path):
|
15 |
+
"""
|
16 |
+
翻译任务核心逻辑(支持多参数)[^4]
|
17 |
+
:param task_id: 任务ID
|
18 |
+
:param origin_path: 原始文件绝对路径
|
19 |
+
:param target_path: 目标文件绝对路径
|
20 |
+
:param config: 翻译配置字典
|
21 |
+
:return: 是否成功
|
22 |
+
"""
|
23 |
+
try:
|
24 |
+
# 获取任务对象
|
25 |
+
task = Translate.query.get(task_id)
|
26 |
+
if not task:
|
27 |
+
current_app.logger.error(f"任务 {task_id} 不存在")
|
28 |
+
return False
|
29 |
+
# 设置OpenAI API
|
30 |
+
|
31 |
+
# 初始化翻译配置
|
32 |
+
_init_translate_config(task)
|
33 |
+
to_translate.init_openai(config['api_url'], config['api_key'])
|
34 |
+
# 获取文件扩展名
|
35 |
+
extension = os.path.splitext(origin_path)[1].lower()
|
36 |
+
print('文件扩展名',extension,origin_path)
|
37 |
+
# 调用文件处理器
|
38 |
+
handler_map = {
|
39 |
+
('.docx', '.doc'): word,
|
40 |
+
('.xlsx', '.xls'): excel,
|
41 |
+
('.pptx', '.ppt'): powerpoint,
|
42 |
+
('.pdf',): pdf,
|
43 |
+
('.txt',): txt,
|
44 |
+
('.csv',): csv_handle,
|
45 |
+
('.md',): md
|
46 |
+
}
|
47 |
+
|
48 |
+
# 查找匹配的处理器
|
49 |
+
for ext_group, handler in handler_map.items():
|
50 |
+
if extension in ext_group:
|
51 |
+
# if extension == '.pdf':
|
52 |
+
# status = handler(config, origin_path) # 传递 origin_path
|
53 |
+
# else:
|
54 |
+
# status = handler.start(config) # 传递翻译配置
|
55 |
+
status = handler.start(
|
56 |
+
# origin_path=origin_path,
|
57 |
+
# target_path=target_path,
|
58 |
+
trans=config # 传递翻译配置
|
59 |
+
)
|
60 |
+
print('config配置项', config)
|
61 |
+
return status
|
62 |
+
|
63 |
+
current_app.logger.error(f"不支持的文件类型: {extension}")
|
64 |
+
return False
|
65 |
+
|
66 |
+
except Exception as e:
|
67 |
+
current_app.logger.error(f"翻译任务执行异常: {str(e)}", exc_info=True)
|
68 |
+
return False
|
69 |
+
|
70 |
+
|
71 |
+
def _init_translate_config1(task):
|
72 |
+
"""初始化翻译配置(如OpenAI)"""
|
73 |
+
if task.api_url and task.api_key:
|
74 |
+
import openai
|
75 |
+
openai.api_base = task.api_url
|
76 |
+
openai.api_key = task.api_key
|
77 |
+
|
78 |
+
def pdf_handler(config, origin_path):
|
79 |
+
return gptpdf.start(config)
|
80 |
+
# if pdf.is_scanned_pdf(origin_path):
|
81 |
+
# return gptpdf.start(config)
|
82 |
+
# else:
|
83 |
+
# # 这里均使用gptpdf实现
|
84 |
+
# return gptpdf.start(config)
|
85 |
+
# # return pdf.start(config)
|
86 |
+
def _init_translate_config(trans):
|
87 |
+
"""
|
88 |
+
初始化翻译配置[^5]
|
89 |
+
:param trans: 翻译任务对象
|
90 |
+
"""
|
91 |
+
# 设置OpenAI API
|
92 |
+
if trans.api_url and trans.api_key:
|
93 |
+
set_openai_config(trans.api_url, trans.api_key)
|
94 |
+
|
95 |
+
# 加载术语对照表
|
96 |
+
if trans.comparison_id:
|
97 |
+
comparison = get_comparison(trans.comparison_id)
|
98 |
+
trans.prompt = f"{comparison}\n{trans.prompt}"
|
99 |
+
|
100 |
+
# 加载提示词模板
|
101 |
+
if trans.prompt_id:
|
102 |
+
prompt = get_prompt(trans.prompt_id)
|
103 |
+
trans.prompt = prompt
|
104 |
+
|
105 |
+
|
106 |
+
def set_openai_config(api_url, api_key):
|
107 |
+
"""
|
108 |
+
设置OpenAI API配置[^6]
|
109 |
+
"""
|
110 |
+
import openai
|
111 |
+
openai.api_base = api_url
|
112 |
+
openai.api_key = api_key
|
113 |
+
|
114 |
+
|
115 |
+
def get_comparison(comparison_id):
|
116 |
+
"""
|
117 |
+
加载术语对照表
|
118 |
+
:param comparison_id: 术语对照表ID
|
119 |
+
:return: 术语对照表内容
|
120 |
+
"""
|
121 |
+
comparison = db.session.query(Comparison).filter_by(id=comparison_id).first()
|
122 |
+
if comparison and comparison.content:
|
123 |
+
return comparison.content.replace(',', ':').replace(';', '\n')
|
124 |
+
return ""
|
125 |
+
|
126 |
+
|
127 |
+
def get_prompt(prompt_id):
|
128 |
+
"""
|
129 |
+
加载提示词模板
|
130 |
+
:param prompt_id: 提示词模板ID
|
131 |
+
:return: 提示词内容
|
132 |
+
"""
|
133 |
+
prompt = db.session.query(Prompt).filter_by(id=prompt_id).first()
|
134 |
+
if prompt and prompt.content:
|
135 |
+
return prompt.content
|
136 |
+
return ""
|
app/resources/task/translate.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# tasks/translate.py
|
2 |
+
import subprocess
|
3 |
+
|
4 |
+
from flask import current_app
|
5 |
+
|
6 |
+
from app import db
|
7 |
+
from app.models.translate import Translate
|
8 |
+
|
9 |
+
|
10 |
+
def start_translate_task(task_id):
|
11 |
+
"""启动翻译子进程[^2]"""
|
12 |
+
translate = Translate.query.get(task_id)
|
13 |
+
if not translate:
|
14 |
+
return False
|
15 |
+
|
16 |
+
try:
|
17 |
+
# 构建命令参数
|
18 |
+
storage_path = current_app.config['UPLOAD_FOLDER']
|
19 |
+
cmd = [
|
20 |
+
'python3',
|
21 |
+
'translate/main.py',
|
22 |
+
translate.uuid,
|
23 |
+
storage_path
|
24 |
+
]
|
25 |
+
|
26 |
+
# 启动子进程
|
27 |
+
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
28 |
+
|
29 |
+
# 更新任务状态
|
30 |
+
translate.status = 'process'
|
31 |
+
db.session.commit()
|
32 |
+
return True
|
33 |
+
|
34 |
+
except Exception as e:
|
35 |
+
translate.status = 'failed'
|
36 |
+
translate.failed_reason = str(e)
|
37 |
+
db.session.commit()
|
38 |
+
return False
|
app/resources/task/translate_service.py
ADDED
@@ -0,0 +1,644 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import threading
|
3 |
+
from threading import Thread
|
4 |
+
from flask import current_app
|
5 |
+
from app.models.translate import Translate
|
6 |
+
from app.extensions import db
|
7 |
+
from .main import main_wrapper
|
8 |
+
from ...models.comparison import Comparison
|
9 |
+
from ...models.prompt import Prompt
|
10 |
+
|
11 |
+
|
12 |
+
|
13 |
+
|
14 |
+
class TranslateEngine99:
|
15 |
+
def __init__(self, task_id):
|
16 |
+
self.task_id = task_id
|
17 |
+
self.app = current_app._get_current_object() # 获取真实app对象
|
18 |
+
|
19 |
+
def execute(self):
|
20 |
+
"""启动翻译任务入口"""
|
21 |
+
try:
|
22 |
+
# 主线程预处理
|
23 |
+
with self.app.app_context():
|
24 |
+
task = self._prepare_task()
|
25 |
+
|
26 |
+
# 启动异步线程(传递真实app对象)
|
27 |
+
thr = threading.Thread(
|
28 |
+
target=self._async_wrapper,
|
29 |
+
args=(self.app, self.task_id)
|
30 |
+
)
|
31 |
+
thr.start()
|
32 |
+
return True
|
33 |
+
except Exception as e:
|
34 |
+
self.app.logger.error(f"任务初始化失败: {str(e)}", exc_info=True)
|
35 |
+
return False
|
36 |
+
|
37 |
+
def _async_wrapper(self, app, task_id):
|
38 |
+
"""异步执行包装器"""
|
39 |
+
with app.app_context():
|
40 |
+
try:
|
41 |
+
# 每个线程独立获取任务对象
|
42 |
+
task = db.session.query(Translate).get(task_id)
|
43 |
+
self._async_execute(task)
|
44 |
+
except Exception as e:
|
45 |
+
app.logger.error(f"任务执行异常: {str(e)}", exc_info=True)
|
46 |
+
self._complete_task(False)
|
47 |
+
finally:
|
48 |
+
db.session.remove() # 关键:清理线程会话
|
49 |
+
|
50 |
+
def _async_execute(self, task):
|
51 |
+
"""执行核心翻译逻辑"""
|
52 |
+
try:
|
53 |
+
# 初始化配置(使用线程内session)
|
54 |
+
config = self._build_config(task)
|
55 |
+
|
56 |
+
# 调用翻译核心
|
57 |
+
success = main_wrapper(
|
58 |
+
task=task,
|
59 |
+
origin_path=task.origin_filepath,
|
60 |
+
target_path=task.target_filepath,
|
61 |
+
config=config
|
62 |
+
)
|
63 |
+
self._complete_task(success)
|
64 |
+
except Exception as e:
|
65 |
+
current_app.logger.error(f"翻译执行失败: {str(e)}", exc_info=True)
|
66 |
+
self._complete_task(False)
|
67 |
+
|
68 |
+
def _build_config(self, task):
|
69 |
+
"""构建线程安全配置"""
|
70 |
+
return {
|
71 |
+
'lang': task.lang,
|
72 |
+
'model': task.model,
|
73 |
+
'type': task.type,
|
74 |
+
'prompt': self._load_prompt(task),
|
75 |
+
'threads': task.threads,
|
76 |
+
'api_url': task.api_url,
|
77 |
+
'api_key': task.api_key,
|
78 |
+
'comparison': self._load_comparison(task.comparison_id)
|
79 |
+
}
|
80 |
+
|
81 |
+
def _load_prompt(self, task):
|
82 |
+
"""加载提示词(线程安全)"""
|
83 |
+
if task.prompt_id:
|
84 |
+
prompt = db.session.query(Prompt).get(task.prompt_id)
|
85 |
+
return prompt.content if prompt else ""
|
86 |
+
return task.prompt
|
87 |
+
|
88 |
+
def _load_comparison(self, comparison_id):
|
89 |
+
"""加载术语对照表(线程安全)"""
|
90 |
+
if not comparison_id:
|
91 |
+
return ""
|
92 |
+
comparison = db.session.query(Comparison).get(comparison_id)
|
93 |
+
return comparison.content.replace(',', ':').replace(';', '\n') if comparison else ""
|
94 |
+
|
95 |
+
def _prepare_task(self):
|
96 |
+
"""任务预处理"""
|
97 |
+
task = db.session.query(Translate).get(self.task_id)
|
98 |
+
if not task:
|
99 |
+
raise ValueError(f"任务 {self.task_id} 不存在")
|
100 |
+
|
101 |
+
if not os.path.exists(task.origin_filepath):
|
102 |
+
raise FileNotFoundError(f"文件不存在: {task.origin_filepath}")
|
103 |
+
|
104 |
+
task.status = 'process'
|
105 |
+
task.start_at = db.func.now()
|
106 |
+
db.session.commit()
|
107 |
+
return task
|
108 |
+
|
109 |
+
def _complete_task(self, success):
|
110 |
+
"""完成处理"""
|
111 |
+
try:
|
112 |
+
task = db.session.query(Translate).get(self.task_id)
|
113 |
+
task.status = 'done' if success else 'failed'
|
114 |
+
task.end_at = db.func.now()
|
115 |
+
db.session.commit()
|
116 |
+
except Exception as e:
|
117 |
+
db.session.rollback()
|
118 |
+
self.app.logger.error(f"状态更新失败: {str(e)}", exc_info=True)
|
119 |
+
|
120 |
+
|
121 |
+
|
122 |
+
|
123 |
+
class TranslateEngine666:
|
124 |
+
def __init__(self, task_id):
|
125 |
+
self.task_id = task_id
|
126 |
+
self.app = current_app._get_current_object() # 获取真实app对象
|
127 |
+
|
128 |
+
def _build_trans_config(self, task):
|
129 |
+
"""构建符合文件处理器要求的trans字典"""
|
130 |
+
return {
|
131 |
+
'id': task.id, # 任务ID
|
132 |
+
'threads': task.threads,
|
133 |
+
'file_path': task.origin_filepath, # 原始文件绝对路径
|
134 |
+
'target_file': task.target_filepath, # 目标文件绝对路径
|
135 |
+
'api_url': task.api_url,
|
136 |
+
'api_key': task.api_key, # 新增API密钥字段
|
137 |
+
'type': task.type,
|
138 |
+
'lang': task.lang,
|
139 |
+
'run_complete': True, # 默认设为True
|
140 |
+
# 以下是可能需要添加的额外字段
|
141 |
+
'prompt': task.prompt,
|
142 |
+
'model': task.model,
|
143 |
+
'backup_model': task.backup_model,
|
144 |
+
'comparison_id': task.comparison_id,
|
145 |
+
'prompt_id': task.prompt_id,
|
146 |
+
'extension':'.docx'
|
147 |
+
}
|
148 |
+
|
149 |
+
def execute(self):
|
150 |
+
"""启动任务入口"""
|
151 |
+
try:
|
152 |
+
# 在主线程上下文中准备任务
|
153 |
+
with self.app.app_context():
|
154 |
+
task = self._prepare_task()
|
155 |
+
|
156 |
+
# 启动线程时传递真实app对象和任务ID
|
157 |
+
thr = Thread(
|
158 |
+
target=self._async_wrapper,
|
159 |
+
args=(self.app, self.task_id)
|
160 |
+
)
|
161 |
+
thr.start()
|
162 |
+
return True
|
163 |
+
except Exception as e:
|
164 |
+
self.app.logger.error(f"任务初始化失败: {str(e)}", exc_info=True)
|
165 |
+
return False
|
166 |
+
|
167 |
+
def _async_wrapper(self, app, task_id):
|
168 |
+
"""异步执行包装器"""
|
169 |
+
with app.app_context():
|
170 |
+
from app.extensions import db # 确保在每个线程中导入
|
171 |
+
try:
|
172 |
+
# 使用新会话获取任务对象
|
173 |
+
task = db.session.query(Translate).get(task_id)
|
174 |
+
if not task:
|
175 |
+
app.logger.error(f"任务 {task_id} 不存在")
|
176 |
+
return
|
177 |
+
|
178 |
+
# 执行核心逻辑
|
179 |
+
success = self._execute_core(task)
|
180 |
+
self._complete_task(success)
|
181 |
+
except Exception as e:
|
182 |
+
app.logger.error(f"任务执行异常: {str(e)}", exc_info=True)
|
183 |
+
self._complete_task(False)
|
184 |
+
finally:
|
185 |
+
db.session.remove() # 重要!清理线程局部session
|
186 |
+
|
187 |
+
def _execute_core(self, task):
|
188 |
+
"""执行核心翻译逻辑"""
|
189 |
+
try:
|
190 |
+
# 初始化翻译配置
|
191 |
+
self._init_translate_config(task)
|
192 |
+
|
193 |
+
# 选择处理器
|
194 |
+
handler = self._get_file_handler(task.origin_filepath)
|
195 |
+
if not handler:
|
196 |
+
current_app.logger.error(f"不支持的文件类型: {task.origin_filepath}")
|
197 |
+
return False
|
198 |
+
|
199 |
+
# 构建符合要求的trans字典
|
200 |
+
trans_config = self._build_trans_config(task)
|
201 |
+
|
202 |
+
# 调用处理器
|
203 |
+
return handler.start(trans=trans_config) # 正确传递参数
|
204 |
+
except Exception as e:
|
205 |
+
current_app.logger.error(f"翻译执行失败: {str(e)}", exc_info=True)
|
206 |
+
return False
|
207 |
+
|
208 |
+
def _prepare_task(self):
|
209 |
+
"""准备翻译任务"""
|
210 |
+
task = Translate.query.get(self.task_id)
|
211 |
+
if not task:
|
212 |
+
raise ValueError(f"任务 {self.task_id} 不存在")
|
213 |
+
|
214 |
+
# 验证文件存在性
|
215 |
+
if not os.path.exists(task.origin_filepath):
|
216 |
+
raise FileNotFoundError(f"原始文件不存在: {task.origin_filepath}")
|
217 |
+
|
218 |
+
# 更新任务状态
|
219 |
+
task.status = 'process'
|
220 |
+
task.start_at = db.func.now()
|
221 |
+
db.session.commit()
|
222 |
+
return task
|
223 |
+
|
224 |
+
def _init_translate_config(self, task):
|
225 |
+
"""初始化翻译配置"""
|
226 |
+
if task.api_url and task.api_key:
|
227 |
+
import openai
|
228 |
+
openai.api_base = task.api_url
|
229 |
+
openai.api_key = task.api_key
|
230 |
+
|
231 |
+
# 加载术语对照表
|
232 |
+
if task.comparison_id:
|
233 |
+
from app.models import Comparison
|
234 |
+
comparison = db.session.query(Comparison).get(task.comparison_id)
|
235 |
+
if comparison:
|
236 |
+
task.prompt = f"术语对照表:\n{comparison.content.replace(',', ':')}\n{task.prompt}"
|
237 |
+
|
238 |
+
# 加载提示词模板
|
239 |
+
if task.prompt_id:
|
240 |
+
from app.models import Prompt
|
241 |
+
prompt = db.session.query(Prompt).get(task.prompt_id)
|
242 |
+
if prompt:
|
243 |
+
task.prompt = prompt.content
|
244 |
+
|
245 |
+
def _get_file_handler(self, file_path):
|
246 |
+
from app.translate import (
|
247 |
+
word, excel, powerpoint, pdf,
|
248 |
+
gptpdf, txt, csv_handle, md
|
249 |
+
)
|
250 |
+
|
251 |
+
try:
|
252 |
+
current_app.logger.debug(f"正在解析文件路径: {file_path}")
|
253 |
+
# 标准化路径(处理不同OS的斜杠和大小写)
|
254 |
+
normalized_path = os.path.normpath(file_path).lower()
|
255 |
+
current_app.logger.debug(f"标准化路径: {normalized_path}")
|
256 |
+
|
257 |
+
ext = os.path.splitext(normalized_path)[1]
|
258 |
+
# 获取标准化后的扩展名
|
259 |
+
# ext = os.path.splitext(file_path)[1].lower()
|
260 |
+
current_app.logger.debug(f"提取的扩展名: {ext}")
|
261 |
+
|
262 |
+
# 处理器映射表
|
263 |
+
handler_map = {
|
264 |
+
'.docx': word,
|
265 |
+
'.doc': word,
|
266 |
+
'.xlsx': excel,
|
267 |
+
'.xls': excel,
|
268 |
+
'.pptx': powerpoint,
|
269 |
+
'.ppt': powerpoint,
|
270 |
+
'.pdf': pdf,#if not pdf.is_scanned_pdf(file_path) else gptpdf,
|
271 |
+
'.txt': txt,
|
272 |
+
'.csv': csv_handle,
|
273 |
+
'.md': md
|
274 |
+
}
|
275 |
+
|
276 |
+
current_app.logger.debug(f"当前处理器映射表: {handler_map}")
|
277 |
+
|
278 |
+
# 匹配处理器
|
279 |
+
handler = handler_map.get(ext)
|
280 |
+
if not handler:
|
281 |
+
current_app.logger.error(f"未找到匹配的处理器,扩展名: {ext}")
|
282 |
+
return None
|
283 |
+
|
284 |
+
current_app.logger.info(f"成功匹配处理器: {handler.__name__}")
|
285 |
+
return handler
|
286 |
+
|
287 |
+
except Exception as e:
|
288 |
+
current_app.logger.error(f"获取文件处理器失败: {str(e)}", exc_info=True)
|
289 |
+
return None
|
290 |
+
|
291 |
+
def _build_config(self, task):
|
292 |
+
"""构建配置字典"""
|
293 |
+
return {
|
294 |
+
'lang': task.lang,
|
295 |
+
'model': task.model,
|
296 |
+
'type': task.type,
|
297 |
+
'prompt': task.prompt,
|
298 |
+
'threads': task.threads,
|
299 |
+
'api_url': task.api_url,
|
300 |
+
'api_key': task.api_key,
|
301 |
+
'origin_lang': task.origin_lang,
|
302 |
+
'backup_model': task.backup_model,
|
303 |
+
'doc2x_flag': task.doc2x_flag,
|
304 |
+
'doc2x_secret_key': task.doc2x_secret_key,
|
305 |
+
'comparison_id': task.comparison_id,
|
306 |
+
'word_count': task.word_count,
|
307 |
+
'prompt_id': task.prompt_id,
|
308 |
+
'rand_user_id': task.rand_user_id,
|
309 |
+
'origin_filesize': task.origin_filesize,
|
310 |
+
'origin_filename': task.origin_filename,
|
311 |
+
'target_filesize': task.target_filesize,
|
312 |
+
'target_filename': task.target_filename,
|
313 |
+
'target_filepath': task.target_filepath,
|
314 |
+
'origin_filepath': task.origin_filepath,
|
315 |
+
}
|
316 |
+
|
317 |
+
def _complete_task(self, success):
|
318 |
+
"""更新任务状态"""
|
319 |
+
from app.extensions import db
|
320 |
+
try:
|
321 |
+
with self.app.app_context():
|
322 |
+
task = db.session.query(Translate).get(self.task_id)
|
323 |
+
if task:
|
324 |
+
task.status = 'done' if success else 'failed'
|
325 |
+
task.end_at = db.func.now()
|
326 |
+
db.session.commit()
|
327 |
+
except Exception as e:
|
328 |
+
db.session.rollback()
|
329 |
+
self.app.logger.error(f"状态更新失败: {str(e)}", exc_info=True)
|
330 |
+
|
331 |
+
|
332 |
+
|
333 |
+
|
334 |
+
class TranslateEngine9999:
|
335 |
+
def __init__(self, task_id):
|
336 |
+
self.task_id = task_id
|
337 |
+
self.app = current_app._get_current_object() # 获取真实app对象
|
338 |
+
|
339 |
+
def _build_trans_config(self, task):
|
340 |
+
"""构建符合文件处理器要求的 trans 字典[^1]"""
|
341 |
+
return {
|
342 |
+
'id': task.id, # 任务ID
|
343 |
+
'threads': task.threads,
|
344 |
+
'file_path': task.origin_filepath, # 原始文件绝对路径
|
345 |
+
'target_file': task.target_filepath, # 目标文件绝对路径
|
346 |
+
'api_url': task.api_url,
|
347 |
+
'api_key': task.api_key, # 新增API密钥字段
|
348 |
+
'type': task.type,
|
349 |
+
'lang': task.lang,
|
350 |
+
'run_complete': True, # 默认设为True
|
351 |
+
# 以下是可能需要添加的额外字段
|
352 |
+
'prompt': task.prompt,
|
353 |
+
'model': task.model,
|
354 |
+
'backup_model': task.backup_model,
|
355 |
+
'comparison_id': task.comparison_id,
|
356 |
+
'prompt_id': task.prompt_id,
|
357 |
+
'extension': os.path.splitext(task.origin_filepath)[1] # 动态获取文件扩展名
|
358 |
+
}
|
359 |
+
|
360 |
+
def execute(self):
|
361 |
+
"""启动任务入口[^2]"""
|
362 |
+
try:
|
363 |
+
# 在主线程上下文中准备任务
|
364 |
+
with self.app.app_context():
|
365 |
+
task = self._prepare_task()
|
366 |
+
|
367 |
+
# 启动线程时传递真实app对象和任务ID
|
368 |
+
thr = Thread(
|
369 |
+
target=self._async_wrapper,
|
370 |
+
args=(self.app, self.task_id)
|
371 |
+
)
|
372 |
+
thr.start()
|
373 |
+
return True
|
374 |
+
except Exception as e:
|
375 |
+
self.app.logger.error(f"任务初始化失败: {str(e)}", exc_info=True)
|
376 |
+
return False
|
377 |
+
|
378 |
+
def _async_wrapper(self, app, task_id):
|
379 |
+
"""异步执行包装器[^3]"""
|
380 |
+
with app.app_context():
|
381 |
+
from app.extensions import db # 确保在每个线程中导入
|
382 |
+
try:
|
383 |
+
# 使用新会话获取任务对象
|
384 |
+
task = db.session.query(Translate).get(task_id)
|
385 |
+
if not task:
|
386 |
+
app.logger.error(f"任务 {task_id} 不存在")
|
387 |
+
return
|
388 |
+
|
389 |
+
# 执行核心逻辑
|
390 |
+
success = self._execute_core(task)
|
391 |
+
self._complete_task(success)
|
392 |
+
except Exception as e:
|
393 |
+
app.logger.error(f"任务执行异常: {str(e)}", exc_info=True)
|
394 |
+
self._complete_task(False)
|
395 |
+
finally:
|
396 |
+
db.session.remove() # 重要!清理线程局部session
|
397 |
+
|
398 |
+
def _execute_core(self, task):
|
399 |
+
"""执行核心翻译逻辑[^4]"""
|
400 |
+
try:
|
401 |
+
# 初始化翻译配置
|
402 |
+
self._init_translate_config(task)
|
403 |
+
|
404 |
+
# 构建符合要求的 trans 字典
|
405 |
+
trans_config = self._build_trans_config(task)
|
406 |
+
|
407 |
+
# 调用 main_wrapper 执行翻译
|
408 |
+
return main_wrapper(task_id=task.id, origin_path=task.origin_filepath,config=trans_config)
|
409 |
+
except Exception as e:
|
410 |
+
current_app.logger.error(f"翻译执行失败: {str(e)}", exc_info=True)
|
411 |
+
return False
|
412 |
+
|
413 |
+
def _prepare_task(self):
|
414 |
+
"""准备翻译任务[^5]"""
|
415 |
+
task = Translate.query.get(self.task_id)
|
416 |
+
if not task:
|
417 |
+
raise ValueError(f"任务 {self.task_id} 不存在")
|
418 |
+
|
419 |
+
# 验证文件存在性
|
420 |
+
if not os.path.exists(task.origin_filepath):
|
421 |
+
raise FileNotFoundError(f"原始文件不存在: {task.origin_filepath}")
|
422 |
+
|
423 |
+
# 更新任务状态
|
424 |
+
task.status = 'process'
|
425 |
+
task.start_at = db.func.now()
|
426 |
+
db.session.commit()
|
427 |
+
return task
|
428 |
+
|
429 |
+
def _init_translate_config(self, task):
|
430 |
+
"""初始化翻译配置[^6]"""
|
431 |
+
if task.api_url and task.api_key:
|
432 |
+
import openai
|
433 |
+
openai.api_base = task.api_url
|
434 |
+
openai.api_key = task.api_key
|
435 |
+
|
436 |
+
# 加载术语对照表
|
437 |
+
if task.comparison_id:
|
438 |
+
from app.models import Comparison
|
439 |
+
comparison = db.session.query(Comparison).get(task.comparison_id)
|
440 |
+
if comparison:
|
441 |
+
task.prompt = f"术语对照表:\n{comparison.content.replace(',', ':')}\n{task.prompt}"
|
442 |
+
|
443 |
+
# 加载提示词模板
|
444 |
+
if task.prompt_id:
|
445 |
+
from app.models import Prompt
|
446 |
+
prompt = db.session.query(Prompt).get(task.prompt_id)
|
447 |
+
if prompt:
|
448 |
+
task.prompt = prompt.content
|
449 |
+
|
450 |
+
def _get_file_handler(self, file_path):
|
451 |
+
"""获取文件处理器[^7]"""
|
452 |
+
from app.translate import (
|
453 |
+
word, excel, powerpoint, pdf,
|
454 |
+
gptpdf, txt, csv_handle, md
|
455 |
+
)
|
456 |
+
|
457 |
+
try:
|
458 |
+
current_app.logger.debug(f"正在解析文件路径: {file_path}")
|
459 |
+
# 标准化路径(处理不同OS的斜杠和大小写)
|
460 |
+
normalized_path = os.path.normpath(file_path).lower()
|
461 |
+
current_app.logger.debug(f"标准化路径: {normalized_path}")
|
462 |
+
|
463 |
+
ext = os.path.splitext(normalized_path)[1]
|
464 |
+
current_app.logger.debug(f"提取的扩展名: {ext}")
|
465 |
+
|
466 |
+
# 处理器映射表
|
467 |
+
handler_map = {
|
468 |
+
'.docx': word,
|
469 |
+
'.doc': word,
|
470 |
+
'.xlsx': excel,
|
471 |
+
'.xls': excel,
|
472 |
+
'.pptx': powerpoint,
|
473 |
+
'.ppt': powerpoint,
|
474 |
+
'.pdf': pdf, # if not pdf.is_scanned_pdf(file_path) else gptpdf,
|
475 |
+
'.txt': txt,
|
476 |
+
'.csv': csv_handle,
|
477 |
+
'.md': md
|
478 |
+
}
|
479 |
+
|
480 |
+
current_app.logger.debug(f"当前处理器映射表: {handler_map}")
|
481 |
+
|
482 |
+
# 匹配处理器
|
483 |
+
handler = handler_map.get(ext)
|
484 |
+
if not handler:
|
485 |
+
current_app.logger.error(f"未找到匹配的处理器,扩展名: {ext}")
|
486 |
+
return None
|
487 |
+
|
488 |
+
current_app.logger.info(f"成功匹配处理器: {handler.__name__}")
|
489 |
+
return handler
|
490 |
+
|
491 |
+
except Exception as e:
|
492 |
+
current_app.logger.error(f"获取文件处理器失败: {str(e)}", exc_info=True)
|
493 |
+
return None
|
494 |
+
|
495 |
+
def _complete_task(self, success):
|
496 |
+
"""更新任务状态[^8]"""
|
497 |
+
from app.extensions import db
|
498 |
+
try:
|
499 |
+
with self.app.app_context():
|
500 |
+
task = db.session.query(Translate).get(self.task_id)
|
501 |
+
if task:
|
502 |
+
task.status = 'done' if success else 'failed'
|
503 |
+
task.end_at = db.func.now()
|
504 |
+
task.process = 100.00 if success else 0.00
|
505 |
+
db.session.commit()
|
506 |
+
except Exception as e:
|
507 |
+
db.session.rollback()
|
508 |
+
self.app.logger.error(f"状态更新失败: {str(e)}", exc_info=True)
|
509 |
+
|
510 |
+
|
511 |
+
class TranslateEngine:
|
512 |
+
def __init__(self, task_id):
|
513 |
+
self.task_id = task_id
|
514 |
+
self.app = current_app._get_current_object() # 获取真实app对象
|
515 |
+
|
516 |
+
def _build_trans_config(self, task):
|
517 |
+
"""构建符合文件处理器要求的 trans 字典[^1]"""
|
518 |
+
config = {
|
519 |
+
'id': task.id, # 任务ID
|
520 |
+
'target_lang': task.lang,
|
521 |
+
# 'origin_lang': task.origin_lang,
|
522 |
+
'uuid':task.uuid,
|
523 |
+
'target_path_dir':os.path.dirname(task.target_filepath),
|
524 |
+
'threads': task.threads,
|
525 |
+
'file_path': task.origin_filepath, # 原始文件绝对路径
|
526 |
+
'target_file': task.target_filepath, # 目标文件绝对路径
|
527 |
+
'api_url': task.api_url,
|
528 |
+
'api_key': task.api_key, # 新增API密钥字段
|
529 |
+
'type': task.type,
|
530 |
+
'lang': task.lang,
|
531 |
+
'run_complete': True, # 默认设为True
|
532 |
+
# 以下是可能需要添加的额外字段
|
533 |
+
'prompt': task.prompt,
|
534 |
+
'model': task.model,
|
535 |
+
'backup_model': task.backup_model,
|
536 |
+
'comparison_id': task.comparison_id,
|
537 |
+
'prompt_id': task.prompt_id,
|
538 |
+
'extension': os.path.splitext(task.origin_filepath)[1] # 动态获取文件扩展名
|
539 |
+
}
|
540 |
+
|
541 |
+
# 加载术语表
|
542 |
+
if task.comparison_id:
|
543 |
+
comparison = db.session.query(Comparison).get(task.comparison_id)
|
544 |
+
if comparison:
|
545 |
+
config['comparison'] = comparison.content.replace(',', ':').replace(';', '\n')
|
546 |
+
|
547 |
+
# 加载提示语模板
|
548 |
+
if task.prompt_id:
|
549 |
+
prompt = db.session.query(Prompt).get(task.prompt_id)
|
550 |
+
if prompt:
|
551 |
+
config['prompt'] = prompt.content
|
552 |
+
|
553 |
+
return config
|
554 |
+
|
555 |
+
def execute(self):
|
556 |
+
"""启动任务入口[^2]"""
|
557 |
+
try:
|
558 |
+
# 在主线程上下文中准备任务
|
559 |
+
with self.app.app_context():
|
560 |
+
task = self._prepare_task()
|
561 |
+
|
562 |
+
# 启动线程时传递真实app对象和任务ID
|
563 |
+
thr = Thread(
|
564 |
+
target=self._async_wrapper,
|
565 |
+
args=(self.app, self.task_id)
|
566 |
+
)
|
567 |
+
thr.start()
|
568 |
+
return True
|
569 |
+
except Exception as e:
|
570 |
+
self.app.logger.error(f"任务初始化失败: {str(e)}", exc_info=True)
|
571 |
+
return False
|
572 |
+
|
573 |
+
def _async_wrapper(self, app, task_id):
|
574 |
+
"""异步执行包装器[^3]"""
|
575 |
+
with app.app_context():
|
576 |
+
from app.extensions import db # 确保在每个线程中导入
|
577 |
+
try:
|
578 |
+
# 使用新会话获取任务对象
|
579 |
+
task = db.session.query(Translate).get(task_id)
|
580 |
+
if not task:
|
581 |
+
app.logger.error(f"任务 {task_id} 不存在")
|
582 |
+
return
|
583 |
+
|
584 |
+
# 执行核心逻辑
|
585 |
+
success = self._execute_core(task)
|
586 |
+
self._complete_task(success)
|
587 |
+
except Exception as e:
|
588 |
+
app.logger.error(f"任务执行异常: {str(e)}", exc_info=True)
|
589 |
+
self._complete_task(False)
|
590 |
+
finally:
|
591 |
+
db.session.remove() # 重要!清理线程局部session
|
592 |
+
|
593 |
+
def _execute_core(self, task):
|
594 |
+
"""执行核心翻译逻辑[^4]"""
|
595 |
+
try:
|
596 |
+
# 初始化翻译配置
|
597 |
+
self._init_translate_config(task)
|
598 |
+
|
599 |
+
# 构建符合要求的 trans 字典
|
600 |
+
trans_config = self._build_trans_config(task)
|
601 |
+
|
602 |
+
# 调用 main_wrapper 执行翻译
|
603 |
+
return main_wrapper(task_id=task.id, origin_path=task.origin_filepath, config=trans_config)
|
604 |
+
except Exception as e:
|
605 |
+
current_app.logger.error(f"翻译执行失败: {str(e)}", exc_info=True)
|
606 |
+
return False
|
607 |
+
|
608 |
+
def _prepare_task(self):
|
609 |
+
"""准备翻译任务[^5]"""
|
610 |
+
task = Translate.query.get(self.task_id)
|
611 |
+
if not task:
|
612 |
+
raise ValueError(f"任务 {self.task_id} 不存在")
|
613 |
+
|
614 |
+
# 验证文件存在性
|
615 |
+
if not os.path.exists(task.origin_filepath):
|
616 |
+
raise FileNotFoundError(f"原始文件不存在: {task.origin_filepath}")
|
617 |
+
|
618 |
+
# 更新任务状态
|
619 |
+
task.status = 'process'
|
620 |
+
task.start_at = db.func.now()
|
621 |
+
db.session.commit()
|
622 |
+
return task
|
623 |
+
|
624 |
+
def _init_translate_config(self, task):
|
625 |
+
"""初始化翻译配置[^6]"""
|
626 |
+
if task.api_url and task.api_key:
|
627 |
+
import openai
|
628 |
+
openai.api_base = task.api_url
|
629 |
+
openai.api_key = task.api_key
|
630 |
+
|
631 |
+
def _complete_task(self, success):
|
632 |
+
"""更新任务状态[^7]"""
|
633 |
+
from app.extensions import db
|
634 |
+
try:
|
635 |
+
with self.app.app_context():
|
636 |
+
task = db.session.query(Translate).get(self.task_id)
|
637 |
+
if task:
|
638 |
+
task.status = 'done' if success else 'failed'
|
639 |
+
task.end_at = db.func.now()
|
640 |
+
task.process = 100.00 if success else 0.00
|
641 |
+
db.session.commit()
|
642 |
+
except Exception as e:
|
643 |
+
db.session.rollback()
|
644 |
+
self.app.logger.error(f"状态更新失败: {str(e)}", exc_info=True)
|