353 lines
12 KiB
PowerShell
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
|
|
}
|