969 lines
37 KiB
Swift
969 lines
37 KiB
Swift
//
|
||
// OrderModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/22.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 订单 Tab 内的子入口,表示订单管理或核销订单。
|
||
enum OrdersEntry: Hashable {
|
||
case storeOrders
|
||
case verificationOrders
|
||
}
|
||
|
||
/// 订单状态筛选实体,表示订单管理列表顶部的状态过滤项。
|
||
struct OrderStatusFilter: Identifiable, Hashable {
|
||
let id: Int
|
||
let title: String
|
||
}
|
||
|
||
/// 订单筛选常量,集中维护旧工程同步过来的订单状态。
|
||
enum OrderFilters {
|
||
static let statusFilters: [OrderStatusFilter] = [
|
||
.init(id: 0, title: "全部"),
|
||
.init(id: 18, title: "已付定金"),
|
||
.init(id: 30, title: "已完成"),
|
||
.init(id: 50, title: "已退款"),
|
||
.init(id: -1, title: "需要精修"),
|
||
.init(id: -2, title: "无需精修")
|
||
]
|
||
|
||
/// 店长订单 Tab 筛选项(无精修筛选项)。
|
||
static let storeAdminStatusFilters: [OrderStatusFilter] = [
|
||
.init(id: 0, title: "全部"),
|
||
.init(id: 18, title: "已付定金"),
|
||
.init(id: 30, title: "已完成"),
|
||
.init(id: 50, title: "已退款")
|
||
]
|
||
|
||
/// 核销管理页状态 Tab 筛选项。
|
||
static let writeOffStatusFilters: [OrderStatusFilter] = [
|
||
.init(id: 0, title: "全部"),
|
||
.init(id: 18, title: "已支付"),
|
||
.init(id: 30, title: "已完成"),
|
||
.init(id: 1, title: "已核销"),
|
||
.init(id: 2, title: "未核销")
|
||
]
|
||
}
|
||
|
||
/// 店铺订单类型常量,与 Android StoreOrderItem 约定一致。
|
||
enum StoreOrderType {
|
||
static let offlineProduct = 4
|
||
static let multiTravelShoot = 19
|
||
}
|
||
|
||
/// 订单管理列表实体,承载订单卡片展示和筛选判断所需字段。
|
||
struct OrderEntity: Decodable, Identifiable, Equatable, Hashable {
|
||
var id: String { orderNumber }
|
||
|
||
let photogUid: Int
|
||
let orderNumber: String
|
||
let remark: String
|
||
let scenicAreaId: Int
|
||
let payTime: String
|
||
let completeTime: String
|
||
let orderStatus: Int
|
||
let orderStatusName: String
|
||
let storeId: Int?
|
||
let userId: Int
|
||
let projectId: Int
|
||
let phone: String
|
||
let orderAmount: String
|
||
let actualPayAmount: String
|
||
let actualRefundAmount: String
|
||
let refundAmount: String
|
||
let payTypeName: String
|
||
let payType: Int
|
||
let projectName: String
|
||
let orderType: Int
|
||
let orderTypeLabel: String
|
||
let depositPayTime: String
|
||
let createdAt: String
|
||
let photoTravel: OrderPhotoTravel?
|
||
let multiTravel: OrderListMultiTravel?
|
||
let isNeedEdit: Bool
|
||
var isRefined: Int
|
||
let canBindReferral: Int
|
||
let showOrderSource: Int
|
||
let referralOrderNo: String
|
||
let referralSalerName: String
|
||
let referralSalerPhotogRemarkName: String
|
||
let referralOrder: BoundReferralOrder?
|
||
|
||
/// 订单卡片展示的获客员名称,优先备注名。
|
||
var displayReferralSalerName: String {
|
||
referralSalerPhotogRemarkName.saleUserDisplayName(primaryName: referralSalerName)
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case photogUid = "photog_uid"
|
||
case orderNumber = "order_number"
|
||
case remark
|
||
case scenicAreaId = "scenic_area_id"
|
||
case payTime = "pay_time"
|
||
case completeTime = "complete_time"
|
||
case orderStatus = "order_status"
|
||
case orderStatusName = "order_status_name"
|
||
case storeId = "store_id"
|
||
case userId = "user_id"
|
||
case projectId = "project_id"
|
||
case phone
|
||
case orderAmount = "order_amount"
|
||
case actualPayAmount = "actual_pay_amount"
|
||
case actualRefundAmount = "actual_refund_amount"
|
||
case refundAmount = "refund_amount"
|
||
case payTypeName = "pay_type_name"
|
||
case payType = "pay_type"
|
||
case projectName = "project_name"
|
||
case orderType = "order_type"
|
||
case orderTypeLabel = "order_type_label"
|
||
case depositPayTime = "deposit_pay_time"
|
||
case createdAt = "created_at"
|
||
case photoTravel = "photo_travel"
|
||
case multiTravel = "multi_travel"
|
||
case isNeedEdit
|
||
case isRefined = "is_refined"
|
||
case canBindReferral = "can_bind_referral"
|
||
case showOrderSource = "show_order_source"
|
||
case referralOrderNo = "referral_order_no"
|
||
case referralSalerName = "referral_saler_name"
|
||
case referralSalerPhotogRemarkName = "photog_remark_name"
|
||
case referralOrder = "referral_order"
|
||
}
|
||
|
||
/// 宽松解码订单字段,兼容后端数字和字符串混用。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
photogUid = try container.decodeLossyInt(forKey: .photogUid) ?? 0
|
||
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
|
||
remark = try container.decodeLossyString(forKey: .remark)
|
||
scenicAreaId = try container.decodeLossyInt(forKey: .scenicAreaId) ?? 0
|
||
payTime = try container.decodeLossyString(forKey: .payTime)
|
||
completeTime = try container.decodeLossyString(forKey: .completeTime)
|
||
orderStatus = try container.decodeLossyInt(forKey: .orderStatus) ?? 0
|
||
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
|
||
storeId = try container.decodeLossyInt(forKey: .storeId)
|
||
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
||
projectId = try container.decodeLossyInt(forKey: .projectId) ?? 0
|
||
phone = try container.decodeLossyString(forKey: .phone)
|
||
orderAmount = try container.decodeLossyString(forKey: .orderAmount)
|
||
actualPayAmount = try container.decodeLossyString(forKey: .actualPayAmount)
|
||
actualRefundAmount = try container.decodeLossyString(forKey: .actualRefundAmount)
|
||
refundAmount = try container.decodeLossyString(forKey: .refundAmount)
|
||
payTypeName = try container.decodeLossyString(forKey: .payTypeName)
|
||
payType = try container.decodeLossyInt(forKey: .payType) ?? 0
|
||
projectName = try container.decodeLossyString(forKey: .projectName)
|
||
orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0
|
||
orderTypeLabel = try container.decodeLossyString(forKey: .orderTypeLabel)
|
||
depositPayTime = try container.decodeLossyString(forKey: .depositPayTime)
|
||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||
photoTravel = try container.decodeIfPresent(OrderPhotoTravel.self, forKey: .photoTravel)
|
||
multiTravel = try container.decodeIfPresent(OrderListMultiTravel.self, forKey: .multiTravel)
|
||
isNeedEdit = try container.decodeLossyBool(forKey: .isNeedEdit) ?? true
|
||
isRefined = try container.decodeLossyInt(forKey: .isRefined) ?? 0
|
||
canBindReferral = try container.decodeLossyInt(forKey: .canBindReferral) ?? 0
|
||
showOrderSource = try container.decodeLossyInt(forKey: .showOrderSource) ?? 0
|
||
referralOrderNo = try container.decodeLossyString(forKey: .referralOrderNo)
|
||
referralSalerName = try container.decodeLossyString(forKey: .referralSalerName)
|
||
referralSalerPhotogRemarkName = try container.decodeLossyString(forKey: .referralSalerPhotogRemarkName)
|
||
referralOrder = try container.decodeIfPresent(BoundReferralOrder.self, forKey: .referralOrder)
|
||
}
|
||
}
|
||
|
||
/// 旅拍订单附加信息实体,表示修图、视频和核销相关数量。
|
||
struct OrderPhotoTravel: Decodable, Equatable, Hashable {
|
||
let orderPhotoNum: Int
|
||
let orderVideoNum: Int
|
||
let checkInTime: String
|
||
let retouchGiftPhotoNum: Int
|
||
let retouchGiftVideoNum: Int
|
||
let canCancelOrder: Bool
|
||
let needCheckIn: Bool
|
||
let canGiftRetouch: Bool
|
||
let retouchGiftTime: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderPhotoNum = "order_photo_num"
|
||
case orderVideoNum = "order_video_num"
|
||
case checkInTime = "check_in_time"
|
||
case retouchGiftPhotoNum = "retouch_gift_photo_num"
|
||
case retouchGiftVideoNum = "retouch_gift_video_num"
|
||
case canCancelOrder = "can_cancel_order"
|
||
case needCheckIn = "need_check_in"
|
||
case canGiftRetouch = "can_gift_retouch"
|
||
case retouchGiftTime = "retouch_gift_time"
|
||
}
|
||
|
||
/// 宽松解码旅拍附加信息。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
orderPhotoNum = try container.decodeLossyInt(forKey: .orderPhotoNum) ?? 0
|
||
orderVideoNum = try container.decodeLossyInt(forKey: .orderVideoNum) ?? 0
|
||
checkInTime = try container.decodeLossyString(forKey: .checkInTime)
|
||
retouchGiftPhotoNum = try container.decodeLossyInt(forKey: .retouchGiftPhotoNum) ?? 0
|
||
retouchGiftVideoNum = try container.decodeLossyInt(forKey: .retouchGiftVideoNum) ?? 0
|
||
canCancelOrder = try container.decodeLossyBool(forKey: .canCancelOrder) ?? false
|
||
needCheckIn = try container.decodeLossyBool(forKey: .needCheckIn) ?? false
|
||
canGiftRetouch = try container.decodeLossyBool(forKey: .canGiftRetouch) ?? false
|
||
retouchGiftTime = try container.decodeLossyString(forKey: .retouchGiftTime)
|
||
}
|
||
}
|
||
|
||
/// 订单列表多点位旅拍缩略图实体,仅用于列表卡片历史拍摄预览。
|
||
struct OrderListMultiTravel: Decodable, Equatable, Hashable {
|
||
let materialList: [OrderMediaFile]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case materialList = "material_list"
|
||
}
|
||
|
||
/// 宽松解码列表缩略图字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
materialList = try container.decodeIfPresent([OrderMediaFile].self, forKey: .materialList) ?? []
|
||
}
|
||
}
|
||
|
||
/// 店长订单 Tab 列表实体,对齐 Android StoreOrderItem。
|
||
struct StoreOrderListItem: Decodable, Identifiable, Equatable, Hashable {
|
||
var id: String { orderNumber }
|
||
|
||
let orderNumber: String
|
||
let orderType: Int
|
||
let orderTypeLabel: String
|
||
let createdAt: String
|
||
let orderStatus: Int
|
||
let orderStatusName: String
|
||
let actualPayAmount: String
|
||
let actualRefundAmount: String
|
||
let payTypeName: String
|
||
let payTime: String
|
||
let completeTime: String
|
||
let userId: Int
|
||
let phone: String
|
||
let projectId: Int
|
||
let projectName: String
|
||
let remark: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case orderType = "order_type"
|
||
case orderTypeLabel = "order_type_label"
|
||
case createdAt = "created_at"
|
||
case orderStatus = "order_status"
|
||
case orderStatusName = "order_status_name"
|
||
case actualPayAmount = "actual_pay_amount"
|
||
case actualRefundAmount = "actual_refund_amount"
|
||
case payTypeName = "pay_type_name"
|
||
case payTime = "pay_time"
|
||
case completeTime = "complete_time"
|
||
case userId = "user_id"
|
||
case phone
|
||
case projectId = "project_id"
|
||
case projectName = "project_name"
|
||
case remark
|
||
}
|
||
|
||
/// 宽松解码店长订单列表字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
|
||
orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0
|
||
orderTypeLabel = try container.decodeLossyString(forKey: .orderTypeLabel)
|
||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||
orderStatus = try container.decodeLossyInt(forKey: .orderStatus) ?? 0
|
||
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
|
||
actualPayAmount = try container.decodeLossyString(forKey: .actualPayAmount)
|
||
actualRefundAmount = try container.decodeLossyString(forKey: .actualRefundAmount)
|
||
payTypeName = try container.decodeLossyString(forKey: .payTypeName)
|
||
payTime = try container.decodeLossyString(forKey: .payTime)
|
||
completeTime = try container.decodeLossyString(forKey: .completeTime)
|
||
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
||
phone = try container.decodeLossyString(forKey: .phone)
|
||
projectId = try container.decodeLossyInt(forKey: .projectId) ?? 0
|
||
projectName = try container.decodeLossyString(forKey: .projectName)
|
||
remark = try container.decodeLossyString(forKey: .remark)
|
||
}
|
||
}
|
||
|
||
/// 核销订单列表实体,表示可核销订单卡片的展示和操作状态。
|
||
struct WriteOffOrderItem: Decodable, Identifiable, Equatable, Hashable {
|
||
var id: String { orderNumber }
|
||
|
||
let orderNumber: String
|
||
let orderVerificationStatus: String
|
||
let payTime: String
|
||
let projectName: String
|
||
let userPhone: String
|
||
let orderAmount: String
|
||
let orderStatusName: String
|
||
let orderVerificationTime: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case orderVerificationStatus = "order_verification_status"
|
||
case payTime = "pay_time"
|
||
case projectName = "project_name"
|
||
case userPhone = "user_phone"
|
||
case orderAmount = "order_amount"
|
||
case orderStatusName = "order_status_name"
|
||
case orderVerificationTime = "order_verification_time"
|
||
}
|
||
|
||
/// 宽松解码核销订单字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
|
||
orderVerificationStatus = try container.decodeLossyString(forKey: .orderVerificationStatus)
|
||
payTime = try container.decodeLossyString(forKey: .payTime)
|
||
projectName = try container.decodeLossyString(forKey: .projectName)
|
||
userPhone = try container.decodeLossyString(forKey: .userPhone)
|
||
orderAmount = try container.decodeLossyString(forKey: .orderAmount)
|
||
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
|
||
orderVerificationTime = try container.decodeLossyString(forKey: .orderVerificationTime)
|
||
}
|
||
}
|
||
|
||
/// 核销订单请求实体,提交手动输入的订单号。
|
||
struct WriteOffRequest: Encodable {
|
||
let orderNumber: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
}
|
||
}
|
||
|
||
/// 押金订单列表实体,表示首页押金订单卡片的基础信息和操作状态。
|
||
struct DepositOrderListItem: Decodable, Identifiable, Equatable, Hashable {
|
||
let id: Int
|
||
let orderNumber: String
|
||
let userPhone: String
|
||
let amount: String
|
||
let status: Int
|
||
let statusName: String
|
||
let createdAt: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case orderNumber = "order_number"
|
||
case userPhone = "user_phone"
|
||
case amount
|
||
case status
|
||
case statusName = "status_name"
|
||
case createdAt = "created_at"
|
||
}
|
||
|
||
/// 宽松解码押金订单字段,兼容后端数字和字符串混用。
|
||
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)
|
||
userPhone = try container.decodeLossyString(forKey: .userPhone)
|
||
amount = try container.decodeLossyString(forKey: .amount)
|
||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||
statusName = try container.decodeLossyString(forKey: .statusName)
|
||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||
}
|
||
}
|
||
|
||
/// 押金订单核销请求实体。
|
||
struct DepositOrderWriteOffRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
}
|
||
}
|
||
|
||
/// 押金订单退款请求实体。
|
||
struct DepositOrderRefundRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
let refundReason: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case refundReason = "refund_reason"
|
||
}
|
||
}
|
||
|
||
/// 普通订单退款模式,表示全额退款或部分退款。
|
||
enum OrderRefundMode: Int, CaseIterable, Identifiable, Hashable {
|
||
case full = 1
|
||
case partial = 3
|
||
|
||
var id: Int { rawValue }
|
||
|
||
/// 退款模式展示文案。
|
||
var title: String {
|
||
switch self {
|
||
case .full:
|
||
return "全额退款"
|
||
case .partial:
|
||
return "部分退款"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 取消订单请求实体。
|
||
struct CancelOrderRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
}
|
||
}
|
||
|
||
/// 赠送修图/视频请求实体。
|
||
struct SendGiftRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
let photoNum: Int
|
||
let videoNum: Int
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case photoNum = "photo_num"
|
||
case videoNum = "video_num "
|
||
}
|
||
}
|
||
|
||
/// 更新精修选择请求实体。
|
||
struct UpdateRefinedRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
let isRefined: Int
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case isRefined = "is_refined"
|
||
}
|
||
}
|
||
|
||
/// 店长订单退款请求实体。
|
||
struct StoreOrderRefundRequest: Encodable, Equatable {
|
||
let storeId: Int
|
||
let orderNumber: String
|
||
let refundType: Int
|
||
let refundAmount: String
|
||
let refundReason: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case storeId = "store_id"
|
||
case orderNumber = "order_number"
|
||
case refundType = "refund_type"
|
||
case refundAmount = "refund_amount"
|
||
case refundReason = "refund_reason"
|
||
}
|
||
}
|
||
|
||
/// 普通订单退款请求实体。
|
||
struct OrderRefundRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
let refundType: Int
|
||
let refundAmount: String
|
||
let refundReason: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case refundType = "refund_type"
|
||
case refundAmount = "refund_amount"
|
||
case refundReason = "refund_reason"
|
||
}
|
||
}
|
||
|
||
/// 订单详情路由实体,表示订单模块内部可 push 的二级页面。
|
||
enum OrdersRoute: Hashable {
|
||
case depositDetail(orderNumber: String)
|
||
case depositShootingInfo(orderNumber: String, scenicSpotId: Int, photogUid: Int)
|
||
case historicalShooting(orderNumber: String)
|
||
case multiTravelTaskUpload(orderNumber: String)
|
||
case orderTrailer(orderNumber: String, title: String)
|
||
case writeOffList
|
||
}
|
||
|
||
/// 订单详情接口响应实体,承载门店订单详情页的完整展示数据。
|
||
struct StoreOrderDetailResponse: Decodable, Equatable, Hashable {
|
||
let orderNumber: String
|
||
let orderType: Int
|
||
let orderTypeLabel: String
|
||
let createdAt: String
|
||
let orderStatus: Int
|
||
let orderStatusName: String
|
||
let actualPayAmount: String
|
||
let actualRefundAmount: String
|
||
let payTypeName: String
|
||
let payTime: String
|
||
let completeTime: String
|
||
let userId: Int
|
||
let phone: String
|
||
let projectId: Int
|
||
let projectName: String
|
||
let remark: String
|
||
let multiTravel: StoreOrderMultiTravelInfo?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case orderType = "order_type"
|
||
case orderTypeLabel = "order_type_label"
|
||
case createdAt = "created_at"
|
||
case orderStatus = "order_status"
|
||
case orderStatusName = "order_status_name"
|
||
case actualPayAmount = "actual_pay_amount"
|
||
case actualRefundAmount = "actual_refund_amount"
|
||
case payTypeName = "pay_type_name"
|
||
case payTime = "pay_time"
|
||
case completeTime = "complete_time"
|
||
case userId = "user_id"
|
||
case phone
|
||
case projectId = "project_id"
|
||
case projectName = "project_name"
|
||
case remark
|
||
case multiTravel = "multi_travel"
|
||
}
|
||
|
||
/// 宽松解码订单详情字段,兼容后端数字和字符串混用。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
orderNumber = try container.decodeLossyString(forKey: .orderNumber)
|
||
orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0
|
||
orderTypeLabel = try container.decodeLossyString(forKey: .orderTypeLabel)
|
||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||
orderStatus = try container.decodeLossyInt(forKey: .orderStatus) ?? 0
|
||
orderStatusName = try container.decodeLossyString(forKey: .orderStatusName)
|
||
actualPayAmount = try container.decodeLossyString(forKey: .actualPayAmount)
|
||
actualRefundAmount = try container.decodeLossyString(forKey: .actualRefundAmount)
|
||
payTypeName = try container.decodeLossyString(forKey: .payTypeName)
|
||
payTime = try container.decodeLossyString(forKey: .payTime)
|
||
completeTime = try container.decodeLossyString(forKey: .completeTime)
|
||
userId = try container.decodeLossyInt(forKey: .userId) ?? 0
|
||
phone = try container.decodeLossyString(forKey: .phone)
|
||
projectId = try container.decodeLossyInt(forKey: .projectId) ?? 0
|
||
projectName = try container.decodeLossyString(forKey: .projectName)
|
||
remark = try container.decodeLossyString(forKey: .remark)
|
||
multiTravel = try container.decodeIfPresent(StoreOrderMultiTravelInfo.self, forKey: .multiTravel)
|
||
}
|
||
}
|
||
|
||
/// 多景点旅拍详情实体,表示项目配置和各拍摄点状态。
|
||
struct StoreOrderMultiTravelInfo: Decodable, Equatable, Hashable {
|
||
let projectInfo: StoreOrderProjectInfo?
|
||
let shootingList: [StoreOrderShootingListItem]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case projectInfo = "project_info"
|
||
case shootingList = "shooting_list"
|
||
}
|
||
|
||
/// 宽松解码多景点旅拍信息。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
projectInfo = try container.decodeIfPresent(StoreOrderProjectInfo.self, forKey: .projectInfo)
|
||
shootingList = try container.decodeIfPresent([StoreOrderShootingListItem].self, forKey: .shootingList) ?? []
|
||
}
|
||
}
|
||
|
||
/// 订单项目配置实体,表示打卡点数量和单点素材数量。
|
||
struct StoreOrderProjectInfo: Decodable, Equatable, Hashable {
|
||
let settleSpotNum: Int
|
||
let singleSpotMaterialNum: Int
|
||
let singleSpotPhotoNum: Int
|
||
let singleSpotVideoNum: Int
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case settleSpotNum = "settle_spot_num"
|
||
case singleSpotMaterialNum = "single_spot_material_num"
|
||
case singleSpotPhotoNum = "single_spot_photo_num"
|
||
case singleSpotVideoNum = "single_spot_video_num"
|
||
}
|
||
|
||
/// 宽松解码项目配置字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
settleSpotNum = try container.decodeLossyInt(forKey: .settleSpotNum) ?? 0
|
||
singleSpotMaterialNum = try container.decodeLossyInt(forKey: .singleSpotMaterialNum) ?? 0
|
||
singleSpotPhotoNum = try container.decodeLossyInt(forKey: .singleSpotPhotoNum) ?? 0
|
||
singleSpotVideoNum = try container.decodeLossyInt(forKey: .singleSpotVideoNum) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 拍摄点状态实体,表示订单在某个打卡点的拍摄人员和完成情况。
|
||
struct StoreOrderShootingListItem: Decodable, Identifiable, Equatable, Hashable {
|
||
var id: String { "\(scenicSpotId)-\(photogUid)" }
|
||
|
||
let scenicSpotId: Int
|
||
let photogUid: Int
|
||
let scenicSpotName: String
|
||
let staffName: String
|
||
let photogName: String
|
||
let status: Int
|
||
let startAvg: Double
|
||
let start: Double
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case scenicSpotId = "scenic_spot_id"
|
||
case photogUid = "photog_uid"
|
||
case scenicSpotName = "scenic_spot_name"
|
||
case staffName = "staff_name"
|
||
case photogName = "photog_name"
|
||
case status
|
||
case startAvg = "start_avg"
|
||
case start
|
||
}
|
||
|
||
/// 宽松解码拍摄点状态字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
scenicSpotId = try container.decodeLossyInt(forKey: .scenicSpotId) ?? 0
|
||
photogUid = try container.decodeLossyInt(forKey: .photogUid) ?? 0
|
||
scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName)
|
||
staffName = try container.decodeLossyString(forKey: .staffName)
|
||
photogName = try container.decodeLossyString(forKey: .photogName)
|
||
status = try container.decodeLossyInt(forKey: .status) ?? 0
|
||
startAvg = try container.decodeLossyDouble(forKey: .startAvg) ?? 0
|
||
start = try container.decodeLossyDouble(forKey: .start) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 订单拍摄信息响应实体,表示某个打卡点的评价、底片和成片。
|
||
struct StoreOrderShootingDetailResponse: Decodable, Equatable, Hashable {
|
||
let scenicSpotId: Int
|
||
let scenicSpotName: String
|
||
let orderType: Int
|
||
let orderTypeName: String
|
||
let orderComment: StoreOrderComment?
|
||
let materialList: [OrderMediaFile]
|
||
let completeList: [OrderMediaFile]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case scenicSpotId = "scenic_spot_id"
|
||
case scenicSpotName = "scenic_spot_name"
|
||
case orderType = "order_type"
|
||
case orderTypeName = "order_type_name"
|
||
case orderComment = "order_comment"
|
||
case materialList = "material_list"
|
||
case completeList = "complete_list"
|
||
}
|
||
|
||
/// 宽松解码拍摄信息详情。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
scenicSpotId = try container.decodeLossyInt(forKey: .scenicSpotId) ?? 0
|
||
scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName)
|
||
orderType = try container.decodeLossyInt(forKey: .orderType) ?? 0
|
||
orderTypeName = try container.decodeLossyString(forKey: .orderTypeName)
|
||
orderComment = try container.decodeIfPresent(StoreOrderComment.self, forKey: .orderComment)
|
||
materialList = try container.decodeIfPresent([OrderMediaFile].self, forKey: .materialList) ?? []
|
||
completeList = try container.decodeIfPresent([OrderMediaFile].self, forKey: .completeList) ?? []
|
||
}
|
||
}
|
||
|
||
/// 订单评价实体,表示拍摄、修图、景区、服务和相机评分。
|
||
struct StoreOrderComment: Decodable, Equatable, Hashable {
|
||
let starShooting: Int
|
||
let starRetouching: Int
|
||
let starScenery: Int
|
||
let starService: Int
|
||
let starCamera: Int
|
||
let content: String
|
||
let createdAt: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case starShooting = "star_shooting"
|
||
case starRetouching = "star_retouching"
|
||
case starScenery = "star_scenery"
|
||
case starService = "star_service"
|
||
case starCamera = "star_camera"
|
||
case content
|
||
case createdAt = "created_at"
|
||
}
|
||
|
||
/// 宽松解码评价字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
starShooting = try container.decodeLossyInt(forKey: .starShooting) ?? 0
|
||
starRetouching = try container.decodeLossyInt(forKey: .starRetouching) ?? 0
|
||
starScenery = try container.decodeLossyInt(forKey: .starScenery) ?? 0
|
||
starService = try container.decodeLossyInt(forKey: .starService) ?? 0
|
||
starCamera = try container.decodeLossyInt(forKey: .starCamera) ?? 0
|
||
content = try container.decodeLossyString(forKey: .content)
|
||
createdAt = try container.decodeLossyString(forKey: .createdAt)
|
||
}
|
||
}
|
||
|
||
/// 订单媒体文件实体,表示底片、成片或历史拍摄媒体。
|
||
struct OrderMediaFile: Decodable, Identifiable, Equatable, Hashable {
|
||
var id: String { "\(fileUrl)-\(fileName)-\(uploadTime)" }
|
||
|
||
let fileName: String
|
||
let fileUrl: String
|
||
let fileType: Int
|
||
let fileSize: Int64
|
||
let coverUrl: String
|
||
let uploadTime: String
|
||
|
||
var isVideo: Bool {
|
||
fileType == 1
|
||
}
|
||
|
||
var previewURLString: String {
|
||
coverUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? fileUrl : coverUrl
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case fileName = "file_name"
|
||
case fileUrl = "file_url"
|
||
case fileType = "file_type"
|
||
case fileSize = "file_size"
|
||
case coverUrl = "cover_url"
|
||
case uploadTime = "upload_time"
|
||
}
|
||
|
||
/// 宽松解码媒体文件字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
fileName = try container.decodeLossyString(forKey: .fileName)
|
||
fileUrl = try container.decodeLossyString(forKey: .fileUrl)
|
||
fileType = try container.decodeLossyInt(forKey: .fileType) ?? 0
|
||
fileSize = Int64(try container.decodeLossyInt(forKey: .fileSize) ?? 0)
|
||
coverUrl = try container.decodeLossyString(forKey: .coverUrl)
|
||
uploadTime = try container.decodeLossyString(forKey: .uploadTime)
|
||
}
|
||
}
|
||
|
||
/// 历史拍摄响应实体,表示订单多点位历史拍摄媒体。
|
||
struct MultiTravelShootHistoryResponse: Decodable, Equatable, Hashable {
|
||
let projectName: String
|
||
let projectType: Int
|
||
let projectTypeName: String
|
||
let photogSpotList: [PhotogSpotItem]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case projectName = "project_name"
|
||
case projectType = "project_type"
|
||
case projectTypeName = "project_type_name"
|
||
case photogSpotList = "photog_spot_list"
|
||
}
|
||
|
||
/// 宽松解码历史拍摄响应。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
projectName = try container.decodeLossyString(forKey: .projectName)
|
||
projectType = try container.decodeLossyInt(forKey: .projectType) ?? 0
|
||
projectTypeName = try container.decodeLossyString(forKey: .projectTypeName)
|
||
photogSpotList = try container.decodeIfPresent([PhotogSpotItem].self, forKey: .photogSpotList) ?? []
|
||
}
|
||
}
|
||
|
||
/// 历史拍摄打卡点实体,表示某个摄影师在打卡点上传过的媒体。
|
||
struct PhotogSpotItem: Decodable, Identifiable, Equatable, Hashable {
|
||
var id: String { "\(scenicSpotId)-\(photogUid)" }
|
||
|
||
let scenicSpotId: Int
|
||
let photogUid: Int
|
||
let scenicSpotName: String
|
||
let photogNickname: String
|
||
let photogName: String
|
||
let files: [OrderMediaFile]
|
||
|
||
var photographerDisplayName: String {
|
||
let nickname = photogNickname.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
return nickname.isEmpty ? photogName : nickname
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case scenicSpotId = "scenic_spot_id"
|
||
case photogUid = "photog_uid"
|
||
case scenicSpotName = "scenic_spot_name"
|
||
case photogNickname = "photog_nickname"
|
||
case photogName = "photog_name"
|
||
case files
|
||
}
|
||
|
||
/// 宽松解码历史拍摄打卡点。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
scenicSpotId = try container.decodeLossyInt(forKey: .scenicSpotId) ?? 0
|
||
photogUid = try container.decodeLossyInt(forKey: .photogUid) ?? 0
|
||
scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName)
|
||
photogNickname = try container.decodeLossyString(forKey: .photogNickname)
|
||
photogName = try container.decodeLossyString(forKey: .photogName)
|
||
files = try container.decodeIfPresent([OrderMediaFile].self, forKey: .files) ?? []
|
||
}
|
||
}
|
||
|
||
/// 多点旅拍已核销打卡点实体,用于任务上传时选择目标打卡点。
|
||
struct MultiTravelVerifiedScenicSpotItem: Decodable, Identifiable, Equatable, Hashable {
|
||
let id: Int
|
||
let name: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case name
|
||
}
|
||
|
||
/// 宽松解码已核销打卡点字段。
|
||
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 MultiTravelUploadMaterialRequest: Encodable, Equatable {
|
||
let orderNumber: String
|
||
let scenicSpotId: Int
|
||
let cloudFile: [MultiTravelCloudFileItem]
|
||
let uploadFile: [MultiTravelUploadFileItem]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderNumber = "order_number"
|
||
case scenicSpotId = "scenic_spot_id"
|
||
case cloudFile = "cloud_file"
|
||
case uploadFile = "upload_file"
|
||
}
|
||
}
|
||
|
||
/// 多点旅拍云盘素材提交项。
|
||
struct MultiTravelCloudFileItem: Encodable, Equatable {
|
||
let fileId: Int
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case fileId = "file_id"
|
||
}
|
||
}
|
||
|
||
/// 多点旅拍本地上传素材提交项。
|
||
struct MultiTravelUploadFileItem: Encodable, Equatable {
|
||
let fileName: String
|
||
let fileUrl: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case fileName = "file_name"
|
||
case fileUrl = "file_url"
|
||
}
|
||
}
|
||
|
||
/// 订单号解析器,负责从扫码内容或手动输入中提取可核销订单号。
|
||
enum OrderNumberParser {
|
||
/// 从原始文本中解析订单号,兼容 URL、键值格式和纯订单号。
|
||
static func parse(_ raw: String) -> String? {
|
||
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !text.isEmpty else { return nil }
|
||
|
||
if let url = URL(string: text),
|
||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||
let queryItems = components.queryItems,
|
||
let value = queryItems.first(where: { $0.name.lowercased() == "order_number" })?.value,
|
||
!value.isEmpty {
|
||
return value
|
||
}
|
||
|
||
let patterns = [
|
||
"order[_-]?number[=:]([A-Za-z0-9_-]+)",
|
||
"([A-Za-z0-9_-]{8,})"
|
||
]
|
||
for pattern in patterns {
|
||
if let match = firstCapture(in: text, pattern: pattern) {
|
||
return match
|
||
}
|
||
}
|
||
|
||
if !text.contains(" "), text.count >= 6 {
|
||
return text
|
||
}
|
||
return nil
|
||
}
|
||
|
||
/// 返回第一个正则捕获组。
|
||
private static func firstCapture(in text: String, pattern: String) -> String? {
|
||
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
|
||
return nil
|
||
}
|
||
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
||
guard let match = regex.firstMatch(in: text, options: [], range: range),
|
||
match.numberOfRanges > 1,
|
||
let captureRange = Range(match.range(at: 1), in: text) else {
|
||
return nil
|
||
}
|
||
return String(text[captureRange])
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/// 将 Double、Int 或数字字符串宽松解码成浮点数。
|
||
func decodeLossyDouble(forKey key: Key) throws -> Double? {
|
||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||
return value
|
||
}
|
||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||
return Double(value)
|
||
}
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
return Double(value.trimmingCharacters(in: .whitespacesAndNewlines))
|
||
}
|
||
return nil
|
||
}
|
||
}
|