from flask import Flask, jsonify, send_from_directory, request from flask_cors import CORS import os import random from db import ( init_storage, get_profile, get_projects, get_contacts, get_techstack, get_all, update_profile, create_project, update_project, delete_project, create_contact, update_contact, delete_contact, set_techstack_title, create_techstack_item, update_techstack_item, delete_techstack_item, ) # 检测运行模式:通过环境变量控制 RUN_MODE = os.environ.get('RUN_MODE', 'development') # development 或 production # 数据文件路径 - 支持环境变量配置(需要先定义,因为后面会用到) DATA_DIR = os.environ.get('DATA_DIR', os.path.join(os.path.dirname(__file__), 'data')) # 根据运行模式配置 # 检查是否有前端构建文件(前后端分离时可能没有) FRONTEND_BUILD_PATH = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'build') HAS_FRONTEND_BUILD = os.path.exists(FRONTEND_BUILD_PATH) and os.path.isdir(FRONTEND_BUILD_PATH) # 背景图片目录 - 固定使用数据目录中的 background 文件夹 # 支持通过环境变量配置,默认在数据目录中 BACKGROUND_DIR = os.environ.get('BACKGROUND_DIR', os.path.join(DATA_DIR, 'background')) ADMIN_TOKEN = os.environ.get('ADMIN_TOKEN', 'shumengya520') if RUN_MODE == 'production' and HAS_FRONTEND_BUILD: # 生产环境:使用构建后的前端(如果存在) app = Flask(__name__, static_folder=FRONTEND_BUILD_PATH, static_url_path='') else: # 开发环境或纯后端模式:只提供 API app = Flask(__name__) CORS(app, allow_headers=['Content-Type', 'X-Admin-Token']) init_storage() def require_admin(): if request.headers.get('X-Admin-Token', '') != ADMIN_TOKEN: return jsonify({'error': 'unauthorized'}), 401 return None @app.route('/api/profile', methods=['GET']) def get_profile_route(): """获取个人基本信息""" data = get_profile() if data: return jsonify(data) return jsonify({"error": "Profile没有找到"}), 404 @app.route('/api/projects', methods=['GET']) def get_projects_route(): """获取全部项目列表""" return jsonify(get_projects()) @app.route('/api/contacts', methods=['GET']) def get_contacts_route(): """获取联系方式""" return jsonify(get_contacts()) @app.route('/api/techstack', methods=['GET']) def get_techstack_route(): """获取技术栈""" return jsonify(get_techstack()) @app.route('/api/logo/', methods=['GET']) def get_logo(filename): """提供技术栈图标文件""" logo_dir = os.path.join(DATA_DIR, 'logo') try: # 安全检查:防止路径遍历攻击 if '..' in filename or '/' in filename or '\\' in filename: return jsonify({"error": "无效的文件名"}), 400 # 检查文件是否存在 file_path = os.path.join(logo_dir, filename) if not os.path.exists(file_path): print(f"图标文件不存在: {file_path}") return jsonify({"error": f"图标文件未找到: {filename}"}), 404 # 检查目录是否存在 if not os.path.exists(logo_dir): print(f"图标目录不存在: {logo_dir}") return jsonify({"error": "图标目录未找到"}), 404 return send_from_directory(logo_dir, filename) except Exception as e: print(f"获取图标文件出错: {e}") print(f"尝试访问的文件: {os.path.join(logo_dir, filename)}") return jsonify({"error": f"图标文件未找到: {filename}"}), 404 @app.route('/api/random-background', methods=['GET']) def get_random_background(): """获取随机背景图片""" try: # 获取背景图片目录中的所有图片 if os.path.exists(BACKGROUND_DIR) and os.path.isdir(BACKGROUND_DIR): images = [f for f in os.listdir(BACKGROUND_DIR) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif'))] if images: random_image = random.choice(images) # 返回完整的 API 路径 return jsonify({"image": f"/api/background/{random_image}"}) else: print(f"背景图片目录不存在: {BACKGROUND_DIR}") return jsonify({"image": None}) except Exception as e: print(f"获取随机背景出错: {e}") print(f"背景目录路径: {BACKGROUND_DIR}") import traceback traceback.print_exc() return jsonify({"image": None}) @app.route('/api/background/', methods=['GET']) def get_background_image(filename): """提供背景图片文件""" try: # 安全检查:防止路径遍历攻击 if '..' in filename or '/' in filename or '\\' in filename: return jsonify({"error": "无效的文件名"}), 400 # 检查目录是否存在 if not os.path.exists(BACKGROUND_DIR): print(f"背景图片目录不存在: {BACKGROUND_DIR}") return jsonify({"error": "背景图片目录未找到"}), 404 # 检查文件是否存在 file_path = os.path.join(BACKGROUND_DIR, filename) if not os.path.exists(file_path): print(f"背景图片文件不存在: {file_path}") return jsonify({"error": f"背景图片未找到: {filename}"}), 404 return send_from_directory(BACKGROUND_DIR, filename) except Exception as e: print(f"获取背景图片出错: {e}") return jsonify({"error": f"背景图片未找到: {filename}"}), 404 @app.route('/api/all', methods=['GET']) def get_all_route(): """获取聚合站点数据;带管理员令牌时可列出隐藏的联系方式。""" admin_ok = request.headers.get('X-Admin-Token', '') == ADMIN_TOKEN return jsonify(get_all(include_hidden_contacts=admin_ok)) @app.route('/api/admin/ping', methods=['GET']) def admin_ping(): err = require_admin() if err: return err return ('', 204) @app.route('/api/admin/profile', methods=['PUT']) def admin_put_profile(): err = require_admin() if err: return err body = request.get_json(silent=True) or {} update_profile(body) return jsonify(get_profile()) @app.route('/api/admin/projects', methods=['POST']) def admin_post_project(): err = require_admin() if err: return err body = request.get_json(silent=True) or {} if not body.get('title'): return jsonify({'error': 'title required'}), 400 pid = create_project(body) return jsonify({'id': pid}), 201 @app.route('/api/admin/projects/', methods=['PUT']) def admin_put_project(pid): err = require_admin() if err: return err body = request.get_json(silent=True) or {} update_project(pid, body) return jsonify({'ok': True}) @app.route('/api/admin/projects/', methods=['DELETE']) def admin_delete_project(pid): err = require_admin() if err: return err delete_project(pid) return jsonify({'ok': True}) @app.route('/api/admin/contacts', methods=['POST']) def admin_post_contact(): err = require_admin() if err: return err body = request.get_json(silent=True) or {} if not body.get('type'): return jsonify({'error': 'type required'}), 400 cid = create_contact(body) return jsonify({'id': cid}), 201 @app.route('/api/admin/contacts/', methods=['PUT']) def admin_put_contact(cid): err = require_admin() if err: return err body = request.get_json(silent=True) or {} update_contact(cid, body) return jsonify({'ok': True}) @app.route('/api/admin/contacts/', methods=['DELETE']) def admin_delete_contact(cid): err = require_admin() if err: return err delete_contact(cid) return jsonify({'ok': True}) @app.route('/api/admin/techstack/title', methods=['PUT']) def admin_put_techstack_title(): err = require_admin() if err: return err body = request.get_json(silent=True) or {} set_techstack_title(body.get('title', '')) return jsonify({'ok': True}) @app.route('/api/admin/techstack/items', methods=['POST']) def admin_post_tech_item(): err = require_admin() if err: return err body = request.get_json(silent=True) or {} if not body.get('name'): return jsonify({'error': 'name required'}), 400 iid = create_techstack_item(body) return jsonify({'id': iid}), 201 @app.route('/api/admin/techstack/items/', methods=['PUT']) def admin_put_tech_item(iid): err = require_admin() if err: return err body = request.get_json(silent=True) or {} update_techstack_item(iid, body) return jsonify({'ok': True}) @app.route('/api/admin/techstack/items/', methods=['DELETE']) def admin_delete_tech_item(iid): err = require_admin() if err: return err delete_techstack_item(iid) return jsonify({'ok': True}) @app.route('/', methods=['GET']) def index(): """服务前端页面或API信息""" if RUN_MODE == 'production' and app.static_folder and os.path.exists(os.path.join(app.static_folder, 'index.html')): # 生产环境,返回前端页面(如果存在) try: return send_from_directory(app.static_folder, 'index.html') except: pass # 返回API信息 return jsonify({ "message": "萌芽主页 后端API", "author": "树萌芽", "version": "1.0.0", "mode": RUN_MODE, "note": "这是一个纯后端API服务,前端请访问独立的前端应用", "api_base": "https://nav.api.shumengya.top/api", "storage": "SQLite (site.db)", "endpoints": { "/api/profile": "获取个人信息", "/api/techstack": "获取技术栈", "/api/projects": "获取项目列表", "/api/contacts": "获取联系方式", "/api/random-background": "获取随机背景图片", "/api/all": "获取所有数据" } }) @app.route('/admin') def admin(): """服务管理员页面(也是前端)""" if RUN_MODE == 'production' and app.static_folder and os.path.exists(os.path.join(app.static_folder, 'index.html')): try: return send_from_directory(app.static_folder, 'index.html') except: pass return jsonify({ "error": "管理员页面未找到", "note": "这是一个纯后端API服务,请访问独立的前端应用", "api_base": "https://nav.api.shumengya.top/api" }), 404 @app.route('/api') def api_info(): """API信息""" return jsonify({ "message": "萌芽主页 后端API", "author":"树萌芽", "version": "1.0.0", "storage": "SQLite (site.db)", "endpoints": { "/api/profile": "获取个人信息", "/api/techstack": "获取技术栈", "/api/projects": "获取项目列表", "/api/contacts": "获取联系方式", "/api/random-background": "获取随机背景图片", "/api/all": "获取所有数据" } }) # 处理404错误 @app.errorhandler(404) def not_found(e): """处理404错误""" # 检查是否为API请求 if request.path.startswith('/api'): return jsonify({"error": "API endpoint not found"}), 404 # 非API请求 - 如果是前后端分离,返回API信息 if RUN_MODE == 'production' and app.static_folder and os.path.exists(os.path.join(app.static_folder, 'index.html')): # 如果有前端构建文件,尝试返回 try: return send_from_directory(app.static_folder, 'index.html') except: pass # 返回API信息 return jsonify({ "error": "页面未找到", "message": "这是一个纯后端API服务", "api_base": "https://nav.api.shumengya.top/api", "endpoints": { "/api/profile": "获取个人信息", "/api/techstack": "获取技术栈", "/api/projects": "获取项目列表", "/api/contacts": "获取联系方式", "/api/random-background": "获取随机背景图片", "/api/all": "获取所有数据" } }), 404 if __name__ == '__main__': # 从环境变量获取端口,默认为 5000 port = int(os.environ.get('PORT', 5000)) # 生产环境关闭 debug 模式 debug_mode = RUN_MODE != 'production' app.run(debug=debug_mode, host='0.0.0.0', port=port)