// // OrdersViewModel.swift // suixinkan // // Created by Codex on 2026/6/22. // import Foundation import Combine @MainActor /// 订单页面 ViewModel,管理订单列表、核销列表、筛选条件和手动核销流程。 final class OrdersViewModel: ObservableObject { @Published var selectedEntry: OrdersEntry = .storeOrders @Published var selectedStatus = 0 @Published var searchPhone = "" @Published var filterStartDate: Date? @Published var filterEndDate: Date? @Published private(set) var loading = false @Published private(set) var loadingMore = false @Published private(set) var isVerifying = false @Published private(set) var currentVerifyingOrderNumber: String? @Published private(set) var storeOrders: [OrderEntity] = [] @Published private(set) var storeTotal = 0 @Published private(set) var storePage = 1 @Published private(set) var storeHasMore = false @Published private(set) var writeOffOrders: [WriteOffOrderItem] = [] @Published private(set) var writeOffTotal = 0 @Published private(set) var writeOffPage = 1 @Published private(set) var writeOffHasMore = false @Published var orderSourcePeople: [OrderSourcePerson] = [] @Published var orderSourceLeads: [String: [OrderSourceLead]] = [:] @Published var orderSourceSelections: [String: OrderSourceSelection] = [:] @Published var loadingOrderSourceLeadsFor: String? @Published var refundingOrder: OrderEntity? @Published var sendGiftContext: SendGiftContext? private var loadingOrderSourcePeople = false /// 赠送修图/视频弹窗上下文。 struct SendGiftContext: Equatable, Identifiable { var id: String { orderNumber } let orderNumber: String let nowPhotoNum: Int let nowVideoNum: Int var sendPhotoNum: Int var sendVideoNum: Int } static let orderTypePhotographerFollow = 11 static let orderTypePhotographerFollowOfflineScan = 14 /// 返回去除首尾空白后的手机号搜索值。 var normalizedPhone: String? { let text = searchPhone.trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } /// 根据当前子入口刷新对应列表。 func reload(api: OrderServing, scenicId: Int?, storeId: Int? = nil, appRole: AppRoleCode?, showLoading: Bool = true) async throws { switch selectedEntry { case .storeOrders: try await reloadStoreOrders(api: api, scenicId: scenicId, appRole: appRole, showLoading: showLoading) case .verificationOrders: try await reloadWriteOffOrders(api: api, scenicId: scenicId, storeId: storeId, showLoading: showLoading) } } /// 刷新订单管理列表,缺少景区时清空旧数据。 func reloadStoreOrders(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?, showLoading: Bool = true) async throws { guard let scenicId else { storeOrders = [] storeTotal = 0 storePage = 1 storeHasMore = false return } if showLoading { loading = true } defer { if showLoading { loading = false } } let result = try await api.orderList( scenicId: scenicId, page: 1, pageSize: pageSize, orderStatus: orderStatusQuery, userPhone: normalizedPhone, startTime: startTimeQuery, endTime: endTimeQuery, isRefined: refinedQuery, isScenicAdmin: appRole?.isScenicAdmin == true ) storeOrders = result.list storeTotal = result.total storePage = 1 storeHasMore = storeOrders.count < result.total && !result.list.isEmpty syncOrderSourceSelections(from: result.list) } /// 加载订单管理下一页。 func loadMoreStoreOrders(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?) async throws { guard !loadingMore, storeHasMore, let scenicId else { return } loadingMore = true defer { loadingMore = false } let nextPage = storePage + 1 let result = try await api.orderList( scenicId: scenicId, page: nextPage, pageSize: pageSize, orderStatus: orderStatusQuery, userPhone: normalizedPhone, startTime: startTimeQuery, endTime: endTimeQuery, isRefined: refinedQuery, isScenicAdmin: appRole?.isScenicAdmin == true ) storeOrders.append(contentsOf: result.list) storeTotal = result.total storePage = nextPage storeHasMore = storeOrders.count < result.total && !result.list.isEmpty } /// 刷新核销订单列表,缺少景区时清空旧数据。 func reloadWriteOffOrders(api: OrderServing, scenicId: Int?, storeId: Int? = nil, showLoading: Bool = true) async throws { guard let scenicId else { writeOffOrders = [] writeOffTotal = 0 writeOffPage = 1 writeOffHasMore = false return } if showLoading { loading = true } defer { if showLoading { loading = false } } let result = try await api.writeOffList( scenicId: scenicId, storeId: storeId, page: 1, pageSize: pageSize, orderStatus: nil, orderVerifyStatus: nil, startTime: nil, endTime: nil, userPhone: nil ) writeOffOrders = result.list writeOffTotal = result.total writeOffPage = 1 writeOffHasMore = writeOffOrders.count < result.total && !result.list.isEmpty } /// 加载核销订单下一页。 func loadMoreWriteOffOrders(api: OrderServing, scenicId: Int?, storeId: Int? = nil) async throws { guard !loadingMore, writeOffHasMore, let scenicId else { return } loadingMore = true defer { loadingMore = false } let nextPage = writeOffPage + 1 let result = try await api.writeOffList( scenicId: scenicId, storeId: storeId, page: nextPage, pageSize: pageSize, orderStatus: nil, orderVerifyStatus: nil, startTime: nil, endTime: nil, userPhone: nil ) writeOffOrders.append(contentsOf: result.list) writeOffTotal = result.total writeOffPage = nextPage writeOffHasMore = writeOffOrders.count < result.total && !result.list.isEmpty } /// 手动核销订单,并在成功后刷新核销列表。 func verify(api: OrderServing, scenicId: Int, storeId: Int? = nil, orderNumber: String) async throws { guard !isVerifying else { return } isVerifying = true currentVerifyingOrderNumber = orderNumber defer { isVerifying = false currentVerifyingOrderNumber = nil } try await api.writeOff(orderNumber: orderNumber) let refreshed = try await api.writeOffList( scenicId: scenicId, storeId: storeId, page: 1, pageSize: pageSize, orderStatus: nil, orderVerifyStatus: nil, startTime: nil, endTime: nil, userPhone: nil ) writeOffOrders = refreshed.list writeOffTotal = refreshed.total writeOffPage = 1 writeOffHasMore = writeOffOrders.count < refreshed.total && !refreshed.list.isEmpty } /// 判断订单卡片是否展示退款按钮。 func shouldShowRefundButton(order: OrderEntity, appRole: AppRoleCode?) -> Bool { guard !Self.isScenicAdminReadOnly(appRole: appRole) else { return false } return (order.orderStatus == 18 || order.orderStatus == 30) && order.orderType != 19 } /// 判断订单卡片是否展示核销按钮。 func shouldShowVerifyButton(order: OrderEntity, appRole: AppRoleCode?) -> Bool { guard !Self.isScenicAdminReadOnly(appRole: appRole) else { return false } if order.orderType == 19 { return true } guard let photoTravel = order.photoTravel else { return false } return photoTravel.needCheckIn && order.orderStatus != 30 } /// 更新订单精修选择并同步本地列表状态。 func updateRefinedChoice(api: OrderServing, orderNumber: String, isRefined: Int) async throws { try await api.updateRefinedChoice(orderNumber: orderNumber, isRefined: isRefined) if let index = storeOrders.firstIndex(where: { $0.orderNumber == orderNumber }) { storeOrders[index].isRefined = isRefined } } /// 取消订单并刷新列表。 func cancelOrder(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?, orderNumber: String) async throws { try await api.cancelOrder(orderNumber: orderNumber) try await reloadStoreOrders(api: api, scenicId: scenicId, appRole: appRole, showLoading: false) } /// 打开赠送修图/视频弹窗。 func prepareSendGift(for order: OrderEntity) { let photoTravel = order.photoTravel sendGiftContext = SendGiftContext( orderNumber: order.orderNumber, nowPhotoNum: photoTravel?.orderPhotoNum ?? 0, nowVideoNum: photoTravel?.orderVideoNum ?? 0, sendPhotoNum: photoTravel?.retouchGiftPhotoNum ?? 0, sendVideoNum: photoTravel?.retouchGiftVideoNum ?? 0 ) } /// 提交赠送修图/视频并刷新列表。 func submitSendGift( api: OrderServing, scenicId: Int?, appRole: AppRoleCode?, photoNum: Int, videoNum: Int ) async throws { guard let context = sendGiftContext else { return } try await api.sendGift(orderNumber: context.orderNumber, photoNum: photoNum, videoNum: videoNum) sendGiftContext = nil try await reloadStoreOrders(api: api, scenicId: scenicId, appRole: appRole, showLoading: false) } /// 当前筛选标题。 var currentFilterTitle: String { OrderFilters.statusFilters.first(where: { $0.id == selectedStatus })?.title ?? "全部" } /// 判断订单卡片是否展示订单来源入口。 func shouldShowOrderSource(order: OrderEntity, appRole: AppRoleCode?) -> Bool { guard !Self.isScenicAdminReadOnly(appRole: appRole) else { return false } return order.orderType == Self.orderTypePhotographerFollow || order.orderType == Self.orderTypePhotographerFollowOfflineScan } /// 景区管理员只读模式,不可操作订单来源。 static func isScenicAdminReadOnly(appRole: AppRoleCode?) -> Bool { appRole?.isScenicAdmin == true } /// 获取订单已选中的订单来源。 func getOrderSourceSelection(for order: OrderEntity) -> OrderSourceSelection? { orderSourceSelections[order.orderNumber] ?? order.toBoundOrderSourceSelection() } /// 从列表数据同步已绑定订单来源。 func syncOrderSourceSelections(from orders: [OrderEntity]) { var updated = orderSourceSelections for order in orders { if let selection = order.toBoundOrderSourceSelection() { updated[order.orderNumber] = selection } } orderSourceSelections = updated } /// 打开订单来源 Sheet 前预加载获客员列表。 func prepareOrderSourceSheet(api: CooperationOrderServing, storeId: Int?) async { orderSourceLeads = [:] await loadOrderSourcePeople(api: api, storeId: storeId) } /// 加载合作获客员列表供订单来源选择。 func loadOrderSourcePeople(api: CooperationOrderServing, storeId: Int?) async { if loadingOrderSourcePeople { return } loadingOrderSourcePeople = true defer { loadingOrderSourcePeople = false } do { let storeParam = storeId.flatMap { $0 > 0 ? $0 : nil } let data = try await api.cooperativeSalers(storeId: storeParam, page: 1, pageSize: 50) orderSourcePeople = data.list.map { $0.toOrderSourcePerson() } } catch { orderSourcePeople = [] } } /// 加载指定获客员的可绑定带客单。 func loadOrderSourceLeads(api: CooperationOrderServing, person: OrderSourcePerson) async { if loadingOrderSourceLeadsFor == person.key { return } loadingOrderSourceLeadsFor = person.key defer { loadingOrderSourceLeadsFor = nil } guard let saleUserId = Int(person.key) else { return } do { let data = try await api.saleUserReferralOrders(saleUserId: saleUserId, page: 1, pageSize: 50) orderSourceLeads[person.key] = data.list.map { $0.toOrderSourceLead() } } catch { orderSourceLeads[person.key] = [] } } /// 绑定订单来源带客单。 func bindOrderSource( api: CooperationOrderServing, orderNumber: String, selection: OrderSourceSelection ) async throws { try await api.saleUserBindReferralOrder( orderNumber: orderNumber, referralOrderNo: selection.lead.key ) orderSourceSelections[orderNumber] = selection } /// 解析扫码结果并返回当前核销列表中的匹配订单。 func matchedWriteOffOrder(for rawCode: String) -> (orderNumber: String, matched: WriteOffOrderItem?)? { guard let orderNumber = OrderNumberParser.parse(rawCode) else { return nil } let matched = writeOffOrders.first { $0.orderNumber.caseInsensitiveCompare(orderNumber) == .orderedSame } return (matched?.orderNumber ?? orderNumber, matched) } /// 根据订单号查找当前核销列表中的订单。 func writeOffOrder(matching orderNumber: String) -> WriteOffOrderItem? { writeOffOrders.first { $0.orderNumber.caseInsensitiveCompare(orderNumber) == .orderedSame } } /// 用于测试重复提交场景的内部状态设置。 func markVerifyingForTests(orderNumber: String) { isVerifying = true currentVerifyingOrderNumber = orderNumber } private let pageSize = 20 private var orderStatusQuery: Int? { selectedStatus > 0 ? selectedStatus : nil } private var startTimeQuery: String? { filterStartDate.map { Self.dayFormatter.string(from: $0) } } private var endTimeQuery: String? { filterEndDate.map { Self.dayFormatter.string(from: $0) } } private var refinedQuery: Int? { switch selectedStatus { case -1: return 1 case -2: return 2 default: return nil } } private static let dayFormatter: DateFormatter = { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd" return formatter }() }