- 原生 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>
125 lines
6.1 KiB
Bash
Executable File
125 lines
6.1 KiB
Bash
Executable File
#!/bin/bash
|
||
# 花魁短剧测试服:检查 → 构建 → dist.zip → 上传 → Supervisor 重启/健康检查。
|
||
# DEPLOY_DRY_RUN=1 ./deploy_test.sh 仅核对,不构建、不连接服务器。
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||
cd "$SCRIPT_DIR"
|
||
|
||
HOST="${DEPLOY_HOST:-root@172.104.181.99}"
|
||
SSH_PORT="${DEPLOY_SSH_PORT:-22889}"
|
||
REMOTE_DIR="${DEPLOY_DIR:-/server/hkdj_net/h5}"
|
||
CHECK_URL="${DEPLOY_CHECK_URL:-https://hkdjneth5.remoces.com}"
|
||
PACKAGE="$SCRIPT_DIR/dist.zip"
|
||
CHECKSUM="$SCRIPT_DIR/dist.zip.sha256"
|
||
|
||
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 ;;
|
||
-h|--help)
|
||
printf '用法:%s [--dry-run]\n默认目标 root@172.104.181.99:22889 /server/hkdj_net/h5/\n可覆盖 DEPLOY_HOST、DEPLOY_SSH_PORT、DEPLOY_DIR、DEPLOY_CHECK_URL;DEPLOY_DRY_RUN=1 等同 --dry-run。\n' "$0"
|
||
exit 0 ;;
|
||
*) fail "不支持的参数 $argument;使用 --help 查看用法" ;;
|
||
esac
|
||
done
|
||
|
||
# 这些值会作为远端路径/命令使用,仅允许普通 SSH 目标和绝对路径。
|
||
[[ "$HOST" =~ ^[a-zA-Z0-9_][a-zA-Z0-9_.@-]*$ ]] || fail "DEPLOY_HOST 格式无效"
|
||
[[ "$SSH_PORT" =~ ^[0-9]+$ ]] && [ "$SSH_PORT" -ge 1 ] && [ "$SSH_PORT" -le 65535 ] || fail "DEPLOY_SSH_PORT 无效"
|
||
[[ "$REMOTE_DIR" =~ ^/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*/?$ ]] || fail "DEPLOY_DIR 必须是普通绝对路径"
|
||
|
||
for tool in node npm zip unzip ssh scp; 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,推荐 Node.js 22"
|
||
node -e 'try { const u=new URL(process.argv[1]); if (!["http:","https:"].includes(u.protocol) || u.username || u.password || u.search || u.hash || u.pathname!=="/") process.exit(1); } catch { process.exit(1); }' "$CHECK_URL" || fail "DEPLOY_CHECK_URL 必须是 HTTP(S) 站点根地址"
|
||
for file in config/test.env config/secrets/m3u8.key deploy/up_next_h5.sh package-lock.json; do
|
||
[ -s "$file" ] || fail "缺少 $file"
|
||
done
|
||
bash -n deploy/up_next_h5.sh
|
||
|
||
if [ "${DEPLOY_DRY_RUN:-0}" = 1 ]; then
|
||
printf 'DRY RUN:测试配置 config/test.env;上游与端口可由服务器 config/test.local.env 覆盖。\n'
|
||
# 不输出本地覆盖/进程配置中的凭证;只展示仓库公共配置。
|
||
node --input-type=module -e 'import {readFileSync} from "node:fs"; import {parse} from "dotenv"; const c=parse(readFileSync("config/test.env")); console.log(` API=${c.UPSTREAM_API_URL} PORT=${c.PORT}`)'
|
||
printf ' npm run check → npm run build:test → npm run bundle:test\n'
|
||
printf ' dist.zip(不含 node_modules / 本地覆盖)+ SHA-256 → %s:%s/(SSH %s)\n' "$HOST" "$REMOTE_DIR" "$SSH_PORT"
|
||
printf ' ssh 执行 %s/up_next_h5.sh:安装 Linux 运行依赖 → next/ → Supervisor hkdj_next_h5 → 健康检查\n' "$REMOTE_DIR"
|
||
printf ' 公网检查 %s:/healthz(test)+ 首页(Next HTML);此模式不访问域名。\n' "$CHECK_URL"
|
||
exit 0
|
||
fi
|
||
|
||
step '检查类型和单元测试'
|
||
npm run check
|
||
step '构建并整理测试环境 standalone'
|
||
npm run build:test
|
||
npm run bundle:test
|
||
|
||
for file in start.mjs server.js .next-test/BUILD_ID config/test.env config/secrets/m3u8.key; do
|
||
[ -s "dist/test/$file" ] || fail "测试包缺少 $file"
|
||
done
|
||
[ -d dist/test/.next-test/static ] && [ -d dist/test/public ] || fail '测试包缺少静态资源'
|
||
|
||
# Next 本机的原生模块不能直接用于 Linux:远端用 lockfile 安装运行依赖。
|
||
# dotenv 属于 dependencies,start.mjs 在 npm ci --omit=dev 后仍可加载配置。
|
||
cp package.json package-lock.json dist/test/
|
||
step '生成 dist.zip 和 SHA-256(保留本地包便于排查)'
|
||
rm -f "$PACKAGE" "$CHECKSUM"
|
||
(
|
||
cd dist/test
|
||
COPYFILE_DISABLE=1 zip -q -r "$PACKAGE" . \
|
||
-x 'node_modules/*' '*/node_modules/*' '*.local.env' '.env*' '*/.env*' '._*' '*/._*' '*.log'
|
||
)
|
||
chmod 600 "$PACKAGE"
|
||
if command -v sha256sum >/dev/null; then
|
||
sha256sum dist.zip > "$CHECKSUM"
|
||
else
|
||
shasum -a 256 dist.zip > "$CHECKSUM"
|
||
fi
|
||
unzip -tq "$PACKAGE"
|
||
|
||
step "上传到 $HOST:$REMOTE_DIR/"
|
||
ssh -p "$SSH_PORT" "$HOST" "mkdir -p '$REMOTE_DIR'"
|
||
scp -P "$SSH_PORT" "$PACKAGE" "$CHECKSUM" deploy/up_next_h5.sh "$HOST:$REMOTE_DIR/"
|
||
step '执行服务器更新脚本'
|
||
ssh -p "$SSH_PORT" "$HOST" "/bin/bash '$REMOTE_DIR/up_next_h5.sh'"
|
||
step "检查公网测试站 $CHECK_URL"
|
||
if ! node --input-type=module - "$CHECK_URL" <<'NODE'
|
||
const origin = process.argv[2];
|
||
async function read(path) {
|
||
const response = await fetch(new URL(path, origin), { signal: AbortSignal.timeout(4000), redirect: 'error', cache: 'no-store' });
|
||
if (response.status !== 200) throw new Error(`${path} HTTP ${response.status}`);
|
||
let text = '', size = 0;
|
||
const decoder = new TextDecoder();
|
||
for await (const part of response.body) {
|
||
size += part.length;
|
||
if (size > 262144) throw new Error(`${path} 响应超过检查上限`);
|
||
text += decoder.decode(part, { stream: true });
|
||
if (path === '/' && text.includes('/_next/')) break;
|
||
}
|
||
return { text, type: response.headers.get('content-type') || '' };
|
||
}
|
||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||
try {
|
||
const [health, home] = await Promise.all([read('/healthz'), read('/')]);
|
||
let status;
|
||
try { status = JSON.parse(health.text); } catch { throw new Error('/healthz 返回无效 JSON'); }
|
||
if (status?.status !== 'ok' || status?.environment !== 'test') throw new Error('/healthz 未命中 Next 测试服务');
|
||
if (!/^text\/html(?:;|$)/i.test(home.type) || !home.text.includes('/_next/')) throw new Error('首页不是 Next HTML');
|
||
console.log('公网 /healthz 与 Next 首页检查通过');
|
||
process.exit(0);
|
||
} catch (error) {
|
||
console.error(`公网检查 ${attempt}/3 失败:${error.message}`);
|
||
if (attempt < 3) await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
}
|
||
process.exit(1);
|
||
NODE
|
||
then
|
||
fail '服务器服务已更新,但域名检查失败,请检查 Nginx 和 Cloudflare 回源配置。'
|
||
fi
|
||
printf '\n测试服更新完成:%s:%s/next(Supervisor:hkdj_next_h5)\n' "$HOST" "$REMOTE_DIR"
|