diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..26d33521
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/PythonPlantsVsZombies.iml b/.idea/PythonPlantsVsZombies.iml
new file mode 100644
index 00000000..d0876a78
--- /dev/null
+++ b/.idea/PythonPlantsVsZombies.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..105ce2da
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..2a2c1b9b
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..35c0ec56
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 00000000..9ea1dc3a
--- /dev/null
+++ b/backend/__init__.py
@@ -0,0 +1,6 @@
+# 导入Flask应用和扩展
+from .app import app, db
+from .models import User, Role, Permission, Log, Resource
+
+# 导入创建初始数据的函数
+from .models import create_initial_data
\ No newline at end of file
diff --git a/backend/__pycache__/__init__.cpython-312.pyc b/backend/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..4fc768ea
Binary files /dev/null and b/backend/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc
new file mode 100644
index 00000000..707451d9
Binary files /dev/null and b/backend/__pycache__/app.cpython-312.pyc differ
diff --git a/backend/__pycache__/models.cpython-312.pyc b/backend/__pycache__/models.cpython-312.pyc
new file mode 100644
index 00000000..3e098054
Binary files /dev/null and b/backend/__pycache__/models.cpython-312.pyc differ
diff --git a/backend/app.db b/backend/app.db
new file mode 100644
index 00000000..a80497e4
Binary files /dev/null and b/backend/app.db differ
diff --git a/backend/app.py b/backend/app.py
new file mode 100644
index 00000000..7dad97c7
--- /dev/null
+++ b/backend/app.py
@@ -0,0 +1,44 @@
+from flask import Flask, jsonify
+from flask_sqlalchemy import SQLAlchemy
+from flask_migrate import Migrate
+from flask_jwt_extended import JWTManager
+from flask_cors import CORS
+import os
+
+# 创建Flask应用
+app = Flask(__name__)
+CORS(app)
+
+# 配置应用
+app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'your-secret-key-here'
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'app.db')
+app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
+
+# 配置JWT
+app.config['JWT_SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY') or 'your-jwt-secret-key-here'
+app.config['JWT_ACCESS_TOKEN_EXPIRES'] = 3600 # 1小时
+app.config['JWT_REFRESH_TOKEN_EXPIRES'] = 604800 # 7天
+
+# 初始化扩展
+db = SQLAlchemy(app)
+migrate = Migrate(app, db)
+jwt = JWTManager(app)
+
+# 导入模型
+from .models import User, Role, Permission, Log, Resource
+
+# 导入路由
+from .routes import auth, logs, resources
+
+# 注册蓝图
+app.register_blueprint(auth.bp, url_prefix='/api/auth')
+app.register_blueprint(logs.bp, url_prefix='/api/logs')
+app.register_blueprint(resources.bp, url_prefix='/api/resources')
+
+# 根路由
+@app.route('/')
+def index():
+ return jsonify({'message': 'Welcome to Plants vs Zombies API'})
+
+if __name__ == '__main__':
+ app.run(debug=True)
\ No newline at end of file
diff --git a/backend/migrations/README b/backend/migrations/README
new file mode 100644
index 00000000..0e048441
--- /dev/null
+++ b/backend/migrations/README
@@ -0,0 +1 @@
+Single-database configuration for Flask.
diff --git a/backend/migrations/__pycache__/env.cpython-312.pyc b/backend/migrations/__pycache__/env.cpython-312.pyc
new file mode 100644
index 00000000..2abc36f4
Binary files /dev/null and b/backend/migrations/__pycache__/env.cpython-312.pyc differ
diff --git a/backend/migrations/alembic.ini b/backend/migrations/alembic.ini
new file mode 100644
index 00000000..ec9d45c2
--- /dev/null
+++ b/backend/migrations/alembic.ini
@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/backend/migrations/env.py b/backend/migrations/env.py
new file mode 100644
index 00000000..4c970927
--- /dev/null
+++ b/backend/migrations/env.py
@@ -0,0 +1,113 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+ try:
+ # this works with Flask-SQLAlchemy<3 and Alchemical
+ return current_app.extensions['migrate'].db.get_engine()
+ except (TypeError, AttributeError):
+ # this works with Flask-SQLAlchemy>=3
+ return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+ try:
+ return get_engine().url.render_as_string(hide_password=False).replace(
+ '%', '%%')
+ except AttributeError:
+ return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+ if hasattr(target_db, 'metadatas'):
+ return target_db.metadatas[None]
+ return target_db.metadata
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url, target_metadata=get_metadata(), literal_binds=True
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ conf_args = current_app.extensions['migrate'].configure_args
+ if conf_args.get("process_revision_directives") is None:
+ conf_args["process_revision_directives"] = process_revision_directives
+
+ connectable = get_engine()
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection,
+ target_metadata=get_metadata(),
+ **conf_args
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako
new file mode 100644
index 00000000..2c015630
--- /dev/null
+++ b/backend/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/backend/migrations/versions/0016b73ba5fb_initial_migration.py b/backend/migrations/versions/0016b73ba5fb_initial_migration.py
new file mode 100644
index 00000000..807f0ae6
--- /dev/null
+++ b/backend/migrations/versions/0016b73ba5fb_initial_migration.py
@@ -0,0 +1,102 @@
+"""Initial migration
+
+Revision ID: 0016b73ba5fb
+Revises:
+Create Date: 2025-11-03 19:21:31.818139
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '0016b73ba5fb'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('permission',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=50), nullable=False),
+ sa.Column('description', sa.String(length=255), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('name')
+ )
+ op.create_table('role',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=50), nullable=False),
+ sa.Column('description', sa.String(length=255), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('name')
+ )
+ op.create_table('user',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('username', sa.String(length=80), nullable=False),
+ sa.Column('email', sa.String(length=120), nullable=False),
+ sa.Column('password_hash', sa.String(length=128), nullable=False),
+ sa.Column('is_verified', sa.Boolean(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('email'),
+ sa.UniqueConstraint('username')
+ )
+ op.create_table('log',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('title', sa.String(length=100), nullable=False),
+ sa.Column('content', sa.Text(), nullable=False),
+ sa.Column('tags', sa.String(length=255), nullable=True),
+ sa.Column('importance', sa.Integer(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('resource',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('name', sa.String(length=100), nullable=False),
+ sa.Column('type', sa.String(length=50), nullable=False),
+ sa.Column('file_path', sa.String(length=255), nullable=False),
+ sa.Column('thumbnail_path', sa.String(length=255), nullable=True),
+ sa.Column('description', sa.String(length=255), nullable=True),
+ sa.Column('is_active', sa.Boolean(), nullable=True),
+ sa.Column('created_at', sa.DateTime(), nullable=True),
+ sa.Column('updated_at', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('roles_permissions',
+ sa.Column('role_id', sa.Integer(), nullable=False),
+ sa.Column('permission_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], ),
+ sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
+ sa.PrimaryKeyConstraint('role_id', 'permission_id')
+ )
+ op.create_table('roles_users',
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.Column('role_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('user_id', 'role_id')
+ )
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('roles_users')
+ op.drop_table('roles_permissions')
+ op.drop_table('resource')
+ op.drop_table('log')
+ op.drop_table('user')
+ op.drop_table('role')
+ op.drop_table('permission')
+ # ### end Alembic commands ###
diff --git a/backend/migrations/versions/__pycache__/0016b73ba5fb_initial_migration.cpython-312.pyc b/backend/migrations/versions/__pycache__/0016b73ba5fb_initial_migration.cpython-312.pyc
new file mode 100644
index 00000000..20d2e5af
Binary files /dev/null and b/backend/migrations/versions/__pycache__/0016b73ba5fb_initial_migration.cpython-312.pyc differ
diff --git a/backend/models.py b/backend/models.py
new file mode 100644
index 00000000..d886e52a
--- /dev/null
+++ b/backend/models.py
@@ -0,0 +1,158 @@
+from datetime import datetime
+from .app import db
+from werkzeug.security import generate_password_hash, check_password_hash
+from flask_jwt_extended import create_access_token, create_refresh_token
+
+# 用户角色关联表
+roles_users = db.Table('roles_users',
+ db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
+ db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True)
+)
+
+# 角色权限关联表
+roles_permissions = db.Table('roles_permissions',
+ db.Column('role_id', db.Integer, db.ForeignKey('role.id'), primary_key=True),
+ db.Column('permission_id', db.Integer, db.ForeignKey('permission.id'), primary_key=True)
+)
+
+class User(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(80), unique=True, nullable=False)
+ email = db.Column(db.String(120), unique=True, nullable=False)
+ password_hash = db.Column(db.String(128), nullable=False)
+ is_verified = db.Column(db.Boolean, default=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+
+ # 关联角色
+ roles = db.relationship('Role', secondary=roles_users, lazy='subquery',
+ backref=db.backref('users', lazy=True))
+
+ def __repr__(self):
+ return f''
+
+ def set_password(self, password):
+ self.password_hash = generate_password_hash(password)
+
+ def check_password(self, password):
+ return check_password_hash(self.password_hash, password)
+
+ def get_access_token(self):
+ return create_access_token(identity=self.id)
+
+ def get_refresh_token(self):
+ return create_refresh_token(identity=self.id)
+
+ def has_permission(self, permission_name):
+ for role in self.roles:
+ for permission in role.permissions:
+ if permission.name == permission_name:
+ return True
+ return False
+
+class Role(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(50), unique=True, nullable=False)
+ description = db.Column(db.String(255))
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+
+ # 关联权限
+ permissions = db.relationship('Permission', secondary=roles_permissions, lazy='subquery',
+ backref=db.backref('roles', lazy=True))
+
+ def __repr__(self):
+ return f''
+
+class Permission(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(50), unique=True, nullable=False)
+ description = db.Column(db.String(255))
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+
+ def __repr__(self):
+ return f''
+
+class Log(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+ title = db.Column(db.String(100), nullable=False)
+ content = db.Column(db.Text, nullable=False)
+ tags = db.Column(db.String(255))
+ importance = db.Column(db.Integer, default=1) # 1-5
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+
+ # 关联用户
+ user = db.relationship('User', backref=db.backref('logs', lazy=True))
+
+ def __repr__(self):
+ return f''
+
+class Resource(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
+ name = db.Column(db.String(100), nullable=False)
+ type = db.Column(db.String(50), nullable=False) # skin, model, etc.
+ file_path = db.Column(db.String(255), nullable=False)
+ thumbnail_path = db.Column(db.String(255))
+ description = db.Column(db.String(255))
+ is_active = db.Column(db.Boolean, default=False)
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+
+ # 关联用户
+ user = db.relationship('User', backref=db.backref('resources', lazy=True))
+
+ def __repr__(self):
+ return f''
+
+# 创建初始数据
+def create_initial_data():
+ # 创建权限
+ permissions = [
+ ('log_create', 'Create log'),
+ ('log_read', 'Read log'),
+ ('log_update', 'Update log'),
+ ('log_delete', 'Delete log'),
+ ('resource_upload', 'Upload resource'),
+ ('resource_read', 'Read resource'),
+ ('resource_update', 'Update resource'),
+ ('resource_delete', 'Delete resource'),
+ ('user_manage', 'Manage user'),
+ ('role_manage', 'Manage role'),
+ ]
+
+ for name, description in permissions:
+ permission = Permission.query.filter_by(name=name).first()
+ if not permission:
+ permission = Permission(name=name, description=description)
+ db.session.add(permission)
+
+ # 创建角色
+ admin_role = Role.query.filter_by(name='admin').first()
+ if not admin_role:
+ admin_role = Role(name='admin', description='Administrator role')
+ # 赋予所有权限
+ admin_role.permissions = Permission.query.all()
+ db.session.add(admin_role)
+
+ user_role = Role.query.filter_by(name='user').first()
+ if not user_role:
+ user_role = Role(name='user', description='Regular user role')
+ # 赋予基本权限
+ user_role.permissions = Permission.query.filter_by(name.in_([
+ 'log_create', 'log_read', 'log_update', 'log_delete',
+ 'resource_upload', 'resource_read', 'resource_update', 'resource_delete'
+ ])).all()
+ db.session.add(user_role)
+
+ # 创建管理员用户
+ admin_user = User.query.filter_by(username='admin').first()
+ if not admin_user:
+ admin_user = User(username='admin', email='admin@example.com', is_verified=True)
+ admin_user.set_password('admin123')
+ admin_user.roles.append(admin_role)
+ db.session.add(admin_user)
+
+ db.session.commit()
\ No newline at end of file
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 00000000..3a7bc3bd
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,9 @@
+Flask==2.3.3
+Flask-SQLAlchemy==3.0.5
+Flask-Migrate==4.0.5
+Flask-JWT-Extended==4.5.3
+Flask-CORS==4.0.0
+Werkzeug==2.3.7
+itsdangerous==2.1.2
+Pillow==10.0.0
+python-dotenv==1.0.0
\ No newline at end of file
diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py
new file mode 100644
index 00000000..ece10e7e
--- /dev/null
+++ b/backend/routes/__init__.py
@@ -0,0 +1,4 @@
+# 导入路由蓝图
+from .auth import bp as auth_bp
+from .logs import bp as logs_bp
+from .resources import bp as resources_bp
\ No newline at end of file
diff --git a/backend/routes/__pycache__/__init__.cpython-312.pyc b/backend/routes/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..1f3c90b9
Binary files /dev/null and b/backend/routes/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/routes/__pycache__/auth.cpython-312.pyc b/backend/routes/__pycache__/auth.cpython-312.pyc
new file mode 100644
index 00000000..2041980e
Binary files /dev/null and b/backend/routes/__pycache__/auth.cpython-312.pyc differ
diff --git a/backend/routes/__pycache__/logs.cpython-312.pyc b/backend/routes/__pycache__/logs.cpython-312.pyc
new file mode 100644
index 00000000..551e1b32
Binary files /dev/null and b/backend/routes/__pycache__/logs.cpython-312.pyc differ
diff --git a/backend/routes/__pycache__/resources.cpython-312.pyc b/backend/routes/__pycache__/resources.cpython-312.pyc
new file mode 100644
index 00000000..83fbc294
Binary files /dev/null and b/backend/routes/__pycache__/resources.cpython-312.pyc differ
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
new file mode 100644
index 00000000..855637d3
--- /dev/null
+++ b/backend/routes/auth.py
@@ -0,0 +1,224 @@
+from flask import Blueprint, request, jsonify
+from backend.app import db
+from backend.models import User, Role
+from flask_jwt_extended import jwt_required, get_jwt_identity
+from werkzeug.security import generate_password_hash, check_password_hash
+from itsdangerous import URLSafeTimedSerializer
+import smtplib
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+import os
+
+bp = Blueprint('auth', __name__)
+
+# 配置邮箱
+MAIL_SERVER = os.environ.get('MAIL_SERVER') or 'smtp.gmail.com'
+MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587)
+MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') or True
+MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'your-email@gmail.com'
+MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'your-email-password'
+MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER') or 'your-email@gmail.com'
+
+# 创建序列化器
+serializer = URLSafeTimedSerializer(os.environ.get('SECRET_KEY') or 'your-secret-key-here')
+
+@bp.route('/register', methods=['POST'])
+def register():
+ data = request.get_json()
+
+ if not data or not data.get('username') or not data.get('email') or not data.get('password'):
+ return jsonify({'message': 'Missing required fields'}), 400
+
+ # 检查用户名是否已存在
+ if User.query.filter_by(username=data.get('username')).first():
+ return jsonify({'message': 'Username already exists'}), 409
+
+ # 检查邮箱是否已存在
+ if User.query.filter_by(email=data.get('email')).first():
+ return jsonify({'message': 'Email already exists'}), 409
+
+ # 创建新用户
+ user = User(
+ username=data.get('username'),
+ email=data.get('email')
+ )
+ user.set_password(data.get('password'))
+
+ # 赋予默认角色
+ default_role = Role.query.filter_by(name='user').first()
+ if default_role:
+ user.roles.append(default_role)
+
+ db.session.add(user)
+ db.session.commit()
+
+ # 发送验证邮件
+ send_verification_email(user)
+
+ return jsonify({'message': 'User created successfully. Please check your email to verify your account.'}), 201
+
+@bp.route('/verify/', methods=['GET'])
+def verify_email(token):
+ try:
+ email = serializer.loads(token, salt='email-verification-salt', max_age=3600) # 1小时有效期
+ except:
+ return jsonify({'message': 'Invalid or expired token'}), 400
+
+ user = User.query.filter_by(email=email).first()
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if user.is_verified:
+ return jsonify({'message': 'Email already verified'}), 400
+
+ user.is_verified = True
+ db.session.commit()
+
+ return jsonify({'message': 'Email verified successfully'}), 200
+
+@bp.route('/login', methods=['POST'])
+def login():
+ data = request.get_json()
+
+ if not data or not data.get('email') or not data.get('password'):
+ return jsonify({'message': 'Missing required fields'}), 400
+
+ user = User.query.filter_by(email=data.get('email')).first()
+ if not user or not user.check_password(data.get('password')):
+ return jsonify({'message': 'Invalid email or password'}), 401
+
+ if not user.is_verified:
+ return jsonify({'message': 'Please verify your email first'}), 403
+
+ # 创建访问令牌和刷新令牌
+ access_token = user.get_access_token()
+ refresh_token = user.get_refresh_token()
+
+ return jsonify({
+ 'access_token': access_token,
+ 'refresh_token': refresh_token,
+ 'user': {
+ 'id': user.id,
+ 'username': user.username,
+ 'email': user.email,
+ 'roles': [role.name for role in user.roles]
+ }
+ }), 200
+
+@bp.route('/refresh', methods=['POST'])
+@jwt_required(refresh=True)
+def refresh():
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ access_token = user.get_access_token()
+
+ return jsonify({'access_token': access_token}), 200
+
+@bp.route('/forgot-password', methods=['POST'])
+def forgot_password():
+ data = request.get_json()
+
+ if not data or not data.get('email'):
+ return jsonify({'message': 'Missing required fields'}), 400
+
+ user = User.query.filter_by(email=data.get('email')).first()
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ # 发送重置密码邮件
+ send_password_reset_email(user)
+
+ return jsonify({'message': 'Password reset email sent. Please check your email.'}), 200
+
+@bp.route('/reset-password/', methods=['POST'])
+def reset_password(token):
+ try:
+ email = serializer.loads(token, salt='password-reset-salt', max_age=3600) # 1小时有效期
+ except:
+ return jsonify({'message': 'Invalid or expired token'}), 400
+
+ user = User.query.filter_by(email=email).first()
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ data = request.get_json()
+ if not data or not data.get('password') or not data.get('confirm_password'):
+ return jsonify({'message': 'Missing required fields'}), 400
+
+ if data.get('password') != data.get('confirm_password'):
+ return jsonify({'message': 'Passwords do not match'}), 400
+
+ user.set_password(data.get('password'))
+ db.session.commit()
+
+ return jsonify({'message': 'Password reset successfully'}), 200
+
+@bp.route('/me', methods=['GET'])
+@jwt_required()
+def get_current_user():
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ return jsonify({
+ 'id': user.id,
+ 'username': user.username,
+ 'email': user.email,
+ 'is_verified': user.is_verified,
+ 'roles': [role.name for role in user.roles],
+ 'created_at': user.created_at,
+ 'updated_at': user.updated_at
+ }), 200
+
+# 发送验证邮件
+def send_verification_email(user):
+ token = serializer.dumps(user.email, salt='email-verification-salt')
+ verification_url = f'http://localhost:5000/api/auth/verify/{token}'
+
+ subject = 'Verify your email for Plants vs Zombies'
+ body = f'Hi {user.username},\n\nPlease click the link below to verify your email:\n{verification_url}\n\nThis link will expire in 1 hour.\n\nThanks,\nThe Plants vs Zombies Team'
+
+ send_email(user.email, subject, body)
+
+# 发送重置密码邮件
+def send_password_reset_email(user):
+ token = serializer.dumps(user.email, salt='password-reset-salt')
+ reset_url = f'http://localhost:5000/api/auth/reset-password/{token}'
+
+ subject = 'Reset your password for Plants vs Zombies'
+ body = f"Hi {user.username},\n\nYou requested a password reset. Please click the link below to reset your password:\n{reset_url}\n\nThis link will expire in 1 hour.\n\nIf you didn't request this, you can safely ignore this email.\n\nThanks,\nThe Plants vs Zombies Team"
+
+ send_email(user.email, subject, body)
+
+# 发送邮件
+def send_email(to, subject, body):
+ try:
+ # 创建邮件消息
+ msg = MIMEMultipart()
+ msg['From'] = MAIL_DEFAULT_SENDER
+ msg['To'] = to
+ msg['Subject'] = subject
+
+ # 添加邮件正文
+ msg.attach(MIMEText(body, 'plain'))
+
+ # 连接到SMTP服务器
+ server = smtplib.SMTP(MAIL_SERVER, MAIL_PORT)
+ server.starttls()
+ server.login(MAIL_USERNAME, MAIL_PASSWORD)
+
+ # 发送邮件
+ server.send_message(msg)
+
+ # 关闭连接
+ server.quit()
+ except Exception as e:
+ print(f'Error sending email: {str(e)}')
+ # 在这里可以添加日志记录
+ pass
\ No newline at end of file
diff --git a/backend/routes/logs.py b/backend/routes/logs.py
new file mode 100644
index 00000000..11ff54ab
--- /dev/null
+++ b/backend/routes/logs.py
@@ -0,0 +1,221 @@
+from flask import Blueprint, request, jsonify
+from backend.app import db
+from backend.models import Log, User
+from flask_jwt_extended import jwt_required, get_jwt_identity
+from datetime import datetime
+
+bp = Blueprint('logs', __name__)
+
+@bp.route('/logs', methods=['POST'])
+@jwt_required()
+def create_log():
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('log_create'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ data = request.get_json()
+
+ if not data or not data.get('title') or not data.get('content'):
+ return jsonify({'message': 'Missing required fields'}), 400
+
+ # 创建新日志
+ log = Log(
+ user_id=current_user_id,
+ title=data.get('title'),
+ content=data.get('content'),
+ tags=data.get('tags'),
+ importance=data.get('importance', 1)
+ )
+
+ db.session.add(log)
+ db.session.commit()
+
+ return jsonify({
+ 'message': 'Log created successfully',
+ 'log': {
+ 'id': log.id,
+ 'title': log.title,
+ 'content': log.content,
+ 'tags': log.tags,
+ 'importance': log.importance,
+ 'created_at': log.created_at,
+ 'updated_at': log.updated_at
+ }
+ }), 201
+
+@bp.route('/logs', methods=['GET'])
+@jwt_required()
+def get_logs():
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('log_read'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ # 获取查询参数
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 10, type=int)
+ sort_by = request.args.get('sort_by', 'created_at')
+ sort_order = request.args.get('sort_order', 'desc')
+ tags = request.args.get('tags')
+ importance = request.args.get('importance', type=int)
+ start_date = request.args.get('start_date')
+ end_date = request.args.get('end_date')
+
+ # 构建查询
+ query = Log.query.filter_by(user_id=current_user_id)
+
+ # 过滤标签
+ if tags:
+ tag_list = tags.split(',')
+ for tag in tag_list:
+ query = query.filter(Log.tags.like(f'%{tag}%'))
+
+ # 过滤重要程度
+ if importance:
+ query = query.filter_by(importance=importance)
+
+ # 过滤日期范围
+ if start_date:
+ try:
+ start = datetime.strptime(start_date, '%Y-%m-%d')
+ query = query.filter(Log.created_at >= start)
+ except ValueError:
+ return jsonify({'message': 'Invalid start date format. Use YYYY-MM-DD'}), 400
+
+ if end_date:
+ try:
+ end = datetime.strptime(end_date, '%Y-%m-%d')
+ query = query.filter(Log.created_at <= end)
+ except ValueError:
+ return jsonify({'message': 'Invalid end date format. Use YYYY-MM-DD'}), 400
+
+ # 排序
+ if sort_order == 'desc':
+ query = query.order_by(getattr(Log, sort_by).desc())
+ else:
+ query = query.order_by(getattr(Log, sort_by).asc())
+
+ # 分页
+ paginated_logs = query.paginate(page=page, per_page=per_page, error_out=False)
+
+ # 格式化结果
+ logs = []
+ for log in paginated_logs.items:
+ logs.append({
+ 'id': log.id,
+ 'title': log.title,
+ 'content': log.content,
+ 'tags': log.tags,
+ 'importance': log.importance,
+ 'created_at': log.created_at,
+ 'updated_at': log.updated_at
+ })
+
+ return jsonify({
+ 'logs': logs,
+ 'total': paginated_logs.total,
+ 'pages': paginated_logs.pages,
+ 'current_page': paginated_logs.page
+ }), 200
+
+@bp.route('/logs/', methods=['GET'])
+@jwt_required()
+def get_log(log_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('log_read'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ log = Log.query.filter_by(id=log_id, user_id=current_user_id).first()
+
+ if not log:
+ return jsonify({'message': 'Log not found'}), 404
+
+ return jsonify({
+ 'id': log.id,
+ 'title': log.title,
+ 'content': log.content,
+ 'tags': log.tags,
+ 'importance': log.importance,
+ 'created_at': log.created_at,
+ 'updated_at': log.updated_at
+ }), 200
+
+@bp.route('/logs/', methods=['PUT'])
+@jwt_required()
+def update_log(log_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('log_update'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ log = Log.query.filter_by(id=log_id, user_id=current_user_id).first()
+
+ if not log:
+ return jsonify({'message': 'Log not found'}), 404
+
+ data = request.get_json()
+
+ # 更新日志信息
+ if data.get('title'):
+ log.title = data.get('title')
+ if data.get('content'):
+ log.content = data.get('content')
+ if data.get('tags') is not None:
+ log.tags = data.get('tags')
+ if data.get('importance') is not None:
+ log.importance = data.get('importance')
+
+ db.session.commit()
+
+ return jsonify({
+ 'message': 'Log updated successfully',
+ 'log': {
+ 'id': log.id,
+ 'title': log.title,
+ 'content': log.content,
+ 'tags': log.tags,
+ 'importance': log.importance,
+ 'created_at': log.created_at,
+ 'updated_at': log.updated_at
+ }
+ }), 200
+
+@bp.route('/logs/', methods=['DELETE'])
+@jwt_required()
+def delete_log(log_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('log_delete'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ log = Log.query.filter_by(id=log_id, user_id=current_user_id).first()
+
+ if not log:
+ return jsonify({'message': 'Log not found'}), 404
+
+ db.session.delete(log)
+ db.session.commit()
+
+ return jsonify({'message': 'Log deleted successfully'}), 200
\ No newline at end of file
diff --git a/backend/routes/resources.py b/backend/routes/resources.py
new file mode 100644
index 00000000..ac0e0013
--- /dev/null
+++ b/backend/routes/resources.py
@@ -0,0 +1,330 @@
+from flask import Blueprint, request, jsonify, send_from_directory
+from backend.app import db
+from backend.models import Resource, User
+from flask_jwt_extended import jwt_required, get_jwt_identity
+import os
+from werkzeug.utils import secure_filename
+from PIL import Image
+
+bp = Blueprint('resources', __name__)
+
+# 配置上传设置
+UPLOAD_FOLDER = os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'uploads')
+THUMBNAIL_FOLDER = os.path.join(UPLOAD_FOLDER, 'thumbnails')
+ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'obj', 'fbx', 'glb', 'gltf'}
+MAX_CONTENT_LENGTH = 100 * 1024 * 1024 # 100MB
+
+# 创建上传目录
+if not os.path.exists(UPLOAD_FOLDER):
+ os.makedirs(UPLOAD_FOLDER)
+if not os.path.exists(THUMBNAIL_FOLDER):
+ os.makedirs(THUMBNAIL_FOLDER)
+
+# 检查文件扩展名是否允许
+def allowed_file(filename):
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+
+# 生成缩略图
+def generate_thumbnail(image_path, thumbnail_path, size=(150, 150)):
+ try:
+ with Image.open(image_path) as img:
+ img.thumbnail(size)
+ img.save(thumbnail_path)
+ return True
+ except Exception as e:
+ print(f'Error generating thumbnail: {str(e)}')
+ return False
+
+@bp.route('/resources', methods=['POST'])
+@jwt_required()
+def upload_resource():
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('resource_upload'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ # 检查请求中是否包含文件部分
+ if 'file' not in request.files:
+ return jsonify({'message': 'No file part'}), 400
+
+ file = request.files['file']
+
+ # 如果用户没有选择文件,浏览器会提交一个空文件
+ if file.filename == '':
+ return jsonify({'message': 'No selected file'}), 400
+
+ # 检查文件类型
+ if not allowed_file(file.filename):
+ return jsonify({'message': 'File type not allowed'}), 400
+
+ # 获取其他表单数据
+ name = request.form.get('name', file.filename.rsplit('.', 1)[0])
+ type = request.form.get('type', 'other')
+ description = request.form.get('description', '')
+
+ # 安全地保存文件名
+ filename = secure_filename(file.filename)
+
+ # 为每个用户创建单独的目录
+ user_upload_folder = os.path.join(UPLOAD_FOLDER, str(current_user_id))
+ if not os.path.exists(user_upload_folder):
+ os.makedirs(user_upload_folder)
+
+ # 保存文件
+ file_path = os.path.join(user_upload_folder, filename)
+ file.save(file_path)
+
+ # 生成缩略图(仅对图片文件)
+ thumbnail_path = None
+ if filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'bmp'}:
+ thumbnail_filename = f'thumbnail_{filename}'
+ thumbnail_path = os.path.join(THUMBNAIL_FOLDER, str(current_user_id), thumbnail_filename)
+
+ # 为每个用户创建单独的缩略图目录
+ user_thumbnail_folder = os.path.join(THUMBNAIL_FOLDER, str(current_user_id))
+ if not os.path.exists(user_thumbnail_folder):
+ os.makedirs(user_thumbnail_folder)
+
+ # 生成缩略图
+ generate_thumbnail(file_path, thumbnail_path)
+
+ # 创建资源记录
+ resource = Resource(
+ user_id=current_user_id,
+ name=name,
+ type=type,
+ file_path=file_path,
+ thumbnail_path=thumbnail_path,
+ description=description
+ )
+
+ db.session.add(resource)
+ db.session.commit()
+
+ return jsonify({
+ 'message': 'Resource uploaded successfully',
+ 'resource': {
+ 'id': resource.id,
+ 'name': resource.name,
+ 'type': resource.type,
+ 'description': resource.description,
+ 'is_active': resource.is_active,
+ 'created_at': resource.created_at,
+ 'updated_at': resource.updated_at
+ }
+ }), 201
+
+@bp.route('/resources', methods=['GET'])
+@jwt_required()
+def get_resources():
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('resource_read'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ # 获取查询参数
+ page = request.args.get('page', 1, type=int)
+ per_page = request.args.get('per_page', 10, type=int)
+ sort_by = request.args.get('sort_by', 'created_at')
+ sort_order = request.args.get('sort_order', 'desc')
+ type = request.args.get('type')
+ is_active = request.args.get('is_active', type=bool)
+
+ # 构建查询
+ query = Resource.query.filter_by(user_id=current_user_id)
+
+ # 过滤类型
+ if type:
+ query = query.filter_by(type=type)
+
+ # 过滤激活状态
+ if is_active is not None:
+ query = query.filter_by(is_active=is_active)
+
+ # 排序
+ if sort_order == 'desc':
+ query = query.order_by(getattr(Resource, sort_by).desc())
+ else:
+ query = query.order_by(getattr(Resource, sort_by).asc())
+
+ # 分页
+ paginated_resources = query.paginate(page=page, per_page=per_page, error_out=False)
+
+ # 格式化结果
+ resources = []
+ for resource in paginated_resources.items:
+ resources.append({
+ 'id': resource.id,
+ 'name': resource.name,
+ 'type': resource.type,
+ 'description': resource.description,
+ 'is_active': resource.is_active,
+ 'created_at': resource.created_at,
+ 'updated_at': resource.updated_at
+ })
+
+ return jsonify({
+ 'resources': resources,
+ 'total': paginated_resources.total,
+ 'pages': paginated_resources.pages,
+ 'current_page': paginated_resources.page
+ }), 200
+
+@bp.route('/resources/', methods=['GET'])
+@jwt_required()
+def get_resource(resource_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('resource_read'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first()
+
+ if not resource:
+ return jsonify({'message': 'Resource not found'}), 404
+
+ return jsonify({
+ 'id': resource.id,
+ 'name': resource.name,
+ 'type': resource.type,
+ 'description': resource.description,
+ 'is_active': resource.is_active,
+ 'created_at': resource.created_at,
+ 'updated_at': resource.updated_at
+ }), 200
+
+@bp.route('/resources/', methods=['PUT'])
+@jwt_required()
+def update_resource(resource_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('resource_update'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first()
+
+ if not resource:
+ return jsonify({'message': 'Resource not found'}), 404
+
+ data = request.form
+
+ # 更新资源信息
+ if data.get('name'):
+ resource.name = data.get('name')
+ if data.get('type'):
+ resource.type = data.get('type')
+ if data.get('description') is not None:
+ resource.description = data.get('description')
+ if data.get('is_active') is not None:
+ resource.is_active = data.get('is_active') == 'true' or data.get('is_active') == True
+
+ # 检查是否有新文件上传
+ if 'file' in request.files:
+ file = request.files['file']
+ if file.filename != '' and allowed_file(file.filename):
+ # 删除旧文件
+ if os.path.exists(resource.file_path):
+ os.remove(resource.file_path)
+
+ # 删除旧缩略图
+ if resource.thumbnail_path and os.path.exists(resource.thumbnail_path):
+ os.remove(resource.thumbnail_path)
+
+ # 保存新文件
+ filename = secure_filename(file.filename)
+ user_upload_folder = os.path.join(UPLOAD_FOLDER, str(current_user_id))
+ file_path = os.path.join(user_upload_folder, filename)
+ file.save(file_path)
+ resource.file_path = file_path
+
+ # 生成新的缩略图(仅对图片文件)
+ if filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif', 'bmp'}:
+ thumbnail_filename = f'thumbnail_{filename}'
+ thumbnail_path = os.path.join(THUMBNAIL_FOLDER, str(current_user_id), thumbnail_filename)
+ generate_thumbnail(file_path, thumbnail_path)
+ resource.thumbnail_path = thumbnail_path
+ else:
+ resource.thumbnail_path = None
+
+ db.session.commit()
+
+ return jsonify({
+ 'message': 'Resource updated successfully',
+ 'resource': {
+ 'id': resource.id,
+ 'name': resource.name,
+ 'type': resource.type,
+ 'description': resource.description,
+ 'is_active': resource.is_active,
+ 'created_at': resource.created_at,
+ 'updated_at': resource.updated_at
+ }
+ }), 200
+
+@bp.route('/resources/', methods=['DELETE'])
+@jwt_required()
+def delete_resource(resource_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('resource_delete'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first()
+
+ if not resource:
+ return jsonify({'message': 'Resource not found'}), 404
+
+ # 删除文件
+ if os.path.exists(resource.file_path):
+ os.remove(resource.file_path)
+
+ # 删除缩略图
+ if resource.thumbnail_path and os.path.exists(resource.thumbnail_path):
+ os.remove(resource.thumbnail_path)
+
+ db.session.delete(resource)
+ db.session.commit()
+
+ return jsonify({'message': 'Resource deleted successfully'}), 200
+
+@bp.route('/resources//download', methods=['GET'])
+@jwt_required()
+def download_resource(resource_id):
+ current_user_id = get_jwt_identity()
+ user = User.query.get(current_user_id)
+
+ if not user:
+ return jsonify({'message': 'User not found'}), 404
+
+ if not user.has_permission('resource_read'):
+ return jsonify({'message': 'Permission denied'}), 403
+
+ resource = Resource.query.filter_by(id=resource_id, user_id=current_user_id).first()
+
+ if not resource:
+ return jsonify({'message': 'Resource not found'}), 404
+
+ if not os.path.exists(resource.file_path):
+ return jsonify({'message': 'File not found'}), 404
+
+ return send_from_directory(os.path.dirname(resource.file_path), os.path.basename(resource.file_path), as_attachment=True)
\ No newline at end of file
diff --git a/ke b/ke
new file mode 100644
index 00000000..e69de29b
diff --git a/source/__pycache__/__init__.cpython-312.pyc b/source/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..73ec3c53
Binary files /dev/null and b/source/__pycache__/__init__.cpython-312.pyc differ
diff --git a/source/__pycache__/constants.cpython-312.pyc b/source/__pycache__/constants.cpython-312.pyc
new file mode 100644
index 00000000..6fcfa450
Binary files /dev/null and b/source/__pycache__/constants.cpython-312.pyc differ
diff --git a/source/__pycache__/main.cpython-312.pyc b/source/__pycache__/main.cpython-312.pyc
new file mode 100644
index 00000000..b84b6c22
Binary files /dev/null and b/source/__pycache__/main.cpython-312.pyc differ
diff --git a/source/__pycache__/tool.cpython-312.pyc b/source/__pycache__/tool.cpython-312.pyc
new file mode 100644
index 00000000..69e5afe0
Binary files /dev/null and b/source/__pycache__/tool.cpython-312.pyc differ
diff --git a/source/component/__pycache__/__init__.cpython-312.pyc b/source/component/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..1f9988a3
Binary files /dev/null and b/source/component/__pycache__/__init__.cpython-312.pyc differ
diff --git a/source/component/__pycache__/map.cpython-312.pyc b/source/component/__pycache__/map.cpython-312.pyc
new file mode 100644
index 00000000..92b1ac8f
Binary files /dev/null and b/source/component/__pycache__/map.cpython-312.pyc differ
diff --git a/source/component/__pycache__/menubar.cpython-312.pyc b/source/component/__pycache__/menubar.cpython-312.pyc
new file mode 100644
index 00000000..51a487c5
Binary files /dev/null and b/source/component/__pycache__/menubar.cpython-312.pyc differ
diff --git a/source/component/__pycache__/plant.cpython-312.pyc b/source/component/__pycache__/plant.cpython-312.pyc
new file mode 100644
index 00000000..154eaabf
Binary files /dev/null and b/source/component/__pycache__/plant.cpython-312.pyc differ
diff --git a/source/component/__pycache__/zombie.cpython-312.pyc b/source/component/__pycache__/zombie.cpython-312.pyc
new file mode 100644
index 00000000..73ff2b1c
Binary files /dev/null and b/source/component/__pycache__/zombie.cpython-312.pyc differ
diff --git a/source/state/__pycache__/__init__.cpython-312.pyc b/source/state/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 00000000..58f7eae1
Binary files /dev/null and b/source/state/__pycache__/__init__.cpython-312.pyc differ
diff --git a/source/state/__pycache__/level.cpython-312.pyc b/source/state/__pycache__/level.cpython-312.pyc
new file mode 100644
index 00000000..2e9dd927
Binary files /dev/null and b/source/state/__pycache__/level.cpython-312.pyc differ
diff --git a/source/state/__pycache__/mainmenu.cpython-312.pyc b/source/state/__pycache__/mainmenu.cpython-312.pyc
new file mode 100644
index 00000000..a64d3d7a
Binary files /dev/null and b/source/state/__pycache__/mainmenu.cpython-312.pyc differ
diff --git a/source/state/__pycache__/screen.cpython-312.pyc b/source/state/__pycache__/screen.cpython-312.pyc
new file mode 100644
index 00000000..ce3bd80d
Binary files /dev/null and b/source/state/__pycache__/screen.cpython-312.pyc differ