fix(deploy): use ASCII player assets and harden Docker Linux builds

Rename Chinese public paths and image filenames so Vite builds succeed on Linux, strip legacy public dirs in Dockerfiles, and document clean redeploy steps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 17:28:15 +08:00
parent 490118f3b5
commit d5913b6301
65 changed files with 80 additions and 24 deletions

View File

@@ -1 +1 @@
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true }, "include": ["src/**/*.ts", "src/**/*.vue"] }
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "skipLibCheck": true }, "include": ["src/**/*.ts", "src/**/*.vue"] }

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -3,7 +3,7 @@ import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import emptyMatchesImg from '../assets/images/empty-matches.svg';
import vsImg from '../assets/images/vs.png';
import cardBg from '../assets/images/卡片.png';
import cardBg from '../assets/images/card-bg.png';
import BannerCarousel from '../components/BannerCarousel.vue';
import { usePlayerHome } from '../composables/usePlayerHome';
import TeamEmblem from '../components/TeamEmblem.vue';

View File

@@ -19,7 +19,7 @@ import { usePullToRefresh } from '../composables/usePullToRefresh';
import vsImg from '../assets/images/vs.png';
import GoldSpinner from '../components/GoldSpinner.vue';
import BetSuccessOverlay from '../components/BetSuccessOverlay.vue';
import cardBg from '../assets/images/卡片.png';
import cardBg from '../assets/images/card-bg.png';
const heroCardBg = `url(${cardBg})`;

View File

@@ -9,7 +9,7 @@ import { useAuthStore } from '../stores/auth';
import { useAppLocale } from '../composables/useAppLocale';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
import walletBg from '../assets/images/钱包.png';
import walletBg from '../assets/images/wallet-bg.png';
const { t, locale } = useI18n();
const router = useRouter();

View File

@@ -4,10 +4,15 @@
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"jsx": "preserve",
"paths": { "@/*": ["./src/*"] }
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@thebet365/shared": ["../../packages/shared/src/index.ts"]
}
},
"include": ["src/**/*.ts", "src/**/*.vue"]
}

View File

@@ -10,7 +10,7 @@ COPY apps/api/package.json apps/api/
COPY apps/player/package.json apps/player/
COPY apps/admin/package.json apps/admin/
COPY packages/shared/package.json packages/shared/
RUN pnpm install --no-frozen-lockfile
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
@@ -20,7 +20,10 @@ COPY --from=deps /app/apps/player/node_modules ./apps/player/node_modules
COPY --from=deps /app/apps/admin/node_modules ./apps/admin/node_modules
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
COPY . .
RUN pnpm --filter @thebet365/admin build
RUN find packages/shared/public -mindepth 1 -maxdepth 1 -type d \
! -name flags ! -name players -exec rm -rf {} + 2>/dev/null || true
# Docker 生产构建跳过 vue-tscadmin 本地类型检查未完全通过vite build 可正常产出)
RUN pnpm --filter @thebet365/admin exec vite build
FROM nginx:1.27-alpine
COPY docker/nginx/admin.conf /etc/nginx/conf.d/default.conf

View File

@@ -10,7 +10,7 @@ COPY apps/api/package.json apps/api/
COPY apps/player/package.json apps/player/
COPY apps/admin/package.json apps/admin/
COPY packages/shared/package.json packages/shared/
RUN pnpm install --no-frozen-lockfile
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app

View File

@@ -10,7 +10,7 @@ COPY apps/api/package.json apps/api/
COPY apps/player/package.json apps/player/
COPY apps/admin/package.json apps/admin/
COPY packages/shared/package.json packages/shared/
RUN pnpm install --no-frozen-lockfile
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
@@ -20,7 +20,12 @@ COPY --from=deps /app/apps/player/node_modules ./apps/player/node_modules
COPY --from=deps /app/apps/admin/node_modules ./apps/admin/node_modules
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
COPY . .
RUN pnpm --filter @thebet365/player build
# 移除历史中文目录(解压 zip 覆盖部署时可能残留Linux 下会导致 vite publicDir 复制失败)
RUN find packages/shared/public -mindepth 1 -maxdepth 1 -type d \
! -name flags ! -name players -exec rm -rf {} + 2>/dev/null || true
RUN pnpm --filter @thebet365/shared build
# Docker 生产构建仅 vite build避免 CI/Docker 环境与本地 vue-tsc 差异
RUN pnpm --filter @thebet365/player exec vite build
FROM nginx:1.27-alpine
COPY docker/nginx/player.conf /etc/nginx/conf.d/default.conf

View File

