迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。
新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -0,0 +1,517 @@
|
||||
//
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
/// 将 Int、Double、Bool 或数字字符串宽松解码成整数。
|
||||
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
|
||||
}
|
||||
|
||||
/// 将 Bool、Int 或字符串宽松解码成布尔值。
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user