├── .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 |
4 |
5 |
妙笔 · 智能编辑器
6 |
7 | 
8 | 
9 | 
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 | | [
](https://flask.palletsprojects.com/en/3.0.x/) | [
](https://www.mysql.com/cn/) | [
](https://redis.io/) | [
](https://jwt.io/) | [
](https://ernie-bot-agent.readthedocs.io/zh-cn/stable/) | [
](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 | 
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 |
--------------------------------------------------------------------------------