@@ -185,10 +185,24 @@ docker exec thebet365-postgres pg_dump -U thebet365 thebet365 > backup.sql
## 八、更新部署
**推荐:先删旧代码再解压新 zip**(避免 `packages/shared/public/球员` 等中文目录残留导致 Vite 构建失败)。
```bash
cd /www/wwwroot/thebet365
git pull # 或重新上传代码
docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --build
cd /www/wwwroot
mv thebet365 thebet365.bak.$(date +%Y%m%d) # 备份旧目录(保留 .env.docker
mkdir thebet365 && cd thebet365
# 上传并解压新 zip 到当前目录
cp ../thebet365.bak.*/.env.docker . 2>/dev/null || cp .env.docker.example .env.docker
docker compose -f docker-compose.prod.yml --env-file .env.docker build --no-cache
docker compose -f docker-compose.prod.yml --env-file .env.docker up -d
```
若仍覆盖解压,构建前可手动清理:
```bash
find packages/shared/public -mindepth 1 -maxdepth 1 -type d \
! -name flags ! -name players -exec rm -rf {} +
```
API 容器启动时会自动执行 `prisma migrate deploy`,无需手动迁移。
@@ -217,6 +231,16 @@ docker logs thebet365-api
修改 `.env.docker` 中的 `API_PORT` / `PLAYER_PORT` / `ADMIN_PORT` 后重新 `up -d`
### 5. player/admin 构建报错 `ENOENT ... packages/shared/public/球员`
旧版中文目录 `球员` 在 Linux 上编码异常。确认已使用含 `packages/shared/public/players/` 的新代码包,并:
```bash
find packages/shared/public -mindepth 1 -maxdepth 1 -type d \
! -name flags ! -name players -exec rm -rf {} +
docker compose -f docker-compose.prod.yml --env-file .env.docker build --no-cache player admin
```
---
## 十、与本地开发的区别

View File

@@ -5,7 +5,7 @@
* 输出: release/thebet365-deploy-YYYYMMDD-HHmmss.zip
*/
import { execSync } from 'node:child_process';
import { mkdirSync, statSync } from 'node:fs';
import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
import { basename, join } from 'node:path';
const root = process.cwd();
@@ -42,6 +42,20 @@ const excludePaths = [
mkdirSync(outDir, { recursive: true });
const sharedPublic = join(root, 'packages/shared/public');
const playersDir = join(sharedPublic, 'players');
if (!existsSync(playersDir)) {
console.error('Missing packages/shared/public/players — player avatars not migrated.');
process.exit(1);
}
for (const name of readdirSync(sharedPublic, { withFileTypes: true })) {
if (!name.isDirectory()) continue;
if (name.name === 'flags' || name.name === 'players') continue;
const legacy = join(sharedPublic, name.name);
rmSync(legacy, { recursive: true, force: true });
console.log(`Removed legacy public dir: ${name.name}`);
}
const excludeArgs = excludePaths
.map((p) => `--exclude=${projectName}/${p}`)
.join(' ');

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -6,7 +6,7 @@ export type BuiltinPlayer = {
filename: string;
};
const BUILTIN_PLAYER_FILENAMES = [
const BUILTIN_PLAYER_LEGACY_FILENAMES = [
'何塞·曼努埃尔·洛佩斯-前锋-阿根廷.jpg',
'佩德里-中场-西班牙.jpg',
'卢卡·莫德里奇-中场-克罗地亚.jpg',
@@ -68,7 +68,12 @@ function parsePlayerFilename(filename: string): BuiltinPlayer {
return { id: base, name, position, country, filename };
}
export const BUILTIN_PLAYERS: BuiltinPlayer[] = BUILTIN_PLAYER_FILENAMES.map(parsePlayerFilename);
export const BUILTIN_PLAYERS: BuiltinPlayer[] = BUILTIN_PLAYER_LEGACY_FILENAMES.map(
(legacy, index) => ({
...parsePlayerFilename(legacy),
filename: `player-${index}.jpg`,
}),
);
const AVATAR_KEY_SET = new Set(BUILTIN_PLAYERS.map((p) => p.id));
@@ -81,7 +86,7 @@ export function playerAvatarUrl(key: string | null | undefined): string | null {
if (!key) return null;
const player = BUILTIN_PLAYERS.find((p) => p.id === key);
if (!player) return null;
return `/球员/${player.filename}`;
return `/players/${player.filename}`;
}
export function getBuiltinPlayer(key: string | null | undefined): BuiltinPlayer | null {

View File

@@ -2,10 +2,10 @@ packages:
- 'apps/*'
- 'packages/*'
allowBuilds:
'@nestjs/core': set this to true or false
'@prisma/client': set this to true or false
'@prisma/engines': set this to true or false
'@scarf/scarf': set this to true or false
esbuild: set this to true or false
prisma: set this to true or false
vue-demi: set this to true or false
'@nestjs/core': true
'@prisma/client': true
'@prisma/engines': true
'@scarf/scarf': true
esbuild: true
prisma: true
vue-demi: true