Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
857 lines
32 KiB
Swift
857 lines
32 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: "无需精修")
|
||
]
|
||
}
|
||
|
||
/// 订单管理列表实体,承载订单卡片展示和筛选判断所需字段。
|
||
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 isNeedEdit: Bool
|
||
let isRefined: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
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 isNeedEdit
|
||
case isRefined = "is_refined"
|
||
}
|
||
|
||
/// 宽松解码订单字段,兼容后端数字和字符串混用。
|
||
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)
|
||
isNeedEdit = try container.decodeLossyBool(forKey: .isNeedEdit) ?? true
|
||
isRefined = try container.decodeLossyInt(forKey: .isRefined) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 旅拍订单附加信息实体,表示修图、视频和核销相关数量。
|
||
struct OrderPhotoTravel: Decodable, Equatable, Hashable {
|
||
let orderPhotoNum: Int
|
||
let orderVideoNum: Int
|
||
let checkInTime: String
|
||
let retouchGiftPhotoNum: Int
|
||
let retouchGiftVideoNum: Int
|
||
|
||
/// 枚举,定义相关常量或状态。
|
||
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"
|
||
}
|
||
|
||
/// 宽松解码旅拍附加信息。
|
||
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
|
||
}
|
||
}
|
||
|
||
/// 核销订单列表实体,表示可核销订单卡片的展示和操作状态。
|
||
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 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 storeDetail(OrderEntity)
|
||
case writeOffDetail(WriteOffOrderItem)
|
||
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)
|
||
}
|
||
|
||
/// 订单详情接口响应实体,承载门店订单详情页的完整展示数据。
|
||
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"
|
||
}
|
||
}
|
||
|
||
/// 订单详情展示适配实体,统一列表摘要和详情接口响应的读取方式。
|
||
struct StoreOrderDetailDisplay: Equatable {
|
||
let orderNumber: String
|
||
let orderTypeLabel: String
|
||
let createdAt: String
|
||
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
|
||
|
||
/// 使用订单列表项构建兜底展示数据。
|
||
init(item: OrderEntity) {
|
||
orderNumber = item.orderNumber
|
||
orderTypeLabel = item.orderTypeLabel
|
||
createdAt = item.createdAt
|
||
orderStatusName = item.orderStatusName
|
||
actualPayAmount = item.actualPayAmount
|
||
actualRefundAmount = item.actualRefundAmount.isEmpty ? item.refundAmount : item.actualRefundAmount
|
||
payTypeName = item.payTypeName
|
||
payTime = item.payTime
|
||
completeTime = item.completeTime
|
||
userId = item.userId
|
||
phone = item.phone
|
||
projectId = item.projectId
|
||
projectName = item.projectName
|
||
remark = item.remark
|
||
}
|
||
|
||
/// 使用详情接口响应构建完整展示数据。
|
||
init(detail: StoreOrderDetailResponse) {
|
||
orderNumber = detail.orderNumber
|
||
orderTypeLabel = detail.orderTypeLabel
|
||
createdAt = detail.createdAt
|
||
orderStatusName = detail.orderStatusName
|
||
actualPayAmount = detail.actualPayAmount
|
||
actualRefundAmount = detail.actualRefundAmount
|
||
payTypeName = detail.payTypeName
|
||
payTime = detail.payTime
|
||
completeTime = detail.completeTime
|
||
userId = detail.userId
|
||
phone = detail.phone
|
||
projectId = detail.projectId
|
||
projectName = detail.projectName
|
||
remark = detail.remark
|
||
}
|
||
}
|
||
|
||
/// 订单号解析器,负责从扫码内容或手动输入中提取可核销订单号。
|
||
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
|
||
}
|
||
}
|