chore(config): Improve HA orchestration and media synchronization
This commit is contained in:
@@ -39,8 +39,9 @@ SENTINEL_HOST=sentinel
|
||||
SENTINEL_PORT=8080
|
||||
|
||||
# MinIO配置
|
||||
MINIO_HOST=minio
|
||||
MINIO_PORT=9000
|
||||
# 方案A:所有上传统一写入 Node-01,本节点既是写入点也是读取点
|
||||
MINIO_HOST=192.168.1.101
|
||||
MINIO_PORT=9100
|
||||
MINIO_EXTERNAL_PORT=9100
|
||||
MINIO_ACCESS_KEY=replace-with-minio-access-key
|
||||
MINIO_SECRET_KEY=replace-with-minio-secret-key
|
||||
|
||||
@@ -39,8 +39,9 @@ SENTINEL_HOST=sentinel
|
||||
SENTINEL_PORT=8080
|
||||
|
||||
# MinIO配置
|
||||
MINIO_HOST=minio
|
||||
MINIO_PORT=9000
|
||||
# 方案A:所有上传统一写入 Node-01,Node-02 仅作为同步副本
|
||||
MINIO_HOST=192.168.1.101
|
||||
MINIO_PORT=9100
|
||||
MINIO_EXTERNAL_PORT=9100
|
||||
MINIO_ACCESS_KEY=replace-with-minio-access-key
|
||||
MINIO_SECRET_KEY=replace-with-minio-secret-key
|
||||
|
||||
@@ -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}
|
||||
- ROCKETMQ_HOST=rmqnamesrv
|
||||
- ROCKETMQ_PORT=9876
|
||||
- MINIO_HOST=minio
|
||||
- MINIO_PORT=9000
|
||||
- MINIO_HOST=192.168.1.101
|
||||
- MINIO_PORT=9100
|
||||
- MINIO_ACCESS_KEY=minioadmin
|
||||
- MINIO_SECRET_KEY=minioadmin123
|
||||
- MINIO_BUCKET=video-platform
|
||||
|
||||
@@ -331,6 +331,8 @@ services:
|
||||
- MYSQL_PASSWORD=${MYSQL_ROOT_PASSWORD:-root123456}
|
||||
- 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}
|
||||
- MINIO_HOST=192.168.1.101
|
||||
- MINIO_PORT=9100
|
||||
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||
- MINIO_BUCKET=${MINIO_BUCKET}
|
||||
|
||||
@@ -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"
|
||||
@@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS `user_info` (
|
||||
`following_count` int NOT NULL DEFAULT '0' COMMENT '关注数',
|
||||
`video_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 '收藏数',
|
||||
`view_count` bigint NOT NULL DEFAULT '0' COMMENT '播放数',
|
||||
`level` tinyint NOT NULL DEFAULT '1' COMMENT '等级',
|
||||
|
||||
@@ -105,6 +105,11 @@ spring:
|
||||
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 服务配置
|
||||
spring:
|
||||
codec:
|
||||
max-in-memory-size: 10MB
|
||||
webflux:
|
||||
multipart:
|
||||
max-in-memory-size: 10MB
|
||||
cloud:
|
||||
gateway:
|
||||
discovery:
|
||||
@@ -120,10 +125,18 @@ spring:
|
||||
uri: lb://video-user
|
||||
predicates:
|
||||
- Path=/user/**
|
||||
filters:
|
||||
- name: RequestSize
|
||||
args:
|
||||
maxSize: 10MB
|
||||
- id: video-content
|
||||
uri: lb://video-content
|
||||
predicates:
|
||||
- Path=/content/**
|
||||
filters:
|
||||
- name: RequestSize
|
||||
args:
|
||||
maxSize: 1024MB
|
||||
- id: video-comment
|
||||
uri: lb://video-comment
|
||||
predicates:
|
||||
@@ -219,6 +232,10 @@ spring:
|
||||
data:
|
||||
redis:
|
||||
database: 1
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 10MB
|
||||
max-request-size: 10MB
|
||||
cloud:
|
||||
sentinel:
|
||||
transport:
|
||||
|
||||
@@ -27,6 +27,11 @@ spring:
|
||||
file-extension: yaml
|
||||
username: ${NACOS_USERNAME: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:
|
||||
endpoints:
|
||||
|
||||
@@ -27,6 +27,14 @@ spring:
|
||||
file-extension: yaml
|
||||
username: ${NACOS_USERNAME: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:
|
||||
endpoints:
|
||||
|
||||
Reference in New Issue
Block a user