├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── __init__.py ├── auth │ ├── __init__.py │ ├── models.py │ └── views.py ├── document │ ├── __init__.py │ ├── models.py │ └── views.py └── function │ ├── __init__.py │ └── views.py ├── database.py ├── mail.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用Python作为基础镜像 2 | FROM python:3.12 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 将当前目录下的所有文件复制到工作目录 8 | COPY . /app 9 | 10 | # 安装项目依赖 11 | RUN pip install -r requirements.txt 12 | 13 | # 暴露所使用的端口 14 | EXPOSE 8000 15 | 16 | # 启动Flask应用程序 17 | CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "run:app"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 电子笨蛋 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | logo 4 |

5 |

妙笔 · 智能编辑器

6 | 7 | ![GitHub License](https://img.shields.io/github/license/electronic-pig/SmartEditor_backend) 8 | ![python version](https://img.shields.io/badge/python-3.8+-orange.svg) 9 | ![GitHub Repo stars](https://img.shields.io/github/stars/electronic-pig/SmartEditor_backend) 10 | 11 |
12 | 13 | # ✨ 简介 14 | 15 | > 2024年中国软件杯A10赛题 16 | 17 | 妙笔 —— 基于大小模型的在线文档富文本编辑器,通过结合AI技术,为用户提供了一个全面、高效的文档编辑平台。妙笔不仅支持智能润色、多媒体信息提取和智能格式排版等核心功能,还提供了用户友好的交互界面和安全的文档管理体验,极大地提升了内容创作的效率和质量。 18 | 19 | 前端仓库请移步[SmartEditor](https://github.com/electronic-pig/SmartEditor). 20 | 21 | # 🎉 特性 22 | 23 | ## 核心功能 24 | 25 | - 用户认证 26 | - 文档管理 27 | - 富文本编辑 28 | - AI功能 29 | 30 | ## AI辅助 31 | 32 | - 智能润色 33 | - 多媒体信息提取 34 | - 智能格式排版 35 | 36 | # 🛠 技术栈 37 | 38 | | [flask](https://flask.palletsprojects.com/en/3.0.x/) | [mysql](https://www.mysql.com/cn/) | [redis](https://redis.io/) | [jwt](https://jwt.io/) | [erniebot](https://ernie-bot-agent.readthedocs.io/zh-cn/stable/) | [paddlepaddle](https://aistudio.baidu.com/overview) | 39 | |:---:|:---:|:---:|:---:|:---:|:---:| 40 | | [flask](https://flask.palletsprojects.com/en/3.0.x/) | [mysql](https://www.mysql.com/cn/) | [redis](https://redis.io/) | [jwt](https://jwt.io/) | [erniebot](https://ernie-bot-agent.readthedocs.io/zh-cn/stable/) | [paddlepaddle](https://aistudio.baidu.com/overview) | 41 | 42 | # 🗄️ 数据库 43 | 本项目使用`MySQL 5.7.44`作为数据库,数据库脚本文件并未存放在本项目中,您可以通过提交Issue的方式获取 44 | 45 | # 🚀 运行 46 | ### 配置环境变量 47 | 在项目根目录创建并编辑`.env`文件,填写相应的变量值 48 | ```bash 49 | SQLALCHEMY_DATABASE_URI = 50 | REDIS_DATABASE_URI = 51 | MAIL_USERNAME = 52 | MAIL_PASSWORD = 53 | JWT_SECRET = 54 | ACCESS_TOKEN = 55 | OCR_API_URL = 56 | ``` 57 | ### 安装依赖 58 | ```sh 59 | pip install -r requirements.txt 60 | ``` 61 | ### 项目运行 62 | ```sh 63 | python run.py 64 | ``` 65 | # 🧩 系统架构 66 | ![image](https://github.com/user-attachments/assets/cdf5d549-6873-407c-bc39-3884f3a0a930) 67 | 68 | # ✍ 写在最后 69 | 项目制作不易,如果它对你有帮助的话,请务必给作者点一个免费的⭐,万分感谢!🙏🙏🙏 70 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from flask import Flask 5 | from flask_cors import CORS 6 | from flask_jwt_extended import JWTManager 7 | 8 | from database import * 9 | from mail import mail 10 | from .auth import auth as auth_blueprint 11 | from .document import document as document_blueprint 12 | from .function import function as function_blueprint 13 | 14 | 15 | def create_app(): 16 | app = Flask(__name__) 17 | CORS(app, supports_credentials=True) # 允许跨域请求 18 | 19 | load_dotenv() # 加载 .env 文件(存储敏感信息) 20 | app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET') 21 | app.config['JWT_ACCESS_TOKEN_EXPIRES'] = False # 设置ACCESS_TOKEN的永不过期 22 | app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('SQLALCHEMY_DATABASE_URI') 23 | app.config['REDIS_URL'] = os.getenv('REDIS_DATABASE_URI') 24 | app.config['MAIL_SERVER'] = 'smtp.qq.com' 25 | app.config['MAIL_PORT'] = 465 26 | app.config['MAIL_USE_SSL'] = True 27 | app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME') 28 | app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD') 29 | 30 | db.init_app(app) # 创建mysql连接 31 | redis_client.init_app(app) # 创建 Redis 连接 32 | mail.init_app(app) # 创建邮件客户端连接 33 | JWTManager(app) # 创建 JWTManager 实例 34 | 35 | app.register_blueprint(auth_blueprint, url_prefix='/auth') # 注册蓝图 36 | app.register_blueprint(document_blueprint, url_prefix='/document') # 注册蓝图 37 | app.register_blueprint(function_blueprint, url_prefix='/function') # 注册蓝图 38 | 39 | return app 40 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | auth = Blueprint('auth', __name__) 4 | 5 | from . import views, models 6 | -------------------------------------------------------------------------------- /app/auth/models.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash, check_password_hash 2 | 3 | from database import db 4 | 5 | 6 | class Users(db.Model): 7 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 8 | username = db.Column(db.String(32), unique=True, nullable=False) 9 | password_hash = db.Column(db.String(256), nullable=False) 10 | email = db.Column(db.String(64), unique=True, nullable=False) 11 | 12 | def __repr__(self): 13 | return '' % self.username 14 | 15 | def set_password(self, password): 16 | self.password_hash = generate_password_hash(password) 17 | 18 | def check_password(self, password): 19 | return check_password_hash(self.password_hash, password) 20 | -------------------------------------------------------------------------------- /app/auth/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | from flask import jsonify, request 5 | from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity 6 | from flask_mail import Message 7 | 8 | from database import * 9 | from mail import mail 10 | from . import auth 11 | from .models import Users 12 | 13 | 14 | # 生成验证码并发送邮件 15 | @auth.route('/varify/&') 16 | def varify(username, email): 17 | # 生成一个6位数的验证码 18 | verification_code = str(random.randint(100000, 999999)) 19 | # 将验证码和用户的邮箱一起存储到 Redis 中 20 | redis_client.setex(f'verification_code:{email}', 300, verification_code) 21 | # 创建邮件消息 22 | msg = Message('【妙笔】用户注册邮箱验证', sender=os.getenv('MAIL_USERNAME'), recipients=[email]) 23 | msg.body = ('Hi,【{}】:\n\n您正尝试通过本邮箱接收注册【妙笔】时所需的验证码。\n\n' 24 | '验证码:【{}】,5分钟内有效,如非本人操作,请忽略本邮件。').format(username, verification_code) 25 | # 发送邮件 26 | mail.send(msg) 27 | return jsonify({'message': '验证码已发送,请注意查收!', 'code': 200}) 28 | 29 | 30 | # 用户注册 31 | @auth.route('/register', methods=['POST']) 32 | def register(): 33 | data = request.get_json() 34 | # 从 Redis 中获取验证码 35 | verification_code = redis_client.get(f'verification_code:{data["email"]}') 36 | # 验证验证码 37 | if verification_code is None or data['verification_code'] != verification_code: 38 | return jsonify({'message': '验证码错误或已失效!', 'code': 400}) 39 | # 验证邮箱是否已被注册 40 | if Users.query.filter_by(email=data['email']).first(): 41 | return jsonify({'message': '邮箱已被注册!', 'code': 400}) 42 | # 注册新用户 43 | new_user = Users(username=data['username'], email=data['email']) 44 | new_user.set_password(data['password']) 45 | db.session.add(new_user) 46 | db.session.commit() 47 | return jsonify({'message': '用户注册成功!', 'code': 200}) 48 | 49 | 50 | # 用户登录 51 | @auth.route('/login', methods=['POST']) 52 | def login(): 53 | data = request.get_json() 54 | # 验证用户是否存在 55 | user = Users.query.filter_by(email=data['email']).first() 56 | if user is None: 57 | return jsonify({'message': '邮箱未注册!', 'code': 400}) 58 | # 验证密码是否正确 59 | if not user.check_password(data['password']): 60 | return jsonify({'message': '密码错误!', 'code': 400}) 61 | # 用户登录成功,生成 JWT 62 | access_token = create_access_token(identity=user.id) 63 | # 将 JWT 发送给前端 64 | return jsonify({'message': '用户登录成功!', 'code': 200, 65 | 'data': {'access_token': access_token, 'username': user.username, 'email': user.email}}) 66 | 67 | 68 | # 重置密码验证 69 | @auth.route("/reset_varify", methods=["GET"]) 70 | @jwt_required() 71 | def reset_varify(): 72 | # 使用get_jwt_identity访问当前用户的身份 73 | current_user = get_jwt_identity() 74 | user = Users.query.get(current_user) 75 | email = user.email 76 | # 生成一个6位数的验证码 77 | verification_code = str(random.randint(100000, 999999)) 78 | # 将验证码和用户的邮箱一起存储到 Redis 中 79 | redis_client.setex(f'verification_code:{email}', 300, verification_code) 80 | # 创建邮件消息 81 | msg = Message('【妙笔】用户密码重置', sender=os.getenv('MAIL_USERNAME'), recipients=[email]) 82 | msg.body = ('Hi,【{}】:\n\n您正尝试通过本邮箱重置【妙笔】时所需的验证码。\n\n' 83 | '验证码:【{}】,5分钟内有效,如非本人操作,请忽略本邮件。').format(user.username, verification_code) 84 | # 发送邮件 85 | mail.send(msg) 86 | return jsonify({'message': '验证码已发送,请注意查收!', 'code': 200}) 87 | 88 | 89 | # 用户密码重置 90 | @auth.route("/reset_password", methods=["POST"]) 91 | @jwt_required() 92 | def reset_password(): 93 | current_user = get_jwt_identity() 94 | user = Users.query.get(current_user) 95 | email = user.email 96 | data = request.get_json() 97 | # 从 Redis 中获取验证码 98 | verification_code = redis_client.get(f'verification_code:{email}') 99 | # 验证验证码 100 | if verification_code is None or data['verification_code'] != verification_code: 101 | return jsonify({'message': '验证码错误或已失效!', 'code': 400}) 102 | # 重置用户密码 103 | user.set_password(data['password']) 104 | db.session.commit() 105 | return jsonify({'message': '用户密码重置成功!', 'code': 200}) 106 | -------------------------------------------------------------------------------- /app/document/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | document = Blueprint('document', __name__) 4 | 5 | from . import views, models 6 | -------------------------------------------------------------------------------- /app/document/models.py: -------------------------------------------------------------------------------- 1 | from database import db 2 | from datetime import datetime 3 | import pytz # 导入 pytz 以处理时区 4 | 5 | class Documents(db.Model): 6 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 7 | user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) 8 | title = db.Column(db.String(64), nullable=False) 9 | content = db.Column(db.Text, nullable=False) 10 | created_at = db.Column(db.DateTime, default=lambda: datetime.now(pytz.timezone('Asia/Shanghai')), nullable=False) 11 | updated_at = db.Column(db.DateTime, default=lambda: datetime.now(pytz.timezone('Asia/Shanghai')), nullable=False) 12 | is_favorite = db.Column(db.Boolean, default=False) # 表示文档是否被收藏 13 | is_deleted = db.Column(db.Boolean, default=False) # 表示文档是否被逻辑删除 14 | is_template = db.Column(db.Boolean, default=False) # 表示文档是否为模板 15 | 16 | def __repr__(self): 17 | return '' % self.title 18 | 19 | def to_dict(self): 20 | return {c.name: getattr(self, c.name) for c in self.__table__.columns} 21 | -------------------------------------------------------------------------------- /app/document/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | 4 | from flask import request, jsonify 5 | from flask_jwt_extended import jwt_required, get_jwt_identity 6 | 7 | from database import * 8 | from . import document 9 | from .models import Documents 10 | 11 | 12 | class CustomJSONEncoder(json.JSONEncoder): 13 | def default(self, obj): 14 | if isinstance(obj, datetime): 15 | return obj.isoformat() 16 | return json.JSONEncoder.default(self, obj) 17 | 18 | 19 | # 创建文档 20 | @document.route('', methods=['POST']) 21 | @jwt_required() 22 | def create_document(): 23 | user_id = get_jwt_identity() 24 | data = request.get_json() 25 | new_document = Documents(user_id=user_id, title=data['title'], content=data['content']) 26 | db.session.add(new_document) 27 | db.session.commit() 28 | return jsonify({'message': '创建成功!', 'id': new_document.id, 'code': '200'}) 29 | 30 | 31 | # 查询单个文档 32 | @document.route('/', methods=['GET']) 33 | @jwt_required() 34 | def get_document(document_id): 35 | cache_key = f"document:{document_id}" 36 | cached_doc = redis_client.get(cache_key) 37 | if cached_doc: 38 | print('cache hit!') 39 | return jsonify({'document': json.loads(cached_doc), 'code': '200'}) 40 | else: 41 | doc = Documents.query.get(document_id) 42 | if doc is None: 43 | return jsonify({'message': '查询失败!', 'code': '400'}) 44 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 45 | return jsonify({'document': doc.to_dict(), 'code': '200'}) 46 | 47 | 48 | # 查询用户的所有文档 49 | @document.route('/user', methods=['GET']) 50 | @jwt_required() 51 | def get_documents_by_user(): 52 | user_id = get_jwt_identity() 53 | docs = Documents.query.filter_by(user_id=user_id, is_deleted=False).all() 54 | if not docs: 55 | return jsonify({'message': '该用户无任何文档!', 'code': '400'}) 56 | return jsonify({'documents': [doc.to_dict() for doc in docs], 'code': '200'}) 57 | 58 | 59 | # 更新文档 60 | @document.route('/', methods=['PUT']) 61 | @jwt_required() 62 | def update_document(document_id): 63 | data = request.get_json() 64 | doc = Documents.query.get(document_id) 65 | if doc is None: 66 | return jsonify({'message': '查询失败!', 'code': '400'}) 67 | doc.title = data['title'] 68 | doc.content = data['content'] 69 | db.session.commit() 70 | # 更新Redis缓存 71 | cache_key = f"document:{document_id}" 72 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 73 | return jsonify({'message': '更新成功!', 'code': '200'}) 74 | 75 | 76 | # 物理删除文档 77 | @document.route('/', methods=['DELETE']) 78 | @jwt_required() 79 | def delete_document(document_id): 80 | doc = Documents.query.get(document_id) 81 | if doc is None: 82 | return jsonify({'message': '查询失败!', 'code': '400'}) 83 | db.session.delete(doc) 84 | db.session.commit() 85 | # 清理Redis缓存 86 | cache_key = f"document:{document_id}" 87 | redis_client.delete(cache_key) 88 | return jsonify({'message': '删除成功!', 'code': '200'}) 89 | 90 | 91 | # 收藏文档 92 | @document.route('/favorite/', methods=['PUT']) 93 | @jwt_required() 94 | def favorite_document(document_id): 95 | doc = Documents.query.get(document_id) 96 | if doc is None: 97 | return jsonify({'message': '查询失败!', 'code': '400'}) 98 | doc.is_favorite = True 99 | db.session.commit() 100 | # 更新Redis缓存 101 | cache_key = f"document:{document_id}" 102 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 103 | return jsonify({'message': '收藏成功!', 'code': '200'}) 104 | 105 | 106 | # 取消收藏文档 107 | @document.route('/unfavorite/', methods=['PUT']) 108 | @jwt_required() 109 | def unfavorite_document(document_id): 110 | doc = Documents.query.get(document_id) 111 | if doc is None: 112 | return jsonify({'message': '查询失败!', 'code': '400'}) 113 | doc.is_favorite = False 114 | db.session.commit() 115 | # 更新Redis缓存 116 | cache_key = f"document:{document_id}" 117 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 118 | return jsonify({'message': '取消收藏成功!', 'code': '200'}) 119 | 120 | 121 | # 查询用户的所有收藏文档 122 | @document.route('/favorites/user', methods=['GET']) 123 | @jwt_required() 124 | def get_favorite_documents(): 125 | user_id = get_jwt_identity() 126 | docs = Documents.query.filter_by(user_id=user_id, is_favorite=True).all() 127 | if not docs: 128 | return jsonify({'message': '该用户无任何收藏文档!', 'code': '400'}) 129 | return jsonify({'documents': [doc.to_dict() for doc in docs], 'code': '200'}) 130 | 131 | 132 | # 逻辑删除文档 133 | @document.route('/delete/', methods=['PUT']) 134 | @jwt_required() 135 | def delete_document_logic(document_id): 136 | doc = Documents.query.get(document_id) 137 | if doc is None: 138 | return jsonify({'message': '查询失败!', 'code': '400'}) 139 | doc.is_deleted = True 140 | doc.is_favorite = False 141 | doc.is_template = False 142 | db.session.commit() 143 | # 更新Redis缓存 144 | cache_key = f"document:{document_id}" 145 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 146 | return jsonify({'message': '放入回收站成功!', 'code': '200'}) 147 | 148 | 149 | # 恢复逻辑删除的文档 150 | @document.route('/recover/', methods=['PUT']) 151 | @jwt_required() 152 | def recover_document(document_id): 153 | doc = Documents.query.get(document_id) 154 | if doc is None: 155 | return jsonify({'message': '查询失败!', 'code': '400'}) 156 | doc.is_deleted = False 157 | db.session.commit() 158 | # 更新Redis缓存 159 | cache_key = f"document:{document_id}" 160 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 161 | return jsonify({'message': '恢复成功!', 'code': '200'}) 162 | 163 | 164 | # 查询用户的所有逻辑删除的文档 165 | @document.route('/deleted/user', methods=['GET']) 166 | @jwt_required() 167 | def get_deleted_documents(): 168 | user_id = get_jwt_identity() 169 | docs = Documents.query.filter_by(user_id=user_id, is_deleted=True).all() 170 | if not docs: 171 | return jsonify({'message': '该用户无任何回收站文档!', 'code': '400'}) 172 | return jsonify({'documents': [doc.to_dict() for doc in docs], 'code': '200'}) 173 | 174 | 175 | # 查询模板库文档 176 | @document.route('/template', methods=['GET']) 177 | def get_document_template(): 178 | docs = Documents.query.filter_by(user_id=1).all() 179 | if not docs: 180 | return jsonify({'message': '模板库无任何文档!', 'code': '400'}) 181 | return jsonify({'documents': [doc.to_dict() for doc in docs], 'code': '200'}) 182 | 183 | 184 | # 根据用户的查询参数进行模糊查询 185 | @document.route('/search/', methods=['GET']) 186 | @jwt_required() 187 | def search_documents_by_user(title): 188 | user_id = get_jwt_identity() 189 | docs = Documents.query.filter(Documents.user_id == user_id, 190 | Documents.is_deleted == False, 191 | Documents.title.like(f"%{title}%")).all() 192 | if not docs: 193 | return jsonify({'message': '未查询到匹配文档!', 'code': '400'}) 194 | return jsonify({'documents': [doc.to_dict() for doc in docs], 'code': '200'}) 195 | 196 | 197 | # 查询用户的模板文档 198 | @document.route('/template/user', methods=['GET']) 199 | @jwt_required() 200 | def get_template_documents_by_user(): 201 | user_id = get_jwt_identity() 202 | docs = Documents.query.filter_by(user_id=user_id, is_template=True, is_deleted=False).all() 203 | if not docs: 204 | return jsonify({'message': '该用户无任何模板文档!', 'code': '400'}) 205 | return jsonify({'documents': [doc.to_dict() for doc in docs], 'code': '200'}) 206 | 207 | 208 | # 将文档设置为模板 209 | @document.route('/template/', methods=['PUT']) 210 | @jwt_required() 211 | def set_document_template(document_id): 212 | doc = Documents.query.get(document_id) 213 | if doc is None: 214 | return jsonify({'message': '查询失败!', 'code': '400'}) 215 | doc.is_template = True 216 | db.session.commit() 217 | # 更新Redis缓存 218 | cache_key = f"document:{document_id}" 219 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 220 | return jsonify({'message': '另存为模板成功!', 'code': '200'}) 221 | 222 | 223 | # 将模板文档取消模板 224 | @document.route('/untemplate/', methods=['PUT']) 225 | @jwt_required() 226 | def unset_document_template(document_id): 227 | doc = Documents.query.get(document_id) 228 | if doc is None: 229 | return jsonify({'message': '查询失败!', 'code': '400'}) 230 | doc.is_template = False 231 | db.session.commit() 232 | # 更新Redis缓存 233 | cache_key = f"document:{document_id}" 234 | redis_client.set(cache_key, json.dumps(doc.to_dict(), cls=CustomJSONEncoder)) 235 | return jsonify({'message': '撤销模板成功!', 'code': '200'}) 236 | -------------------------------------------------------------------------------- /app/function/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | function = Blueprint('function', __name__) 4 | 5 | from . import views 6 | -------------------------------------------------------------------------------- /app/function/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | import base64 4 | import requests 5 | from dotenv import load_dotenv 6 | from flask import jsonify, request, Response 7 | from flask_jwt_extended import jwt_required 8 | import erniebot 9 | 10 | from . import function 11 | 12 | load_dotenv() 13 | erniebot.api_type = "aistudio" 14 | erniebot.access_token = os.getenv('ACCESS_TOKEN') 15 | 16 | 17 | @function.route('/ocr', methods=['POST']) 18 | def ocr(): 19 | # 检查是否有文件被上传 20 | if 'file' not in request.files: 21 | return jsonify({'message': '无文件上传!', 'code': 400}) 22 | file = request.files['file'] 23 | # 如果用户没有选择文件,浏览器也会提交一个空的文件部分,所以需要检查文件是否存在 24 | if file.filename == '': 25 | return jsonify({'message': '无文件上传!', 'code': 400}) 26 | # 二进制读取文件内容 27 | image_bytes = file.read() 28 | image_base64 = base64.b64encode(image_bytes).decode('ascii') 29 | # 设置鉴权信息 30 | headers = { 31 | "Authorization": f"token {os.getenv('ACCESS_TOKEN')}", 32 | "Content-Type": "application/json" 33 | } 34 | # 设置请求体 35 | payload = { 36 | "image": image_base64 # Base64编码的文件内容或者文件链接 37 | } 38 | try: 39 | resp = requests.post(url=os.getenv('OCR_API_URL'), json=payload, headers=headers) 40 | resp.raise_for_status() # 将引发异常,如果状态码不是 200-399 41 | ocr_result = resp.json()["result"] 42 | result = '' 43 | for text in ocr_result["texts"]: 44 | result += text["text"] 45 | result += '\n' 46 | return jsonify({'message': result, 'code': 200}) 47 | except Exception as e: 48 | print(f"处理响应时发生错误: {e}") 49 | return jsonify({'message': '后端小模型OCR服务未启动!', 'code': 400}) 50 | 51 | 52 | @function.route('/asr', methods=['POST']) 53 | def asr(): 54 | # 检查是否有文件被上传 55 | if 'file' not in request.files: 56 | return jsonify({'message': '无文件上传!', 'code': 400}) 57 | 58 | file = request.files['file'] 59 | 60 | # 如果用户没有选择文件,浏览器也会提交一个空的文件部分,所以需要检查文件是否存在 61 | if file.filename == '': 62 | return jsonify({'message': '无文件上传!', 'code': 400}) 63 | 64 | # TODO:调用后端小模型ASR服务 65 | sleep(1.33) 66 | return jsonify({'message': '后端小模型ASR服务未启动!', 'code': 400}) 67 | 68 | # Demo:返回固定文本 69 | # return jsonify({'message': '早上八点我从北京到广州花了四百二十六元', 'code': 200}) 70 | 71 | 72 | @function.route('/AIFunc', methods=['POST']) 73 | # @jwt_required() 74 | def AIFunc(): 75 | data = request.get_json() 76 | command = data['command'] 77 | text = data['text'] 78 | if command == '续写': 79 | prompt = ("这是从文档截取的一部分文本内容。\n" + text + 80 | "\n请帮我续写这部分内容,保持原有的写作风格和语气。续写内容应连贯且自然,长度约为两段,每段不少于100字。" 81 | "请确保续写部分与原文内容主题一致,并继续探讨相关话题。只需要续写内容,不需要返回其他内容。") 82 | elif command == '润色': 83 | prompt = ("这是从文档截取的一部分文本内容。\n" + text + 84 | "\n请帮我润色这部分内容,保持原有的写作风格和语气。润色后的内容应更加流畅、自然,并纠正任何语法或拼写错误。" 85 | "请确保内容的主题和信息不变。只需要返回润色后的内容,不需要返回其他内容。") 86 | elif command == '校对': 87 | prompt = ("这是从文档截取的一部分文本内容。\n" + text + 88 | "\n请帮我校对这部分内容,保持原有的写作风格和语气。校对后的内容应纠正所有语法、拼写和标点错误。" 89 | "请确保不改变原文的主题和信息。只需要返回校对后的内容,不需要返回其他内容。") 90 | elif command == '翻译': 91 | prompt = ("这是从文档截取的一部分文本内容。\n" + text + 92 | "\n根据原有的语言,请帮我将这部分内容翻译成中文或英文,保持原有的写作风格和语气。" 93 | "翻译后的内容应准确传达原文的意思,并且自然流畅。只需要返回翻译后的内容,不需要返回其他内容。") 94 | elif command == '内容简化': 95 | prompt = ("这是一份文档的文本内容。\n" + text + 96 | "\n请帮我简化这些内容,使其更易于理解。保留关键信息和主要观点,去除冗余和复杂的表达。" 97 | "简化后的内容应保持原文的主题和信息不变,但更简洁明了。只需要返回简化后的内容,不需要返回其他内容。") 98 | elif command == '全文翻译': 99 | prompt = ("这是一份文档的文本内容。\n" + text + 100 | "\n根据原有的语言,请将这些内容翻译成中文或英文,保持原有的写作风格和语气。" 101 | "翻译后的内容应准确传达原文的意思,并且自然流畅。只需要返回翻译后的内容,不需要返回其他内容。") 102 | elif command == '全文总结': 103 | prompt = ("这是一份文档的文本内容。\n" + text + 104 | "\n请帮我总结这些内容,保持原有的写作风格和语气。" 105 | "总结后的内容应概括文档的主要观点和结论,并且简洁明了。只需要返回总结后的内容,不需要返回其他内容。") 106 | elif command == '重点提取': 107 | prompt = ("这是一份文档的文本内容。\n" + text + 108 | "\n请帮我提取这些内容的重点信息。重点信息应包括主要观点、关键数据和重要结论。" 109 | "提取后的内容应简洁明了,涵盖文档的核心内容。只需要返回提取后的内容,不需要返回其他内容。") 110 | else: 111 | prompt = f"请采用{data['tone']}的生成风格,{data['prompt']}" if data['tone'] else data['prompt'] 112 | 113 | def generate(): 114 | response = erniebot.ChatCompletion.create(model="ernie-4.0", 115 | messages=[{"role": "user", "content": prompt}], 116 | stream=True) 117 | for chunk in response: 118 | result = chunk.get_result() 119 | yield f"{result}" 120 | 121 | return Response(generate(), content_type='text/event-stream') 122 | 123 | 124 | @function.route('/typography', methods=['POST']) 125 | # @jwt_required() 126 | def typography(): 127 | data = request.get_json() 128 | text = data['text'] 129 | title = data['title'] 130 | font = data['font'] 131 | font_size = data['font_size'] 132 | line_spacing = data['line_spacing'] 133 | paragraph = data['paragraph'] 134 | prompt = ( 135 | f"这是一份文档的HTML文本内容。\n" 136 | f"{text}\n" 137 | f"请将上述HTML内容重新排版为{title}的格式。要求如下:\n" 138 | f"- 字体:{font}\n" 139 | f"- 字号:{font_size}\n" 140 | f"- 行距:{line_spacing}\n" 141 | f"- 段落:{paragraph}\n" 142 | f"只需要返回生成后的HTML文本,不需要返回其他内容。" 143 | ) 144 | 145 | def generate(): 146 | response = erniebot.ChatCompletion.create(model="ernie-4.0", 147 | messages=[{"role": "user", "content": prompt}], 148 | stream=True) 149 | for chunk in response: 150 | result = chunk.get_result() 151 | yield f"{result}" 152 | 153 | return Response(generate(), content_type='text/event-stream') 154 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | # database.py 2 | from flask_redis import FlaskRedis 3 | from flask_sqlalchemy import SQLAlchemy 4 | 5 | db = SQLAlchemy() 6 | redis_client = FlaskRedis(decode_responses=True) 7 | -------------------------------------------------------------------------------- /mail.py: -------------------------------------------------------------------------------- 1 | # mail.py 2 | from flask_mail import Mail 3 | 4 | mail = Mail() 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.3 2 | Werkzeug==3.0.6 3 | Flask-SQLAlchemy==3.1.1 4 | PyMySQL==1.1.1 5 | python-dotenv==1.0.1 6 | erniebot==0.5.8 7 | PyJWT==2.8.0 8 | flask_cors==6.0.0 9 | flask_jwt_extended==4.6.0 10 | flask_mail==0.10.0 11 | flask_redis==0.4.0 12 | gunicorn==23.0.0 13 | requests==2.32.4 14 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import create_app 2 | 3 | app = create_app() 4 | 5 | 6 | # 为了方便测试,添加一个简单的路由 7 | @app.route('/') 8 | def index(): 9 | return 'Hello, World!' 10 | 11 | 12 | if __name__ == '__main__': 13 | app.run(host='0.0.0.0', port=5000, debug=True) 14 | --------------------------------------------------------------------------------