Files
suixinkan_ios_new/suixinkan/Features/CooperationOrder/Models/CooperationOrderModels.swift
汉秋 1970572edd 迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。
新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 13:33:00 +08:00

518 lines
18 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.

//
// CooperationOrderModels.swift
// suixinkan
//
// Created by Codex on 2026/6/29.
//
import Foundation
extension String {
/// fallback
func cooperationNonEmptyOrDefault(_ fallback: String) -> String {
let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? fallback : trimmed
}
}
///
struct ReferralOrderStatusUpdateRequest: Encodable {
let status: Int
}
///
struct CooperativeSalerListPayload: Decodable {
let total: Int
let list: [CooperativeSaler]
let page: Int
let pageSize: Int
enum CodingKeys: String, CodingKey {
case total
case list
case page
case pageSize = "page_size"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decodeLossyInt(forKey: .total) ?? 0
list = try container.decodeIfPresent([CooperativeSaler].self, forKey: .list) ?? []
page = try container.decodeLossyInt(forKey: .page) ?? 0
pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 0
}
}
///
struct CooperativeSaler: Decodable, Identifiable {
let saleUserId: Int
let bindingId: Int
let name: String
let salerName: String
let phone: String
let phoneMasked: String
let salerPhone: String
let shareCode: String
let storeId: Int
let bindTime: String
let boundAt: String
var id: Int { saleUserId }
var displayId: Int { saleUserId }
var displayName: String {
name.cooperationNonEmptyOrDefault(salerName)
}
var displayPhone: String {
salerPhone.cooperationNonEmptyOrDefault(phone)
}
var displayBindTime: String {
bindTime.cooperationNonEmptyOrDefault(boundAt)
}
enum CodingKeys: String, CodingKey {
case saleUserId = "sale_user_id"
case bindingId = "binding_id"
case name
case salerName = "saler_name"
case phone
case phoneMasked = "phone_masked"
case salerPhone = "saler_phone"
case shareCode = "share_code"
case storeId = "store_id"
case bindTime = "bind_time"
case boundAt = "bound_at"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
bindingId = try container.decodeLossyInt(forKey: .bindingId) ?? 0
name = try container.decodeLossyString(forKey: .name)
salerName = try container.decodeLossyString(forKey: .salerName)
phone = try container.decodeLossyString(forKey: .phone)
phoneMasked = try container.decodeLossyString(forKey: .phoneMasked)
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
shareCode = try container.decodeLossyString(forKey: .shareCode)
storeId = try container.decodeLossyInt(forKey: .storeId) ?? 0
bindTime = try container.decodeLossyString(forKey: .bindTime)
boundAt = try container.decodeLossyString(forKey: .boundAt)
}
}
///
struct SaleUserInviteInfo: Decodable {
let saleUserId: Int
let isBound: Bool
let photographerName: String
let photographerPhone: String
let salerName: String
let salerPhone: String
let shareCode: String
let subtitle: String
let title: String
enum CodingKeys: String, CodingKey {
case saleUserId = "sale_user_id"
case isBound = "is_bound"
case photographerName = "photographer_name"
case photographerPhone = "photographer_phone"
case salerName = "saler_name"
case salerPhone = "saler_phone"
case shareCode = "share_code"
case subtitle
case title
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
isBound = try container.decodeLossyBool(forKey: .isBound) ?? false
photographerName = try container.decodeLossyString(forKey: .photographerName)
photographerPhone = try container.decodeLossyString(forKey: .photographerPhone)
salerName = try container.decodeLossyString(forKey: .salerName)
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
shareCode = try container.decodeLossyString(forKey: .shareCode)
subtitle = try container.decodeLossyString(forKey: .subtitle)
title = try container.decodeLossyString(forKey: .title)
}
init(
saleUserId: Int,
isBound: Bool = false,
photographerName: String = "",
photographerPhone: String = "",
salerName: String = "",
salerPhone: String = "",
shareCode: String = "",
subtitle: String = "",
title: String = ""
) {
self.saleUserId = saleUserId
self.isBound = isBound
self.photographerName = photographerName
self.photographerPhone = photographerPhone
self.salerName = salerName
self.salerPhone = salerPhone
self.shareCode = shareCode
self.subtitle = subtitle
self.title = title
}
}
/// 线
struct ReferralLead: Decodable, Identifiable {
let id: Int
let leadId: Int
let userPhone: String
let userMobileMasked: String
let phone: String
let remark: String
let createdAt: String
let imageUrls: [String]
let images: [String]
var displayId: String {
let value = leadId > 0 ? leadId : id
return "\(value)"
}
var displayPhone: String {
userMobileMasked.cooperationNonEmptyOrDefault(userPhone.cooperationNonEmptyOrDefault(phone))
}
var displayImages: [String] {
imageUrls.isEmpty ? images : imageUrls
}
enum CodingKeys: String, CodingKey {
case id
case leadId = "lead_id"
case userPhone = "user_phone"
case userMobileMasked = "user_mobile_masked"
case phone
case remark
case createdAt = "created_at"
case imageUrls = "image_urls"
case images
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
leadId = try container.decodeLossyInt(forKey: .leadId) ?? 0
userPhone = try container.decodeLossyString(forKey: .userPhone)
userMobileMasked = try container.decodeLossyString(forKey: .userMobileMasked)
phone = try container.decodeLossyString(forKey: .phone)
remark = try container.decodeLossyString(forKey: .remark)
createdAt = try container.decodeLossyString(forKey: .createdAt)
imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? []
images = try container.decodeIfPresent([String].self, forKey: .images) ?? []
}
}
///
struct AcquisitionOrder: Decodable, Identifiable {
let id: Int
let orderNumber: String
let projectName: String
let orderType: Int
let orderTypeLabel: String
let orderAmount: String
let amount: String
let actualPayAmount: String
let createdAt: String
let customerPhone: String
let userMobileMasked: String
let phone: String
let orderStatusName: String
let status: String
let saleUserName: String
let partnerName: String
let partnerRole: String
let remark: String
let imageUrls: [String]
var displayAmount: String {
orderAmount.cooperationNonEmptyOrDefault(amount.cooperationNonEmptyOrDefault(actualPayAmount))
}
var displayCustomerPhone: String {
userMobileMasked.cooperationNonEmptyOrDefault(customerPhone.cooperationNonEmptyOrDefault(phone))
}
var displayStatus: String {
orderStatusName.cooperationNonEmptyOrDefault(status)
}
var displayPartner: String {
if !saleUserName.isEmpty { return saleUserName }
if !partnerName.isEmpty { return partnerName }
return ""
}
enum CodingKeys: String, CodingKey {
case id
case orderNumber = "order_number"
case projectName = "project_name"
case orderType = "order_type"
case orderTypeLabel = "order_type_label"
case orderAmount = "order_amount"
case amount
case actualPayAmount = "actual_pay_amount"
case createdAt = "created_at"
case customerPhone = "customer_phone"
case userMobileMasked = "user_mobile_masked"
case phone
case orderStatusName = "order_status_name"
case status
case saleUserName = "sale_user_name"
case partnerName = "partner_name"
case partnerRole = "partner_role"
case remark
case imageUrls = "image_urls"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
projectName = try container.decodeLossyString(forKey: .projectName)
orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0
orderTypeLabel = try container.decodeLossyString(forKey: .orderTypeLabel)
orderAmount = try container.decodeLossyString(forKey: .orderAmount)
amount = try container.decodeLossyString(forKey: .amount)
actualPayAmount = try container.decodeLossyString(forKey: .actualPayAmount)
createdAt = try container.decodeLossyString(forKey: .createdAt)
customerPhone = try container.decodeLossyString(forKey: .customerPhone)
userMobileMasked = try container.decodeLossyString(forKey: .userMobileMasked)
phone = try container.decodeLossyString(forKey: .phone)
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
status = try container.decodeLossyString(forKey: .status)
saleUserName = try container.decodeLossyString(forKey: .saleUserName)
partnerName = try container.decodeLossyString(forKey: .partnerName)
partnerRole = try container.decodeLossyString(forKey: .partnerRole)
remark = try container.decodeLossyString(forKey: .remark)
imageUrls = try container.decodeIfPresent([String].self, forKey: .imageUrls) ?? []
}
}
///
struct ReferralOrderSaler: Decodable {
let saleUserId: Int
let salerName: String
let salerPhone: String
enum CodingKeys: String, CodingKey {
case saleUserId = "sale_user_id"
case salerName = "saler_name"
case salerPhone = "saler_phone"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
salerName = try container.decodeLossyString(forKey: .salerName)
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
}
}
///
struct ReferralOrderListPayload: Decodable {
let total: Int
let list: [ReferralOrder]
let page: Int
let pageSize: Int
let saler: ReferralOrderSaler?
enum CodingKeys: String, CodingKey {
case total
case list
case page
case pageSize = "page_size"
case saler
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decodeLossyInt(forKey: .total) ?? 0
list = try container.decodeIfPresent([ReferralOrder].self, forKey: .list) ?? []
page = try container.decodeLossyInt(forKey: .page) ?? 0
pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 0
saler = try container.decodeIfPresent(ReferralOrderSaler.self, forKey: .saler)
}
}
///
struct ReferralOrder: Decodable, Identifiable {
let id: Int
let referralOrderNo: String
let orderNo: String
let userMobile: String
let remark: String
let createdAt: String
let status: Int
let statusLabel: String
let images: [String]
var displayReferralOrderNo: String {
referralOrderNo.cooperationNonEmptyOrDefault(orderNo)
}
enum CodingKeys: String, CodingKey {
case id
case referralOrderNo = "referral_order_no"
case orderNo = "order_no"
case userMobile = "user_mobile"
case remark
case createdAt = "created_at"
case status
case statusLabel = "status_label"
case images
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
referralOrderNo = try container.decodeLossyString(forKey: .referralOrderNo)
orderNo = try container.decodeLossyString(forKey: .orderNo)
userMobile = try container.decodeLossyString(forKey: .userMobile)
remark = try container.decodeLossyString(forKey: .remark)
createdAt = try container.decodeLossyString(forKey: .createdAt)
status = try container.decodeLossyInt(forKey: .status) ?? 0
statusLabel = try container.decodeLossyString(forKey: .statusLabel)
images = try container.decodeIfPresent([String].self, forKey: .images) ?? []
}
}
/// listv2 referral_order
struct BoundReferralOrder: Decodable, Equatable, Hashable {
let id: Int
let orderNo: String
let saleUserId: Int
let salerName: String
let salerPhone: String
let displayText: String
let userMobile: String
let remark: String
let bindOrderNumber: String
let sourceType: Int
let sourceTypeLabel: String
let status: Int
let statusLabel: String
let createdAt: String
enum CodingKeys: String, CodingKey {
case id
case orderNo = "order_no"
case saleUserId = "sale_user_id"
case salerName = "saler_name"
case salerPhone = "saler_phone"
case displayText = "display_text"
case userMobile = "user_mobile"
case remark
case bindOrderNumber = "bind_order_number"
case sourceType = "source_type"
case sourceTypeLabel = "source_type_label"
case status
case statusLabel = "status_label"
case createdAt = "created_at"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
orderNo = try container.decodeLossyString(forKey: .orderNo)
saleUserId = try container.decodeLossyInt(forKey: .saleUserId) ?? 0
salerName = try container.decodeLossyString(forKey: .salerName)
salerPhone = try container.decodeLossyString(forKey: .salerPhone)
displayText = try container.decodeLossyString(forKey: .displayText)
userMobile = try container.decodeLossyString(forKey: .userMobile)
remark = try container.decodeLossyString(forKey: .remark)
bindOrderNumber = try container.decodeLossyString(forKey: .bindOrderNumber)
sourceType = try container.decodeLossyInt(forKey: .sourceType) ?? 0
sourceTypeLabel = try container.decodeLossyString(forKey: .sourceTypeLabel)
status = try container.decodeLossyInt(forKey: .status) ?? 0
statusLabel = try container.decodeLossyString(forKey: .statusLabel)
createdAt = try container.decodeLossyString(forKey: .createdAt)
}
}
/// Sheet
struct BindAcquirerSheetItem: Identifiable {
let saleUserId: Int
var id: Int { saleUserId }
}
///
enum CooperationOrderRoute: Hashable {
case acquirer
case bind(saleUserId: Int)
}
private extension KeyedDecodingContainer {
/// JSON
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 ""
}
/// IntDoubleBool
func decodeLossyInt(forKey key: Key) throws -> Int? {
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
if let intValue = Int(text) {
return intValue
}
if let doubleValue = Double(text) {
return Int(doubleValue)
}
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return Int(value)
}
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value ? 1 : 0
}
return nil
}
/// BoolInt
func decodeLossyBool(forKey key: Key) throws -> Bool? {
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value != 0
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
switch value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
case "1", "true", "yes":
return true
case "0", "false", "no":
return false
default:
return nil
}
}
return nil
}
}