furniture-3d-system-Wu/scripts/create-yydd-prod-deploy.ps1

353 lines
12 KiB
PowerShell

param(
[string]$ProjectRoot = "D:\AI Code\3danzhuang\furniture-3d-system",
[string]$OutputRoot = "D:\AI Code\3danzhuang",
[string]$RemoteBase = "/opt/furniture-3d-system-prod",
[string]$ServerHost = "ltai5tdb2ogyj59fquyedh.yydd.ai",
[int]$AppPort = 3001,
[string]$PublicAppUrl = "",
[string]$Pm2Name = "furniture-3d-system-prod",
[string]$DbName = "furniture3d_prod",
[string]$DbUser = "furniture3d_prod",
[string]$DbPass = "",
[string]$HrSyncSecret = "",
[string]$S3Endpoint = "http://192.168.58.20:9000",
[string]$S3Bucket = "gongxiangpan",
[string]$S3Region = "us-east-1",
[string]$S3PublicDomain = "https://s3.jiansun.vip/gongxiangpan",
[string]$S3FilePath = "3danzhuang/furniture-3d-system/"
)
$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
}
function New-RandomSecret([int]$Length = 48) {
$bytes = New-Object byte[] $Length
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
return [Convert]::ToBase64String($bytes).Replace('+', 'A').Replace('/', 'B').TrimEnd('=').Substring(0, $Length)
}
$localEnvPath = Join-Path $ProjectRoot '.env'
$jwtSecret = Get-EnvValue -Path $localEnvPath -Key 'JWT_SECRET'
if ([string]::IsNullOrWhiteSpace($jwtSecret) -or $jwtSecret.Length -lt 32) {
$jwtSecret = New-RandomSecret 48
}
$nextAuthSecret = Get-EnvValue -Path $localEnvPath -Key 'NEXTAUTH_SECRET'
if ([string]::IsNullOrWhiteSpace($nextAuthSecret) -or $nextAuthSecret.Length -lt 32) {
$nextAuthSecret = New-RandomSecret 48
}
if ([string]::IsNullOrWhiteSpace($DbPass)) {
$DbPass = New-RandomSecret 32
}
if ([string]::IsNullOrWhiteSpace($HrSyncSecret)) {
$HrSyncSecret = Get-EnvValue -Path $localEnvPath -Key 'HR_SYNC_SECRET'
}
if ([string]::IsNullOrWhiteSpace($HrSyncSecret)) {
$HrSyncSecret = Get-EnvValue -Path $localEnvPath -Key 'INTERNAL_SYNC_SECRET'
}
if ([string]::IsNullOrWhiteSpace($HrSyncSecret) -or $HrSyncSecret.Length -lt 32) {
$HrSyncSecret = New-RandomSecret 48
}
if ([string]::IsNullOrWhiteSpace($PublicAppUrl)) {
$publicAppUrl = "http://$ServerHost`:$AppPort"
} else {
$publicAppUrl = $PublicAppUrl.TrimEnd('/')
}
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$archiveName = "deploy-furniture-prod-$timestamp.tar.gz"
$scriptName = "deploy-furniture-prod-$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=__REMOTE_BASE__
APP=$BASE/furniture-3d-system
ARCHIVE=$BASE/__ARCHIVE_NAME__
DB_NAME=__DB_NAME__
DB_USER=__DB_USER__
DB_PASS='__DB_PASS__'
APP_PORT=__APP_PORT__
PM2_NAME=__PM2_NAME__
PUBLIC_APP_URL='__PUBLIC_APP_URL__'
S3_ENDPOINT='__S3_ENDPOINT__'
S3_BUCKET='__S3_BUCKET__'
S3_REGION='__S3_REGION__'
S3_PUBLIC_DOMAIN='__S3_PUBLIC_DOMAIN__'
S3_FILE_PATH='__S3_FILE_PATH__'
NEXTAUTH_SECRET='__NEXTAUTH_SECRET__'
JWT_SECRET='__JWT_SECRET__'
HR_SYNC_SECRET='__HR_SYNC_SECRET__'
EXISTING_ENV_PATH="$BASE/.env"
read_env_value() {
local key="$1"
if [ ! -f "$EXISTING_ENV_PATH" ]; then
return 0
fi
local line
line=$(grep -E "^${key}=" "$EXISTING_ENV_PATH" | head -n 1 || true)
if [ -z "$line" ]; then
return 0
fi
local value="${line#*=}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "$value"
}
S3_ACCESS_KEY_VALUE="${S3_ACCESS_KEY:-${S3_ACCESS_KEY_ID:-$(read_env_value S3_ACCESS_KEY)}}"
S3_SECRET_KEY_VALUE="${S3_SECRET_KEY:-${S3_SECRET_ACCESS_KEY:-$(read_env_value S3_SECRET_KEY)}}"
NEXTAUTH_SECRET_VALUE="${NEXTAUTH_SECRET:-$(read_env_value NEXTAUTH_SECRET)}"
JWT_SECRET_VALUE="${JWT_SECRET:-$(read_env_value JWT_SECRET)}"
DINGTALK_CALLBACK_ORIGIN_VALUE="${DINGTALK_CALLBACK_ORIGIN:-$(read_env_value DINGTALK_CALLBACK_ORIGIN)}"
DINGTALK_CLIENT_ID_VALUE="${DINGTALK_CLIENT_ID:-$(read_env_value DINGTALK_CLIENT_ID)}"
DINGTALK_CLIENT_SECRET_VALUE="${DINGTALK_CLIENT_SECRET:-$(read_env_value DINGTALK_CLIENT_SECRET)}"
HR_SYNC_SECRET_VALUE="${HR_SYNC_SECRET:-${INTERNAL_SYNC_SECRET:-$(read_env_value HR_SYNC_SECRET)}}"
if [ -z "$NEXTAUTH_SECRET_VALUE" ]; then
NEXTAUTH_SECRET_VALUE="$NEXTAUTH_SECRET"
fi
if [ -z "$JWT_SECRET_VALUE" ]; then
JWT_SECRET_VALUE="$JWT_SECRET"
fi
if [ -z "$DINGTALK_CALLBACK_ORIGIN_VALUE" ]; then
DINGTALK_CALLBACK_ORIGIN_VALUE="$PUBLIC_APP_URL"
fi
if [ -z "$HR_SYNC_SECRET_VALUE" ]; then
HR_SYNC_SECRET_VALUE="$HR_SYNC_SECRET"
fi
if [ -z "$S3_ACCESS_KEY_VALUE" ] || [ -z "$S3_SECRET_KEY_VALUE" ]; then
echo 'Missing shared S3 credentials. Export S3_ACCESS_KEY / S3_SECRET_KEY (or AWS-style names) before running this script.'
exit 1
fi
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
\$\$;
ALTER ROLE $DB_USER WITH PASSWORD '$DB_PASS';
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
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
cat > $BASE/.env <<EOF
NODE_ENV=production
PORT=$APP_PORT
HOSTNAME=0.0.0.0
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=$PUBLIC_APP_URL
NEXT_PUBLIC_APP_URL=$PUBLIC_APP_URL
NEXTAUTH_SECRET=$NEXTAUTH_SECRET_VALUE
JWT_SECRET=$JWT_SECRET_VALUE
S3_ENDPOINT=$S3_ENDPOINT
S3_BUCKET=$S3_BUCKET
S3_REGION=$S3_REGION
S3_ENABLE_PATH_STYLE=1
S3_PUBLIC_DOMAIN=$S3_PUBLIC_DOMAIN
S3_FILE_PATH=$S3_FILE_PATH
S3_ACCESS_KEY=$S3_ACCESS_KEY_VALUE
S3_SECRET_KEY=$S3_SECRET_KEY_VALUE
PUPPETEER_SKIP_DOWNLOAD=true
DINGTALK_CALLBACK_ORIGIN=$DINGTALK_CALLBACK_ORIGIN_VALUE
HR_SYNC_SECRET=$HR_SYNC_SECRET_VALUE
EOF
if [ -n "$DINGTALK_CLIENT_ID_VALUE" ]; then
echo "DINGTALK_CLIENT_ID=$DINGTALK_CLIENT_ID_VALUE" >> $BASE/.env
fi
if [ -n "$DINGTALK_CLIENT_SECRET_VALUE" ]; then
echo "DINGTALK_CLIENT_SECRET=$DINGTALK_CLIENT_SECRET_VALUE" >> $BASE/.env
fi
ln -sfn $BASE/.env $APP/.env
cd $APP
export PUPPETEER_SKIP_DOWNLOAD=true
if [ ! -d "$APP/node_modules" ] || [ "$OLD_LOCK_HASH" != "$NEW_LOCK_HASH" ]; then
echo '=== Install or update dependencies ==='
npm install --no-audit --no-fund
else
echo '=== Reuse existing dependencies (package-lock unchanged) ==='
fi
if find /root/.cache/puppeteer -type f -name chrome-headless-shell 2>/dev/null | grep -q .; then
echo '=== Reuse existing chrome-headless-shell ==='
else
echo '=== Install chrome-headless-shell ==='
PUPPETEER_CACHE_DIR=/root/.cache/puppeteer npx puppeteer browsers install chrome-headless-shell@stable
fi
npx prisma generate
PRISMA_SCHEMA_SYNC_SQL=/tmp/furniture-3d-system-prod-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 '=== Sync Prisma schema diff ==='
sudo -u postgres psql -v ON_ERROR_STOP=1 -d $DB_NAME -f "$PRISMA_SCHEMA_SYNC_SQL"
else
echo '=== Prisma schema already up to date ==='
fi
npm run build
mkdir -p "$APP/.next/standalone/.next"
rm -rf "$APP/.next/standalone/.next/static" "$APP/.next/standalone/public"
cp -R "$APP/.next/static" "$APP/.next/standalone/.next/static"
cp -R "$APP/public" "$APP/.next/standalone/public"
pm2 delete $PM2_NAME >/dev/null 2>&1 || true
pm2 start /bin/bash --name $PM2_NAME -- -lc "cd '$APP' && set -a && source '$APP/.env' && set +a && mkdir -p '$APP/.next/standalone/.next' && rm -rf '$APP/.next/standalone/.next/static' '$APP/.next/standalone/public' && cp -R '$APP/.next/static' '$APP/.next/standalone/.next/static' && cp -R '$APP/public' '$APP/.next/standalone/public' && exec env PORT=$APP_PORT HOSTNAME=0.0.0.0 node '$APP/.next/standalone/server.js'"
pm2 save >/dev/null
sleep 5
curl -I -s http://127.0.0.1:$APP_PORT | sed -n '1,12p'
echo '=== DONE ==='
pm2 describe $PM2_NAME | sed -n '1,35p'
'@
$shellScript = $shellScript.Replace('__REMOTE_BASE__', $RemoteBase)
$shellScript = $shellScript.Replace('__ARCHIVE_NAME__', $archiveName)
$shellScript = $shellScript.Replace('__DB_NAME__', $DbName)
$shellScript = $shellScript.Replace('__DB_USER__', $DbUser)
$shellScript = $shellScript.Replace('__DB_PASS__', $DbPass)
$shellScript = $shellScript.Replace('__APP_PORT__', [string]$AppPort)
$shellScript = $shellScript.Replace('__PM2_NAME__', $Pm2Name)
$shellScript = $shellScript.Replace('__PUBLIC_APP_URL__', $publicAppUrl)
$shellScript = $shellScript.Replace('__S3_ENDPOINT__', $S3Endpoint)
$shellScript = $shellScript.Replace('__S3_BUCKET__', $S3Bucket)
$shellScript = $shellScript.Replace('__S3_REGION__', $S3Region)
$shellScript = $shellScript.Replace('__S3_PUBLIC_DOMAIN__', $S3PublicDomain)
$shellScript = $shellScript.Replace('__S3_FILE_PATH__', $S3FilePath)
$shellScript = $shellScript.Replace('__NEXTAUTH_SECRET__', $nextAuthSecret)
$shellScript = $shellScript.Replace('__JWT_SECRET__', $jwtSecret)
$shellScript = $shellScript.Replace('__HR_SYNC_SECRET__', $HrSyncSecret)
[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
RemoteBase = $RemoteBase
AppPort = $AppPort
PublicAppUrl = $publicAppUrl
S3Bucket = $S3Bucket
S3FilePath = $S3FilePath
}