feat: 文传易匿名临时文件传输站初始版本
- 后端 .NET 8 + FreeSql + 阿里云 OSS,支持共享/私密/标签三种文件模式 - 前端 Vue3 + Vite + TDesign,上传/取件/管理三页 - 取件码/二维码/海报合成(含保存二维码为图片) - deploy/ 部署文档与一键 FTP 发布脚本 - appsettings.json 含真实凭据,已 gitignore 不入库
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 文传易 · IIS 部署与 FTP 发布手册
|
||||
|
||||
> 目标环境:Windows Server + IIS 10 + .NET 8 Hosting Bundle
|
||||
> 站点地址:`https://wenchuanyi.bbitcn.net`
|
||||
> 发布方式:FTP(`ftp://116.198.221.125`,默认端口 21,用户 `wenchuanyi`)
|
||||
|
||||
> **当前状态(2026-08-23)**:本地全流程已通过冒烟测试——
|
||||
> 上传(共享/私密/标签,含 200MB 拦截)→ 二维码与取件码 → 取件查询/下载(计数)→ 标签列表 → 管理列表(含上传 IP)/删除,均已验证;数据库表已建、OSS 真实凭据已写入 `appsettings.json`(该文件不入库)。服务器上首次发布后,建议用手机微信扫码访问正式站再做一轮真机验证(含微信聊天记录选文件)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 部署架构
|
||||
|
||||
```
|
||||
浏览器
|
||||
│ https://wenchuanyi.bbitcn.net
|
||||
▼
|
||||
IIS 站点(物理路径 = 站点根目录)
|
||||
├── index.html / assets/* ← 前端 dist 构建产物
|
||||
├── web.config ← 由 dotnet publish 自动生成(ANCM InProcess)
|
||||
├── WenChuanyi.Api.dll ← 后端发布输出
|
||||
└── appsettings.json ← 数据库连接串 / OSS 凭据 / 上传限制
|
||||
```
|
||||
|
||||
- 前端静态资源与后端发布输出**放在同一 IIS 站点物理路径**(默认文件夹或 `wwwroot`)。
|
||||
- 后端 `UseStaticFiles` 提供前端静态文件,`MapFallbackToFile("index.html")` 兜底 SPA 路由;
|
||||
`/api` 与 `/api/open` 由 ASP.NET Core Module(ANCM)直接处理。
|
||||
- 站点根目录布局见上;后端发布产物全部文件与前端 `dist/` 内容合并进同一目录。
|
||||
|
||||
---
|
||||
|
||||
## 2. 服务器一次性准备(首次部署前)
|
||||
|
||||
1. **安装 .NET 8 Hosting Bundle**
|
||||
- 下载:https://dotnet.microsoft.com/download/dotnet/8.0(选择 **Hosting Bundle**)
|
||||
- 安装后 IIS 中会出现 **ASP.NET Core Module v2**。
|
||||
2. **创建 IIS 站点**
|
||||
- IIS → 右键「网站」→ 添加网站;
|
||||
- 站点名称:`wenchuanyi`;
|
||||
- 物理路径:`D:\wenchuanyi`(或任意磁盘,站点根目录);
|
||||
- 端口 80(HTTP)→ 后续绑定 443 + 证书,域名 `wenchuanyi.bbitcn.net`;
|
||||
- 应用程序池:.NET CLR 版本选「**无托管代码**」(InProcess 托管由 ANCM 处理)。
|
||||
3. **HTTPS 证书**:为 `wenchuanyi.bbitcn.net` 绑定 SSL 证书(企业已有证书或申请免费证书)。
|
||||
4. **防火墙**:放行 80 / 443(及测试期 5280)。
|
||||
|
||||
> 排障:若站点 502.5 / 500.30,临时开启 web.config 中的 `stdoutLogEnabled="true"` 查看 `stdoutLog` 输出。
|
||||
|
||||
---
|
||||
|
||||
## 3. 构建发布包(开发机执行)
|
||||
|
||||
### 3.1 后端
|
||||
|
||||
```powershell
|
||||
cd backend/WenChuanyi.Api
|
||||
dotnet publish -c Release
|
||||
```
|
||||
|
||||
- 输出目录:`bin/Release/net8.0/publish/`
|
||||
- Framework-dependent 模式,web.config 自动生成(`hostingModel="inprocess"`)。
|
||||
- 发布前确认 `appsettings.json` 已配置真实凭据(见第 4 节)。
|
||||
|
||||
### 3.2 前端
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
npm install # 首次
|
||||
npm run build
|
||||
```
|
||||
|
||||
- 输出目录:`frontend/dist/`(index.html + assets/*)。
|
||||
- 构建目标 ES2018,兼容微信 X5 内核。
|
||||
|
||||
---
|
||||
|
||||
## 4. 敏感配置(appsettings.json)
|
||||
|
||||
`backend/WenChuanyi.Api/appsettings.json` 中需填写真实值,**该文件已在 .gitignore 中,不会入库**:
|
||||
|
||||
| 配置节 | 键 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `ConnectionStrings:MySql` | Password | 数据库密码(`r7P^f*v7rFts`) |
|
||||
| `Oss` | AccessKeyId | 阿里云 OSS AK |
|
||||
| `Oss` | AccessKeySecret | 阿里云 OSS SK |
|
||||
| `OpenApi` | UploadRoot | 公开 API 可读取的服务端目录白名单 |
|
||||
| `App` | BaseUrl | 站点地址,用于生成取件页链接(默认 `https://wenchuanyi.bbitcn.net`) |
|
||||
|
||||
- 连接串密码含 `^`、`*`,不含 `;` / `=`,无需特殊转义,JSON 原样写入即可。
|
||||
- 模板参考:`appsettings.example.json`(占位符 `YOUR_DB_PASSWORD` / `YOUR_ACCESS_KEY_ID` / `YOUR_ACCESS_KEY_SECRET`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. FTP 发布
|
||||
|
||||
### 5.1 手动发布
|
||||
|
||||
用任意 FTP 客户端(FileZilla / WinSCP 等,**被动模式**)登录:
|
||||
|
||||
- 主机:`116.198.221.125`
|
||||
- 端口:`21`
|
||||
- 用户:`wenchuanyi`
|
||||
- 密码:同数据库密码
|
||||
|
||||
上传清单(目标 = IIS 站点根目录,如 `D:\wenchuanyi`):
|
||||
|
||||
```
|
||||
前端 dist/ 全部文件 → 站点根目录
|
||||
后端 bin/Release/net8.0/publish/* → 站点根目录(覆盖)
|
||||
```
|
||||
|
||||
> 注意:web.config、appsettings.json 等必须位于站点根目录,勿放入子文件夹。
|
||||
|
||||
### 5.2 一键脚本发布
|
||||
|
||||
已提供 `deploy/publish.ps1`,一条命令完成「后端 publish + 前端 build + FTP 上传」:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File deploy/publish.ps1
|
||||
```
|
||||
|
||||
### 5.3 发布后生效
|
||||
|
||||
- FTP 上传完成后,**回收应用程序池**使新版本生效:
|
||||
- IIS 管理器 → 应用程序池 → `wenchuanyi` → 右键「回收」;
|
||||
- 或命令行:`C:\Windows\System32\inetsrv\appcmd recycle apppool /apppool.name:wenchuanyi`
|
||||
- 浏览器验证 `https://wenchuanyi.bbitcn.net`:
|
||||
1. 首页正常显示;
|
||||
2. `/#/pickup`、`/#/admin` 路由可访问(hash 路由不受 IIS 影响);
|
||||
3. 上传一个测试文件 → 得到取件码 → 取件页下载成功 → 管理码列表可见。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证清单
|
||||
|
||||
| 项 | 验证方式 | 预期 |
|
||||
| --- | --- | --- |
|
||||
| 首页静态资源 | 访问 `/` | 页面正常渲染,无 404 |
|
||||
| SPA 路由 | 访问 `/#/pickup` | 取件页正常(hash 路由不受 IIS 影响) |
|
||||
| API | `GET /api/open/download/000000` | 返回 JSON(凭证不存在提示),非 404 |
|
||||
| 上传下载 | 上传 → 取件 → 下载 | 全流程通过,文件名还原 |
|
||||
| 在线预览 | 文本/PDF/图片/音视频 | 预览正常 |
|
||||
| 清理任务 | 查看日志 | 每 30 分钟扫描,无异常报错 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 常见问题
|
||||
|
||||
| 现象 | 原因与处理 |
|
||||
| --- | --- |
|
||||
| 502.5 / 500.30 | Hosting Bundle 未装或版本不匹配;或 appsettings.json 语法错误。检查事件查看器与 stdoutLog。 |
|
||||
| 403.14 | 站点根目录无默认文档且 ANCM 未接管——确认 web.config 在站点根目录。 |
|
||||
| 404 静态资源 | 前端产物未上传到站点根目录,或未启用 `UseStaticFiles`。 |
|
||||
| API 连接 MySQL 失败 | 服务器 3306 未放行 / 白名单未加服务器出口 IP。 |
|
||||
| 上传 OSS 失败 | OSS AK/SK 未填或权限不足;确认 Bucket `bbit-f8-web` 有 Put/Get/Delete 权限。 |
|
||||
| 修改不生效 | 上传后未回收应用程序池。 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全提示
|
||||
|
||||
- `appsettings.json` 含数据库密码与 OSS 凭据,**不要上传到代码仓库 / 不要外传**(已在 .gitignore)。
|
||||
- FTP 凭据仅运维持有,建议定期轮换。
|
||||
- 日志不会打印 OSS 凭据、数据库密码与文件内容明文。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 强制覆盖单个文件 (UTF-8 BOM)
|
||||
param([string]$LocalPath, [string]$RemotePath)
|
||||
$FtpHost="116.198.221.125"
|
||||
$FtpPort=21
|
||||
$FtpUser="wenchuanyi"
|
||||
$FtpPass="r7P^f*v7rFts"
|
||||
$r=[System.Net.FtpWebRequest]::Create("ftp://${FtpHost}:${FtpPort}/"+$RemotePath.TrimStart('/'))
|
||||
$r.Method=[System.Net.WebRequestMethods+Ftp]::UploadFile
|
||||
$r.Credentials=New-Object System.Net.NetworkCredential($FtpUser,$FtpPass)
|
||||
$r.UsePassive=$true
|
||||
$r.UseBinary=$true
|
||||
$r.KeepAlive=$false
|
||||
$r.Timeout=120000
|
||||
$b=[System.IO.File]::ReadAllBytes($LocalPath)
|
||||
$r.ContentLength=$b.Length
|
||||
$s=$r.GetRequestStream()
|
||||
try{ $s.Write($b,0,$b.Length) }finally{ $s.Close() }
|
||||
$resp=$r.GetResponse()
|
||||
try{ $resp.Close() }catch{}
|
||||
Write-Host "FORCED_UPLOAD_OK $RemotePath"
|
||||
@@ -0,0 +1,59 @@
|
||||
# FTP 上传脚本 (UTF-8 BOM) - 健壮版
|
||||
$FtpHost="116.198.221.125"
|
||||
$FtpPort=21
|
||||
$FtpUser="wenchuanyi"
|
||||
$FtpPass="r7P^f*v7rFts"
|
||||
$RemoteRoot="/"
|
||||
$Stage = Join-Path $env:TEMP "wcy_publish_staging"
|
||||
|
||||
function New-FtpReq([string]$path,[string]$method){
|
||||
$uri="ftp://${FtpHost}:${FtpPort}/"+$path.TrimStart('/')
|
||||
$r=[System.Net.FtpWebRequest]::Create($uri)
|
||||
$r.Method=$method
|
||||
$r.Credentials=New-Object System.Net.NetworkCredential($FtpUser,$FtpPass)
|
||||
$r.UsePassive=$true
|
||||
$r.UseBinary=$true
|
||||
$r.KeepAlive=$false
|
||||
$r.Timeout=120000
|
||||
return $r
|
||||
}
|
||||
function MkDir([string]$p){
|
||||
$r=New-FtpReq $p ([System.Net.WebRequestMethods+Ftp]::MakeDirectory)
|
||||
try{ $null=$r.GetResponse() }catch{}
|
||||
}
|
||||
function Exist([string]$p){
|
||||
$r=New-FtpReq $p ([System.Net.WebRequestMethods+Ftp]::GetDateTimestamp)
|
||||
try{ $null=$r.GetResponse(); return $true }catch{ return $false }
|
||||
}
|
||||
function UpFile([string]$lp,[string]$rp){
|
||||
$r=New-FtpReq $rp ([System.Net.WebRequestMethods+Ftp]::UploadFile)
|
||||
$b=[System.IO.File]::ReadAllBytes($lp)
|
||||
$r.ContentLength=$b.Length
|
||||
$s=$r.GetRequestStream()
|
||||
try{ $s.Write($b,0,$b.Length) }finally{ $s.Close() }
|
||||
$resp=$r.GetResponse()
|
||||
try{ $resp.Close() }catch{}
|
||||
}
|
||||
function UpDir([string]$ld,[string]$rd){
|
||||
foreach($d in Get-ChildItem $ld -Directory){
|
||||
$c="$rd/$($d.Name)"
|
||||
if(-not (Exist $c)){ MkDir $c }
|
||||
UpDir $d.FullName $c
|
||||
}
|
||||
foreach($f in Get-ChildItem $ld -File){
|
||||
$rf="$rd/$($f.Name)"
|
||||
try{
|
||||
if(-not (Exist $rf)){ UpFile $f.FullName $rf; Write-Host "UP $rf" }
|
||||
else { Write-Host "SKIP(exists) $rf" }
|
||||
}catch{
|
||||
Write-Host "RETRY $rf"
|
||||
try{ UpFile $f.FullName $rf; Write-Host "OK $rf" }catch{ Write-Host "FAIL $rf : $_" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(-not (Test-Path $Stage)){ Write-Host "STAGING_MISSING"; exit 1 }
|
||||
if(-not (Exist $RemoteRoot)){ MkDir $RemoteRoot }
|
||||
UpDir $Stage $RemoteRoot
|
||||
Write-Host "FTP_UPLOAD_DONE"
|
||||
Remove-Item $Stage -Recurse -Force
|
||||
@@ -0,0 +1,119 @@
|
||||
# ============================================================
|
||||
# 文传易 一键发布脚本
|
||||
# 1. dotnet publish 后端 (Release, net8.0)
|
||||
# 2. npm run build 前端 (Vite, dist)
|
||||
# 3. FTP 上传合并产物至 IIS 站点根目录
|
||||
#
|
||||
# 用法:
|
||||
# powershell -ExecutionPolicy Bypass -File deploy/publish.ps1
|
||||
#
|
||||
# 前置: 本机已安装 .NET 8 SDK / Node.js >=18; FTP 凭据见下方变量
|
||||
# ============================================================
|
||||
param(
|
||||
[string]$FtpHost = "116.198.221.125",
|
||||
[int] $FtpPort = 21,
|
||||
[string]$FtpUser = "wenchuanyi",
|
||||
[string]$FtpPass = "r7P^f*v7rFts",
|
||||
[string]$RemoteRoot = "/", # FTP 远端目标目录 (IIS 站点根目录)
|
||||
[switch]$SkipBuild # 跳过构建, 仅上传已有产物
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
$BackendDir = Join-Path $Root "backend\WenChuanyi.Api"
|
||||
$FrontendDir = Join-Path $Root "frontend"
|
||||
$PublishDir = Join-Path $BackendDir "bin\Release\net8.0\publish"
|
||||
$DistDir = Join-Path $FrontendDir "dist"
|
||||
$StagingDir = Join-Path $env:TEMP "wcy_publish_staging"
|
||||
|
||||
# ---------------- 1. 构建 ----------------
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "==> [1/3] 构建后端..." -ForegroundColor Cyan
|
||||
Push-Location $BackendDir
|
||||
try { dotnet publish -c Release --nologo -v q }
|
||||
finally { Pop-Location }
|
||||
|
||||
Write-Host "==> [2/3] 构建前端..." -ForegroundColor Cyan
|
||||
if (-not (Test-Path (Join-Path $FrontendDir "node_modules"))) {
|
||||
Write-Host " 首次构建, 安装 npm 依赖..." -ForegroundColor Yellow
|
||||
Push-Location $FrontendDir
|
||||
try { npm install --no-audit --no-fund }
|
||||
finally { Pop-Location }
|
||||
}
|
||||
Push-Location $FrontendDir
|
||||
try { npm run build }
|
||||
finally { Pop-Location }
|
||||
} else {
|
||||
Write-Host "==> [1-2/3] 跳过构建 (SkipBuild)..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (-not (Test-Path $PublishDir)) { throw "后端发布目录不存在: $PublishDir (请先 dotnet publish)" }
|
||||
if (-not (Test-Path $DistDir)) { throw "前端产物目录不存在: $DistDir (请先 npm run build)" }
|
||||
|
||||
# ---------------- 2. 合并产物到临时目录 ----------------
|
||||
Write-Host "==> [3/3] 合并产物并 FTP 上传..." -ForegroundColor Cyan
|
||||
if (Test-Path $StagingDir) { Remove-Item $StagingDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $StagingDir -Force | Out-Null
|
||||
|
||||
Copy-Item "$DistDir\*" $StagingDir -Recurse -Force
|
||||
Copy-Item "$PublishDir\*" $StagingDir -Recurse -Force
|
||||
|
||||
Write-Host " 产物合计: $((Get-ChildItem $StagingDir -File -Recurse | Measure-Object).Count) 个文件" -ForegroundColor Gray
|
||||
|
||||
# ---------------- FTP 工具函数 ----------------
|
||||
function New-FtpRequest([string]$path, [string]$method) {
|
||||
$uri = "ftp://${FtpHost}:${FtpPort}/" + $path.TrimStart('/')
|
||||
$req = [System.Net.FtpWebRequest]::Create($uri)
|
||||
$req.Method = $method
|
||||
$req.Credentials = New-Object System.Net.NetworkCredential($FtpUser, $FtpPass)
|
||||
$req.UsePassive = $true
|
||||
$req.UseBinary = $true
|
||||
$req.KeepAlive = $false
|
||||
return $req
|
||||
}
|
||||
|
||||
function New-FtpDirectory([string]$remotePath) {
|
||||
$req = New-FtpRequest $remotePath ([System.Net.WebRequestMethods+Ftp]::MakeDirectory)
|
||||
try { $null = $req.GetResponse() }
|
||||
catch { } # 目录已存在时报错, 忽略
|
||||
}
|
||||
|
||||
function Send-FtpFile([string]$localPath, [string]$remotePath) {
|
||||
$req = New-FtpRequest $remotePath ([System.Net.WebRequestMethods+Ftp]::UploadFile)
|
||||
$bytes = [System.IO.File]::ReadAllBytes($localPath)
|
||||
$req.ContentLength = $bytes.Length
|
||||
$stream = $req.GetRequestStream()
|
||||
try {
|
||||
$stream.Write($bytes, 0, $bytes.Length)
|
||||
} finally {
|
||||
$stream.Close()
|
||||
}
|
||||
$resp = $req.GetResponse()
|
||||
try { $resp.Close() } catch { }
|
||||
}
|
||||
|
||||
function Send-FtpDirectory([string]$localDir, [string]$remoteDir) {
|
||||
foreach ($dir in Get-ChildItem $localDir -Directory) {
|
||||
$child = "$remoteDir/$($dir.Name)"
|
||||
New-FtpDirectory $child
|
||||
Send-FtpDirectory $dir.FullName $child
|
||||
}
|
||||
foreach ($file in Get-ChildItem $localDir -File) {
|
||||
$remoteFile = "$remoteDir/$($file.Name)"
|
||||
Send-FtpFile $file.FullName $remoteFile
|
||||
Write-Host " UP $remoteFile" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------- 上传 ----------------
|
||||
New-FtpDirectory $RemoteRoot
|
||||
Send-FtpDirectory $StagingDir $RemoteRoot
|
||||
|
||||
Remove-Item $StagingDir -Recurse -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " 发布完成! 请手动回收 IIS 应用程序池使新版本生效:" -ForegroundColor Green
|
||||
Write-Host " IIS 管理器 -> 应用程序池 -> wenchuanyi -> 右键回收" -ForegroundColor Green
|
||||
Write-Host " 或: appcmd recycle apppool /apppool.name:wenchuanyi" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user