新增直播模块,包含推流管理与相册流程
将直播推流管理与直播相册从首页占位页迁移为完整实现,包含 alive album OSS 上传、首页路由与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
386
suixinkan/Features/Live/Models/LiveModels.swift
Normal file
386
suixinkan/Features/Live/Models/LiveModels.swift
Normal file
@ -0,0 +1,386 @@
|
||||
//
|
||||
// LiveModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 直播列表响应实体。
|
||||
struct LiveListResponse: Decodable {
|
||||
let items: [LiveEntity]
|
||||
let total: Int
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case items
|
||||
case total
|
||||
case page
|
||||
case pageSize = "page_size"
|
||||
}
|
||||
|
||||
init(items: [LiveEntity] = [], total: Int = 0, page: Int = 1, pageSize: Int = 10) {
|
||||
self.items = items
|
||||
self.total = total
|
||||
self.page = page
|
||||
self.pageSize = pageSize
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
items = (try? container.decode([LiveEntity].self, forKey: .items)) ?? []
|
||||
total = try container.liveDecodeLossyInt(forKey: .total) ?? 0
|
||||
page = try container.liveDecodeLossyInt(forKey: .page) ?? 1
|
||||
pageSize = try container.liveDecodeLossyInt(forKey: .pageSize) ?? 10
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播实体,表示一场手动直播。
|
||||
struct LiveEntity: Decodable, Identifiable, Equatable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let coverImg: String
|
||||
let pushUrl: String
|
||||
let startTime: Int64
|
||||
let endTime: Int64
|
||||
let duration: Int64
|
||||
let status: Int
|
||||
let statusLabel: String
|
||||
let manualPushMode: Int
|
||||
let manualPushState: Int
|
||||
let viewsCount: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case coverImg = "cover_img"
|
||||
case pushUrl = "push_url"
|
||||
case startTime = "start_time"
|
||||
case endTime = "end_time"
|
||||
case duration
|
||||
case status
|
||||
case statusLabel = "status_label"
|
||||
case manualPushMode = "manaul_push_mode"
|
||||
case manualPushState = "manaul_push_state"
|
||||
case viewsCount = "views_count"
|
||||
}
|
||||
|
||||
init(
|
||||
id: Int = 0,
|
||||
title: String = "",
|
||||
coverImg: String = "",
|
||||
pushUrl: String = "",
|
||||
startTime: Int64 = 0,
|
||||
endTime: Int64 = 0,
|
||||
duration: Int64 = 0,
|
||||
status: Int = 0,
|
||||
statusLabel: String = "",
|
||||
manualPushMode: Int = 1,
|
||||
manualPushState: Int = 0,
|
||||
viewsCount: Int = 0
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.coverImg = coverImg
|
||||
self.pushUrl = pushUrl
|
||||
self.startTime = startTime
|
||||
self.endTime = endTime
|
||||
self.duration = duration
|
||||
self.status = status
|
||||
self.statusLabel = statusLabel
|
||||
self.manualPushMode = manualPushMode
|
||||
self.manualPushState = manualPushState
|
||||
self.viewsCount = viewsCount
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.liveDecodeLossyInt(forKey: .id) ?? 0
|
||||
title = try container.liveDecodeLossyString(forKey: .title)
|
||||
coverImg = try container.liveDecodeLossyString(forKey: .coverImg)
|
||||
pushUrl = try container.liveDecodeLossyString(forKey: .pushUrl)
|
||||
startTime = Int64(try container.liveDecodeLossyInt(forKey: .startTime) ?? 0)
|
||||
endTime = Int64(try container.liveDecodeLossyInt(forKey: .endTime) ?? 0)
|
||||
duration = Int64(try container.liveDecodeLossyInt(forKey: .duration) ?? 0)
|
||||
status = try container.liveDecodeLossyInt(forKey: .status) ?? 0
|
||||
statusLabel = try container.liveDecodeLossyString(forKey: .statusLabel)
|
||||
manualPushMode = try container.liveDecodeLossyInt(forKey: .manualPushMode) ?? 1
|
||||
manualPushState = try container.liveDecodeLossyInt(forKey: .manualPushState) ?? 0
|
||||
viewsCount = try container.liveDecodeLossyInt(forKey: .viewsCount) ?? 0
|
||||
}
|
||||
|
||||
var displayStatus: String {
|
||||
statusLabel.liveNonEmpty ?? "状态\(status)"
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建直播请求实体。
|
||||
struct LiveCreateRequest: Encodable, Equatable {
|
||||
let scenicId: String
|
||||
let title: String
|
||||
let coverImg: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case title
|
||||
case coverImg = "cover_img"
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播控制请求实体。
|
||||
struct LiveControlRequest: Encodable, Equatable {
|
||||
let liveId: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case liveId = "live_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播推流模式切换请求实体。
|
||||
struct LivePushModeRequest: Encodable, Equatable {
|
||||
let liveId: Int
|
||||
let manualPushMode: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case liveId = "live_id"
|
||||
case manualPushMode = "manual_push_mode"
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播相册列表响应实体。
|
||||
struct LiveAlbumFolderListResponse: Decodable {
|
||||
let items: [LiveAlbumFolderItem]
|
||||
let page: Int
|
||||
let pageSize: Int
|
||||
let total: Int
|
||||
let totalPages: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case items
|
||||
case page
|
||||
case pageSize = "page_size"
|
||||
case total
|
||||
case totalPages = "total_pages"
|
||||
}
|
||||
|
||||
init(items: [LiveAlbumFolderItem] = [], page: Int = 1, pageSize: Int = 10, total: Int = 0, totalPages: Int = 0) {
|
||||
self.items = items
|
||||
self.page = page
|
||||
self.pageSize = pageSize
|
||||
self.total = total
|
||||
self.totalPages = totalPages
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
items = (try? container.decode([LiveAlbumFolderItem].self, forKey: .items)) ?? []
|
||||
page = try container.liveDecodeLossyInt(forKey: .page) ?? 1
|
||||
pageSize = try container.liveDecodeLossyInt(forKey: .pageSize) ?? 10
|
||||
total = try container.liveDecodeLossyInt(forKey: .total) ?? 0
|
||||
totalPages = try container.liveDecodeLossyInt(forKey: .totalPages) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播相册文件夹实体。
|
||||
struct LiveAlbumFolderItem: Decodable, Identifiable, Equatable {
|
||||
let id: Int
|
||||
let albumId: Int
|
||||
let name: String
|
||||
let creator: String
|
||||
let items: [LiveAlbumFileItem]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case albumId = "album_id"
|
||||
case name
|
||||
case creator
|
||||
case items
|
||||
}
|
||||
|
||||
init(id: Int = 0, albumId: Int = 0, name: String = "", creator: String = "", items: [LiveAlbumFileItem] = []) {
|
||||
self.id = id
|
||||
self.albumId = albumId
|
||||
self.name = name
|
||||
self.creator = creator
|
||||
self.items = items
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.liveDecodeLossyInt(forKey: .id) ?? 0
|
||||
albumId = try container.liveDecodeLossyInt(forKey: .albumId) ?? 0
|
||||
name = try container.liveDecodeLossyString(forKey: .name)
|
||||
creator = try container.liveDecodeLossyString(forKey: .creator)
|
||||
items = (try? container.decode([LiveAlbumFileItem].self, forKey: .items)) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/// 直播相册素材实体。
|
||||
struct LiveAlbumFileItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let url: String
|
||||
let type: Int
|
||||
let size: Int64
|
||||
let coverImg: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case url
|
||||
case type
|
||||
case size
|
||||
case coverImg = "cover_img"
|
||||
}
|
||||
|
||||
init(id: Int = 0, url: String = "", type: Int = 1, size: Int64 = 0, coverImg: String? = nil) {
|
||||
self.id = id
|
||||
self.url = url
|
||||
self.type = type
|
||||
self.size = size
|
||||
self.coverImg = coverImg
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.liveDecodeLossyInt(forKey: .id) ?? 0
|
||||
url = try container.liveDecodeLossyString(forKey: .url)
|
||||
type = try container.liveDecodeLossyInt(forKey: .type) ?? 1
|
||||
size = Int64(try container.liveDecodeLossyInt(forKey: .size) ?? 0)
|
||||
coverImg = try? container.decodeIfPresent(String.self, forKey: .coverImg)
|
||||
}
|
||||
|
||||
var isVideo: Bool { type == 2 }
|
||||
|
||||
var previewURL: String {
|
||||
if let cover = coverImg?.liveTrimmed, !cover.isEmpty {
|
||||
return cover
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建直播相册请求实体。
|
||||
struct LiveAlbumCreateFolderRequest: Encodable, Equatable {
|
||||
let scenicId: String
|
||||
let name: String
|
||||
let items: [LiveAlbumCreateFileItem]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case name
|
||||
case items
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建直播相册素材请求实体。
|
||||
struct LiveAlbumCreateFileItem: Encodable, Equatable {
|
||||
let url: String
|
||||
let type: Int
|
||||
let size: Int64
|
||||
let coverImg: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case url
|
||||
case type
|
||||
case size
|
||||
case coverImg = "cover_img"
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除直播相册请求实体。
|
||||
struct LiveAlbumDeleteFolderRequest: Encodable, Equatable {
|
||||
let folderId: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case folderId = "folder_id"
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除直播相册素材请求实体。
|
||||
struct LiveAlbumDeleteFilesRequest: Encodable, Equatable {
|
||||
let folderId: Int
|
||||
let fileIds: [Int]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case folderId = "folder_id"
|
||||
case fileIds = "file_ids"
|
||||
}
|
||||
}
|
||||
|
||||
/// 本地待上传直播相册素材实体。
|
||||
struct LiveAlbumLocalUploadFile: Identifiable, Equatable {
|
||||
let id: UUID
|
||||
let data: Data
|
||||
let fileName: String
|
||||
let fileType: Int
|
||||
let size: Int64
|
||||
var uploadedURL: String?
|
||||
|
||||
init(id: UUID = UUID(), data: Data, fileName: String, fileType: Int, size: Int64? = nil, uploadedURL: String? = nil) {
|
||||
self.id = id
|
||||
self.data = data
|
||||
self.fileName = fileName
|
||||
self.fileType = fileType
|
||||
self.size = size ?? Int64(data.count)
|
||||
self.uploadedURL = uploadedURL
|
||||
}
|
||||
|
||||
var isVideo: Bool { fileType == 2 }
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
func liveDecodeLossyString(forKey key: Key) throws -> String {
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int64.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||||
return value ? "1" : "0"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func liveDecodeLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int64.self, forKey: key) {
|
||||
return Int(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return Int(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let intValue = Int(trimmed) {
|
||||
return intValue
|
||||
}
|
||||
if let doubleValue = Double(trimmed) {
|
||||
return Int(doubleValue)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// 直播模块内部使用的去空白字符串。
|
||||
var liveTrimmed: String {
|
||||
trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
/// 直播模块内部使用的非空字符串兜底。
|
||||
var liveNonEmpty: String? {
|
||||
let value = liveTrimmed
|
||||
return value.isEmpty ? nil : value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user