Sync order detail and scanner verification
This commit is contained in:
@ -32,7 +32,7 @@ enum OrderFilters {
|
||||
}
|
||||
|
||||
/// 订单管理列表实体,承载订单卡片展示和筛选判断所需字段。
|
||||
struct OrderEntity: Decodable, Identifiable, Equatable {
|
||||
struct OrderEntity: Decodable, Identifiable, Equatable, Hashable {
|
||||
var id: String { orderNumber }
|
||||
|
||||
let photogUid: Int
|
||||
@ -124,7 +124,7 @@ struct OrderEntity: Decodable, Identifiable, Equatable {
|
||||
}
|
||||
|
||||
/// 旅拍订单附加信息实体,表示修图、视频和核销相关数量。
|
||||
struct OrderPhotoTravel: Decodable, Equatable {
|
||||
struct OrderPhotoTravel: Decodable, Equatable, Hashable {
|
||||
let orderPhotoNum: Int
|
||||
let orderVideoNum: Int
|
||||
let checkInTime: String
|
||||
@ -151,7 +151,7 @@ struct OrderPhotoTravel: Decodable, Equatable {
|
||||
}
|
||||
|
||||
/// 核销订单列表实体,表示可核销订单卡片的展示和操作状态。
|
||||
struct WriteOffOrderItem: Decodable, Identifiable, Equatable {
|
||||
struct WriteOffOrderItem: Decodable, Identifiable, Equatable, Hashable {
|
||||
var id: String { orderNumber }
|
||||
|
||||
let orderNumber: String
|
||||
@ -197,6 +197,255 @@ struct WriteOffRequest: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 订单详情路由实体,表示订单模块内部可 push 的二级页面。
|
||||
enum OrdersRoute: Hashable {
|
||||
case storeDetail(OrderEntity)
|
||||
case writeOffDetail(WriteOffOrderItem)
|
||||
}
|
||||
|
||||
/// 订单详情接口响应实体,承载门店订单详情页的完整展示数据。
|
||||
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 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 {
|
||||
@ -258,4 +507,18 @@ private extension KeyedDecodingContainer {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user