- 原生 Next.js App Router + 严格 TypeScript 实现花魁短剧 PC/H5 页面,按 src/features/{catalog,content,playback} 组织,保留完整 SEO(metadata、JSON-LD、canonical、robots、llms、sitemap、IndexNow)。
- 主域名通过 SITE_URL=https://huakuidj.com 强制覆盖后端 foreverUrlAddr(config/*.env、src/server/config.ts、src/server/seo/shared.ts、src/server/services/shared.ts),支持邮箱改为 support@huakuidj.com。
- 移除登录与账户体系:删除 /account 全部页面、登录/注册/充值弹窗、收藏点赞、评论、金币解锁及对应 BFF 接口;旧账户地址 GET/HEAD 统一 301 到首页(src/lib/routes.ts REMOVED_ACCOUNT_PATHS),保留游客会话、反馈、图片上传、观看上报与搜索联想。
- 播放页:评论位改为同类推荐 + 热播榜单,锁定集改为 App 观看遮罩与底部弹层,新增简介标签、分集图例与独立视觉皮肤(src/features/playback、src/styles/pages/playback.css)。
- 卡片:短剧/视频卡状态与时长移至信息行胶囊标签,封面角标移至左上角(src/components/catalog/Cards.tsx、src/styles/foundations/base.css)。
- 同步更新单元测试、浏览器冒烟脚本、路由清单与 README。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
6.8 KiB
Bash
Executable File
132 lines
6.8 KiB
Bash
Executable File
#!/bin/bash
|
||
# 花魁短剧正式服:检查 → 构建 → Linux 运行包 + MD5 → 跳板机 running.sh。
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||
cd "$SCRIPT_DIR"
|
||
JUMP_HOST="${DEPLOY_JUMP_HOST:-server@116.212.120.2}"
|
||
JUMP_DIR="${DEPLOY_JUMP_DIR:-/home/server/web_hkdj/h5/nuxt}"
|
||
# 按正式服约定分别设置:SCP 为 22889,触发 SSH 为默认 22。
|
||
SCP_PORT="${DEPLOY_SCP_PORT:-22889}"
|
||
SSH_PORT="${DEPLOY_SSH_PORT:-22}"
|
||
TARGET_ARCH="${DEPLOY_TARGET_ARCH:-x64}"
|
||
TARGET_LIBC="${DEPLOY_TARGET_LIBC:-glibc}"
|
||
PACKAGE_NAME=web_hkdj_h5.tar.gz
|
||
MD5_NAME=web_hkdj_h5.md5
|
||
PACKAGE="$SCRIPT_DIR/$PACKAGE_NAME"
|
||
MD5_FILE="$SCRIPT_DIR/$MD5_NAME"
|
||
RELEASE="$SCRIPT_DIR/dist/production-linux-$TARGET_ARCH-$TARGET_LIBC"
|
||
PACKAGE_ONLY=0
|
||
|
||
fail() { printf '错误:%s\n' "$*" >&2; exit 1; }
|
||
step() { printf '\n▸ %s\n' "$*"; }
|
||
for argument in "$@"; do
|
||
case "$argument" in
|
||
--dry-run) DEPLOY_DRY_RUN=1 ;;
|
||
--package-only) PACKAGE_ONLY=1 ;;
|
||
-h|--help)
|
||
printf '用法:%s [--dry-run | --package-only]\n' "$0"
|
||
printf '默认上传 server@116.212.120.2:/home/server/web_hkdj/h5/nuxt/,SCP 22889,SSH 22。\n'
|
||
printf '可覆盖 DEPLOY_JUMP_HOST、DEPLOY_JUMP_DIR、DEPLOY_SCP_PORT、DEPLOY_SSH_PORT、DEPLOY_TARGET_ARCH、DEPLOY_TARGET_LIBC。\n'
|
||
exit 0 ;;
|
||
*) fail "不支持的参数 $argument;使用 --help 查看用法" ;;
|
||
esac
|
||
done
|
||
[[ "$JUMP_HOST" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.@-]*$ ]] || fail 'DEPLOY_JUMP_HOST 格式无效'
|
||
[[ "$JUMP_DIR" =~ ^/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*/?$ ]] || fail 'DEPLOY_JUMP_DIR 必须是普通绝对路径'
|
||
JUMP_DIR="${JUMP_DIR%/}"
|
||
for port in "$SCP_PORT" "$SSH_PORT"; do
|
||
[[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || fail 'SCP/SSH 端口必须在 1–65535 内'
|
||
done
|
||
case "$TARGET_ARCH" in x64|arm64) ;; *) fail 'DEPLOY_TARGET_ARCH 仅支持 x64 或 arm64' ;; esac
|
||
case "$TARGET_LIBC" in glibc|musl) ;; *) fail 'DEPLOY_TARGET_LIBC 仅支持 glibc 或 musl' ;; esac
|
||
for tool in node npm tar; do
|
||
command -v "$tool" >/dev/null || fail "缺少命令 $tool"
|
||
done
|
||
node -e 'const [a,b]=process.versions.node.split(".").map(Number); if(a<20 || a===20 && b<9) process.exit(1)' || fail '需要 Node.js >=20.9,推荐 22'
|
||
node -e 'const [a,b,c]=process.argv[1].split(".").map(Number); if(a<10 || a===10 && (b<9 || b===9 && c<2)) process.exit(1)' "$(npm --version)" || fail '跨平台运行依赖打包需要 npm >=10.9.2'
|
||
command -v md5sum >/dev/null || command -v md5 >/dev/null || fail '缺少 md5sum 或 macOS md5'
|
||
for file in config/production.env config/secrets/m3u8.key package-lock.json scripts/prepare-production-runtime.ts; do
|
||
[ -s "$file" ] || fail "缺少 $file"
|
||
done
|
||
[ -x node_modules/.bin/tsx ] || fail '请先执行 npm ci 安装本地依赖'
|
||
|
||
if [ "${DEPLOY_DRY_RUN:-0}" = 1 ]; then
|
||
printf 'DRY RUN:正式环境;Linux %s/%s;只核对,不构建、不连接服务器。\n' "$TARGET_ARCH" "$TARGET_LIBC"
|
||
node --input-type=module -e 'import {readFileSync} from "node:fs"; import {parse} from "dotenv"; const c=parse(readFileSync("config/production.env")); console.log(` 服务端口 PORT=${c.PORT};仅打包 config/production.env,不上传本地覆盖。`)'
|
||
printf ' npm run check → build → bundle → bundle:linux -- %s %s\n' "$TARGET_ARCH" "$TARGET_LIBC"
|
||
printf ' 保留 standalone 裁剪依赖,仅补目标 Linux Sharp/libvips;不安装完整生产依赖。\n'
|
||
printf ' tar 根目录:start.mjs、server.js、node_modules、.next、public、config 等\n'
|
||
printf ' 生成 %s + %s(纯 MD5 hash)\n' "$PACKAGE_NAME" "$MD5_NAME"
|
||
if [ "$PACKAGE_ONLY" = 1 ]; then
|
||
printf ' --package-only:生成后结束,不上传。\n'
|
||
else
|
||
printf ' scp -P %s %s %s %s:%s/\n' "$SCP_PORT" "$PACKAGE_NAME" "$MD5_NAME" "$JUMP_HOST" "$JUMP_DIR"
|
||
printf ' ssh -p %s %s %s/running.sh(不带脚本参数)\n' "$SSH_PORT" "$JUMP_HOST" "$JUMP_DIR"
|
||
fi
|
||
exit 0
|
||
fi
|
||
if [ "$PACKAGE_ONLY" = 0 ]; then
|
||
for tool in ssh scp tee; do command -v "$tool" >/dev/null || fail "缺少命令 $tool"; done
|
||
fi
|
||
|
||
# 同仓库只能有一次构建/打包/上传,避免并发覆盖同名包或提前触发 running.sh。
|
||
mkdir -p "$SCRIPT_DIR/dist"
|
||
DEPLOY_LOCK="$SCRIPT_DIR/dist/.production-deploy.lock"
|
||
mkdir "$DEPLOY_LOCK" 2>/dev/null || fail "已有正式发布进程或残留锁:${DEPLOY_LOCK};请先确认该进程已结束"
|
||
trap 'rmdir "$DEPLOY_LOCK" 2>/dev/null || true' EXIT
|
||
trap 'exit 130' INT
|
||
trap 'exit 143' TERM
|
||
|
||
step '检查类型和单元测试'
|
||
npm run check
|
||
step '构建并整理正式环境 standalone'
|
||
npm run build
|
||
npm run bundle
|
||
step "准备 standalone 精简运行包,仅补 Linux $TARGET_ARCH/$TARGET_LIBC 图片库"
|
||
npm run bundle:linux -- "$TARGET_ARCH" "$TARGET_LIBC"
|
||
for file in start.mjs server.js .next/BUILD_ID config/production.env config/secrets/m3u8.key runtime-target.json; do
|
||
[ -s "$RELEASE/$file" ] || fail "正式运行包缺少 $file"
|
||
done
|
||
for directory in node_modules .next/static public; do
|
||
[ -d "$RELEASE/$directory" ] || fail "正式运行包缺少目录 $directory"
|
||
done
|
||
|
||
step "打包 $PACKAGE_NAME"
|
||
# -C 保持产物直接位于 tar 根部;禁止 AppleDouble、环境覆盖和日志混入。
|
||
COPYFILE_DISABLE=1 COPY_EXTENDED_ATTRIBUTES_DISABLE=1 tar \
|
||
--no-xattrs \
|
||
--exclude='._*' --exclude='.DS_Store' --exclude='.env*' \
|
||
--exclude='*.local.env' --exclude='*.log' \
|
||
-czf "$PACKAGE" -C "$RELEASE" .
|
||
chmod 600 "$PACKAGE"
|
||
if command -v md5sum >/dev/null; then
|
||
md5sum "$PACKAGE" | awk '{print $1}' > "$MD5_FILE"
|
||
else
|
||
md5 -q "$PACKAGE" > "$MD5_FILE"
|
||
fi
|
||
tar -tzf "$PACKAGE" >/dev/null
|
||
printf '运行包已生成:%s;校验文件:%s\n' "$PACKAGE_NAME" "$MD5_NAME"
|
||
node -e 'console.log(`压缩包体积:${(require("node:fs").statSync(process.argv[1]).size / 1024 / 1024).toFixed(2)} MiB`)' "$PACKAGE"
|
||
if [ "$PACKAGE_ONLY" = 1 ]; then exit 0; fi
|
||
|
||
step "上传到跳板机 $JUMP_HOST:$JUMP_DIR/"
|
||
scp -P "$SCP_PORT" "$PACKAGE" "$MD5_FILE" "$JUMP_HOST:$JUMP_DIR/"
|
||
step '执行既有正式服 running.sh'
|
||
# gatekeeper 按完整 .sh 路径放行,不能附参数、cd、bash 前缀或其它命令。
|
||
RUN_LOG=$(mktemp "$SCRIPT_DIR/dist/production-running.XXXXXX")
|
||
printf '远端执行日志:%s\n' "$RUN_LOG"
|
||
if ! ssh -p "$SSH_PORT" "$JUMP_HOST" "$JUMP_DIR/running.sh" 2>&1 | tee "$RUN_LOG"; then
|
||
fail "正式服 running.sh 执行失败;日志:${RUN_LOG}"
|
||
fi
|
||
# 部分既有服务器脚本没有透传 Supervisor 的失败退出码,不能只依据 SSH 返回 0。
|
||
if ! node - "$RUN_LOG" <<'NODE'
|
||
const { readFileSync } = require('node:fs');
|
||
const output = readFileSync(process.argv[2], 'utf8').replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
|
||
if (/\b(?:BACKOFF|FATAL|ERROR)\b|\bspawn\s+error\b/i.test(output)) process.exit(1);
|
||
NODE
|
||
then
|
||
fail "正式服输出包含失败状态,不能确认发布成功;日志:${RUN_LOG}"
|
||
fi
|
||
printf '\n正式服 running.sh 执行完成,未发现失败状态;日志:%s;本地包已保留供排查。\n' "$RUN_LOG"
|