// // 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 referralOrder: BoundReferralOrder? 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 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) 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.. 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 } }