官网下载 APK 直链:强制下载头 + 发布脚本

落地页「官网下载」指向 /media/shaguabijia.apk,但该文件靠手动 cp、且 StaticFiles 给 .apk 的 Content-Type 不对又无 attachment 头,部分国产浏览器不触发下载转甩应用市场。新增 download_apk 路由(挂在静态目录之前),显式回 application/vnd.android.package-archive + Content-Disposition:attachment 强制下载,文件缺失回 404。新增 scripts/publish_apk.sh 把「编 release 包 + 放到 data/media/shaguabijia.apk」固化成一条命令。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
no_gen_mu
2026-06-30 17:22:54 +08:00
parent 40dbe504c3
commit afd92289f2
2 changed files with 80 additions and 10 deletions
+35 -10
View File
@@ -2,18 +2,23 @@
通过 `uvicorn app.main:app --reload` 启动。
"""
from __future__ import annotations
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.api.internal.app_version import router as internal_app_version_router
from app.api.internal.launch_confirm import router as internal_launch_confirm_router
from app.api.internal.price import router as internal_price_router
from app.api.internal.store import router as internal_store_router
from app.api.v1.ad import router as ad_router
from app.api.v1.analytics import router as analytics_router
from app.api.v1.auth import router as auth_router
@@ -21,12 +26,8 @@ from app.api.v1.compare import router as compare_router
from app.api.v1.compare_milestone import router as compare_milestone_router
from app.api.v1.compare_record import router as compare_record_router
from app.api.v1.coupon import router as coupon_router
from app.api.v1.device import router as device_router
from app.api.v1.cps_redirect import router as cps_redirect_router
from app.api.internal.app_version import router as internal_app_version_router
from app.api.internal.launch_confirm import router as internal_launch_confirm_router
from app.api.internal.price import router as internal_price_router
from app.api.internal.store import router as internal_store_router
from app.api.v1.device import router as device_router
from app.api.v1.feedback import router as feedback_router
from app.api.v1.invite import router as invite_router
from app.api.v1.meituan import router as meituan_router
@@ -40,14 +41,14 @@ from app.api.v1.user import router as user_router
from app.api.v1.wallet import router as wallet_router
from app.api.v1.wxpay import router as wxpay_router
from app.core.config import settings
from app.core.heartbeat_monitor_worker import (
start_heartbeat_monitor,
stop_heartbeat_monitor,
)
from app.core.daily_exchange_worker import (
start_daily_exchange_worker,
stop_daily_exchange_worker,
)
from app.core.heartbeat_monitor_worker import (
start_heartbeat_monitor,
stop_heartbeat_monitor,
)
from app.core.logging import setup_logging
from app.core.pricebot_client import aclose_pricebot_client, get_pricebot_client
from app.core.withdraw_reconcile_worker import (
@@ -137,6 +138,30 @@ app.include_router(cps_redirect_router)
# 用户上传文件(头像)静态服务。生产可改由 nginx 直接 serve MEDIA_ROOT。
_media_root = Path(settings.MEDIA_ROOT)
_media_root.mkdir(parents=True, exist_ok=True)
# 官网下载的 APK 直链(落地页 dl.html「官网下载」按钮指向 /media/shaguabijia.apk)。
# 必须在 StaticFiles 挂载【之前】注册,否则被静态挂载吃掉。
# StaticFiles 给 .apk 的 Content-Type 不对、且无 attachment 头 → 部分国产浏览器不触发下载、转甩应用市场;
# 这里显式回 application/vnd.android.package-archive + Content-Disposition:attachment 强制浏览器下载。
# 文件由 scripts/publish_apk.sh 编 release 包后放到 data/media/shaguabijia.apk(*.apk 不入 git,需部署时放)。
_APK_PATH = _media_root / "shaguabijia.apk"
@app.get(f"{settings.MEDIA_URL_PREFIX}/shaguabijia.apk", tags=["meta"], include_in_schema=False)
def download_apk() -> FileResponse:
if not _APK_PATH.is_file():
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="安装包未就绪")
return FileResponse(
_APK_PATH,
media_type="application/vnd.android.package-archive",
filename="shaguabijia.apk",
headers={"Content-Disposition": 'attachment; filename="shaguabijia.apk"'},
)
app.mount(
settings.MEDIA_URL_PREFIX,
StaticFiles(directory=str(_media_root)),
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 编 release 正式包并发布到官网下载位 data/media/shaguabijia.apk。
#
# 背景:落地页 dl.html「官网下载」按钮指向 /media/shaguabijia.apk(见 app/main.py download_apk 路由)。
# 该 apk 不入 git(.gitignore 忽略 *.apk),也没有 CI 自动放 —— 以前全靠手动 cp,常忘/放成 debug 包。
# 本脚本把"编 release + 放到位"固化成一条命令。发版时跑一次即可。
#
# 用法: bash scripts/publish_apk.sh
# 产物: <app-server>/data/media/shaguabijia.apk(release 签名、指向生产后端的正式包)
#
# 注意:① 需要 app/jishisongfu-release.jks 存在(release 签名,仅 CTO 持有);缺失会编译失败。
# ② 服务器部署不在本脚本职责内 —— 它只把 apk 放到本仓 data/media/;上线由部署流程把
# data/media/ 同步到生产(或 nginx serve 同目录)。
set -euo pipefail
# 路径全部相对脚本位置算,跟 CWD 无关。
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ANDROID_DIR="$(cd "$SERVER_DIR/../shaguabijia-app-android" && pwd)"
DEST="$SERVER_DIR/data/media/shaguabijia.apk"
APK_OUT="$ANDROID_DIR/app/build/outputs/apk/release/app-release.apk"
# Android Studio 自带 JBR(本机构建环境约定)。已设 JAVA_HOME 则尊重现有值。
export JAVA_HOME="${JAVA_HOME:-D:/android-studio/jbr}"
echo "[publish_apk] Android 工程: $ANDROID_DIR"
if [ ! -f "$ANDROID_DIR/app/jishisongfu-release.jks" ]; then
echo "[publish_apk] ✗ 缺 app/jishisongfu-release.jks(release 签名),无法出正式包。" >&2
exit 1
fi
echo "[publish_apk] 编 release 包(assembleRelease,R8+release 签名,BASE_URL=生产)…"
( cd "$ANDROID_DIR" && ./gradlew :app:assembleRelease )
if [ ! -f "$APK_OUT" ]; then
echo "[publish_apk] ✗ 没找到产物 $APK_OUT" >&2
exit 1
fi
mkdir -p "$(dirname "$DEST")"
cp "$APK_OUT" "$DEST"
SIZE_MB=$(( $(wc -c < "$DEST") / 1024 / 1024 ))
echo "[publish_apk] ✓ 已发布 → $DEST (${SIZE_MB}MB)"
echo "[publish_apk] 官网下载链接(生产):https://app-api.shaguabijia.com/media/shaguabijia.apk"
echo "[publish_apk] 部署:把 data/media/ 同步到生产服务器(本脚本不负责上线)。"