diff --git a/README.md b/README.md index 18f9137..95a2a38 100644 --- a/README.md +++ b/README.md @@ -137,17 +137,23 @@ cd D:\www\sport-era powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net ``` -全量模式默认只打包以下目录: +全量模式默认以仓库根目录为源,只打包以下三个子目录: ```text -server/app -server/config -server/extend -server/public/dongqiudi-crawler -server/route +server/ +uniapp/ +admin/ ``` -默认会排除运行态或较大目录,例如 `.env`、`vendor`、`runtime`、`public/uploads`、`public/admin`、`public/mobile`、爬虫 `data/logs/venv`。如确实需要包含,可追加参数: +也就是说,远端 `RemoteDir` 下最终会形成: + +```text +/server +/uniapp +/admin +``` + +默认会排除运行态或较大目录,例如 `server/.env`、`server/vendor`、`server/runtime`、`server/public/uploads`、`server/public/admin`、`server/public/mobile`、爬虫 `data/logs/venv`,以及 `uniapp/node_modules`、`uniapp/dist`、`admin/node_modules`、`admin/dist`。如确实需要包含,可追加参数: ```bash --include-env @@ -197,11 +203,11 @@ cd D:\www\sport-era powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -AutoIncremental -RemoteDir /www/wwwroot/test-server.sbnews.net -DryRun ``` -脚本会在打包和上传前先检查 `server` 工作区;如果 `server` 内存在未提交、删除或未跟踪文件,会先自动提交这些变更,再继续计算增量范围。可以通过 `-AutoCommitMessage "提交说明"` 自定义这次自动提交的说明。若存在合并冲突,需要先手动解决。 +脚本会在打包和上传前先检查默认打包范围(`server` + `uniapp` + `admin`);如果这些目录内存在未提交、删除或未跟踪文件,会先自动提交这些变更,再继续计算增量范围。可以通过 `-AutoCommitMessage "提交说明"` 自定义这次自动提交的说明。若存在合并冲突,需要先手动解决。 注意:自动增量只包含指定提交范围内已经提交到 Git 的差异,不会包含工作区未提交文件。需要指定任意起止提交时,继续使用 `-From -To `。 -如果当前根目录 Git 只把 `server` 记录为 gitlink/submodule,且本机没有 `server/.git` 子仓库元数据,脚本无法拿到 `server` 内部的文件级差异;此时会自动降级为打包当前工作区默认的 5 个 server 目录,保证可以继续发布。 +如果当前根目录 Git 只把 `server`、`uniapp` 或 `admin` 记录为 gitlink/submodule,脚本无法拿到对应目录内部的文件级差异;此时会把该目录从当前工作区按源码目录补进增量包,保证可以继续发布。 ### 只演练不上传 @@ -225,14 +231,34 @@ cd D:\www\sport-era powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -RemoteDir /www/wwwroot/test-server.sbnews.net -PostInstall "php think optimize:clear" ``` +### 发布 uniapp 源码到服务器并在服务器构建 + +默认发布命令会把 `server/`、`uniapp/` 和 `admin/` 一起同步到远端根目录;如果需要在服务器端构建 `uniapp`,建议在远端根目录执行构建,再把 H5 产物写回同一套代码下的 `server/public/mobile`: + +```powershell +cd D:\www\sport-era +powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 ` + -Sudo ` + -RemoteDir /www/wwwroot/test-server.sbnews.net ` + -UniappReleasePath ../server/public/mobile ` + -PostInstall "cd uniapp && npm install && npm run build:h5" +``` + +说明: + +- `-RemoteDir` 指向服务器上的项目根目录,解压后里面会同时有 `server/`、`uniapp/` 和 `admin/`。 +- `-UniappReleasePath` 会通过环境变量 `UNIAPP_RELEASE_PATH` 传给 `uniapp/scripts/release.mjs`;这里传 `../server/public/mobile`,是因为构建命令会先 `cd uniapp`,再把 H5 产物回写到兄弟目录 `server/public/mobile`。 +- 服务器需要预先安装 `node >= 18.19.0` 和 `npm`,否则 `npm install` / `npm run build:h5` 无法执行。 + ### 常见问题 1. `ssh: Could not resolve hostname sbnews`:检查 Windows 的 `C:\Users\Administrator\.ssh\config` 是否存在 `Host sbnews`,或直接用 `-HostName <服务器IP>`。 2. PowerShell 禁止执行脚本:使用示例中的 `-ExecutionPolicy Bypass -File`,或以管理员身份调整当前用户执行策略。 3. `tar: Cannot utime` 或 `Cannot change mode`:远端目录权限不足,请加 `-Sudo`。脚本会使用 `sudo tar` 覆盖解压,并默认跳过还原 owner/permission,减少权限冲突。 4. 增量发布提示 commit 无效:确认 commit id 属于当前根目录 Git 可识别的历史。 -5. `server is tracked as a root gitlink`:脚本检测到根 Git 只记录了 `server` 指针,无法做文件级增量,会自动改为打包当前工作区默认 5 个 server 目录。 +5. `root gitlink include paths will be copied from working tree`:脚本检测到默认发布范围里仍有 gitlink/submodule 目录,会把这些目录从当前工作区源码补进增量包。 6. 仍需使用 Bash 兼容版时,可继续运行 `bash ./scripts/deploy-server.sh ...`。 +7. 服务器运行 `uniapp` 报 `node: command not found` 或 `npm: command not found`:先在服务器安装 Node.js 18.19+ 与 npm,再执行 `-SourceDir uniapp` 的发布命令。 ![likeshop全开源商城](https://server.likeshop.cn/uploads/gitad/fastbuildAI.png)
![likeshop全开源商城](https://server.likeshop.cn/uploads/gitad/likeshop.png)
diff --git a/docs/sql/insert_menu_comment_account.sql b/docs/sql/insert_menu_comment_account.sql new file mode 100644 index 0000000..02b9da1 --- /dev/null +++ b/docs/sql/insert_menu_comment_account.sql @@ -0,0 +1,14 @@ +INSERT INTO `la_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`) +SELECT COALESCE((SELECT `pid` FROM `la_system_menu` WHERE `perms` = 'community.communityComment/lists' LIMIT 1), 200), 'C', '评论账号', '', 0, 'community.commentAccount/lists', 'comment_account', 'community/account/index', '', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP() +WHERE NOT EXISTS ( + SELECT 1 FROM `la_system_menu` + WHERE `perms` = 'community.commentAccount/lists' +); + +INSERT INTO `la_system_role_menu` (`role_id`, `menu_id`) +SELECT 1, (SELECT id FROM `la_system_menu` WHERE `perms` = 'community.commentAccount/lists' LIMIT 1) +WHERE NOT EXISTS ( + SELECT 1 FROM `la_system_role_menu` rm + JOIN `la_system_menu` m ON m.id = rm.menu_id + WHERE m.perms = 'community.commentAccount/lists' +); diff --git a/scripts/deploy-server.ps1 b/scripts/deploy-server.ps1 index bb04040..f124bc9 100644 --- a/scripts/deploy-server.ps1 +++ b/scripts/deploy-server.ps1 @@ -1,8 +1,9 @@ [CmdletBinding()] param( - [string]$SourceDir = "server", + [string]$SourceDir = ".", [Parameter(Mandatory = $true)] [string]$RemoteDir, + [string]$UniappReleasePath = "", [string]$HostName = "sbnews", [string]$SshBin = "ssh.exe", [switch]$AutoIncremental, @@ -181,7 +182,9 @@ function Start-ProcessWithInputText { $inputFile = [System.IO.Path]::GetTempFileName() try { - [System.IO.File]::WriteAllText($inputFile, $InputText, $Utf8NoBom) + # Remote shells like `bash -s` choke on Windows CRLF in stdin scripts. + $normalizedInputText = $InputText.Replace("`r`n", "`n").Replace("`r", "`n") + [System.IO.File]::WriteAllText($inputFile, $normalizedInputText, $Utf8NoBom) Start-ProcessWithInputFile -FilePath $FilePath -Arguments $Arguments -InputFile $inputFile } finally { Remove-Item -LiteralPath $inputFile -Force -ErrorAction SilentlyContinue @@ -195,13 +198,123 @@ function Remove-IfExists { } } +function Test-IsRepoRootDeploy { + return ($SourceDir -eq "." -or [string]::IsNullOrWhiteSpace($SourceDir)) +} + +function Get-SourceLabel { + if (Test-IsRepoRootDeploy) { + return "repo-root" + } + return $SourceDir +} + function Get-DeployPathspec { + if (Test-IsRepoRootDeploy) { + return @($FullIncludePaths) + } if ([string]::IsNullOrEmpty($GitPathPrefix)) { return @(".") } return @($GitPathPrefix) } +function Get-FullIncludePaths { + switch ($SourceDir.ToLowerInvariant()) { + "." { + return @("server", "uniapp", "admin", "docker") + } + "server" { + return @("app", "config", "extend", "public", "route") + } + "uniapp" { + return @(".") + } + default { + return @(".") + } + } +} + +function Get-ArchiveExcludePatterns { + $patterns = @( + ".git", + ".idea", + ".vscode", + ".tmp", + ".hbuilderx", + ".gitignore", + "*.log", + "release-*.zip", + "*.zip", + "__pycache__", + "*.pyc", + "node_modules", + "dist" + ) + + switch ($SourceDir.ToLowerInvariant()) { + "." { + $patterns += @("uniapp/node_modules", "uniapp/dist", "admin/node_modules", "admin/dist") + if (-not $IncludeEnv) { + $patterns += "server/.env" + } + if (-not $IncludeVendor) { + $patterns += "server/vendor" + } + if (-not $IncludeRuntime) { + $patterns += "server/runtime" + } + if (-not $IncludeUploads) { + $patterns += "server/public/uploads" + } + if (-not $IncludePublicBuilds) { + $patterns += @("server/public/admin", "server/public/mobile") + } + if (-not $IncludeCrawlerData) { + $patterns += @( + "server/public/dongqiudi-crawler/data", + "server/public/dongqiudi-crawler/logs", + "server/public/dongqiudi-crawler/venv", + "server/public/dongqiudi-crawler/__pycache__" + ) + } + } + "server" { + if (-not $IncludeEnv) { + $patterns += ".env" + } + if (-not $IncludeVendor) { + $patterns += "vendor" + } + if (-not $IncludeRuntime) { + $patterns += "runtime" + } + if (-not $IncludeUploads) { + $patterns += "public/uploads" + } + if (-not $IncludePublicBuilds) { + $patterns += @("public/admin", "public/mobile") + } + if (-not $IncludeCrawlerData) { + $patterns += @( + "public/dongqiudi-crawler/data", + "public/dongqiudi-crawler/logs", + "public/dongqiudi-crawler/venv", + "public/dongqiudi-crawler/__pycache__" + ) + } + } + "uniapp" { + if (-not $IncludeEnv) { + $patterns += ".env" + } + } + } + + return @($patterns) +} + function Commit-SourceChangesIfNeeded { param( [string]$Repository, @@ -220,12 +333,12 @@ function Commit-SourceChangesIfNeeded { $unmerged = @($statusLines | Where-Object { $_ -match "^(DD|AU|UD|UA|DU|AA|UU)" }) if ($unmerged.Count -gt 0) { - Write-DeployLog "unmerged changes detected under ${SourceDir}:" + Write-DeployLog "unmerged changes detected under $(Get-SourceLabel):" $unmerged | Select-Object -First 20 | ForEach-Object { Write-Host " $_" } - Fail "resolve merge conflicts under $SourceDir before deploy" + Fail "resolve merge conflicts under $(Get-SourceLabel) before deploy" } - Write-DeployLog "uncommitted changes detected under $SourceDir; auto committing before deploy:" + Write-DeployLog "uncommitted changes detected under $(Get-SourceLabel); auto committing before deploy:" $maxDisplay = 80 $statusLines | Select-Object -First $maxDisplay | ForEach-Object { Write-Host " $_" } if ($statusLines.Count -gt $maxDisplay) { @@ -234,9 +347,18 @@ function Commit-SourceChangesIfNeeded { Invoke-Native "git" (@("-C", $Repository, "add", "-A", "--") + $Pathspec) + & git -C $Repository diff --cached --quiet -- $Pathspec + if ($LASTEXITCODE -eq 0) { + Write-DeployLog "no committable root changes detected after git add; continue with current working tree payload" + return + } + if ($LASTEXITCODE -ne 1) { + Fail "git diff --cached failed" + } + $commitMessage = $AutoCommitMessage if ([string]::IsNullOrWhiteSpace($commitMessage)) { - $commitMessage = "deploy: auto commit $SourceDir changes $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" + $commitMessage = "deploy: auto commit $(Get-SourceLabel) changes $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" } Invoke-Native "git" (@("-C", $Repository, "commit", "-m", $commitMessage, "--") + $Pathspec) @@ -244,55 +366,42 @@ function Commit-SourceChangesIfNeeded { Write-DeployLog "auto commit created: $newCommit" } -function Copy-FullPayload { - Write-DeployLog "building full payload from current working tree: $SourceDir" - Write-DeployLog "full include paths: $($FullIncludePaths -join ' ')" +function Copy-WorkingTreePayloadPaths { + param( + [string[]]$IncludePaths, + [string]$Reason + ) + + if ($IncludePaths.Count -eq 0) { + return + } + + Write-DeployLog $Reason + Write-DeployLog "working tree include paths: $($IncludePaths -join ' ')" $payloadTar = Join-Path $WorkDir "payload-copy.tar" $tarArgs = @( "-cf", $payloadTar, - "-C", $SourceAbs, - "--exclude=.git", - "--exclude=.idea", - "--exclude=.vscode", - "--exclude=.tmp", - "--exclude=.gitignore", - "--exclude=*.log", - "--exclude=release-*.zip", - "--exclude=*.zip", - "--exclude=__pycache__", - "--exclude=*.pyc" + "-C", $SourceAbs ) - if (-not $IncludeEnv) { - $tarArgs += "--exclude=.env" - } - if (-not $IncludeVendor) { - $tarArgs += "--exclude=vendor" - } - if (-not $IncludeRuntime) { - $tarArgs += "--exclude=runtime" - } - if (-not $IncludeUploads) { - $tarArgs += "--exclude=public/uploads" - } - if (-not $IncludePublicBuilds) { - $tarArgs += @("--exclude=public/admin", "--exclude=public/mobile") - } - if (-not $IncludeCrawlerData) { - $tarArgs += @( - "--exclude=public/dongqiudi-crawler/data", - "--exclude=public/dongqiudi-crawler/logs", - "--exclude=public/dongqiudi-crawler/venv", - "--exclude=public/dongqiudi-crawler/__pycache__" - ) + foreach ($pattern in (Get-ArchiveExcludePatterns)) { + if (-not [string]::IsNullOrWhiteSpace($pattern)) { + $tarArgs += "--exclude=$pattern" + } } - $tarArgs += $FullIncludePaths + $tarArgs += $IncludePaths Invoke-Native "tar" $tarArgs Invoke-Native "tar" @("-xf", $payloadTar, "-C", $PayloadDir) } +function Copy-FullPayload { + Copy-WorkingTreePayloadPaths ` + -IncludePaths $FullIncludePaths ` + -Reason "building full payload from current working tree: $(Get-SourceLabel)" +} + function Strip-SourcePrefix { param([string]$Path) if ([string]::IsNullOrEmpty($GitPathPrefix)) { @@ -308,6 +417,9 @@ function Strip-SourcePrefix { function Test-DeployIncludedPath { param([string]$RelativePath) foreach ($include in $FullIncludePaths) { + if ($include -eq ".") { + return $true + } if ($RelativePath -eq $include -or $RelativePath.StartsWith("$include/")) { return $true } @@ -315,6 +427,25 @@ function Test-DeployIncludedPath { return $false } +function Get-RootGitLinkIncludePaths { + if (-not (Test-IsRepoRootDeploy)) { + return @() + } + + $lsFiles = @(& git -C $RepoRoot ls-files -s -- $FullIncludePaths) + if ($LASTEXITCODE -ne 0) { + Fail "git ls-files failed" + } + + return @( + $lsFiles | + Where-Object { $_ -match "^160000\s+\S+\s+\d+\t(.+)$" } | + ForEach-Object { $Matches[1] } | + Where-Object { Test-DeployIncludedPath $_ } | + Sort-Object -Unique + ) +} + function Test-UnderSourceDir { param([string]$GitPath) if ([string]::IsNullOrEmpty($GitPathPrefix)) { @@ -401,6 +532,11 @@ function Build-IncrementalPayload { return } + $rootGitLinkPayloadPaths = @() + if (Test-IsRepoRootDeploy) { + $rootGitLinkPayloadPaths = @($RootGitLinkIncludePaths) + } + $diffArgs = @("-C", $GitCwd, "diff", "--name-status", "--find-renames", $FromCommit, $ToCommit) if (-not [string]::IsNullOrEmpty($GitPathPrefix)) { $diffArgs += @("--", $GitPathPrefix) @@ -448,11 +584,15 @@ function Build-IncrementalPayload { } } - $script:ChangedDisplayPaths = @($script:ChangedDisplayPaths | Sort-Object -Unique) $script:DeletedDisplayPaths = @($script:DeletedDisplayPaths | Sort-Object -Unique) $changedGitPaths = @($changedGitPaths | Sort-Object -Unique) - if ($changedGitPaths.Count -eq 0 -and $script:DeletedDisplayPaths.Count -eq 0) { + if ($rootGitLinkPayloadPaths.Count -gt 0) { + $script:ChangedDisplayPaths += @($rootGitLinkPayloadPaths | ForEach-Object { "$_/" }) + } + $script:ChangedDisplayPaths = @($script:ChangedDisplayPaths | Sort-Object -Unique) + + if ($changedGitPaths.Count -eq 0 -and $script:DeletedDisplayPaths.Count -eq 0 -and $rootGitLinkPayloadPaths.Count -eq 0) { Write-DeployLog "no changed files detected between $FromCommit and $ToCommit under $SourceDir" $script:NoChanges = $true return @@ -477,11 +617,18 @@ function Build-IncrementalPayload { Get-ChildItem -LiteralPath $extractSource -Force | Copy-Item -Destination $PayloadDir -Recurse -Force } } + + if ($rootGitLinkPayloadPaths.Count -gt 0) { + Copy-WorkingTreePayloadPaths ` + -IncludePaths $rootGitLinkPayloadPaths ` + -Reason "building working tree payload for root gitlink include paths" + } } $SourceDir = Normalize-DeployDir $SourceDir $RemoteDir = Normalize-DeployDir $RemoteDir $RemoteTmp = Normalize-DeployDir $RemoteTmp +$SourceLabel = Get-SourceLabel if ($AutoIncremental -and ($From -or $To)) { Fail "-AutoIncremental cannot be used with -From/-To" @@ -519,7 +666,7 @@ if (-not (Test-Path -LiteralPath $SourceAbs -PathType Container)) { Fail "source directory not found: $SourceDir" } -$FullIncludePaths = @("app", "config", "extend", "public", "route") +$FullIncludePaths = @(Get-FullIncludePaths) $GitCwd = $RepoRoot $GitPathPrefix = $SourceDir $SourceIsGitRepo = $false @@ -543,6 +690,10 @@ if (-not $SourceIsGitRepo -and ($lsFiles | Where-Object { $_ -match "^160000\s" } $DeployPathspec = @(Get-DeployPathspec) +$RootGitLinkIncludePaths = @(Get-RootGitLinkIncludePaths) +if ($RootGitLinkIncludePaths.Count -gt 0) { + Write-DeployLog "root gitlink include paths will be copied from working tree: $($RootGitLinkIncludePaths -join ' ')" +} Commit-SourceChangesIfNeeded -Repository $GitCwd -Pathspec $DeployPathspec if ($Mode -eq "incremental") { @@ -564,7 +715,7 @@ $WorkDir = Join-Path $TempRoot ("deploy-server-" + [System.Guid]::NewGuid().ToSt $PackageRoot = Join-Path $WorkDir "package" $PayloadDir = Join-Path $PackageRoot "payload" $ExtractDir = Join-Path $WorkDir "extract" -$ArchivePath = Join-Path $WorkDir ("{0}-{1}.tar.gz" -f ($SourceDir -replace "[\\/]", "-"), $Mode) +$ArchivePath = Join-Path $WorkDir ("{0}-{1}.tar.gz" -f $SourceLabel, $Mode) $DeletedPathsFile = Join-Path $PackageRoot ".deleted-paths.txt" $DeployInfoFile = Join-Path $PackageRoot ".deploy-info.txt" $script:ChangedDisplayPaths = @() @@ -588,9 +739,10 @@ try { $deployInfo = @( "mode=$Mode", - "source_dir=$SourceDir", + "source_dir=$SourceLabel", "host=$HostName", "remote_dir=$RemoteDir", + "uniapp_release_path=$UniappReleasePath", "built_at=$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" ) if ($Mode -eq "incremental") { @@ -619,8 +771,8 @@ try { $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $pidValue = $PID - $remoteWorkDir = "$RemoteTmp/$($SourceDir -replace '[\\/]', '-')-deploy-$timestamp-$pidValue" - $remoteArchive = "$RemoteTmp/$($SourceDir -replace '[\\/]', '-')-$Mode-$timestamp.tar.gz" + $remoteWorkDir = "$RemoteTmp/$SourceLabel-deploy-$timestamp-$pidValue" + $remoteArchive = "$RemoteTmp/$SourceLabel-$Mode-$timestamp.tar.gz" $remoteTmpQ = ConvertTo-RemoteSingleQuoted $RemoteTmp $remoteArchiveQ = ConvertTo-RemoteSingleQuoted $remoteArchive @@ -640,6 +792,7 @@ try { "REMOTE_DIR=$(ConvertTo-RemoteSingleQuoted $RemoteDir)", "REMOTE_ARCHIVE=$(ConvertTo-RemoteSingleQuoted $remoteArchive)", "REMOTE_WORK_DIR=$(ConvertTo-RemoteSingleQuoted $remoteWorkDir)", + "UNIAPP_RELEASE_PATH=$(ConvertTo-RemoteSingleQuoted $UniappReleasePath)", "KEEP_REMOTE_TEMP='$keepRemoteTempValue'", "POST_INSTALL_B64='$postInstallB64'", "USE_SUDO='$useSudoValue'", @@ -762,8 +915,15 @@ if [[ -n "${POST_INSTALL_B64:-}" ]]; then remote_log "run post-install command" POST_INSTALL_CMD="$(printf '%s' "$POST_INSTALL_B64" | base64 -d)" cd "$REMOTE_DIR" + if [[ -n "${UNIAPP_RELEASE_PATH:-}" ]]; then + export UNIAPP_RELEASE_PATH + fi if [[ "${USE_SUDO:-0}" == "1" ]]; then - sudo bash -lc "$POST_INSTALL_CMD" + if [[ -n "${UNIAPP_RELEASE_PATH:-}" ]]; then + sudo env UNIAPP_RELEASE_PATH="$UNIAPP_RELEASE_PATH" bash -lc "$POST_INSTALL_CMD" + else + sudo bash -lc "$POST_INSTALL_CMD" + fi else bash -lc "$POST_INSTALL_CMD" fi @@ -782,7 +942,7 @@ remote_log "remote install finished" Write-DeployLog "deploy finished" Write-DeployLog "mode: $Mode" - Write-DeployLog "source: $SourceDir" + Write-DeployLog "source: $SourceLabel" Write-DeployLog "remote: ${HostName}:$RemoteDir" } finally { if ($WorkDir -and (Test-Path -LiteralPath $WorkDir)) { diff --git a/业务进度管理.md b/业务进度管理.md index d155896..1d869a8 100644 --- a/业务进度管理.md +++ b/业务进度管理.md @@ -13,6 +13,81 @@ ## 一、已完成事项 +### 68. 默认部署切换为根目录同时上传 server、uniapp 与 admin + +- 状态:已完成 - 时间:2026-06-12 + +完成内容: +- `scripts/deploy-server.ps1` 默认 `SourceDir` 改为仓库根目录 `.`,不再先发 `server` 再补发前端目录,而是直接以根目录为源打一个包。 +- 根目录默认只包含 `server/`、`uniapp/` 与 `admin/` 三个子目录,因此远端 `RemoteDir` 下会直接形成三套源码目录,方便后续按 `server/public`、`uniapp`、`admin` 分别配置 Nginx 和构建流程。 +- 根目录模式下,排除规则已同步切到子目录路径:会继续默认排除 `server/.env`、`server/vendor`、`server/runtime`、`server/public/uploads`、`server/public/admin`、`server/public/mobile`、crawler 大文件目录,以及 `uniapp/node_modules`、`uniapp/dist`、`admin/node_modules`、`admin/dist`。 +- 默认增量发布也切到根目录范围,`-AutoIncremental` / `-AutoIncrementalCommits` / `-From` / `-To` 会同时跟踪 `server`、`uniapp` 与 `admin` 的 Git 变更和删除文件。 +- 若根仓库里某个默认目录仍是 gitlink/submodule 记录,脚本会把该目录从当前工作区按源码目录补进增量包,避免 `admin` 这类目录在转换期间漏传。 +- `README.md` 已同步更新根目录发布、远端目录结构以及在远端根目录内执行 `cd uniapp && npm install && npm run build:h5` 的示例。 + +涉及模块: +- `scripts/deploy-server.ps1` +- `README.md` + +验证记录: +- 已在临时最小 Git 仓库执行默认 `powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -DryRun -RemoteDir /tmp/test`,修改前日志只出现 `server` 打包,确认红灯。 +- 修改后已在临时最小 Git 仓库执行默认 `powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -DryRun -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /tmp/test`,日志已同时出现 `server/app/ok.txt`、`uniapp/src/App.vue` 与 `admin/src/main.ts` 的增量变更,确认根目录模式能同时覆盖三套源码。 +- 已补跑 `powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -SourceDir server -DryRun -RemoteDir /tmp/test`,确认显式单独发布 `server` 的老用法仍可继续使用。 +- 已在真实仓库执行 `powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -DryRun -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/test-server.sbnews.net`,日志出现 `root gitlink include paths will be copied from working tree: admin` 与 `+ admin/`,确认 `admin` 仍是 gitlink 记录时也会被补进增量包。 +- 已在真实仓库执行默认全量 DryRun,日志显示 `working tree include paths: server uniapp admin`,确认全量包也会同时包含三套源码。 + +### 67. 部署脚本支持发布 uniapp 源码到服务器 + +- 状态:已完成 - 时间:2026-06-12 + +完成内容: +- `scripts/deploy-server.ps1` 现在支持 `-SourceDir uniapp`,可将 `uniapp` 源码按独立目录发布到服务器,不再只支持 `server` 后端目录。 +- `uniapp` 全量打包默认改为包含整个源码目录,并排除 `node_modules`、`dist`、`.hbuilderx` 等不应上传的本地构建/IDE 目录;增量发布也可正确识别 `uniapp` 目录下的 Git 变更与删除文件。 +- 部署脚本新增 `-UniappReleasePath`,会在远端执行 `-PostInstall` 时通过环境变量 `UNIAPP_RELEASE_PATH` 传给 `uniapp/scripts/release.mjs`,让服务器构建 `npm run build:h5` 后可把 H5 产物直接写入指定站点的 `public/mobile`。 +- `uniapp/scripts/release.mjs` 新增 `UNIAPP_RELEASE_PATH` 覆盖能力,本地构建仍默认输出到 `../server/public/mobile`,服务端构建时则可改写到测试站真实目录。 +- `README.md` 已补充“发布 uniapp 源码到服务器并在服务器构建”的标准命令示例与 Node.js 版本要求。 + +涉及模块: +- `scripts/deploy-server.ps1` +- `uniapp/scripts/release.mjs` +- `README.md` + +验证记录: +- 已先执行 `powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -SourceDir uniapp -RemoteDir /tmp/sport-era-uniapp -DryRun`,修改前失败,确认原脚本不支持 `uniapp` 目录打包。 +- 修改后已执行 `powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -SourceDir uniapp -RemoteDir /tmp/sport-era-uniapp -DryRun`,全量打包演练通过。 +- 已执行 `git diff --check -- scripts/deploy-server.ps1 uniapp/scripts/release.mjs README.md 业务进度管理.md`,未发现空白错误。 +- 已通过 SSH 检查测试服当前缺少 `node` / `npm`,因此本次仅验证到“源码可发布 + 远端构建命令已就绪”,尚未在测试服实际执行 `npm install` / `npm run build:h5`。 + +### 66. 管理端评论账号列表与统一评论列表 + +- 状态:已完成 - 时间:2026-06-12 + +完成内容: +- 管理端新增“评论账号”后台契约与页面,后端 `community.commentAccount/lists` 仅筛选 `ai_comment_*` 虚拟评论账号,返回账号、昵称、人设标签、禁用状态、资讯评论数、社区评论数和总评论数。 +- 现有“社区评论管理”升级为统一评论列表,不再只看社区帖子评论;现在同一页面可同时查看资讯评论与社区评论,并按评论类型、虚拟/真实评论、状态、目标 ID、关键词筛选。 +- 统一评论列表后端通过合并 `la_article_comment` 与 `la_community_comment` 输出统一字段:`comment_type`、`source_type_desc`、`target_id`、`target_title`、`target_summary`、`target_owner_nickname`、`account`、`is_virtual`、`user_type_desc`、`status` 等。 +- 评论审核与删除逻辑已扩展到两类评论:资讯评论操作 `la_article_comment.is_show` 与 `la_article.comment_count`,社区评论操作 `la_community_comment.status` 与 `la_community_post.comment_count`。 +- 新增菜单 SQL `docs/sql/insert_menu_comment_account.sql`,用于把“评论账号”挂到现有“社区管理”菜单下;统一评论列表继续复用原有 `community.communityComment/lists` 菜单入口。 + +涉及模块: +- `server/app/adminapi/controller/community/CommunityCommentController.php` +- `server/app/adminapi/lists/community/CommunityCommentLists.php` +- `server/app/adminapi/logic/community/CommunityCommentLogic.php` +- `server/app/adminapi/validate/community/CommunityCommentValidate.php` +- `server/app/adminapi/controller/community/CommentAccountController.php` +- `server/app/adminapi/lists/community/CommentAccountLists.php` +- `admin/src/api/community.ts` +- `admin/src/views/community/comment/index.vue` +- `admin/src/views/community/account/index.vue` +- `docs/sql/insert_menu_comment_account.sql` +- `qa/backend/test_comment_admin_static.php` + +验证记录: +- 已先执行 `php qa/backend/test_comment_admin_static.php`,新增文件前失败,提示缺少 `admin/src/views/community/account/index.vue`。 +- 修改后已执行 `php qa/backend/test_comment_admin_static.php`,静态检查通过。 +- 已执行 `php -l` 检查 `CommunityCommentController.php`、`CommunityCommentLogic.php`、`CommunityCommentLists.php`、`CommunityCommentValidate.php`、`CommentAccountController.php`、`CommentAccountLists.php`,语法检查通过。 +- 已在 `admin` 目录执行 `npm run build`,构建通过并刷新 `server/public/admin` 产物。 + ### 65. Docker AI评论调度接入帖子与资讯 - 状态:已完成 - 时间:2026-06-12 @@ -613,6 +688,7 @@ - 排查 `scripts/deploy-server.ps1` 全量部署测试服时报 `tar: Cannot change mode ... Operation not permitted`,确认 SSH 用户 `ubuntu` 仅通过 `www` 组拥有目标目录写权限,并不是目录 owner,GNU tar 覆盖解包目录项时会尝试 `chmod` 已有目录导致失败。 - 远端安装 payload 改为优先使用 `rsync -r --links --no-perms --no-owner --no-group`,不保留 owner/group/perms;远端无 rsync 时回退为 `find + mkdir/cp/ln` 逐文件复制,避免对已有目录执行 chmod。 - 修复通过 `ssh bash -s` 下发远端 Bash 文本时可能写入 UTF-8 BOM 的问题,改为用 `UTF8 No BOM` 字节流写入 stdin,避免 `set -euo pipefail` 失效导致部署“假成功”。 +- 2026-06-12 再次排查测试服增量部署时报 `invalid option namepefail`,根因是远端 Bash 脚本 stdin 保留了 Windows `CRLF`,导致 `set -euo pipefail` 被识别成 `pipefail\r` 非法选项;现已在写入 stdin 前统一转换为 `LF`。 - 保留当前全量部署包含整个 `public` 目录的变更,解决包含更多已有目录后触发的部署失败。 涉及模块: