jsdw_ios/QuickLocation/Model/GroupModel.swift

607 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// GroupModel.swift
// QuickLocation
//
// Created by 八条 on 2026/5/30.
//
import ObjectMapper
import RxDataSources
import Kingfisher
private let kGroupTransformInt64 = TransformOf<Int64, Any>(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] = []
var reviewCount: Int = 0
init?(map: Map) {}
mutating func mapping(map: Map) {
code <- (map["code"], kIntTransformStr)
message <- map["message"]
model <- map["data.group"]
list <- map["data.mates"]
reviewCount <- (map["data.reviewCount"], kStrTransformInt)
if reviewCount == 0 {
reviewCount <- (map["data.review_count"], kStrTransformInt)
}
}
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 isCoupleGroup: Bool {
if groupTemplateName.contains("情侣") { return true }
if let limit = group_template?.limit, limit == 2 { return true }
return false
}
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 = ""
/// 当前用户在该圈的隐私权限
var privilege = GroupPrivilege()
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)
privilege <- map["privilege"]
if map["app_usage_record"].isKeyPresent {
privilege.app_usage_record <- (map["app_usage_record"], kFlexibleBool)
}
if map["phone_report"].isKeyPresent {
privilege.phone_report <- (map["phone_report"], kFlexibleBool)
}
if map["app_lock"].isKeyPresent {
privilege.app_lock <- (map["app_lock"], kFlexibleBool)
}
if map["pigeon_mail"].isKeyPresent {
privilege.pigeon_mail <- (map["pigeon_mail"], kFlexibleBool)
}
}
}
struct GroupPrivilege: Mappable, Equatable {
var app_usage_record = false
var phone_report = false
var app_lock = false
var pigeon_mail = false
init() {}
init?(map: Map) {}
mutating func mapping(map: Map) {
app_usage_record <- (map["app_usage_record"], kFlexibleBool)
phone_report <- (map["phone_report"], kFlexibleBool)
app_lock <- (map["app_lock"], kFlexibleBool)
pigeon_mail <- (map["pigeon_mail"], kFlexibleBool)
}
}
private let kFlexibleBool = TransformOf<Bool, Any>(fromJSON: { value in
switch value {
case let value as Bool:
return value
case let value as NSNumber:
return value.boolValue
case let value as Int:
return value != 0
case let value as String:
let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return text == "1" || text == "true" || text == "yes"
default:
return false
}
}, toJSON: { value in
value
})
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,
options: [.onFailureImage(kBrokenNetworkImage)]
)
}
}
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 = ""
var icon: String = ""
var icon_index: Int = 1
var group_template: GroupTemplateModel?
var groupIconURL: String {
let templateIcon = group_template?.icon.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !templateIcon.isEmpty { return templateIcon }
return icon.trimmingCharacters(in: .whitespacesAndNewlines)
}
var groupIcon: UIImage {
UIImage(named: "GroupIcon/\(icon_index)")
?? UIImage(named: "GroupIcon/1")
?? UIImage()
}
init(groupKey: String, groupName: String) {
group_key = groupKey
group_name = groupName
}
init(info: GroupInfoModel) {
group_key = info.group_key
group_name = info.name
icon = info.groupIconURL
icon_index = info.icon_index
group_template = info.group_template
}
init?(map: Map) {}
mutating func mapping(map: Map) {
group_key <- map["group_key"]
group_name <- map["group_name"]
if group_name.isEmpty {
group_name <- map["name"]
}
icon <- map["icon"]
icon_index <- (map["icon_index"], kStrTransformInt)
group_template <- map["group_template"]
if icon.isEmpty {
icon = group_template?.icon ?? ""
}
}
mutating func apply(_ info: GroupInfoModel) {
if group_name.isEmpty || group_name == "未知圈子" {
group_name = info.name
}
if icon.isEmpty { icon = info.groupIconURL }
if icon_index <= 1 { icon_index = info.icon_index }
if group_template == nil { group_template = info.group_template }
}
static func deduplicated(_ groups: [CommonGroupModel]) -> [CommonGroupModel] {
var seenKeys = Set<String>()
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 avater: String = ""
var displayHeadPic: String {
HeadPic.resolved(head_pic, avater)
}
/// 用户头像
var userIcon: UIImage {
HeadPic.image(for: displayHeadPic)
}
/// 昵称
var nick_name: String = ""
/// 备注
var remark: String = ""
var showName: String {
remark.isEmpty ? nick_name : remark
}
/// 状态: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] = []
/// 关系 extra.relation_idx
var extra = MemberExtra()
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"], kFlexString)
head_pic <- (map["head_pic"], kFlexString)
avater <- (map["avater"], kFlexString)
if avater.isEmpty {
avater <- (map["avatar"], kFlexString)
}
nick_name <- map["nick_name"]
remark <- map["remark"]
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)
extra <- (map["extra"], kMemberExtraTransform)
if extra.relation_idx.isEmpty {
extra.relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform)
}
}
}
struct MemberExtra: Mappable, Equatable {
var relation_idx: String = ""
var privilege = GroupPrivilege()
init() {}
init?(map: Map) {}
mutating func mapping(map: Map) {
relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform)
privilege <- map["privilege"]
}
static func decode(_ value: Any?) -> MemberExtra {
if let extra = value as? MemberExtra {
return extra
}
if let json = value as? [String: Any] {
return MemberExtra(JSON: json) ?? MemberExtra()
}
if let json = value as? NSDictionary,
let dictionary = json as? [String: Any] {
return MemberExtra(JSON: dictionary) ?? MemberExtra()
}
let data: Data?
if let value = value as? String {
data = value.data(using: .utf8)
} else {
data = value as? Data
}
guard let data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return MemberExtra()
}
return MemberExtra(JSON: json) ?? MemberExtra()
}
static func relationIndex(from value: Any?) -> String {
let raw = transformStr(value) ?? ""
return raw.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
let kMemberRelationIndexTransform = TransformOf<String, Any>(fromJSON: { value in
MemberExtra.relationIndex(from: value)
}, toJSON: { value in
value
})
let kMemberExtraTransform = TransformOf<MemberExtra, Any>(fromJSON: { value in
MemberExtra.decode(value)
}, toJSON: { value in
value?.toJSON()
})
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
}
}