260 lines
8.6 KiB
PowerShell
260 lines
8.6 KiB
PowerShell
param(
|
||
[string]$ProjectRoot = "D:\AI Code\3danzhuang\furniture-3d-system",
|
||
[string]$OutputRoot = "D:\AI Code\3danzhuang"
|
||
)
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
if (-not (Test-Path -LiteralPath $ProjectRoot)) {
|
||
throw "项目目录不存在: $ProjectRoot"
|
||
}
|
||
|
||
function Get-EnvValue {
|
||
param(
|
||
[string]$Path,
|
||
[string]$Key
|
||
)
|
||
|
||
if (-not (Test-Path -LiteralPath $Path)) {
|
||
return $null
|
||
}
|
||
|
||
$line = Get-Content -LiteralPath $Path | Where-Object { $_ -match "^$([regex]::Escape($Key))=" } | Select-Object -First 1
|
||
if (-not $line) {
|
||
return $null
|
||
}
|
||
|
||
$value = $line.Substring($line.IndexOf('=') + 1).Trim()
|
||
if ($value.Length -ge 2) {
|
||
if (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'"))) {
|
||
$value = $value.Substring(1, $value.Length - 2)
|
||
}
|
||
}
|
||
|
||
return $value
|
||
}
|
||
|
||
$localEnvPath = Join-Path $ProjectRoot '.env'
|
||
$jwtSecret = Get-EnvValue -Path $localEnvPath -Key 'JWT_SECRET'
|
||
if ([string]::IsNullOrWhiteSpace($jwtSecret) -or $jwtSecret.Length -lt 32) {
|
||
$jwtSecret = 'wyj-test-jwt-secret-20260410-solidjoin-3d'
|
||
}
|
||
|
||
$nextAuthSecret = Get-EnvValue -Path $localEnvPath -Key 'NEXTAUTH_SECRET'
|
||
if ([string]::IsNullOrWhiteSpace($nextAuthSecret)) {
|
||
$nextAuthSecret = $jwtSecret
|
||
}
|
||
|
||
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||
$archiveName = "deploy-furniture-wyj-test-$timestamp.tar.gz"
|
||
$scriptName = "deploy-furniture-wyj-test-$timestamp.sh"
|
||
$archivePath = Join-Path $OutputRoot $archiveName
|
||
$scriptPath = Join-Path $OutputRoot $scriptName
|
||
|
||
$includePaths = @(
|
||
'furniture-3d-system/package.json'
|
||
'furniture-3d-system/package-lock.json'
|
||
'furniture-3d-system/.npmrc'
|
||
'furniture-3d-system/next.config.js'
|
||
'furniture-3d-system/postcss.config.mjs'
|
||
'furniture-3d-system/tsconfig.json'
|
||
'furniture-3d-system/next-env.d.ts'
|
||
'furniture-3d-system/prisma.config.ts'
|
||
'furniture-3d-system/src'
|
||
'furniture-3d-system/public'
|
||
'furniture-3d-system/prisma'
|
||
'furniture-3d-system/scripts'
|
||
'furniture-3d-system/lib'
|
||
)
|
||
|
||
Push-Location (Split-Path $ProjectRoot -Parent)
|
||
try {
|
||
if (Test-Path -LiteralPath $archivePath) {
|
||
Remove-Item -LiteralPath $archivePath -Force
|
||
}
|
||
|
||
tar -czf $archivePath @includePaths
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
|
||
$shellScript = @'
|
||
set -e
|
||
BASE=/root/codex-workspaces/furniture-3d-system-wyj-test
|
||
APP=$BASE/furniture-3d-system
|
||
ARCHIVE=/root/codex-workspaces/__ARCHIVE_NAME__
|
||
DB_NAME=furniture3d_wyj_test
|
||
DB_USER=furniture3d_wyj_test
|
||
DB_PASS='F3dWyjTest_3200_20260407'
|
||
APP_PORT=3200
|
||
BUCKET=furniture-3d-models-wyj-test
|
||
MINIO_USER=furniture3dtest
|
||
MINIO_PASS='F3dMinio_39100_20260331'
|
||
S3_ENDPOINT='http://127.0.0.1:39100'
|
||
S3_PUBLIC_ENDPOINT='http://192.168.42.134:39100'
|
||
|
||
mkdir -p $BASE
|
||
mkdir -p $APP
|
||
OLD_LOCK_HASH=""
|
||
if [ -f "$APP/package-lock.json" ]; then
|
||
OLD_LOCK_HASH=$(sha256sum "$APP/package-lock.json" | awk '{print $1}')
|
||
fi
|
||
|
||
rm -rf \
|
||
$APP/src \
|
||
$APP/public \
|
||
$APP/prisma \
|
||
$APP/scripts \
|
||
$APP/lib \
|
||
$APP/.next
|
||
|
||
rm -f \
|
||
$APP/package.json \
|
||
$APP/package-lock.json \
|
||
$APP/.npmrc \
|
||
$APP/next.config.js \
|
||
$APP/postcss.config.mjs \
|
||
$APP/tsconfig.json \
|
||
$APP/next-env.d.ts \
|
||
$APP/prisma.config.ts
|
||
|
||
cd $BASE
|
||
tar -xzf $ARCHIVE
|
||
|
||
NEW_LOCK_HASH=""
|
||
if [ -f "$APP/package-lock.json" ]; then
|
||
NEW_LOCK_HASH=$(sha256sum "$APP/package-lock.json" | awk '{print $1}')
|
||
fi
|
||
|
||
sudo -u postgres psql -d postgres <<SQL
|
||
DO \$\$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '$DB_USER') THEN
|
||
CREATE ROLE $DB_USER LOGIN PASSWORD '$DB_PASS';
|
||
END IF;
|
||
END
|
||
\$\$;
|
||
SQL
|
||
|
||
if ! sudo -u postgres psql -d postgres -Atqc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1; then
|
||
sudo -u postgres createdb -O $DB_USER $DB_NAME
|
||
sudo -u postgres pg_dump -d furniture3d_test --no-owner --no-privileges | sudo -u postgres psql -d $DB_NAME
|
||
fi
|
||
|
||
sudo -u postgres psql -d postgres <<SQL
|
||
GRANT ALL PRIVILEGES ON DATABASE $DB_NAME TO $DB_USER;
|
||
ALTER DATABASE $DB_NAME OWNER TO $DB_USER;
|
||
SQL
|
||
|
||
sudo -u postgres psql -d $DB_NAME <<SQL
|
||
GRANT ALL PRIVILEGES ON SCHEMA public TO $DB_USER;
|
||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO $DB_USER;
|
||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO $DB_USER;
|
||
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO $DB_USER;
|
||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO $DB_USER;
|
||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO $DB_USER;
|
||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO $DB_USER;
|
||
SQL
|
||
|
||
cp /root/codex-workspaces/furniture-3d-system-test/.env $BASE/.env
|
||
python3 - <<PY
|
||
from pathlib import Path
|
||
p = Path('$BASE/.env')
|
||
lines = p.read_text().splitlines()
|
||
repl = {
|
||
'DATABASE_URL': '"postgresql://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME"',
|
||
'DIRECT_URL': '"postgresql://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME"',
|
||
'NEXTAUTH_URL': 'http://192.168.42.134:$APP_PORT',
|
||
'NEXTAUTH_SECRET': '"__NEXTAUTH_SECRET__"',
|
||
'NEXT_PUBLIC_APP_URL': 'http://192.168.42.134:$APP_PORT',
|
||
'S3_ENDPOINT': '"$S3_ENDPOINT"',
|
||
'S3_PUBLIC_ENDPOINT': '"$S3_PUBLIC_ENDPOINT"',
|
||
'S3_PUBLIC_URL': '"$S3_PUBLIC_ENDPOINT"',
|
||
'S3_ACCESS_KEY': '"$MINIO_USER"',
|
||
'S3_SECRET_KEY': '"$MINIO_PASS"',
|
||
'S3_REGION': '"us-east-1"',
|
||
'S3_BUCKET': '"$BUCKET"',
|
||
'JWT_SECRET': '"__JWT_SECRET__"',
|
||
}
|
||
out=[]
|
||
seen=set()
|
||
for line in lines:
|
||
if '=' in line and not line.lstrip().startswith('#'):
|
||
k,_ = line.split('=',1)
|
||
if k in repl:
|
||
out.append(f'{k}={repl[k]}')
|
||
seen.add(k)
|
||
else:
|
||
out.append(line)
|
||
else:
|
||
out.append(line)
|
||
for k,v in repl.items():
|
||
if k not in seen:
|
||
out.append(f'{k}={v}')
|
||
p.write_text('\n'.join(out)+'\n')
|
||
PY
|
||
ln -sfn $BASE/.env $APP/.env
|
||
|
||
docker exec furniture-3d-system-test-minio sh -lc "mc alias set local http://127.0.0.1:9000 $MINIO_USER $MINIO_PASS >/dev/null && mc mb -p local/$BUCKET >/dev/null 2>&1 || true && mc anonymous set download local/$BUCKET >/dev/null"
|
||
|
||
cd $APP
|
||
export PUPPETEER_SKIP_DOWNLOAD=true
|
||
if [ ! -d "$APP/node_modules" ] || [ "$OLD_LOCK_HASH" != "$NEW_LOCK_HASH" ]; then
|
||
echo '=== 安装或更新依赖 ==='
|
||
npm install --no-audit --no-fund
|
||
else
|
||
echo '=== 复用现有依赖(package-lock.json 未变化) ==='
|
||
fi
|
||
|
||
if find /root/.cache/puppeteer -type f -name chrome-headless-shell 2>/dev/null | grep -q .; then
|
||
echo '=== 复用现有 chrome-headless-shell ==='
|
||
else
|
||
echo '=== 安装 chrome-headless-shell ==='
|
||
PUPPETEER_CACHE_DIR=/root/.cache/puppeteer npx puppeteer browsers install chrome-headless-shell@stable
|
||
fi
|
||
|
||
sudo -u postgres psql -d $DB_NAME <<SQL
|
||
ALTER TABLE "User"
|
||
ADD COLUMN IF NOT EXISTS "loginSource" TEXT NOT NULL DEFAULT 'LOCAL',
|
||
ADD COLUMN IF NOT EXISTS "dingtalkUserId" TEXT,
|
||
ADD COLUMN IF NOT EXISTS "dingtalkUnionId" TEXT,
|
||
ADD COLUMN IF NOT EXISTS "dingtalkBoundAt" TIMESTAMP(3);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS "User_dingtalkUserId_key" ON "User"("dingtalkUserId");
|
||
CREATE UNIQUE INDEX IF NOT EXISTS "User_dingtalkUnionId_key" ON "User"("dingtalkUnionId");
|
||
CREATE INDEX IF NOT EXISTS "User_loginSource_idx" ON "User"("loginSource");
|
||
SQL
|
||
npx prisma generate
|
||
PRISMA_SCHEMA_SYNC_SQL=/tmp/furniture-3d-system-wyj-test-prisma-schema-sync.sql
|
||
npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --script -o "$PRISMA_SCHEMA_SYNC_SQL"
|
||
if [ -s "$PRISMA_SCHEMA_SYNC_SQL" ]; then
|
||
echo '=== 同步 Prisma 数据结构差异 ==='
|
||
sudo -u postgres psql -v ON_ERROR_STOP=1 -d $DB_NAME -f "$PRISMA_SCHEMA_SYNC_SQL"
|
||
else
|
||
echo '=== Prisma 数据结构已是最新 ==='
|
||
fi
|
||
npm run build
|
||
|
||
pm2 delete furniture-3d-system-wyj-test >/dev/null 2>&1 || true
|
||
pm2 start /bin/bash --name furniture-3d-system-wyj-test --cwd $APP -- -lc "set -a; source ./.env; set +a; mkdir -p .next/standalone/.next; rm -rf .next/standalone/.next/static .next/standalone/public; cp -R .next/static .next/standalone/.next/static; cp -R public .next/standalone/public; exec env PORT=$APP_PORT HOSTNAME=0.0.0.0 node .next/standalone/server.js"
|
||
pm2 save >/dev/null
|
||
sleep 5
|
||
curl -I -s http://127.0.0.1:$APP_PORT | sed -n '1,12p'
|
||
ls -1dt /root/codex-workspaces/deploy-furniture-wyj-test-*.tar.gz 2>/dev/null | tail -n +4 | xargs -r rm -f
|
||
echo '=== DONE ==='
|
||
pm2 describe furniture-3d-system-wyj-test | sed -n '1,35p'
|
||
'@
|
||
|
||
$shellScript = $shellScript.Replace('__ARCHIVE_NAME__', $archiveName)
|
||
$shellScript = $shellScript.Replace('__JWT_SECRET__', $jwtSecret)
|
||
$shellScript = $shellScript.Replace('__NEXTAUTH_SECRET__', $nextAuthSecret)
|
||
|
||
[System.IO.File]::WriteAllText($scriptPath, $shellScript, [System.Text.UTF8Encoding]::new($false))
|
||
|
||
$archive = Get-Item -LiteralPath $archivePath
|
||
[PSCustomObject]@{
|
||
Archive = $archive.FullName
|
||
ArchiveMB = [math]::Round($archive.Length / 1MB, 2)
|
||
Script = $scriptPath
|
||
IncludedPaths = $includePaths -join ', '
|
||
}
|