继续提交
This commit is contained in:
35
mengyadriftbottle-backend/README.md
Normal file
35
mengyadriftbottle-backend/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Mengya Drift Bottle – Backend
|
||||
|
||||
Flask API that powers the React frontend located in `../mengyadriftbottle-frontend`.
|
||||
|
||||
## Features
|
||||
|
||||
- JSON-first `/api` endpoints for throwing, picking up, and reacting to bottles
|
||||
- Rate limiting per IP (5 seconds) for throw/pickup actions
|
||||
- File-backed persistence with automatic schema upgrades
|
||||
- Lightweight token portal via `/admin?token=...` for quick moderation
|
||||
- Full legacy dashboard (`/admin/login`) preserved for session-based moderation
|
||||
- CORS enabled for local development via Vite
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd mengyadriftbottle-backend
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
python app.py
|
||||
```
|
||||
|
||||
The server listens on `http://localhost:5002` by default and exposes the API under `http://localhost:5002/api`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Name | Description | Default |
|
||||
| ---- | ----------- | ------- |
|
||||
| `DRIFT_BOTTLE_SECRET` | Secret key for Flask sessions | Random value generated at runtime |
|
||||
| `DRIFT_BOTTLE_ADMIN_TOKEN` | Token required for `/admin?token=...` | `shumengya520` |
|
||||
|
||||
## File Storage
|
||||
|
||||
Bottle data, config, mottos, and filter words continue to use the JSON files located at the repository root (`../bottles.json`, etc.). The backend automatically creates them if missing.
|
||||
572
mengyadriftbottle-backend/app.py
Normal file
572
mengyadriftbottle-backend/app.py
Normal file
@@ -0,0 +1,572 @@
|
||||
"""Flask backend for the Mengya Drift Bottle application.
|
||||
|
||||
This module exposes a JSON-first API that is consumed by the React
|
||||
frontend located in ``mengyadriftbottle-frontend``. The historical
|
||||
server-rendered templates are preserved for the admin console, while the
|
||||
public-facing experience now lives entirely in the frontend project.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable
|
||||
|
||||
from flask import (
|
||||
Flask,
|
||||
abort,
|
||||
flash,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from flask_cors import CORS
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths & global configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
BACKEND_ROOT = Path(__file__).resolve().parent # Backend directory
|
||||
PROJECT_ROOT = BACKEND_ROOT.parent # Root project directory
|
||||
FRONTEND_ROOT = PROJECT_ROOT / "mengyadriftbottle-frontend" # Frontend directory
|
||||
TEMPLATES_DIR = FRONTEND_ROOT / "templates" # Templates in frontend
|
||||
STATIC_DIR = FRONTEND_ROOT / "static" # Static files in frontend
|
||||
FRONTEND_DIST = Path(
|
||||
os.environ.get("DRIFT_BOTTLE_FRONTEND_DIST") or PROJECT_ROOT / "frontend-dist"
|
||||
)
|
||||
DATA_DIR = Path(os.environ.get("DRIFT_BOTTLE_DATA_DIR", BACKEND_ROOT))
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
BOTTLES_FILE = DATA_DIR / "bottles.json"
|
||||
FILTER_WORDS_FILE = DATA_DIR / "filter_words.json"
|
||||
MOTTOS_FILE = DATA_DIR / "mottos.json"
|
||||
CONFIG_FILE = DATA_DIR / "config.json"
|
||||
|
||||
API_PREFIX = "/api"
|
||||
OPERATION_INTERVAL = 5 # seconds
|
||||
|
||||
app = Flask(
|
||||
__name__,
|
||||
static_folder=str(STATIC_DIR),
|
||||
template_folder=str(TEMPLATES_DIR),
|
||||
)
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
app.config["JSON_SORT_KEYS"] = False
|
||||
app.secret_key = os.environ.get("DRIFT_BOTTLE_SECRET") or secrets.token_hex(24)
|
||||
|
||||
# Only allow cross-origin requests for the API endpoints so the React app
|
||||
# running on Vite's dev server can communicate with Flask during local dev.
|
||||
CORS(app, resources={f"{API_PREFIX}/*": {"origins": "*"}})
|
||||
|
||||
last_operation_time: Dict[str, Dict[str, float]] = defaultdict(
|
||||
lambda: {"throw": 0.0, "pickup": 0.0}
|
||||
)
|
||||
last_picked_bottle: Dict[str, str | None] = defaultdict(lambda: None)
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"name_limit": 7,
|
||||
"message_limit": 100,
|
||||
"admin_username": "shumengya",
|
||||
"admin_password": hashlib.sha256("tyh@19900420".encode()).hexdigest(),
|
||||
}
|
||||
|
||||
SIMPLE_ADMIN_TOKEN = os.environ.get("DRIFT_BOTTLE_ADMIN_TOKEN", "shumengya520")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def ensure_file(path: Path, default_content: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists():
|
||||
return
|
||||
with path.open("w", encoding="utf-8") as fh:
|
||||
json.dump(default_content, fh, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def ensure_config_file() -> Dict[str, Any]:
|
||||
ensure_file(CONFIG_FILE, DEFAULT_CONFIG)
|
||||
return read_config()
|
||||
|
||||
|
||||
def read_config() -> Dict[str, Any]:
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
with CONFIG_FILE.open("r", encoding="utf-8") as fh:
|
||||
config = json.load(fh)
|
||||
except json.JSONDecodeError:
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
else:
|
||||
config = DEFAULT_CONFIG.copy()
|
||||
|
||||
for key, value in DEFAULT_CONFIG.items():
|
||||
config.setdefault(key, value)
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
with CONFIG_FILE.open("w", encoding="utf-8") as fh:
|
||||
json.dump(config, fh, indent=4, ensure_ascii=False)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_mottos_file() -> None:
|
||||
default_mottos = [
|
||||
"良言一句三冬暖,恶语伤人六月寒",
|
||||
"己所不欲,勿施于人",
|
||||
"赠人玫瑰,手有余香",
|
||||
"海内存知己,天涯若比邻",
|
||||
"一期一会,珍惜每一次相遇",
|
||||
"星河滚烫,你是人间理想",
|
||||
]
|
||||
ensure_file(MOTTOS_FILE, default_mottos)
|
||||
|
||||
|
||||
def read_mottos() -> Iterable[str]:
|
||||
ensure_mottos_file()
|
||||
try:
|
||||
with MOTTOS_FILE.open("r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
except json.JSONDecodeError:
|
||||
return ["良言一句三冬暖,恶语伤人六月寒"]
|
||||
|
||||
|
||||
def get_random_motto() -> str:
|
||||
mottos = list(read_mottos())
|
||||
if not mottos:
|
||||
return "良言一句三冬暖,恶语伤人六月寒"
|
||||
return random.choice(mottos)
|
||||
|
||||
|
||||
def ensure_filter_words_file() -> None:
|
||||
ensure_file(FILTER_WORDS_FILE, ["敏感词1", "敏感词2"])
|
||||
|
||||
|
||||
def read_filter_words() -> Iterable[str]:
|
||||
ensure_filter_words_file()
|
||||
try:
|
||||
with FILTER_WORDS_FILE.open("r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def filter_sensitive_words(text: str) -> str:
|
||||
filtered = text
|
||||
for word in read_filter_words():
|
||||
filtered = re.sub(re.escape(word), "*" * len(word), filtered)
|
||||
return filtered
|
||||
|
||||
|
||||
def read_bottles() -> list[Dict[str, Any]]:
|
||||
if not BOTTLES_FILE.exists():
|
||||
with BOTTLES_FILE.open("w", encoding="utf-8") as fh:
|
||||
json.dump([], fh)
|
||||
return []
|
||||
|
||||
try:
|
||||
with BOTTLES_FILE.open("r", encoding="utf-8") as fh:
|
||||
bottles = json.load(fh)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
for bottle in bottles:
|
||||
bottle.setdefault("likes", 0)
|
||||
bottle.setdefault("dislikes", 0)
|
||||
write_bottles(bottles)
|
||||
return bottles
|
||||
|
||||
|
||||
def write_bottles(bottles: list[Dict[str, Any]]) -> None:
|
||||
BOTTLES_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with BOTTLES_FILE.open("w", encoding="utf-8") as fh:
|
||||
json.dump(bottles, fh, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def get_client_ip() -> str:
|
||||
forwarded = request.headers.get("X-Forwarded-For")
|
||||
if forwarded:
|
||||
return forwarded.split(",")[0].strip()
|
||||
real_ip = request.headers.get("X-Real-IP")
|
||||
if real_ip:
|
||||
return real_ip
|
||||
return request.remote_addr or "unknown"
|
||||
|
||||
|
||||
def check_operation_interval(operation_type: str) -> float:
|
||||
ip = get_client_ip()
|
||||
current_time = time.time()
|
||||
last_time = last_operation_time[ip][operation_type]
|
||||
remaining = OPERATION_INTERVAL - (current_time - last_time)
|
||||
if remaining > 0:
|
||||
return max(0.0, remaining)
|
||||
last_operation_time[ip][operation_type] = current_time
|
||||
return 0.0
|
||||
|
||||
|
||||
def admin_required(func):
|
||||
@wraps(func)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get("admin_logged_in"):
|
||||
return redirect(url_for("admin_login"))
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def get_request_payload() -> Dict[str, Any]:
|
||||
if request.is_json:
|
||||
payload = request.get_json(silent=True)
|
||||
if isinstance(payload, dict):
|
||||
return payload
|
||||
if request.form:
|
||||
return request.form.to_dict()
|
||||
return {}
|
||||
|
||||
|
||||
def normalize_name(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
def is_valid_simple_admin_token(token: str | None) -> bool:
|
||||
if not token:
|
||||
return False
|
||||
# compare_digest防止时间侧信道
|
||||
return secrets.compare_digest(str(token), SIMPLE_ADMIN_TOKEN)
|
||||
|
||||
|
||||
def bootstrap_storage() -> None:
|
||||
ensure_filter_words_file()
|
||||
ensure_mottos_file()
|
||||
ensure_config_file()
|
||||
if not BOTTLES_FILE.exists():
|
||||
write_bottles([])
|
||||
|
||||
|
||||
bootstrap_storage()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# ==============================后端公开API============================== #
|
||||
# ---------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def root() -> Any:
|
||||
return jsonify({
|
||||
"message": "Mengya Drift Bottle后端API正在运行中...",
|
||||
"api_base": API_PREFIX,
|
||||
})
|
||||
|
||||
|
||||
@app.get(f"{API_PREFIX}/health")
|
||||
def health_check() -> Any:
|
||||
return jsonify({"status": "ok", "timestamp": datetime.utcnow().isoformat()})
|
||||
|
||||
|
||||
@app.get(f"{API_PREFIX}/motto")
|
||||
def random_motto() -> Any:
|
||||
return jsonify({"success": True, "motto": get_random_motto()})
|
||||
|
||||
|
||||
@app.get(f"{API_PREFIX}/stats")
|
||||
def get_stats() -> Any:
|
||||
bottles = read_bottles()
|
||||
return jsonify({"success": True, "stats": {"total_bottles": len(bottles)}})
|
||||
|
||||
|
||||
@app.get(f"{API_PREFIX}/config")
|
||||
def get_config() -> Any:
|
||||
config = read_config()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"config": {
|
||||
"name_limit": config.get("name_limit", 7),
|
||||
"message_limit": config.get("message_limit", 100),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@app.post(f"{API_PREFIX}/throw")
|
||||
def throw_bottle() -> Any:
|
||||
wait_time = check_operation_interval("throw")
|
||||
if wait_time > 0:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"error": f"操作太频繁,请等待 {round(wait_time, 1)} 秒后再试。",
|
||||
"wait_time": round(wait_time, 1),
|
||||
}
|
||||
),
|
||||
429,
|
||||
)
|
||||
|
||||
data = get_request_payload()
|
||||
name = normalize_name(data.get("name"))
|
||||
message = normalize_name(data.get("message"))
|
||||
gender = data.get("gender") or "保密"
|
||||
qq_number = normalize_name(data.get("qq") or data.get("qq_number"))
|
||||
|
||||
if not name or not message or not gender:
|
||||
return jsonify({"success": False, "error": "姓名、消息和性别是必需的"}), 400
|
||||
|
||||
config = read_config()
|
||||
name_limit = int(config.get("name_limit", 7))
|
||||
message_limit = int(config.get("message_limit", 100))
|
||||
|
||||
if len(name) > name_limit:
|
||||
return jsonify({"success": False, "error": f"名字最多{name_limit}个字符"}), 400
|
||||
if len(message) > message_limit:
|
||||
return jsonify({"success": False, "error": f"消息内容最多{message_limit}个字符"}), 400
|
||||
|
||||
filtered_name = filter_sensitive_words(name)
|
||||
filtered_message = filter_sensitive_words(message)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
ip_address = get_client_ip()
|
||||
|
||||
qq_avatar_url = None
|
||||
if qq_number and qq_number.isdigit():
|
||||
qq_avatar_url = f"http://q1.qlogo.cn/g?b=qq&nk={qq_number}&s=100"
|
||||
|
||||
new_bottle = {
|
||||
"id": f"{timestamp}_{random.randint(1000, 9999)}",
|
||||
"name": filtered_name,
|
||||
"message": filtered_message,
|
||||
"timestamp": timestamp,
|
||||
"ip_address": ip_address,
|
||||
"gender": gender,
|
||||
"qq_number": qq_number,
|
||||
"qq_avatar_url": qq_avatar_url,
|
||||
"likes": 0,
|
||||
"dislikes": 0,
|
||||
}
|
||||
|
||||
bottles = read_bottles()
|
||||
bottles.append(new_bottle)
|
||||
write_bottles(bottles)
|
||||
|
||||
return jsonify({"success": True, "message": "漂流瓶投放成功!"})
|
||||
|
||||
|
||||
@app.get(f"{API_PREFIX}/pickup")
|
||||
def pickup_bottle() -> Any:
|
||||
wait_time = check_operation_interval("pickup")
|
||||
if wait_time > 0:
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"操作太频繁,请等待 {round(wait_time, 1)} 秒后再试。",
|
||||
"wait_time": round(wait_time, 1),
|
||||
}
|
||||
),
|
||||
429,
|
||||
)
|
||||
|
||||
bottles = read_bottles()
|
||||
if not bottles:
|
||||
return jsonify({"success": False, "message": "海里没有漂流瓶。"})
|
||||
|
||||
ip_address = get_client_ip()
|
||||
last_bottle_id = last_picked_bottle[ip_address]
|
||||
|
||||
if len(bottles) == 1:
|
||||
selected_bottle = bottles[0]
|
||||
else:
|
||||
available = [b for b in bottles if b["id"] != last_bottle_id]
|
||||
available = available or bottles
|
||||
weights = [max(1, 10 - min(9, b.get("dislikes", 0))) for b in available]
|
||||
selected_bottle = random.choices(available, weights=weights, k=1)[0]
|
||||
|
||||
last_picked_bottle[ip_address] = selected_bottle["id"]
|
||||
return jsonify({"success": True, "bottle": selected_bottle})
|
||||
|
||||
|
||||
@app.post(f"{API_PREFIX}/react")
|
||||
def react_to_bottle() -> Any:
|
||||
data = request.get_json(silent=True) or {}
|
||||
bottle_id = data.get("bottle_id")
|
||||
reaction = data.get("reaction")
|
||||
|
||||
if not bottle_id or reaction not in {"like", "dislike"}:
|
||||
return jsonify({"success": False, "error": "参数错误"}), 400
|
||||
|
||||
bottles = read_bottles()
|
||||
for bottle in bottles:
|
||||
if bottle["id"] == bottle_id:
|
||||
key = "likes" if reaction == "like" else "dislikes"
|
||||
bottle[key] = bottle.get(key, 0) + 1
|
||||
write_bottles(bottles)
|
||||
return jsonify({"success": True, "message": "反馈已记录"})
|
||||
|
||||
return jsonify({"success": False, "error": "未找到漂流瓶"}), 404
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 管理员相关操作API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def summarize_bottles(bottles: list[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
return {
|
||||
"total": len(bottles),
|
||||
"likes": sum(int(b.get("likes", 0) or 0) for b in bottles),
|
||||
"dislikes": sum(int(b.get("dislikes", 0) or 0) for b in bottles),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/admin")
|
||||
def admin_simple_portal():
|
||||
token = request.args.get("token", "")
|
||||
status_key = request.args.get("status")
|
||||
status_messages = {
|
||||
"deleted": "漂流瓶已删除",
|
||||
"missing": "未找到指定漂流瓶,可能已经被删除",
|
||||
"unauthorized": "无权执行该操作,请检查token",
|
||||
}
|
||||
feedback = status_messages.get(status_key)
|
||||
|
||||
authorized = is_valid_simple_admin_token(token)
|
||||
bottles_raw = read_bottles() if authorized else []
|
||||
bottles = sorted(
|
||||
bottles_raw,
|
||||
key=lambda item: item.get("timestamp", ""),
|
||||
reverse=True,
|
||||
) if authorized else []
|
||||
stats = summarize_bottles(bottles) if authorized else None
|
||||
|
||||
return render_template(
|
||||
"admin_simple.html",
|
||||
authorized=authorized,
|
||||
token=token,
|
||||
stats=stats,
|
||||
bottles=bottles,
|
||||
feedback=feedback,
|
||||
sample_token=SIMPLE_ADMIN_TOKEN if os.environ.get("DRIFT_BOTTLE_ADMIN_TOKEN") is None else "***",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/admin/simple/delete/<bottle_id>")
|
||||
def admin_simple_delete(bottle_id: str):
|
||||
token = request.form.get("token") or request.args.get("token")
|
||||
if not is_valid_simple_admin_token(token):
|
||||
return redirect(url_for("admin_simple_portal", status="unauthorized"))
|
||||
|
||||
bottles = read_bottles()
|
||||
new_bottles = [b for b in bottles if b["id"] != bottle_id]
|
||||
status = "missing"
|
||||
if len(new_bottles) != len(bottles):
|
||||
write_bottles(new_bottles)
|
||||
status = "deleted"
|
||||
|
||||
return redirect(url_for("admin_simple_portal", token=token, status=status))
|
||||
|
||||
|
||||
@app.route("/admin/login", methods=["GET", "POST"])
|
||||
def admin_login():
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "")
|
||||
password = request.form.get("password", "")
|
||||
|
||||
config = read_config()
|
||||
if (
|
||||
username == config.get("admin_username")
|
||||
and hashlib.sha256(password.encode()).hexdigest()
|
||||
== config.get("admin_password")
|
||||
):
|
||||
session["admin_logged_in"] = True
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
flash("用户名或密码错误", "error")
|
||||
|
||||
return render_template("admin_login.html")
|
||||
|
||||
|
||||
@app.get("/admin/logout")
|
||||
def admin_logout():
|
||||
session.pop("admin_logged_in", None)
|
||||
return redirect(url_for("admin_login"))
|
||||
|
||||
|
||||
@app.get("/admin/dashboard")
|
||||
@admin_required
|
||||
def admin_dashboard():
|
||||
bottles = read_bottles()
|
||||
return render_template("admin_dashboard.html", bottles=bottles)
|
||||
|
||||
|
||||
@app.post("/admin/delete_bottle/<bottle_id>")
|
||||
@admin_required
|
||||
def delete_bottle(bottle_id: str):
|
||||
bottles = [b for b in read_bottles() if b["id"] != bottle_id]
|
||||
write_bottles(bottles)
|
||||
flash("漂流瓶已成功删除", "success")
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@app.route("/admin/settings", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def admin_settings():
|
||||
config = read_config()
|
||||
if request.method == "POST":
|
||||
try:
|
||||
name_limit = int(request.form.get("name_limit", 7))
|
||||
message_limit = int(request.form.get("message_limit", 100))
|
||||
if name_limit < 1 or message_limit < 1:
|
||||
raise ValueError
|
||||
config["name_limit"] = name_limit
|
||||
config["message_limit"] = message_limit
|
||||
if save_config(config):
|
||||
flash("设置已成功保存", "success")
|
||||
else:
|
||||
flash("保存设置时出错", "error")
|
||||
except ValueError:
|
||||
flash("请输入有效的数值", "error")
|
||||
|
||||
return render_template("admin_settings.html", config=config)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- #
|
||||
# ==============================后端公开API============================== #
|
||||
# ---------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>")
|
||||
def serve_frontend_app(path: str):
|
||||
"""Serve the compiled React frontend (single-page app)."""
|
||||
|
||||
if path.startswith("api/"):
|
||||
abort(404)
|
||||
|
||||
if not FRONTEND_DIST.exists():
|
||||
return ("Frontend build not found. Please run npm run build first.", 404)
|
||||
|
||||
if path and (FRONTEND_DIST / path).is_file():
|
||||
return send_from_directory(str(FRONTEND_DIST), path)
|
||||
|
||||
return send_from_directory(str(FRONTEND_DIST), "index.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5002, debug=True)
|
||||
14
mengyadriftbottle-backend/bottles.json
Normal file
14
mengyadriftbottle-backend/bottles.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "2025-11-16 19:00:54_5309",
|
||||
"name": "树萌芽",
|
||||
"message": "Hello World!",
|
||||
"timestamp": "2025-11-16 19:00:54",
|
||||
"ip_address": "127.0.0.1",
|
||||
"gender": "男",
|
||||
"qq_number": "3205788256",
|
||||
"qq_avatar_url": "http://q1.qlogo.cn/g?b=qq&nk=3205788256&s=100",
|
||||
"likes": 1,
|
||||
"dislikes": 1
|
||||
}
|
||||
]
|
||||
6
mengyadriftbottle-backend/config.json
Normal file
6
mengyadriftbottle-backend/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name_limit": 7,
|
||||
"message_limit": 150,
|
||||
"admin_username": "shumengya",
|
||||
"admin_password": "06dc4b37c16a43fa94a3191e3be039ab6b05dbecc1c55e7860cf9911c72e71f8"
|
||||
}
|
||||
19
mengyadriftbottle-backend/filter_words.json
Normal file
19
mengyadriftbottle-backend/filter_words.json
Normal file
@@ -0,0 +1,19 @@
|
||||
[
|
||||
"你TM",
|
||||
"唐伟",
|
||||
"糖伟",
|
||||
"糖萎",
|
||||
"月抛",
|
||||
"约炮",
|
||||
"cnm",
|
||||
"傻逼",
|
||||
"狗日的",
|
||||
"妈卖批",
|
||||
"nmsl",
|
||||
"杂种",
|
||||
"操你妈",
|
||||
"草你妈",
|
||||
"日你妈",
|
||||
"你他妈",
|
||||
"超你妈"
|
||||
]
|
||||
8
mengyadriftbottle-backend/mottos.json
Normal file
8
mengyadriftbottle-backend/mottos.json
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
"良言一句三冬暖,恶语伤人六月寒",
|
||||
"己所不欲,勿施于人",
|
||||
"赠人玫瑰,手有余香",
|
||||
"海内存知己,天涯若比邻",
|
||||
"一期一会,珍惜每一次相遇",
|
||||
"星河滚烫,你是人间理想"
|
||||
]
|
||||
2
mengyadriftbottle-backend/requirements.txt
Normal file
2
mengyadriftbottle-backend/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Flask==3.0.3
|
||||
flask-cors==4.0.0
|
||||
Reference in New Issue
Block a user