[Ai] feat: 花魁短剧 Next.js 站点初始提交
- 原生 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>
This commit is contained in:
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user