// // GroupModel.swift // QuickLocation // // Created by 八条 on 2026/5/30. // import ObjectMapper import RxDataSources import Kingfisher private let kGroupTransformInt64 = TransformOf(fromJSON: { value in switch value { case let value as Int64: return value case let value as Int: return Int64(value) case let value as Double: return Int64(value) case let value as String: return Int64(value) default: return nil } }, toJSON: { value in value }) /// 用户圈子 struct UserGroupResponse: BaseModelProtocol { // 状态码 var code: String? // 消息 var message: String? // var model: GroupModel? init?(map: Map) {} mutating func mapping(map: Map) { code <- map["code"] message <- map["msg"] model <- map["data"] } } /// 推荐圈子 struct RecommandGroupResponse: BaseModelProtocol { // 状态码 var code: String? // 消息 var message: String? // var list: [GroupInfoModel] = [] init?(map: Map) {} mutating func mapping(map: Map) { code <- map["code"] message <- map["msg"] list <- map["data"] } } /// 圈子信息 struct GroupInfoResponse: BaseModelProtocol { // 状态码 var code: String? // 消息 var message: String? // 圈子信息 var model: GroupInfoModel? // 圈子成员列表 var list: [GroupMemberModel] = [] // 待审核数量 var reviewCount: Int = 0 init?(map: Map) {} mutating func mapping(map: Map) { code <- map["code"] message <- map["msg"] model <- map["data.attr"] list <- map["data.employee"] reviewCount <- map["data.reviewCount"] } } /// 指定圈子的资料及成员列表 struct GroupUsersResponse: BaseModelProtocol { var code: String? var message: String? var model: GroupInfoModel? var list: [GroupMemberModel] = [] init?(map: Map) {} mutating func mapping(map: Map) { code <- (map["code"], kIntTransformStr) message <- map["message"] model <- map["data.group"] list <- map["data.mates"] } func isValid(for groupKey: String) -> Bool { code == "0" && model?.group_key == groupKey } } /// 圈子类型模板列表 struct GroupTemplateResponse: BaseModelProtocol { var code: String? var message: String? var list: [GroupTemplateModel] = [] init?(map: Map) {} mutating func mapping(map: Map) { code <- (map["code"], kIntTransformStr) message <- map["message"] list <- map["data"] } } struct GroupTemplateModel: Mappable, Equatable { var name: String = "" var type: Int = 0 var limit: Int = -1 var icon: String = "" init?(map: Map) {} mutating func mapping(map: Map) { name <- map["name"] type <- (map["type"], kStrTransformInt) limit <- (map["limit"], kStrTransformInt) icon <- map["icon"] } } struct ReviewMemberListResponse: BaseModelProtocol { // 状态码 var code: String? // 消息 var message: String? /// 待审核列表 var list: [GroupMemberModel] = [] init?(map: Map) {} mutating func mapping(map: Map) { code <- map["code"] message <- map["msg"] list <- map["data"] } } struct GroupModel: Mappable { /// 当前选中的圈子 var default_group_key: String = "" /// var has_unread_email: Bool = false /// 圈子是否有人SOS var is_anyone_sos: Bool = false /// 圈子列表 var groups: [GroupInfoModel] = [] /// 当前选中圈子里的成员列表 var select_group_employee: [GroupMemberModel] = [] var score: Int = 0 init?(map: Map) { } mutating func mapping(map: Map) { default_group_key <- map["default_group_key"] has_unread_email <- map["has_unread_email"] is_anyone_sos <- map["is_anyone_sos"] score <- map["score"] groups <- map["groups"] select_group_employee <- map["select_group_employee"] } } struct GroupInfoModel: Mappable, Equatable { /// var group_key: String = "" /// var is_owner: Bool = false /// 圈子名字 var name: String = "" /// 圈子icon var icon_index: Int = 1 var groupIcon: UIImage { UIImage(named: "GroupIcon/\(icon_index)") ?? UIImage() } /// 新版圈子类型及模板;icon_index 仅保留给旧数据兼容。 var group_type: Int = 0 var group_template: GroupTemplateModel? var resolvedGroupType: Int { if group_type > 0 { return group_type } if let type = group_template?.type, type > 0 { return type } return icon_index } var groupTemplateName: String { let name = group_template?.name.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return name.isEmpty ? "未知类型" : name } var groupIconURL: String { group_template?.icon.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" } /// 标签 var labels: [String] = [] /// 审核开关 var review: Bool = false /// 人数 var people_no: Int = 0 /// 描述 var description: String = "" /// 会员等级 var level: String = "" /// 邀请码 var share_code: String = "" init?(map: Map) { } mutating func mapping(map: Map) { group_key <- map["group_key"] is_owner <- map["is_owner"] name <- map["name"] icon_index <- map["icon_index"] group_type <- (map["group_type"], kStrTransformInt) group_template <- map["group_template"] labels <- map["labels"] level <- (map["level"], kIntTransformStr) review <- map["review"] description <- map["description"] share_code <- map["share_code"] people_no <- (map["people_no"], kStrTransformInt) } } extension UIImageView { @MainActor func setGroupIcon(_ model: GroupInfoModel) { setGroupIcon(url: model.groupIconURL, fallback: model.groupIcon) } @MainActor func setGroupIcon(url: String, fallback: UIImage?) { kf.cancelDownloadTask() image = fallback guard let imageURL = URL(string: url), !url.isEmpty else { return } kf.setImage(with: imageURL, placeholder: fallback) { [weak self] result in if case .failure = result { self?.image = fallback } } } } extension GroupInfoModel: IdentifiableType { public typealias Identity = String public var identity: String { return group_key } } struct CommonGroupModel: Mappable, Equatable { var group_key: String = "" var group_name: String = "" init(groupKey: String, groupName: String) { group_key = groupKey group_name = groupName } init?(map: Map) {} mutating func mapping(map: Map) { group_key <- map["group_key"] group_name <- map["group_name"] } static func deduplicated(_ groups: [CommonGroupModel]) -> [CommonGroupModel] { var seenKeys = Set() var result: [CommonGroupModel] = [] for group in groups { let key = group.group_key.trimmingCharacters(in: .whitespacesAndNewlines) guard !key.isEmpty, seenKeys.insert(key).inserted else { continue } result.append(group) } return result } } struct GroupMemberModel: Mappable, Equatable { /// var user_id: String = "" /// 头像,对应本地图标 var head_pic: String = "1" /// 用户头像 var userIcon: UIImage { let iconName = head_pic.trimmingCharacters(in: .whitespacesAndNewlines) guard !iconName.isEmpty, let image = UIImage(named: "UserIcon/\(iconName)") else { return UIImage(named: "UserIcon/1") ?? UIImage() } return image } /// 昵称 var nick_name: String = "" /// 会员等级 1普通 2白银 3永久 4钻石 var level: Int = 1 var vipIcon: UIImage { switch level { case 2: // 白银 return UIImage(named: "VipIcon/2") ?? UIImage() case 3: // 永久 return UIImage(named: "VipIcon/1") ?? UIImage() default: return UIImage() } } /// 状态:0、无状态 1、SOS 2、气泡等 var status: Int = 0 /// 入圈天数(若服务端下发) var join_days: Int = 0 /// 入圈时间戳。新接口使用秒,历史数据可能使用毫秒。 var join_time: Int64 = 0 /// 是否在线 var is_online: Bool = false /// 设备电量 var battery: String = "" /// 心情:0 无,1-36 对应 MoodStatusCatalog var mood: Int = 0 /// 最后在线时间 var last_active_time: Int64 = 0 /// 最后在线文本(MQTT 计算) var lastUpdateText: String { let nowMs = Date().timeIntervalSince1970 let msgTimeMs = Double(last_active_time) let diffSec = nowMs - msgTimeMs let isOnline = diffSec < 60 let lastUpdateText: String if isOnline { lastUpdateText = "" } else if diffSec < 3600 { lastUpdateText = "\(Int(diffSec / 60))分钟前" } else if diffSec < 86400 { lastUpdateText = "\(Int(diffSec / 3600))小时前" } else { lastUpdateText = "\(Int(diffSec / 86400))天前" } return lastUpdateText } /// 最后位置 var last_position: String = "" /// 当前用户与该成员共同加入的圈子 var common_groups: [CommonGroupModel] = [] var lastLocation: String { guard last_position.contains(":") else { return "" } let arr = last_position.components(separatedBy: ":") return arr.last ?? "" } init?(map: Map) { } mutating func mapping(map: Map) { user_id <- map["user_id"] head_pic <- map["head_pic"] level <- (map["level"], kStrTransformInt) nick_name <- map["nick_name"] battery <- map["battery"] last_position <- map["last_position"] is_online <- map["is_online"] last_active_time <- map["last_active_time"] status <- map["status"] mood <- (map["mood"], kStrTransformInt) join_days <- (map["join_days"], kStrTransformInt) join_time <- (map["join_time"], kGroupTransformInt64) common_groups <- map["common_groups"] common_groups = CommonGroupModel.deduplicated(common_groups) } } extension GroupMemberModel { /// New responses use seconds; older payloads may still contain milliseconds. var joinDate: Date? { guard join_time > 0 else { return nil } let seconds = join_time >= 100_000_000_000 ? TimeInterval(join_time) / 1000 : TimeInterval(join_time) return Date(timeIntervalSince1970: seconds) } } enum GroupOwnerMatcher { static func matches(userId: String, groupKey: String) -> Bool { !userId.isEmpty && !groupKey.isEmpty && groupKey.contains(userId) } } enum GroupMemberSorter { /// 圈主 -> 当前用户 -> 在线成员 -> 其他成员,同优先级保持接口顺序。 static func sorted(_ members: [GroupMemberModel], groupKey: String, currentUserId: String) -> [GroupMemberModel] { members.enumerated() .sorted { lhs, rhs in let lhsPriority = priority(lhs.element, groupKey: groupKey, currentUserId: currentUserId) let rhsPriority = priority(rhs.element, groupKey: groupKey, currentUserId: currentUserId) if lhsPriority != rhsPriority { return lhsPriority < rhsPriority } return lhs.offset < rhs.offset } .map(\.element) } private static func priority(_ member: GroupMemberModel, groupKey: String, currentUserId: String) -> Int { if GroupOwnerMatcher.matches(userId: member.user_id, groupKey: groupKey) { return 0 } if !currentUserId.isEmpty, member.user_id == currentUserId { return 1 } if member.is_online { return 2 } return 3 } } extension GroupMemberModel: IdentifiableType { public typealias Identity = String public var identity: String { return user_id } }