chore(config): Improve HA orchestration and media synchronization

This commit is contained in:
2026-04-24 17:33:26 +08:00
parent 6a334c5273
commit 6c954542ac
11 changed files with 1935 additions and 7 deletions
+3 -2
View File
@@ -39,8 +39,9 @@ SENTINEL_HOST=sentinel
SENTINEL_PORT=8080 SENTINEL_PORT=8080
# MinIO配置 # MinIO配置
MINIO_HOST=minio # 方案A:所有上传统一写入 Node-01,本节点既是写入点也是读取点
MINIO_PORT=9000 MINIO_HOST=192.168.1.101
MINIO_PORT=9100
MINIO_EXTERNAL_PORT=9100 MINIO_EXTERNAL_PORT=9100
MINIO_ACCESS_KEY=replace-with-minio-access-key MINIO_ACCESS_KEY=replace-with-minio-access-key
MINIO_SECRET_KEY=replace-with-minio-secret-key MINIO_SECRET_KEY=replace-with-minio-secret-key
+3 -2
View File
@@ -39,8 +39,9 @@ SENTINEL_HOST=sentinel
SENTINEL_PORT=8080 SENTINEL_PORT=8080
# MinIO配置 # MinIO配置
MINIO_HOST=minio # 方案A:所有上传统一写入 Node-01,Node-02 仅作为同步副本
MINIO_PORT=9000 MINIO_HOST=192.168.1.101
MINIO_PORT=9100
MINIO_EXTERNAL_PORT=9100 MINIO_EXTERNAL_PORT=9100
MINIO_ACCESS_KEY=replace-with-minio-access-key MINIO_ACCESS_KEY=replace-with-minio-access-key
MINIO_SECRET_KEY=replace-with-minio-secret-key MINIO_SECRET_KEY=replace-with-minio-secret-key
+2 -2
View File
@@ -425,8 +425,8 @@ services:
- REDIS_SENTINEL_NODES=${REDIS_SENTINEL_NODES:-192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379} - REDIS_SENTINEL_NODES=${REDIS_SENTINEL_NODES:-192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379}
- ROCKETMQ_HOST=rmqnamesrv - ROCKETMQ_HOST=rmqnamesrv
- ROCKETMQ_PORT=9876 - ROCKETMQ_PORT=9876
- MINIO_HOST=minio - MINIO_HOST=192.168.1.101
- MINIO_PORT=9000 - MINIO_PORT=9100
- MINIO_ACCESS_KEY=minioadmin - MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin123 - MINIO_SECRET_KEY=minioadmin123
- MINIO_BUCKET=video-platform - MINIO_BUCKET=video-platform
+2
View File
@@ -331,6 +331,8 @@ services:
- MYSQL_PASSWORD=${MYSQL_ROOT_PASSWORD:-root123456} - MYSQL_PASSWORD=${MYSQL_ROOT_PASSWORD:-root123456}
- REDIS_PASSWORD=${REDIS_PASSWORD:-redis_HA_Cluster_2024} - REDIS_PASSWORD=${REDIS_PASSWORD:-redis_HA_Cluster_2024}
- REDIS_SENTINEL_NODES=${REDIS_SENTINEL_NODES:-192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379} - REDIS_SENTINEL_NODES=${REDIS_SENTINEL_NODES:-192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379}
- MINIO_HOST=192.168.1.101
- MINIO_PORT=9100
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY} - MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
- MINIO_BUCKET=${MINIO_BUCKET} - MINIO_BUCKET=${MINIO_BUCKET}
+763
View File
@@ -0,0 +1,763 @@
param(
[string]$VideoRoot = 'D:\视频库',
[string]$BaseUrl = 'http://192.168.1.101:9000',
[string]$OutputPath = '',
[string]$AdminUsername = 'admin',
[string]$AdminPassword = 'admin123',
[string]$DefaultUserPassword = 'Pass@123456',
[string]$RedisHost = '192.168.1.101',
[int]$RedisPort = 6379,
[string]$RedisPassword = '',
[switch]$ReuseExistingUsers
)
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $PSScriptRoot 'VIDEO_IMPORT_RESULTS.json'
}
function New-OrderedMap {
return [ordered]@{}
}
function ConvertTo-PlainObject {
param([object]$Value)
if ($null -eq $Value) {
return $null
}
if ($Value -is [System.Collections.IDictionary]) {
$result = [ordered]@{}
foreach ($key in $Value.Keys) {
$result[$key] = ConvertTo-PlainObject -Value $Value[$key]
}
return $result
}
if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
$items = @()
foreach ($item in $Value) {
$items += ,(ConvertTo-PlainObject -Value $item)
}
return $items
}
$type = $Value.GetType()
if ($type.FullName -like 'System.Management.Automation.PSCustomObject' -or
($type.Namespace -and $type.Namespace -notlike 'System*')) {
$result = [ordered]@{}
foreach ($prop in $Value.PSObject.Properties) {
$result[$prop.Name] = ConvertTo-PlainObject -Value $prop.Value
}
return $result
}
return $Value
}
function Save-Report {
param([hashtable]$Report)
$plain = ConvertTo-PlainObject -Value $Report
$plain | ConvertTo-Json -Depth 20 | Set-Content -Path $OutputPath -Encoding utf8BOM
}
function Invoke-JsonApi {
param(
[Parameter(Mandatory = $true)][string]$Method,
[Parameter(Mandatory = $true)][string]$Url,
[hashtable]$Headers = @{},
$Body = $null,
[int]$TimeoutSec = 180
)
$params = @{
Method = $Method
Uri = $Url
Headers = $Headers
UseBasicParsing = $true
SkipHttpErrorCheck = $true
TimeoutSec = $TimeoutSec
}
if ($null -ne $Body) {
$params.ContentType = 'application/json'
if ($Body -is [string]) {
$params.Body = $Body
} else {
$params.Body = ($Body | ConvertTo-Json -Depth 20)
}
}
try {
$response = Invoke-WebRequest @params
$json = $null
if ($response.Content) {
try {
$json = $response.Content | ConvertFrom-Json -Depth 50
} catch {
$json = $null
}
}
return [pscustomobject]@{
HttpStatus = [int]$response.StatusCode
Json = $json
Raw = $response.Content
}
} catch {
return [pscustomobject]@{
HttpStatus = -1
Json = $null
Raw = $_.Exception.Message
}
}
}
function Invoke-UploadApi {
param(
[Parameter(Mandatory = $true)][string]$Url,
[Parameter(Mandatory = $true)][string]$FilePath,
[hashtable]$Headers = @{},
[int]$TimeoutSec = 7200
)
$handler = $null
$httpClient = $null
$fileStream = $null
try {
$handler = New-Object System.Net.Http.HttpClientHandler
$httpClient = New-Object System.Net.Http.HttpClient($handler)
$httpClient.Timeout = [TimeSpan]::FromSeconds($TimeoutSec)
foreach ($headerKey in $Headers.Keys) {
[void]$httpClient.DefaultRequestHeaders.TryAddWithoutValidation($headerKey, [string]$Headers[$headerKey])
}
$multipart = New-Object System.Net.Http.MultipartFormDataContent
$fileStream = [System.IO.File]::OpenRead($FilePath)
$fileContent = New-Object System.Net.Http.StreamContent($fileStream)
$mimeType = switch ([System.IO.Path]::GetExtension($FilePath).ToLowerInvariant()) {
'.mp4' { 'video/mp4' }
'.mov' { 'video/quicktime' }
'.mkv' { 'video/x-matroska' }
'.avi' { 'video/x-msvideo' }
'.webm' { 'video/webm' }
'.m4v' { 'video/x-m4v' }
'.flv' { 'video/x-flv' }
default { 'video/mp4' }
}
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse($mimeType)
$multipart.Add($fileContent, 'file', [System.IO.Path]::GetFileName($FilePath))
$response = $httpClient.PostAsync($Url, $multipart).GetAwaiter().GetResult()
$raw = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
$json = $null
if ($raw) {
try {
$json = $raw | ConvertFrom-Json -Depth 50
} catch {
$json = $null
}
}
return [pscustomobject]@{
HttpStatus = [int]$response.StatusCode
Json = $json
Raw = $raw
}
} catch {
return [pscustomobject]@{
HttpStatus = -1
Json = $null
Raw = $_.Exception.Message
}
} finally {
if ($fileStream) { $fileStream.Dispose() }
if ($httpClient) { $httpClient.Dispose() }
if ($handler) { $handler.Dispose() }
}
}
function Read-DotEnv {
param([string]$Path)
$map = @{}
if (-not (Test-Path -LiteralPath $Path)) {
return $map
}
foreach ($line in Get-Content -LiteralPath $Path) {
if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) {
continue
}
$parts = $line -split '=', 2
if ($parts.Count -eq 2) {
$map[$parts[0].Trim()] = $parts[1].Trim()
}
}
return $map
}
function Read-RedisLine {
param([System.IO.Stream]$Stream)
$bytes = New-Object System.Collections.Generic.List[byte]
while ($true) {
$b = $Stream.ReadByte()
if ($b -lt 0) {
throw 'Redis 连接已关闭'
}
if ($b -eq 13) {
$next = $Stream.ReadByte()
if ($next -ne 10) {
throw 'Redis 响应格式错误'
}
break
}
$bytes.Add([byte]$b)
}
return [System.Text.Encoding]::UTF8.GetString($bytes.ToArray())
}
function Send-RedisCommand {
param(
[System.IO.Stream]$Stream,
[string[]]$RedisArgs
)
$builder = New-Object System.Text.StringBuilder
[void]$builder.Append("*$($RedisArgs.Count)`r`n")
foreach ($arg in $RedisArgs) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($arg)
[void]$builder.Append("$" + $bytes.Length + "`r`n")
[void]$builder.Append($arg + "`r`n")
}
$payload = [System.Text.Encoding]::UTF8.GetBytes($builder.ToString())
$Stream.Write($payload, 0, $payload.Length)
$Stream.Flush()
$prefix = [char]$Stream.ReadByte()
switch ($prefix) {
'+' { return Read-RedisLine -Stream $Stream }
'-' { throw "Redis 错误: $(Read-RedisLine -Stream $Stream)" }
':' { return [int64](Read-RedisLine -Stream $Stream) }
'$' {
$length = [int](Read-RedisLine -Stream $Stream)
if ($length -lt 0) {
return $null
}
$buffer = New-Object byte[] $length
$offset = 0
while ($offset -lt $length) {
$read = $Stream.Read($buffer, $offset, $length - $offset)
if ($read -le 0) {
throw 'Redis bulk string 读取失败'
}
$offset += $read
}
$null = Read-RedisLine -Stream $Stream
return [System.Text.Encoding]::UTF8.GetString($buffer)
}
'*' {
$count = [int](Read-RedisLine -Stream $Stream)
$items = @()
for ($i = 0; $i -lt $count; $i++) {
$nestedPrefix = [char]$Stream.ReadByte()
if ($nestedPrefix -ne '$') {
throw "暂不支持的 Redis 数组子类型: $nestedPrefix"
}
$len = [int](Read-RedisLine -Stream $Stream)
if ($len -lt 0) {
$items += $null
continue
}
$buf = New-Object byte[] $len
$offset = 0
while ($offset -lt $len) {
$read = $Stream.Read($buf, $offset, $len - $offset)
if ($read -le 0) {
throw 'Redis 数组元素读取失败'
}
$offset += $read
}
$null = Read-RedisLine -Stream $Stream
$items += [System.Text.Encoding]::UTF8.GetString($buf)
}
return $items
}
default { throw "未知 Redis 响应前缀: $prefix" }
}
}
function Get-AvailableRedisPasswords {
$candidates = New-Object System.Collections.Generic.List[string]
foreach ($candidate in @(
$RedisPassword,
'redis_HA_Cluster_2024',
'replace-with-strong-redis-password',
'redis_EsJ8FX'
)) {
if (-not [string]::IsNullOrWhiteSpace($candidate) -and -not $candidates.Contains($candidate)) {
$candidates.Add($candidate)
}
}
return $candidates
}
function Get-RedisValue {
param(
[Parameter(Mandatory = $true)][string]$Key,
[int]$RedisDbIndex = 0
)
$passwords = Get-AvailableRedisPasswords
$lastError = $null
foreach ($passwordCandidate in $passwords) {
$tcp = $null
try {
$tcp = [System.Net.Sockets.TcpClient]::new()
$tcp.ReceiveTimeout = 5000
$tcp.SendTimeout = 5000
$tcp.Connect($RedisHost, $RedisPort)
$stream = $tcp.GetStream()
[void](Send-RedisCommand -Stream $stream -RedisArgs @('AUTH', $passwordCandidate))
if ($RedisDbIndex -gt 0) {
[void](Send-RedisCommand -Stream $stream -RedisArgs @('SELECT', $RedisDbIndex.ToString()))
}
$value = Send-RedisCommand -Stream $stream -RedisArgs @('GET', $Key)
$tcp.Close()
return $value
} catch {
$lastError = $_.Exception.Message
if ($tcp) {
$tcp.Close()
}
}
}
throw "读取 Redis 键失败: $Key;host=${RedisHost}:$RedisPort;lastError=$lastError"
}
function Get-CaptchaCode {
param(
[Parameter(Mandatory = $true)][string]$CaptchaUrl,
[Parameter(Mandatory = $true)][string]$RedisKeyPrefix,
[int]$RedisDb = 0
)
$captchaResp = Invoke-JsonApi -Method GET -Url $CaptchaUrl
if ($captchaResp.HttpStatus -ne 200 -or -not $captchaResp.Json -or -not $captchaResp.Json.data) {
throw "获取验证码失败: $($captchaResp.Raw)"
}
$captchaKey = [string]$captchaResp.Json.data.captchaKey
$code = Get-RedisValue -Key "$RedisKeyPrefix$captchaKey" -RedisDbIndex $RedisDb
return [pscustomobject]@{
CaptchaKey = $captchaKey
CaptchaCode = ([string]$code).Trim('"').Trim()
}
}
function Ensure-ApiSuccess {
param(
[Parameter(Mandatory = $true)]$Response,
[Parameter(Mandatory = $true)][string]$Action
)
if ($Response.HttpStatus -ne 200) {
throw "$Action 失败,HTTP=$($Response.HttpStatus),响应=$($Response.Raw)"
}
if (-not $Response.Json) {
throw "$Action 失败,响应不是 JSON: $($Response.Raw)"
}
if ($Response.Json.code -ne 200) {
throw "$Action 失败,API code=$($Response.Json.code),message=$($Response.Json.message)"
}
}
function New-RandomPhone {
$digits = -join ((0..9) | Get-Random -Count 9)
return "13$digits"
}
function New-Username {
param(
[string]$CategoryName,
[int]$Index
)
$stamp = (Get-Date).ToString('MMddHHmm')
return ("u{0}{1:D2}" -f $stamp, $Index)
}
function Normalize-VideoTitle {
param([string]$FileNameWithoutExtension)
$title = $FileNameWithoutExtension.Trim()
if ($title.Length -lt 5) {
$title = ($title + ' 视频素材').Substring(0, [Math]::Min(($title + ' 视频素材').Length, 100))
}
if ($title.Length -gt 100) {
$title = $title.Substring(0, 100)
}
return $title
}
function Get-UserSession {
param(
[string]$Username,
[string]$Password
)
$captcha = Get-CaptchaCode -CaptchaUrl "$BaseUrl/auth/captcha" -RedisKeyPrefix 'captcha:' -RedisDb 0
$loginResp = Invoke-JsonApi -Method POST -Url "$BaseUrl/auth/login" -Body @{
username = $Username
password = $Password
captcha = $captcha.CaptchaCode
captchaKey = $captcha.CaptchaKey
}
Ensure-ApiSuccess -Response $loginResp -Action "用户登录($Username)"
$accessToken = [string]$loginResp.Json.data.accessToken
$headers = @{ Authorization = "Bearer $accessToken" }
$currentResp = Invoke-JsonApi -Method GET -Url "$BaseUrl/auth/current" -Headers $headers
Ensure-ApiSuccess -Response $currentResp -Action "读取当前用户($Username)"
return [pscustomobject]@{
Username = $Username
Password = $Password
UserId = [long]$currentResp.Json.data.id
Headers = $headers
AccessToken = $accessToken
}
}
function Get-ReusedUsersByCategory {
param([string]$ReportPath)
if (-not (Test-Path -LiteralPath $ReportPath)) {
throw "复用用户失败,报告文件不存在: $ReportPath"
}
$json = Get-Content -LiteralPath $ReportPath -Raw | ConvertFrom-Json -Depth 50
$map = @{}
foreach ($entry in @($json.users)) {
$categoryName = [string]$entry.categoryName
if (-not $map.ContainsKey($categoryName)) {
$map[$categoryName] = New-Object System.Collections.Generic.List[object]
}
$session = Get-UserSession -Username ([string]$entry.username) -Password ([string]$entry.password)
$session | Add-Member -NotePropertyName Nickname -NotePropertyValue ([string]$entry.nickname) -Force
$session | Add-Member -NotePropertyName CategoryName -NotePropertyValue $categoryName -Force
$map[$categoryName].Add($session)
}
return $map
}
function Register-ImportUser {
param(
[string]$CategoryName,
[int]$UserIndex,
[string]$Nickname
)
$username = New-Username -CategoryName $CategoryName -Index $UserIndex
$phone = New-RandomPhone
$email = "$username@example.com"
$captcha = Get-CaptchaCode -CaptchaUrl "$BaseUrl/auth/captcha" -RedisKeyPrefix 'captcha:' -RedisDb 0
$registerResp = Invoke-JsonApi -Method POST -Url "$BaseUrl/auth/register" -Body @{
username = $username
password = $DefaultUserPassword
confirmPassword = $DefaultUserPassword
email = $email
phone = $phone
captcha = $captcha.CaptchaCode
captchaKey = $captcha.CaptchaKey
}
Ensure-ApiSuccess -Response $registerResp -Action "注册用户($username)"
$session = Get-UserSession -Username $username -Password $DefaultUserPassword
$profileResp = Invoke-JsonApi -Method PUT -Url "$BaseUrl/user/profile" -Headers $session.Headers -Body @{
nickname = $Nickname
signature = "批量导入账号 - $CategoryName"
region = '南京'
gender = 0
}
Ensure-ApiSuccess -Response $profileResp -Action "更新资料($username)"
$session | Add-Member -NotePropertyName Nickname -NotePropertyValue $Nickname
$session | Add-Member -NotePropertyName CategoryName -NotePropertyValue $CategoryName
return $session
}
function Get-AdminSession {
$captcha = Get-CaptchaCode -CaptchaUrl "$BaseUrl/admin/auth/captcha" -RedisKeyPrefix 'admin:captcha:' -RedisDb 5
$loginResp = Invoke-JsonApi -Method POST -Url "$BaseUrl/admin/auth/login" -Body @{
username = $AdminUsername
password = $AdminPassword
captcha = $captcha.CaptchaCode
captchaKey = $captcha.CaptchaKey
}
Ensure-ApiSuccess -Response $loginResp -Action '管理员登录'
$token = [string]$loginResp.Json.data.token
$headers = @{ Authorization = "Bearer $token" }
$infoResp = Invoke-JsonApi -Method GET -Url "$BaseUrl/admin/auth/info" -Headers $headers
Ensure-ApiSuccess -Response $infoResp -Action '获取管理员信息'
return [pscustomobject]@{
Headers = $headers
AdminId = [long]$infoResp.Json.data.id
Username = $AdminUsername
}
}
function Get-CategoryMap {
$resp = Invoke-JsonApi -Method GET -Url "$BaseUrl/content/category/list"
Ensure-ApiSuccess -Response $resp -Action '读取分类列表'
$map = @{}
foreach ($item in @($resp.Json.data)) {
$map[[string]$item.name] = $item
}
return $map
}
function Get-VideoFilesForFolder {
param([string]$FolderPath)
$extensions = @('.mp4', '.mov', '.mkv', '.avi', '.flv', '.webm', '.m4v')
return @(Get-ChildItem -LiteralPath $FolderPath -File |
Where-Object { $extensions -contains $_.Extension.ToLowerInvariant() } |
Sort-Object Name)
}
$report = [ordered]@{
generatedAt = (Get-Date).ToString('s')
baseUrl = $BaseUrl
videoRoot = $VideoRoot
outputPath = $OutputPath
userPassword = $DefaultUserPassword
summary = [ordered]@{
categoryCount = 0
expectedUsers = 0
createdUsers = 0
expectedVideos = 0
uploadedVideos = 0
reviewedVideos = 0
failedVideos = 0
}
categories = @()
users = @()
errors = @()
}
if (-not (Test-Path -LiteralPath $VideoRoot)) {
throw "视频根目录不存在: $VideoRoot"
}
$categoryFolders = @(Get-ChildItem -LiteralPath $VideoRoot -Directory | Sort-Object Name)
if ($categoryFolders.Count -ne 5) {
throw "期望在 $VideoRoot 下找到 5 个分类目录,实际找到 $($categoryFolders.Count) 个"
}
$categoryMap = Get-CategoryMap
$adminSession = Get-AdminSession
$reusedUsersByCategory = $null
if ($ReuseExistingUsers) {
$reusedUsersByCategory = Get-ReusedUsersByCategory -ReportPath $OutputPath
$report.users = @()
foreach ($categoryUsers in $reusedUsersByCategory.Values) {
foreach ($user in $categoryUsers) {
$report.users += [ordered]@{
categoryName = $user.CategoryName
username = $user.Username
nickname = $user.Nickname
password = $user.Password
userId = $user.UserId
assignedVideoCount = $null
}
}
}
$report.summary.createdUsers = $report.users.Count
}
Save-Report -Report $report
$report.summary.categoryCount = $categoryFolders.Count
$report.summary.expectedUsers = $categoryFolders.Count * 2
$report.summary.expectedVideos = $categoryFolders.Count * 5
$report.summary.uploadedVideos = 0
$report.summary.reviewedVideos = 0
$report.summary.failedVideos = 0
$report.categories = @()
$report.errors = @()
$globalUserIndex = 1
foreach ($folder in $categoryFolders) {
$categoryName = $folder.Name
if (-not $categoryMap.ContainsKey($categoryName)) {
throw "平台中未找到分类: $categoryName"
}
$videoFiles = Get-VideoFilesForFolder -FolderPath $folder.FullName
if ($videoFiles.Count -ne 5) {
throw "分类 [$categoryName] 期望有 5 个视频,实际找到 $($videoFiles.Count) 个"
}
$platformCategory = $categoryMap[$categoryName]
$categoryReport = [ordered]@{
categoryName = $categoryName
categoryId = [long]$platformCategory.id
folderPath = $folder.FullName
userAssignments = @()
}
$report.categories += $categoryReport
Save-Report -Report $report
$assignmentSpecs = @(
@{ UserLabel = 'A'; VideoIndexes = @(0, 1); VideoCount = 2 },
@{ UserLabel = 'B'; VideoIndexes = @(2, 3, 4); VideoCount = 3 }
)
$reusedCategoryUsers = $null
if ($ReuseExistingUsers) {
if (-not $reusedUsersByCategory.ContainsKey($categoryName) -or $reusedUsersByCategory[$categoryName].Count -ne 2) {
throw "分类 [$categoryName] 没有可复用的 2 个用户"
}
$reusedCategoryUsers = $reusedUsersByCategory[$categoryName].ToArray()
}
$assignmentIndex = 0
foreach ($assignmentSpec in $assignmentSpecs) {
if ($ReuseExistingUsers) {
$userSession = $reusedCategoryUsers[$assignmentIndex]
$nickname = $userSession.Nickname
} else {
$nickname = "$categoryName-导入用户$($assignmentSpec.UserLabel)"
$userSession = Register-ImportUser -CategoryName $categoryName -UserIndex $globalUserIndex -Nickname $nickname
$globalUserIndex++
$report.summary.createdUsers++
$report.users += [ordered]@{
categoryName = $categoryName
username = $userSession.Username
nickname = $nickname
password = $userSession.Password
userId = $userSession.UserId
assignedVideoCount = $assignmentSpec.VideoCount
}
}
$userAssignmentReport = [ordered]@{
userId = $userSession.UserId
username = $userSession.Username
nickname = $nickname
assignedVideoCount = $assignmentSpec.VideoCount
videos = @()
}
foreach ($videoIndex in $assignmentSpec.VideoIndexes) {
$file = $videoFiles[$videoIndex]
$videoReport = [ordered]@{
sourceFile = $file.FullName
fileName = $file.Name
ownerUserId = $userSession.UserId
ownerUsername = $userSession.Username
status = 'processing'
upload = $null
create = $null
review = $null
publicCheck = $null
error = $null
}
try {
$uploadResp = Invoke-UploadApi -Url "$BaseUrl/content/video/upload" -FilePath $file.FullName -Headers $userSession.Headers
Ensure-ApiSuccess -Response $uploadResp -Action "上传视频($($file.Name))"
$videoTitle = Normalize-VideoTitle -FileNameWithoutExtension $file.BaseName
$createResp = Invoke-JsonApi -Method POST -Url "$BaseUrl/content/video" -Headers $userSession.Headers -Body @{
title = $videoTitle
description = "批量导入视频;分类:$categoryName;源文件:$($file.Name)"
categoryId = [long]$platformCategory.id
tagIds = @()
coverUrl = $uploadResp.Json.data.coverUrl
videoUrl = $uploadResp.Json.data.videoUrl
duration = $uploadResp.Json.data.duration
width = $uploadResp.Json.data.width
height = $uploadResp.Json.data.height
format = $uploadResp.Json.data.format
size = $uploadResp.Json.data.size
}
Ensure-ApiSuccess -Response $createResp -Action "创建视频($($file.Name))"
$videoId = [long]$createResp.Json.data
$reviewResp = Invoke-JsonApi -Method POST -Url "$BaseUrl/admin/platform/content/video/review/$videoId?status=1&reason=bulk-import" -Headers $adminSession.Headers
Ensure-ApiSuccess -Response $reviewResp -Action "审核视频($videoId)"
$publicCheckResp = Invoke-JsonApi -Method GET -Url "$BaseUrl/content/video/public/$videoId"
Ensure-ApiSuccess -Response $publicCheckResp -Action "校验公开视频($videoId)"
$videoReport.status = 'completed'
$videoReport.upload = [ordered]@{
videoUrl = $uploadResp.Json.data.videoUrl
coverUrl = $uploadResp.Json.data.coverUrl
duration = $uploadResp.Json.data.duration
width = $uploadResp.Json.data.width
height = $uploadResp.Json.data.height
format = $uploadResp.Json.data.format
size = $uploadResp.Json.data.size
}
$videoReport.create = [ordered]@{
videoId = $videoId
title = $videoTitle
categoryId = [long]$platformCategory.id
}
$videoReport.review = [ordered]@{
status = 1
reviewerAdminId = $adminSession.AdminId
}
$videoReport.publicCheck = [ordered]@{
videoId = $videoId
apiCode = $publicCheckResp.Json.code
}
$report.summary.uploadedVideos++
$report.summary.reviewedVideos++
} catch {
$videoReport.status = 'failed'
$videoReport.error = $_.Exception.Message
$report.summary.failedVideos++
$report.errors += "[$categoryName][$($userSession.Username)][$($file.Name)] $($_.Exception.Message)"
}
$userAssignmentReport.videos += $videoReport
Save-Report -Report $report
}
$categoryReport.userAssignments += $userAssignmentReport
$assignmentIndex++
Save-Report -Report $report
}
Save-Report -Report $report
}
$report.generatedAt = (Get-Date).ToString('s')
Save-Report -Report $report
$report | ConvertTo-Json -Depth 20
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
SRC_ALIAS="${SRC_ALIAS:-node01}"
DST_ALIAS="${DST_ALIAS:-node02}"
SRC_ENDPOINT="${SRC_ENDPOINT:-http://192.168.1.101:9100}"
DST_ENDPOINT="${DST_ENDPOINT:-http://192.168.1.102:9100}"
MINIO_USER="${MINIO_USER:-minioadmin}"
MINIO_PASSWORD="${MINIO_PASSWORD:-minioadmin123}"
BUCKET="${BUCKET:-video-platform}"
MC_BIN="${MC_BIN:-/tmp/mc}"
if [ ! -x "$MC_BIN" ]; then
curl -L -o "$MC_BIN" https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x "$MC_BIN"
fi
"$MC_BIN" alias set "$SRC_ALIAS" "$SRC_ENDPOINT" "$MINIO_USER" "$MINIO_PASSWORD" >/dev/null
"$MC_BIN" alias set "$DST_ALIAS" "$DST_ENDPOINT" "$MINIO_USER" "$MINIO_PASSWORD" >/dev/null
"$MC_BIN" mirror --overwrite "$SRC_ALIAS/$BUCKET" "$DST_ALIAS/$BUCKET"
+1 -1
View File
@@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS `user_info` (
`following_count` int NOT NULL DEFAULT '0' COMMENT '关注数', `following_count` int NOT NULL DEFAULT '0' COMMENT '关注数',
`video_count` int NOT NULL DEFAULT '0' COMMENT '视频数', `video_count` int NOT NULL DEFAULT '0' COMMENT '视频数',
`like_count` int NOT NULL DEFAULT '0' COMMENT '获赞数', `like_count` int NOT NULL DEFAULT '0' COMMENT '获赞数',
`coin_count` int NOT NULL DEFAULT '0' COMMENT '硬币数', `coin_count` int NOT NULL DEFAULT '20' COMMENT '硬币数',
`favorite_count` int NOT NULL DEFAULT '0' COMMENT '收藏数', `favorite_count` int NOT NULL DEFAULT '0' COMMENT '收藏数',
`view_count` bigint NOT NULL DEFAULT '0' COMMENT '播放数', `view_count` bigint NOT NULL DEFAULT '0' COMMENT '播放数',
`level` tinyint NOT NULL DEFAULT '1' COMMENT '等级', `level` tinyint NOT NULL DEFAULT '1' COMMENT '等级',
+17
View File
@@ -105,6 +105,11 @@ spring:
INSERT INTO config_info (data_id, group_id, content, tenant_id, app_name, src_user, encrypted_data_key) VALUES INSERT INTO config_info (data_id, group_id, content, tenant_id, app_name, src_user, encrypted_data_key) VALUES
('video-gateway.yaml', 'DEFAULT_GROUP', '# Gateway 服务配置 ('video-gateway.yaml', 'DEFAULT_GROUP', '# Gateway 服务配置
spring: spring:
codec:
max-in-memory-size: 10MB
webflux:
multipart:
max-in-memory-size: 10MB
cloud: cloud:
gateway: gateway:
discovery: discovery:
@@ -120,10 +125,18 @@ spring:
uri: lb://video-user uri: lb://video-user
predicates: predicates:
- Path=/user/** - Path=/user/**
filters:
- name: RequestSize
args:
maxSize: 10MB
- id: video-content - id: video-content
uri: lb://video-content uri: lb://video-content
predicates: predicates:
- Path=/content/** - Path=/content/**
filters:
- name: RequestSize
args:
maxSize: 1024MB
- id: video-comment - id: video-comment
uri: lb://video-comment uri: lb://video-comment
predicates: predicates:
@@ -219,6 +232,10 @@ spring:
data: data:
redis: redis:
database: 1 database: 1
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
cloud: cloud:
sentinel: sentinel:
transport: transport:
@@ -27,6 +27,11 @@ spring:
file-extension: yaml file-extension: yaml
username: ${NACOS_USERNAME:nacos} username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos} password: ${NACOS_PASSWORD:nacos}
codec:
max-in-memory-size: ${GATEWAY_MAX_IN_MEMORY_SIZE:10MB}
webflux:
multipart:
max-in-memory-size: ${GATEWAY_MULTIPART_MAX_IN_MEMORY_SIZE:10MB}
management: management:
endpoints: endpoints:
@@ -27,6 +27,14 @@ spring:
file-extension: yaml file-extension: yaml
username: ${NACOS_USERNAME:nacos} username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos} password: ${NACOS_PASSWORD:nacos}
servlet:
multipart:
max-file-size: ${USER_AVATAR_MAX_FILE_SIZE:10MB}
max-request-size: ${USER_AVATAR_MAX_REQUEST_SIZE:10MB}
user:
avatar:
max-size-bytes: ${USER_AVATAR_MAX_SIZE_BYTES:5242880}
management: management:
endpoints: endpoints:
File diff suppressed because it is too large Load Diff