哈哈
This commit is contained in:
109
app.py
109
app.py
@@ -236,8 +236,10 @@ FORUM_NOTIFICATION_TYPE_LABELS = {
|
||||
# 论坛高频数据短时缓存(进程内)
|
||||
_FORUM_CACHE_TTL_CATEGORIES = 20.0
|
||||
_FORUM_CACHE_TTL_SIDEBAR = 15.0
|
||||
_FORUM_CACHE_TTL_NOTIF_COUNT = 30.0
|
||||
_FORUM_CATEGORY_CACHE = {}
|
||||
_FORUM_SIDEBAR_CACHE = {"expires_at": 0.0, "data": None}
|
||||
_NOTIF_COUNT_CACHE = {} # user_id -> (count, expires_at)
|
||||
|
||||
_MARKDOWN_ALLOWED_TAGS = [
|
||||
"p", "br", "hr",
|
||||
@@ -416,6 +418,7 @@ def _create_notification(
|
||||
message=message[:255],
|
||||
is_read=False,
|
||||
))
|
||||
_NOTIF_COUNT_CACHE.pop(user_id, None)
|
||||
|
||||
|
||||
def _notification_target_url(notification):
|
||||
@@ -460,16 +463,24 @@ def _get_forum_category_names(active_only=True):
|
||||
return fallback
|
||||
|
||||
|
||||
def _get_notifications_unread_count(user_id):
|
||||
"""已登录用户未读通知数,短时缓存减少每次请求的 count 查询。"""
|
||||
if not user_id:
|
||||
return 0
|
||||
now_ts = monotonic()
|
||||
entry = _NOTIF_COUNT_CACHE.get(user_id)
|
||||
if entry is not None and entry[1] > now_ts:
|
||||
return entry[0]
|
||||
count = ForumNotification.query.filter_by(user_id=user_id, is_read=False).count()
|
||||
_NOTIF_COUNT_CACHE[user_id] = (count, now_ts + _FORUM_CACHE_TTL_NOTIF_COUNT)
|
||||
return count
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_global_user():
|
||||
lang = _get_lang()
|
||||
current_user = _get_current_user()
|
||||
notifications_unread_count = 0
|
||||
if current_user:
|
||||
notifications_unread_count = ForumNotification.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
is_read=False,
|
||||
).count()
|
||||
notifications_unread_count = _get_notifications_unread_count(current_user.id if current_user else None)
|
||||
return {
|
||||
"current_user": current_user,
|
||||
"admin_logged_in": bool(session.get("admin_logged_in")),
|
||||
@@ -891,16 +902,38 @@ I18N = {
|
||||
}
|
||||
|
||||
|
||||
def _query_plans_for_display():
|
||||
"""查询 VPS 方案列表并预加载 provider,避免 to_dict() 时 N+1。"""
|
||||
return (
|
||||
VPSPlan.query
|
||||
.options(joinedload(VPSPlan.provider_rel))
|
||||
.order_by(VPSPlan.provider, VPSPlan.price_cny)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
# /api/plans 短期缓存(秒)
|
||||
_API_PLANS_CACHE_TTL = 60
|
||||
_API_PLANS_CACHE = {"data": None, "expires_at": 0.0}
|
||||
|
||||
|
||||
def _invalidate_plans_cache():
|
||||
"""后台增删改方案后调用,使 /api/plans 缓存失效。"""
|
||||
_API_PLANS_CACHE["expires_at"] = 0.0
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
lang = _get_lang()
|
||||
t = I18N[lang]
|
||||
plans = VPSPlan.query.order_by(VPSPlan.provider, VPSPlan.price_cny).all()
|
||||
plans = _query_plans_for_display()
|
||||
plans_data = [p.to_dict() for p in plans]
|
||||
return render_template(
|
||||
"index.html",
|
||||
site_url=SITE_URL,
|
||||
site_name=SITE_NAME,
|
||||
plans_json_ld=[p.to_dict() for p in plans],
|
||||
plans_json_ld=plans_data,
|
||||
initial_plans_json=plans_data,
|
||||
lang=lang,
|
||||
t=t,
|
||||
)
|
||||
@@ -908,8 +941,19 @@ def index():
|
||||
|
||||
@app.route("/api/plans")
|
||||
def api_plans():
|
||||
plans = VPSPlan.query.order_by(VPSPlan.provider, VPSPlan.price_cny).all()
|
||||
return jsonify([p.to_dict() for p in plans])
|
||||
now_ts = monotonic()
|
||||
cached = _API_PLANS_CACHE.get("data")
|
||||
if cached is not None and _API_PLANS_CACHE.get("expires_at", 0.0) > now_ts:
|
||||
resp = jsonify(cached)
|
||||
resp.headers["Cache-Control"] = "public, max-age=%d" % _API_PLANS_CACHE_TTL
|
||||
return resp
|
||||
plans = _query_plans_for_display()
|
||||
data = [p.to_dict() for p in plans]
|
||||
_API_PLANS_CACHE["data"] = data
|
||||
_API_PLANS_CACHE["expires_at"] = now_ts + _API_PLANS_CACHE_TTL
|
||||
resp = jsonify(data)
|
||||
resp.headers["Cache-Control"] = "public, max-age=%d" % _API_PLANS_CACHE_TTL
|
||||
return resp
|
||||
|
||||
|
||||
# ---------- 前台用户与论坛 ----------
|
||||
@@ -1216,6 +1260,7 @@ def user_notification_read(notification_id):
|
||||
if not n.is_read:
|
||||
n.is_read = True
|
||||
db.session.commit()
|
||||
_NOTIF_COUNT_CACHE.pop(user.id, None)
|
||||
next_url = (request.form.get("next") or "").strip()
|
||||
if next_url.startswith("/") and not next_url.startswith("//"):
|
||||
return redirect(next_url)
|
||||
@@ -1230,6 +1275,8 @@ def user_notifications_read_all():
|
||||
unread = ForumNotification.query.filter_by(user_id=user.id, is_read=False)
|
||||
updated = unread.update({"is_read": True}, synchronize_session=False)
|
||||
db.session.commit()
|
||||
if updated:
|
||||
_NOTIF_COUNT_CACHE.pop(user.id, None)
|
||||
msg = _pick_lang("已全部标记为已读", "All notifications marked as read.", lang) if updated else _pick_lang("没有未读通知", "No unread notifications.", lang)
|
||||
return redirect(url_for("user_notifications", msg=msg))
|
||||
|
||||
@@ -1554,16 +1601,18 @@ def forum_post_detail(post_id):
|
||||
bookmarked_by_me = False
|
||||
can_interact = bool(current_user and not _is_banned_user(current_user))
|
||||
if current_user:
|
||||
liked_by_me = (
|
||||
ForumPostLike.query
|
||||
.filter_by(post_id=post.id, user_id=current_user.id)
|
||||
.first() is not None
|
||||
)
|
||||
bookmarked_by_me = (
|
||||
ForumPostBookmark.query
|
||||
.filter_by(post_id=post.id, user_id=current_user.id)
|
||||
.first() is not None
|
||||
)
|
||||
# 一次查询同时得到当前用户是否点赞/收藏,减少请求次数
|
||||
rows = db.session.execute(
|
||||
text(
|
||||
"(SELECT 'like' AS kind FROM forum_post_likes WHERE post_id=:pid AND user_id=:uid LIMIT 1) "
|
||||
"UNION ALL "
|
||||
"(SELECT 'bookmark' FROM forum_post_bookmarks WHERE post_id=:pid AND user_id=:uid LIMIT 1)"
|
||||
),
|
||||
{"pid": post.id, "uid": current_user.id},
|
||||
).fetchall()
|
||||
kinds = {row[0] for row in rows}
|
||||
liked_by_me = "like" in kinds
|
||||
bookmarked_by_me = "bookmark" in kinds
|
||||
sidebar = _forum_sidebar_data()
|
||||
return render_template(
|
||||
"forum/post_detail.html",
|
||||
@@ -1901,7 +1950,7 @@ def admin_api_plan_price_history(plan_id):
|
||||
@admin_required
|
||||
def admin_dashboard():
|
||||
providers = Provider.query.order_by(Provider.name).all()
|
||||
plans = VPSPlan.query.order_by(VPSPlan.provider, VPSPlan.price_cny).all()
|
||||
plans = _query_plans_for_display()
|
||||
plan_trends = _build_plan_trend_map(plans)
|
||||
return render_template(
|
||||
"admin/dashboard.html",
|
||||
@@ -1941,9 +1990,15 @@ def admin_provider_new():
|
||||
@admin_required
|
||||
def admin_provider_detail(provider_id):
|
||||
provider = Provider.query.get_or_404(provider_id)
|
||||
plans = VPSPlan.query.filter(
|
||||
(VPSPlan.provider_id == provider_id) | (VPSPlan.provider == provider.name)
|
||||
).order_by(VPSPlan.price_cny.asc(), VPSPlan.name).all()
|
||||
plans = (
|
||||
VPSPlan.query
|
||||
.options(joinedload(VPSPlan.provider_rel))
|
||||
.filter(
|
||||
(VPSPlan.provider_id == provider_id) | (VPSPlan.provider == provider.name)
|
||||
)
|
||||
.order_by(VPSPlan.price_cny.asc(), VPSPlan.name)
|
||||
.all()
|
||||
)
|
||||
providers = Provider.query.order_by(Provider.name).all()
|
||||
plan_trends = _build_plan_trend_map(plans)
|
||||
return render_template(
|
||||
@@ -1978,6 +2033,7 @@ def admin_provider_delete(provider_id):
|
||||
VPSPlan.query.filter_by(provider_id=provider_id).update({"provider_id": None})
|
||||
db.session.delete(provider)
|
||||
db.session.commit()
|
||||
_invalidate_plans_cache()
|
||||
return redirect(url_for("admin_providers"))
|
||||
|
||||
|
||||
@@ -3092,6 +3148,7 @@ def _save_plan(plan):
|
||||
db.session.flush()
|
||||
_record_price_history(plan, source="manual")
|
||||
db.session.commit()
|
||||
_invalidate_plans_cache()
|
||||
# 若从厂商详情页进入添加,保存后返回该厂商详情
|
||||
from_provider_id = request.form.get("from_provider_id", type=int)
|
||||
if from_provider_id:
|
||||
@@ -3106,6 +3163,7 @@ def admin_plan_delete(plan_id):
|
||||
PriceHistory.query.filter_by(plan_id=plan_id).delete()
|
||||
db.session.delete(plan)
|
||||
db.session.commit()
|
||||
_invalidate_plans_cache()
|
||||
return redirect(url_for("admin_dashboard"))
|
||||
|
||||
|
||||
@@ -3123,7 +3181,7 @@ def admin_export_excel():
|
||||
ws = wb.active
|
||||
ws.title = "配置"
|
||||
ws.append(EXCEL_HEADERS)
|
||||
plans = VPSPlan.query.order_by(VPSPlan.provider, VPSPlan.price_cny).all()
|
||||
plans = _query_plans_for_display()
|
||||
for p in plans:
|
||||
provider_url = (p.provider_rel.official_url if p.provider_rel else "") or ""
|
||||
ws.append([
|
||||
@@ -3438,6 +3496,7 @@ def admin_import_preview():
|
||||
add_applied += 1
|
||||
_record_price_history(plan, source="import")
|
||||
db.session.commit()
|
||||
_invalidate_plans_cache()
|
||||
session.pop("import_preview", None)
|
||||
msg = "已新增 {} 条,更新 {} 条配置".format(add_applied, update_applied)
|
||||
return redirect(url_for("admin_dashboard") + "?" + urlencode({"msg": msg}))
|
||||
|
||||
@@ -2,9 +2,27 @@
|
||||
# 然后: sudo ln -sf /etc/nginx/sites-available/vps.ddrwode.cn /etc/nginx/sites-enabled/
|
||||
# 先只保留下面这个 server(80),执行 certbot --nginx -d vps.ddrwode.cn 后会自动添加 443 和证书
|
||||
|
||||
# 静态资源:长缓存 + 可选 gzip(若全局未开启)
|
||||
# 若静态由 Flask 提供,则保留 proxy_pass;若改为 Nginx 直接托管,用 alias 指向项目 static 目录
|
||||
server {
|
||||
listen 80;
|
||||
server_name vps.ddrwode.cn;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 256;
|
||||
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
|
||||
|
||||
# 静态资源缓存 7 天,减少重复请求与页面跳转后的再加载
|
||||
location /static/ {
|
||||
proxy_pass http://127.0.0.1:5001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5001;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -68,6 +68,13 @@
|
||||
|
||||
// ==================== 数据获取 ====================
|
||||
function fetchData() {
|
||||
// 优先使用服务端直出的数据,首屏无需再请求 /api/plans
|
||||
if (window.__INITIAL_PLANS__ && Array.isArray(window.__INITIAL_PLANS__) && window.__INITIAL_PLANS__.length >= 0) {
|
||||
allPlans = window.__INITIAL_PLANS__;
|
||||
populateFilters();
|
||||
renderTable();
|
||||
return;
|
||||
}
|
||||
fetch('/api/plans')
|
||||
.then(function(response) {
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
|
||||
@@ -187,6 +187,8 @@
|
||||
load_error: {{ t.load_error|tojson }},
|
||||
btn_visit: {{ ('访问' if lang == 'zh' else 'Visit')|tojson }}
|
||||
};
|
||||
// 首屏直出数据,避免等待 /api/plans 再渲染表格,加快首屏
|
||||
window.__INITIAL_PLANS__ = {{ initial_plans_json|tojson }};
|
||||
</script>
|
||||
<script src="/static/js/main-simple.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user