From 5bb398c7f269b8182b8a0406b82d409c6c7c2f33 Mon Sep 17 00:00:00 2001 From: nyaco3 <3200036132@qq.com> Date: Tue, 26 May 2026 20:05:13 +0800 Subject: [PATCH] backend implemented --- app/__init__.py | 32 +++ app/routes.py | 711 ++++++++++++++++++++++++++++++++++++++++++++++++ config.py | 12 + models.py | 72 +++++ run.py | 6 + 5 files changed, 833 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/routes.py create mode 100644 config.py create mode 100644 models.py create mode 100644 run.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b206caf --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,32 @@ +from flask import Flask +from flask_jwt_extended import JWTManager +from config import Config +from models import db, User, UserRole +from werkzeug.security import generate_password_hash +from .routes import bp_api_v1 + +jwt = JWTManager() + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + db.init_app(app) + jwt.init_app(app) + + app.register_blueprint(bp_api_v1, url_prefix='/api/v1') + + with app.app_context(): + db.create_all() + # Create default Owner if not exists + if not User.query.first(): + default_owner = User( + username=app.config['DEFAULT_OWNER_USERNAME'], + nickname=app.config['DEFAULT_OWNER_USERNAME'], + password=generate_password_hash(app.config['DEFAULT_OWNER_PASSWORD']), + role=UserRole.ADMIN + ) + db.session.add(default_owner) + db.session.commit() + + return app \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..f0af508 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,711 @@ +from datetime import datetime, timedelta +from flask import Blueprint, request, jsonify +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity, get_jwt +from Crypto.PublicKey import RSA +from werkzeug.security import generate_password_hash, check_password_hash +import uuid +from models import db, User, UserRole, PendingPassage, Passage, Paper, PaperPassagesRelation + +bp_api_v1 = Blueprint('api/v1', __name__) +SESSION_EXPIRATION = timedelta(minutes=5) +sessions = {} + + +def cleanup_sessions(): + now = datetime.now() + expired_keys = [sid for sid, info in sessions.items() if now - info['created_at'] > SESSION_EXPIRATION] + for sid in expired_keys: + sessions.pop(sid, None) + + +@bp_api_v1.route('/ping') +def ping(): + return 'pong' + + +@bp_api_v1.route('/get_key') +def get_key(): + cleanup_sessions() + + session_id = str(uuid.uuid4()) + private_key = RSA.generate(2048) + sessions[session_id] = { + 'private_key': private_key, + 'created_at': datetime.now() + } + + public_key_pem = private_key.publickey().export_key(format='PEM').decode('utf-8') + return jsonify({'pkey': public_key_pem, 'session_id': session_id}), 200 + + +@bp_api_v1.route('/register', methods=['POST']) +def register(): + cleanup_sessions() + data = request.get_json() + + session_id = data.get('session_id') + if not session_id or session_id not in sessions: + return jsonify({'error': 'Invalid session_id'}), 400 + + username = data.get('username') + if not username or len(username) == 0 or len(username) > 80: + return jsonify({'error': 'Username is required'}), 400 + if User.query.filter_by(username=username).first(): + return jsonify({'error': 'Username already exists'}), 400 + + nickname = data.get('nickname') + if nickname and len(nickname) > 80: + return jsonify({'error': 'Nickname is too long'}), 400 + + phone = data.get('phone') + email = data.get('email') + qq = data.get('qq') + if phone and (len(phone) != 11 or not phone.isdigit()): + return jsonify({'error': 'Invalid phone number'}), 400 + if email and (not '@' in email or len(email) > 120): + return jsonify({'error': 'Invalid email address'}), 400 + if qq and (len(qq) < 5 or not qq.isdigit() or len(qq) > 10): + return jsonify({'error': 'Invalid QQ number'}), 400 + if not phone and not email and not qq: + return jsonify({'error': 'At least one contact method is required'}), 400 + + password_encrypted = data.get('password') + if not password_encrypted: + return jsonify({'error': 'Password is required'}), 400 + + private_key = sessions[session_id]['private_key'] + try: + password_decrypted = private_key.decrypt(bytes.fromhex(password_encrypted)).decode('utf-8') + password_hash = generate_password_hash(password_decrypted) + except Exception: + return jsonify({'error': 'Failed to decrypt password'}), 400 + + sessions.pop(session_id, None) + new_user = User(username=username, nickname=nickname, password=password_hash, role=UserRole.USER, phone=phone, email=email, qq=qq) + db.session.add(new_user) + db.session.commit() + + return jsonify({'message': 'User registered successfully'}), 201 + + +@bp_api_v1.route('/login', methods=['POST']) +def login(): + cleanup_sessions() + data = request.get_json() + + session_id = data.get('session_id') + if not session_id or session_id not in sessions: + return jsonify({'error': 'Invalid session_id'}), 400 + + username = data.get('username') + password_encrypted = data.get('password') + if not username or not password_encrypted: + return jsonify({'error': 'Username and password are required'}), 400 + + user = User.query.filter_by(username=username).first() + if not user: + return jsonify({'error': 'User has not been registered'}), 401 + + private_key = sessions[session_id]['private_key'] + try: + password_decrypted = private_key.decrypt(bytes.fromhex(password_encrypted)).decode('utf-8') + except Exception: + return jsonify({'error': 'Failed to decrypt password'}), 400 + + if not check_password_hash(user.password, password_decrypted): + return jsonify({'error': 'Incorrect username or password'}), 401 + + sessions.pop(session_id, None) + access_token = create_access_token(identity=user.id, expires_delta=timedelta(days=30 * 6), additional_claims={ + 'role': user.role.value, + 'username': user.username, + 'phone': user.phone, + 'email': user.email, + 'qq': user.qq + }) + return jsonify({'access_token': access_token}), 200 + + +@bp_api_v1.route('/profile') +@jwt_required() +def profile(): + user_id = get_jwt_identity() + user = User.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + return jsonify({ + 'username': user.username, + 'role': user.role.value, + 'created_at': user.created_at.isoformat(), + 'phone': user.phone, + 'email': user.email, + 'qq': user.qq + }), 200 + + +@bp_api_v1.route('/passages/', methods=['GET']) +@jwt_required(optional=True) +def get_passage(passage_id): + passage = Passage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Passage not found'}), 404 + + if not passage.is_posted: + jwt_data = get_jwt() + if not jwt_data or jwt_data.get('role') != UserRole.ADMIN.value: + return jsonify({'error': 'Passage is not posted yet'}), 403 + + return jsonify({ + 'id': passage.id, + 'title': passage.title, + 'content': passage.content, + 'image_url': passage.image_url, + 'authors': passage.authors, + 'created_at': passage.created_at.isoformat(), + 'is_outlink': passage.is_outlink, + 'sender': { + 'id': passage.sender.id, + 'username': passage.sender.username + } + }), 200 + + +@bp_api_v1.route('/passages', methods=['GET']) +@jwt_required() +def list_passages(): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + try: + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 10)) + except ValueError: + return jsonify({'error': 'Invalid pagination parameters'}), 400 + + query = Passage.query.paginate(page=page, per_page=per_page, error_out=False) + passages = query.items + return jsonify([{ + 'id': p.id, + 'title': p.title, + 'image_url': p.image_url, + 'authors': p.authors, + 'created_at': p.created_at.isoformat(), + 'is_outlink': p.is_outlink, + 'sender': { + 'id': p.sender.id, + 'username': p.sender.username + }, + 'has_next': query.has_next + } for p in passages]), 200 + + +@bp_api_v1.route('/papers/', methods=['GET']) +def get_paper(paper_id): + paper = Paper.query.get(paper_id) + if not paper: + return jsonify({'error': 'Paper not found'}), 404 + + return jsonify({ + 'id': paper.id, + 'title': paper.title, + 'description': paper.description, + 'image_url': paper.image_url, + 'created_at': paper.created_at.isoformat(), + 'passages': [p.id for p in paper.passages] + }), 200 + + +@bp_api_v1.route('/papers', methods=['GET']) +def list_papers(): + try: + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 10)) + except ValueError: + return jsonify({'error': 'Invalid pagination parameters'}), 400 + + query = Paper.query.paginate(page=page, per_page=per_page, error_out=False) + papers = query.items + return jsonify([{ + 'id': p.id, + 'title': p.title, + 'description': p.description, + 'image_url': p.image_url, + 'created_at': p.created_at.isoformat(), + 'has_next': query.has_next + } for p in papers]), 200 + + +@bp_api_v1.route('/create_passage', methods=['POST']) +@jwt_required() +def create_passage(): + user_id = get_jwt_identity() + user = User.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + data = request.get_json() + title = data.get('title') + content = data.get('content') + image_url = data.get('image_url') + authors = data.get('authors') + is_outlink = data.get('is_outlink', False) + + if not title or len(title) == 0 or len(title) > 200: + return jsonify({'error': 'Invalid title'}), 400 + if not content or len(content) == 0: + return jsonify({'error': 'Invalid content'}), 400 + if image_url and len(image_url) > 500: + return jsonify({'error': 'Image URL is too long'}), 400 + if not authors or len(authors) == 0 or len(authors) > 60: + return jsonify({'error': 'Invalid authors'}), 400 + + new_passage = PendingPassage( + sender_id=user.id, + title=title, + content=content, + image_url=image_url, + authors=authors, + is_outlink=is_outlink + ) + db.session.add(new_passage) + db.session.commit() + + return jsonify({'message': 'Passage submitted successfully'}), 201 + + +@bp_api_v1.route('/pending_passages', methods=['GET']) +@jwt_required() +def list_pending_passages(): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + try: + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 10)) + except ValueError: + return jsonify({'error': 'Invalid pagination parameters'}), 400 + + query = PendingPassage.query.paginate(page=page, per_page=per_page, error_out=False) + passages = query.items + return jsonify([{ + 'id': p.id, + 'title': p.title, + 'content': p.content, + 'image_url': p.image_url, + 'authors': p.authors, + 'created_at': p.created_at.isoformat(), + 'sender': { + 'id': p.sender.id, + 'username': p.sender.username + }, + 'has_next': query.has_next + } for p in passages]), 200 + + +@bp_api_v1.route('/pending_passages/approve/', methods=['POST']) +@jwt_required() +def approve_passage(passage_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + passage = PendingPassage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Pending passage not found'}), 404 + + new_passage = Passage( + sender_id=passage.sender_id, + processor_id=get_jwt_identity(), + title=passage.title, + content=passage.content, + image_url=passage.image_url, + authors=passage.authors, + is_outlink=passage.is_outlink + ) + db.session.add(new_passage) + db.session.delete(passage) + db.session.commit() + + return jsonify({'message': 'Passage approved successfully'}), 200 + + +@bp_api_v1.route('/pending_passages/reject/', methods=['POST']) +@jwt_required() +def reject_passage(passage_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + passage = PendingPassage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Pending passage not found'}), 404 + + db.session.delete(passage) + db.session.commit() + + return jsonify({'message': 'Passage rejected successfully'}), 200 + + +@bp_api_v1.route('/update_passage/', methods=['POST']) +@jwt_required() +def update_passage(passage_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + passage = Passage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Passage not found'}), 404 + + data = request.get_json() + title = data.get('title') + content = data.get('content') + image_url = data.get('image_url') + authors = data.get('authors') + is_outlink = data.get('is_outlink') + + if title and (len(title) == 0 or len(title) > 200): + return jsonify({'error': 'Invalid title'}), 400 + if content and len(content) == 0: + return jsonify({'error': 'Invalid content'}), 400 + if image_url and len(image_url) > 500: + return jsonify({'error': 'Image URL is too long'}), 400 + if authors and (len(authors) == 0 or len(authors) > 60): + return jsonify({'error': 'Invalid authors'}), 400 + + if title: + passage.title = title + if content: + passage.content = content + if image_url is not None: + passage.image_url = image_url + if authors: + passage.authors = authors + if is_outlink is not None: + passage.is_outlink = is_outlink + + db.session.commit() + + return jsonify({'message': 'Passage updated successfully'}), 200 + + +@bp_api_v1.route('/delete_passage/', methods=['POST']) +@jwt_required() +def delete_passage(passage_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + passage = Passage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Passage not found'}), 404 + + db.session.delete(passage) + db.session.commit() + + return jsonify({'message': 'Passage deleted successfully'}), 200 + +@bp_api_v1.route('/update_pending_passage/', methods=['POST']) +@jwt_required() +def update_pending_passage(passage_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value and (get_jwt_identity() != PendingPassage.query.get(passage_id).sender_id): + return jsonify({'error': 'Permission denied'}), 403 + + passage = PendingPassage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Pending passage not found'}), 404 + + data = request.get_json() + title = data.get('title') + content = data.get('content') + image_url = data.get('image_url') + authors = data.get('authors') + is_outlink = data.get('is_outlink') + + if title and (len(title) == 0 or len(title) > 200): + return jsonify({'error': 'Invalid title'}), 400 + if content and len(content) == 0: + return jsonify({'error': 'Invalid content'}), 400 + if image_url and len(image_url) > 500: + return jsonify({'error': 'Image URL is too long'}), 400 + if authors and (len(authors) == 0 or len(authors) > 60): + return jsonify({'error': 'Invalid authors'}), 400 + + if title: + passage.title = title + if content: + passage.content = content + if image_url is not None: + passage.image_url = image_url + if authors: + passage.authors = authors + if is_outlink is not None: + passage.is_outlink = is_outlink + + db.session.commit() + + return jsonify({'message': 'Pending passage updated successfully'}), 200 + + +@bp_api_v1.route('/delete_pending_passage/', methods=['POST']) +@jwt_required() +def delete_pending_passage(passage_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value and (get_jwt_identity() != PendingPassage.query.get(passage_id).sender_id): + return jsonify({'error': 'Permission denied'}), 403 + + passage = PendingPassage.query.get(passage_id) + if not passage: + return jsonify({'error': 'Pending passage not found'}), 404 + + db.session.delete(passage) + db.session.commit() + + return jsonify({'message': 'Pending passage deleted successfully'}), 200 + + +@bp_api_v1.route('/create_paper', methods=['POST']) +@jwt_required() +def create_paper(): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + data = request.get_json() + title = data.get('title') + description = data.get('description') + image_url = data.get('image_url') + passage_ids = data.get('passage_ids', []) + + if not title or len(title) == 0 or len(title) > 200: + return jsonify({'error': 'Invalid title'}), 400 + if description and len(description) > 500: + return jsonify({'error': 'Description is too long'}), 400 + if image_url and len(image_url) > 500: + return jsonify({'error': 'Image URL is too long'}), 400 + + relations = [] + for pid in passage_ids: + passage = Passage.query.get(pid) + if passage: + relation = PaperPassagesRelation(paper_id=new_paper.id, passage_id=pid) + relations.append(relation) + passage.is_posted = True + else: + db.session.rollback() + return jsonify({'error': f'Passage with id {pid} not found'}), 400 + + + new_paper = Paper( + title=title, + description=description, + image_url=image_url + ) + db.session.add(new_paper) + db.session.commit() + + for relation in relations: + db.session.add(relation) + db.session.commit() + + return jsonify({'message': 'Paper created successfully'}), 201 + + +@bp_api_v1.route('/delete_paper/', methods=['POST']) +@jwt_required() +def delete_paper(paper_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + paper = Paper.query.get(paper_id) + if not paper: + return jsonify({'error': 'Paper not found'}), 404 + + relations = PaperPassagesRelation.query.filter_by(paper_id=paper_id).all() + for relation in relations: + passage = Passage.query.get(relation.passage_id) + if passage: + if PaperPassagesRelation.query.filter_by(passage_id=passage.id).count() == 1: + passage.is_posted = False + db.session.delete(relation) + + db.session.delete(paper) + db.session.commit() + + return jsonify({'message': 'Paper deleted successfully'}), 200 + + +@bp_api_v1.route('/update_paper/', methods=['POST']) +@jwt_required() +def update_paper(paper_id): + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + paper = Paper.query.get(paper_id) + if not paper: + return jsonify({'error': 'Paper not found'}), 404 + + data = request.get_json() + title = data.get('title') + description = data.get('description') + image_url = data.get('image_url') + passage_ids = data.get('passage_ids') + + if title and (len(title) == 0 or len(title) > 200): + return jsonify({'error': 'Invalid title'}), 400 + if description and len(description) > 500: + return jsonify({'error': 'Description is too long'}), 400 + if image_url and len(image_url) > 500: + return jsonify({'error': 'Image URL is too long'}), 400 + + add_relations = [] + remove_relations = [] + if passage_ids is not None: + existing_relations = PaperPassagesRelation.query.filter_by(paper_id=paper_id).all() + existing_passage_ids = {r.passage_id for r in existing_relations} + new_passage_ids = set(passage_ids) + + for pid in new_passage_ids: + if pid not in existing_passage_ids: + passage = Passage.query.get(pid) + if passage: + add_relations.append(PaperPassagesRelation(paper_id=paper_id, passage_id=pid)) + passage.is_posted = True + else: + db.session.rollback() + return jsonify({'error': f'Passage with id {pid} not found'}), 400 + + for relation in existing_relations: + if relation.passage_id not in new_passage_ids: + remove_relations.append(relation) + passage = Passage.query.get(relation.passage_id) + if passage and PaperPassagesRelation.query.filter_by(passage_id=passage.id).count() == 1: + passage.is_posted = False + + if title: + paper.title = title + if description is not None: + paper.description = description + if image_url is not None: + paper.image_url = image_url + + for relation in add_relations: + db.session.add(relation) + for relation in remove_relations: + db.session.delete(relation) + + db.session.commit() + + return jsonify({'message': 'Paper updated successfully'}), 200 + + +@bp_api_v1.route('/update_profile', methods=['POST']) +@jwt_required() +def update_profile(): + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + if user.role != UserRole.ADMIN.value and user.id != current_user_id: + return jsonify({'error': 'Permission denied'}), 403 + + data = request.get_json() + nickname = data.get('nickname') + phone = data.get('phone') + email = data.get('email') + qq = data.get('qq') + + if nickname is not None: + if len(nickname) > 80: + return jsonify({'error': 'Nickname is too long'}), 400 + user.nickname = nickname + + if phone is not None: + user.phone = phone + if email is not None: + user.email = email + if qq is not None: + user.qq = qq + + db.session.commit() + + return jsonify({'message': 'Profile updated successfully'}), 200 + + +@bp_api_v1.route('/change_password', methods=['POST']) +@jwt_required() +def change_password(): + cleanup_sessions() + current_user_id = get_jwt_identity() + user = User.query.get(current_user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + data = request.get_json() + + session_id = data.get('session_id') + if not session_id or session_id not in sessions: + return jsonify({'error': 'Invalid session_id'}), 400 + + old_password_encrypted = data.get('old_password') + new_password_encrypted = data.get('new_password') + if not old_password_encrypted or not new_password_encrypted: + return jsonify({'error': 'Old and new passwords are required'}), 400 + + private_key = sessions[session_id]['private_key'] + try: + old_password_decrypted = private_key.decrypt(bytes.fromhex(old_password_encrypted)).decode('utf-8') + new_password_decrypted = private_key.decrypt(bytes.fromhex(new_password_encrypted)).decode('utf-8') + except Exception: + return jsonify({'error': 'Failed to decrypt passwords'}), 400 + + if not check_password_hash(user.password, old_password_decrypted): + return jsonify({'error': 'Incorrect old password'}), 401 + + sessions.pop(session_id, None) + user.password = generate_password_hash(new_password_decrypted) + db.session.commit() + + return jsonify({'message': 'Password changed successfully'}), 200 + + +@bp_api_v1.route('/admin/reset_password/', methods=['POST']) +@jwt_required() +def admin_reset_password(user_id): + cleanup_sessions() + role = get_jwt()['role'] + if role != UserRole.ADMIN.value: + return jsonify({'error': 'Admin access required'}), 403 + + user = User.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + + data = request.get_json() + + session_id = data.get('session_id') + if not session_id or session_id not in sessions: + return jsonify({'error': 'Invalid session_id'}), 400 + + new_password_encrypted = data.get('new_password') + if not new_password_encrypted: + return jsonify({'error': 'New password is required'}), 400 + + private_key = sessions[session_id]['private_key'] + try: + new_password_decrypted = private_key.decrypt(bytes.fromhex(new_password_encrypted)).decode('utf-8') + except Exception: + return jsonify({'error': 'Failed to decrypt password'}), 400 + + sessions.pop(session_id, None) + user.password = generate_password_hash(new_password_decrypted) + db.session.commit() + + return jsonify({'message': 'Password reset successfully'}), 200 \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..2e83a1b --- /dev/null +++ b/config.py @@ -0,0 +1,12 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key') + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///ouc_posts.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False + JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'jwt-secret-key') + DEFAULT_OWNER_USERNAME = os.getenv('DEFAULT_OWNER_USERNAME', 'ItStudio') + DEFAULT_OWNER_PASSWORD = os.getenv('DEFAULT_OWNER_PASSWORD', 'ItStudio@520,.123456') \ No newline at end of file diff --git a/models.py b/models.py new file mode 100644 index 0000000..485acd9 --- /dev/null +++ b/models.py @@ -0,0 +1,72 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime +from enum import Enum + +db = SQLAlchemy() + + +class UserRole(Enum): + ADMIN = 'Admin' + USER = 'User' + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), nullable=False, unique=True) + nickname = db.Column(db.String(80), nullable=True) + password = db.Column(db.String(256), nullable=False) + role = db.Column(db.Enum(UserRole), nullable=False, default=UserRole.USER) + created_at = db.Column(db.DateTime, default=datetime.now) + phone = db.Column(db.String(11), nullable=True) + email = db.Column(db.String(120), nullable=True) + qq = db.Column(db.String(10), nullable=True) + + +class PaperPassagesRelation(db.Model): + paper_id = db.Column(db.Integer, db.ForeignKey('paper.id'), nullable=False, primary_key=True) + passage_id = db.Column(db.Integer, db.ForeignKey('passage.id'), nullable=False, primary_key=True) + + +class PendingPassage(db.Model): + # 基本信息 + id = db.Column(db.Integer, primary_key=True) + sender = db.relationship('User', foreign_keys='PendingPassage.sender_id') + sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + + # 内容 + title = db.Column(db.String(200), nullable=False) + content = db.Column(db.Text, nullable=False) + image_url = db.Column(db.String(500), nullable=True) + authors = db.Column(db.String(60), nullable=False) + is_outlink = db.Column(db.Boolean, default=False) + + +class Passage(db.Model): + # 基本信息 + id = db.Column(db.Integer, primary_key=True) + sender = db.relationship('User', foreign_keys='Passage.sender_id') + sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now) + + # 审核 + processed_at = db.Column(db.DateTime, nullable=True) + processor = db.relationship('User', foreign_keys='Passage.processor_id') + processor_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) + is_posted = db.Column(db.Boolean, default=False) + + # 内容 + title = db.Column(db.String(200), nullable=False) + content = db.Column(db.Text, nullable=False) + image_url = db.Column(db.String(500), nullable=True) + authors = db.Column(db.String(60), nullable=False) + is_outlink = db.Column(db.Boolean, default=False) + + +class Paper(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text, nullable=False) + image_url = db.Column(db.String(500), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.now) + passages = db.relationship('Passage', secondary='paper_passages_relation', backref=db.backref('papers', lazy=True)) \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..523d51a --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file