gitdeem commited on
Commit
4e9efe9
·
verified ·
1 Parent(s): f7bacb0

Upload 96 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +9 -0
  2. .gitattributes +1 -0
  3. Dockerfile +28 -0
  4. README.md +13 -10
  5. app.py +20 -0
  6. app/__init__.py +52 -0
  7. app/config.py +110 -0
  8. app/extensions.py +40 -0
  9. app/models/__init__.py +7 -0
  10. app/models/cache.py +16 -0
  11. app/models/comparison.py +46 -0
  12. app/models/customer.py +44 -0
  13. app/models/job.py +43 -0
  14. app/models/message.py +32 -0
  15. app/models/migration.py +9 -0
  16. app/models/prompt.py +26 -0
  17. app/models/pwdResetToken.py +9 -0
  18. app/models/send_code.py +16 -0
  19. app/models/session.py +12 -0
  20. app/models/setting.py +25 -0
  21. app/models/translate.py +55 -0
  22. app/models/translateLog.py +25 -0
  23. app/models/translateTask.py +52 -0
  24. app/models/user.py +18 -0
  25. app/models/users.py +14 -0
  26. app/prompt +22 -0
  27. app/resources/__init__.py +0 -0
  28. app/resources/admin/__init__.py +0 -0
  29. app/resources/admin/auth.py +94 -0
  30. app/resources/admin/customer.py +143 -0
  31. app/resources/admin/image.py +43 -0
  32. app/resources/admin/settings.py +116 -0
  33. app/resources/admin/translate.py +241 -0
  34. app/resources/admin/users.py +107 -0
  35. app/resources/api/AccountResource.py +143 -0
  36. app/resources/api/AuthResource.py +142 -0
  37. app/resources/api/__init__.py +0 -0
  38. app/resources/api/common.py +20 -0
  39. app/resources/api/comparison.py +449 -0
  40. app/resources/api/customer.py +32 -0
  41. app/resources/api/files.py +339 -0
  42. app/resources/api/prompt.py +245 -0
  43. app/resources/api/setting.py +27 -0
  44. app/resources/api/translate.py +590 -0
  45. app/resources/hello.py +5 -0
  46. app/resources/task/__init__.py +0 -0
  47. app/resources/task/file_handlers.py +0 -0
  48. app/resources/task/main.py +136 -0
  49. app/resources/task/translate.py +38 -0
  50. 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: Dr
3
- emoji: 🐠
4
- colorFrom: gray
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
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)