// // ScenicPermissionModels.swift // suixinkan // // Created by Codex on 2026/6/23. // import Foundation /// 景区申请地区节点实体,表示省市区树中的一个可选节点。 struct ScenicAreaNode: Decodable, Equatable, Identifiable { let id: String let code: String let name: String let children: [ScenicAreaNode] enum CodingKeys: String, CodingKey { case id case code case name case children } /// 创建地区节点,主要用于单元测试和本地构造。 init(id: String, code: String = "", name: String, children: [ScenicAreaNode] = []) { self.id = id self.code = code self.name = name self.children = children } /// 自定义解码,兼容后端 id/code 为数字或字符串。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) code = try container.decodeLossyString(forKey: .code) let rawId = try container.decodeLossyString(forKey: .id).trimmingCharacters(in: .whitespacesAndNewlines) id = rawId.isEmpty ? code : rawId name = try container.decodeLossyString(forKey: .name) children = try container.decodeIfPresent([ScenicAreaNode].self, forKey: .children) ?? [] } } /// 景区申请待审核列表响应实体,表示当前用户提交过的景区入驻申请。 struct ScenicApplicationPendingsResponse: Decodable, Equatable { let items: [ScenicApplicationPendingResponse] enum CodingKeys: String, CodingKey { case items case list case data } /// 创建待审核列表响应实体,主要用于测试替身。 init(items: [ScenicApplicationPendingResponse] = []) { self.items = items } /// 自定义解码,兼容后端返回 items/list/data 三种列表字段。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) items = try container.decodeIfPresent([ScenicApplicationPendingResponse].self, forKey: .items) ?? container.decodeIfPresent([ScenicApplicationPendingResponse].self, forKey: .list) ?? container.decodeIfPresent([ScenicApplicationPendingResponse].self, forKey: .data) ?? [] } } /// 景区申请待审核实体,表示新增景区申请的审核状态和表单快照。 struct ScenicApplicationPendingResponse: Decodable, Equatable, Identifiable { let id: Int let code: String let scenicId: Int let scenicName: String let scenicImages: String let scenicProvince: String let scenicCity: String let coopType: Int let remark: String? let status: Int let rejectReason: String? let auditedBy: String? let auditedAt: String? let auditNote: String? let createdAt: String enum CodingKeys: String, CodingKey { case id case code case scenicId = "scenic_id" case scenicName = "scenic_name" case scenicImages = "scenic_images" case scenicProvince = "scenic_province" case scenicCity = "scenic_city" case coopType = "coop_type" case remark case status case rejectReason = "reject_reason" case auditedBy = "audited_by" case auditedAt = "audited_at" case auditNote = "audit_note" case createdAt = "created_at" } /// 创建景区申请待审核实体,主要用于测试和本地回填。 init( id: Int, code: String = "", scenicId: Int = 0, scenicName: String, scenicImages: String = "", scenicProvince: String = "", scenicCity: String = "", coopType: Int = 1, remark: String? = nil, status: Int = 1, rejectReason: String? = nil, auditedBy: String? = nil, auditedAt: String? = nil, auditNote: String? = nil, createdAt: String = "" ) { self.id = id self.code = code self.scenicId = scenicId self.scenicName = scenicName self.scenicImages = scenicImages self.scenicProvince = scenicProvince self.scenicCity = scenicCity self.coopType = coopType self.remark = remark self.status = status self.rejectReason = rejectReason self.auditedBy = auditedBy self.auditedAt = auditedAt self.auditNote = auditNote self.createdAt = createdAt } /// 自定义解码,兼容后端数字和字符串混用。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 code = try container.decodeLossyString(forKey: .code) scenicId = try container.decodeLossyInt(forKey: .scenicId) ?? 0 scenicName = try container.decodeLossyString(forKey: .scenicName) if let images = try? container.decode([String].self, forKey: .scenicImages) { scenicImages = images.joined(separator: ",") } else { scenicImages = try container.decodeLossyString(forKey: .scenicImages) } scenicProvince = try container.decodeLossyString(forKey: .scenicProvince) scenicCity = try container.decodeLossyString(forKey: .scenicCity) coopType = try container.decodeLossyInt(forKey: .coopType) ?? 1 remark = try? container.decodeIfPresent(String.self, forKey: .remark) status = try container.decodeLossyInt(forKey: .status) ?? 0 rejectReason = try? container.decodeIfPresent(String.self, forKey: .rejectReason) auditedBy = try? container.decodeIfPresent(String.self, forKey: .auditedBy) auditedAt = try? container.decodeIfPresent(String.self, forKey: .auditedAt) auditNote = try? container.decodeIfPresent(String.self, forKey: .auditNote) createdAt = try container.decodeLossyString(forKey: .createdAt) } } /// 景区申请提交请求实体,表示新增景区入驻申请表单。 struct ScenicApplicationSubmitRequest: Encodable, Equatable { let scenicName: String? let scenicImages: [String]? let scenicProvince: String let scenicCity: String let coopType: Int let remark: String let scenicId: Int enum CodingKeys: String, CodingKey { case scenicName = "scenic_name" case scenicImages = "scenic_images" case scenicProvince = "scenic_province" case scenicCity = "scenic_city" case coopType = "coop_type" case remark case scenicId = "scenic_id" } } /// 角色权限申请实体,表示一条角色/景区权限申请状态。 struct RoleApplyPendingResponse: Decodable, Equatable, Hashable, Identifiable { let id: Int let code: String let roleId: Int let roleName: String let scenicList: [RoleApplyScenicItem] let status: Int let statusLabel: String let createdAt: String let auditedBy: String? let auditedAt: String? let auditNote: String? enum CodingKeys: String, CodingKey { case id case code case roleId = "role_id" case roleName = "role_name" case scenicList = "scenic_list" case status case statusLabel = "status_label" case createdAt = "created_at" case auditedBy = "audited_by" case auditedAt = "audited_at" case auditNote = "audit_note" } /// 创建角色权限申请实体,主要用于测试和状态页回填。 init( id: Int, code: String = "", roleId: Int, roleName: String, scenicList: [RoleApplyScenicItem] = [], status: Int = 1, statusLabel: String = "审核中", createdAt: String = "", auditedBy: String? = nil, auditedAt: String? = nil, auditNote: String? = nil ) { self.id = id self.code = code self.roleId = roleId self.roleName = roleName self.scenicList = scenicList self.status = status self.statusLabel = statusLabel self.createdAt = createdAt self.auditedBy = auditedBy self.auditedAt = auditedAt self.auditNote = auditNote } /// 自定义解码,兼容后端字段类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 code = try container.decodeLossyString(forKey: .code) roleId = try container.decodeLossyInt(forKey: .roleId) ?? 0 roleName = try container.decodeLossyString(forKey: .roleName) scenicList = try container.decodeIfPresent([RoleApplyScenicItem].self, forKey: .scenicList) ?? [] status = try container.decodeLossyInt(forKey: .status) ?? 0 statusLabel = try container.decodeLossyString(forKey: .statusLabel) createdAt = try container.decodeLossyString(forKey: .createdAt) auditedBy = try? container.decodeIfPresent(String.self, forKey: .auditedBy) auditedAt = try? container.decodeIfPresent(String.self, forKey: .auditedAt) auditNote = try? container.decodeIfPresent(String.self, forKey: .auditNote) } } /// 角色权限申请景区实体,表示申请中勾选的一个景区。 struct RoleApplyScenicItem: Decodable, Equatable, Hashable, Identifiable { let id: Int let name: String enum CodingKeys: String, CodingKey { case id case name } /// 创建角色权限申请景区实体,主要用于测试。 init(id: Int, name: String) { self.id = id self.name = name } /// 自定义解码,兼容景区 ID 类型不稳定。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 name = try container.decodeLossyString(forKey: .name) } } /// 角色权限提交请求实体,表示申请某角色在多个景区的权限。 struct RoleApplySubmitRequest: Encodable, Equatable { let scenicId: [Int] let roleId: Int enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case roleId = "role_id" } } /// 上传占位请求实体,表示景区申请已上传文件的轻量元信息。 struct ScenicApplicationUploadPlaceholder: Encodable, Equatable { let fileName: String let fileType: String let fileSize: Int64 enum CodingKeys: String, CodingKey { case fileName = "file_name" case fileType = "file_type" case fileSize = "file_size" } } private extension KeyedDecodingContainer { /// 宽松解码字符串,兼容后端数字、布尔值和空值。 func decodeLossyString(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(Double.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? "1" : "0" } return "" } /// 宽松解码整数,兼容后端字符串和浮点数。 func decodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } if let value = try? decodeIfPresent(String.self, forKey: key) { return Int(value.trimmingCharacters(in: .whitespacesAndNewlines)) } return nil } }