// // OrderListViewModel.swift // suixinkan // import Foundation struct MultiVerifyParseData: Equatable { let orderNumber: String let userId: Int let time: Int64 let checkSum: String } struct OrderSourcePerson: Equatable { let key: String let name: String let phone: String } struct OrderSourceLead: Equatable { let key: String let userPhone: String let remark: String let images: [String] } enum OrderSourceMapping { /// 将 API 带客单实体映射为订单来源选择项,对齐 Android `toOrderSourceLead()`。 static func lead(from entity: ReferralOrderEntity) -> OrderSourceLead { let orderNo = entity.displayReferralOrderNo let remark: String if !entity.remark.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { remark = entity.remark } else if !entity.statusLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { remark = entity.statusLabel } else { remark = "带客单 \(orderNo)" } return OrderSourceLead( key: orderNo, userPhone: entity.userMobile, remark: remark, images: entity.images ) } /// 将已绑定带客单实体映射为订单来源选择项,对齐 Android `toOrderSourceSelection()`。 static func selection(from referral: BoundReferralOrderEntity) -> OrderSourceSelection? { let referralNo = referral.orderNo.trimmingCharacters(in: .whitespacesAndNewlines) guard !referralNo.isEmpty else { return nil } let personName = referral.displaySalerName.isEmpty ? "—" : referral.displaySalerName let person = OrderSourcePerson( key: referral.saleUserId > 0 ? String(referral.saleUserId) : "", name: personName, phone: referral.salerPhone ) let remark: String if !referral.remark.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { remark = referral.remark } else if !referral.displayText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { remark = referral.displayText } else { remark = "带客单号 \(referralNo)" } let lead = OrderSourceLead( key: referralNo, userPhone: referral.userMobile, remark: remark, images: [] ) return OrderSourceSelection(person: person, lead: lead) } } struct OrderSourceSelection: Equatable { let person: OrderSourcePerson let lead: OrderSourceLead } /// 摄影师/景区管理员订单列表 ViewModel,对齐 Android `OrderListViewModel`。 final class OrderListViewModel { private(set) var orderList: [OrderEntity] = [] private(set) var searchText = "" private(set) var filterLabel = "全部" private(set) var tabIndex = 0 private(set) var rTabIndex = 0 private(set) var startTime: Date? private(set) var endTime: Date? private(set) var isRefreshing = false private(set) var canLoadMore = false private(set) var isLoadingMore = false private(set) var multiVerifyPendingData: MultiVerifyParseData? private(set) var multiVerifyScenicSpotList: [MultiTravelScenicSpotEntity] = [] private(set) var selectedMultiVerifySpot: MultiTravelScenicSpotEntity? private(set) var signInResponse: OrderSignInResponse? private(set) var writeOffResponse: WriteOffOrderResponse? private(set) var orderSourcePeople: [OrderSourcePerson] = [] private(set) var orderSourceLeads: [String: [OrderSourceLead]] = [:] private(set) var orderSourceSelections: [String: OrderSourceSelection] = [:] private(set) var loadingOrderSourceLeadsFor: String? private(set) var isLoadingOrderSourcePeople = false var onStateChange: (() -> Void)? var onShowMessage: ((String) -> Void)? var onNavigateBindAcquirer: ((Int) -> Void)? private var currentPage = 1 private var totalCount = 0 private let apiDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "yyyy-MM-dd" return formatter }() var isScenicAdminReadOnly: Bool { AppStore.shared.currentAppRole == .scenicAdmin } var listAPIPath: String { isScenicAdminReadOnly ? "/api/app/scenic-admin/order/list" : "/api/yf-handset-app/photog/order/listv2" } func refreshOrderList(api: OrderAPI) async { currentPage = 1 isRefreshing = true isLoadingMore = false orderList = [] notifyStateChange() await loadOrderList(api: api, append: false) } func loadMore(api: OrderAPI) async { guard !isLoadingMore, !isRefreshing else { return } guard orderList.count < totalCount else { return } isLoadingMore = true currentPage += 1 notifyStateChange() await loadOrderList(api: api, append: true) } func shouldLoadMore(at index: Int) -> Bool { guard canLoadMore, !isRefreshing, !isLoadingMore else { return false } return index >= max(0, orderList.count - 3) } func onSearchTextChange(_ text: String) { searchText = text notifyStateChange() } func onFilterChange(orderStatus: Int, isRefined: Int) { tabIndex = orderStatus rTabIndex = isRefined filterLabel = PhotographerOrderFilter.from(tabIndex: orderStatus, rTabIndex: isRefined).label notifyStateChange() } func onDateRangeSelected(start: Date?, end: Date?) { startTime = start endTime = end notifyStateChange() } func cancelOrder(orderNumber: String, api: OrderAPI) async { guard !abortIfScenicAdminReadOnlyMutation() else { return } do { try await api.cancelOrder(CancelRequest(orderNumber: orderNumber)) onShowMessage?("取消成功") await refreshOrderList(api: api) } catch { onShowMessage?(error.localizedDescription) } } func refundOrder(_ request: OrderRefundRequest, api: OrderAPI) async { guard !abortIfScenicAdminReadOnlyMutation() else { return } do { try await api.refundOrder(request) onShowMessage?("退款成功") await refreshOrderList(api: api) } catch { onShowMessage?(error.localizedDescription) } } func sendGift(orderNumber: String, photoNum: Int, videoNum: Int, api: OrderAPI) async { guard !abortIfScenicAdminReadOnlyMutation() else { return } do { try await api.giftRetouch(SendGiftRequest(orderNumber: orderNumber, photoNum: photoNum, videoNum: videoNum)) onShowMessage?("赠送成功") await refreshOrderList(api: api) } catch { onShowMessage?(error.localizedDescription) } } func updateRefined(orderNumber: String, isRefined: Int, api: OrderAPI) async { guard !abortIfScenicAdminReadOnlyMutation() else { return } do { try await api.updateRefined(orderNumber: orderNumber, isRefined: isRefined) await refreshOrderList(api: api) } catch { onShowMessage?(error.localizedDescription) } } func processScanResult(_ scanResult: String, api: OrderAPI) async { if scanResult.isEmpty { onShowMessage?("请扫描正确的二维码") return } guard !abortIfScenicAdminReadOnlyMutation() else { return } if let parsed = QRCodeScanResult.parse(scanResult) { switch parsed.method { case "multi_verify": guard !parsed.orderNumber.isEmpty, parsed.userId > 0, parsed.timeLong > 0, !parsed.checkSum.isEmpty else { onShowMessage?("请扫描正确的多点位核销码") return } multiVerifyPendingData = MultiVerifyParseData( orderNumber: parsed.orderNumber, userId: parsed.userId, time: parsed.timeLong, checkSum: parsed.checkSum ) notifyStateChange() return case SaleUserQrParser.methodSaleUserBind: if let saleUserId = SaleUserQrParser.parseSaleUserId(scanResult), saleUserId > 0 { onNavigateBindAcquirer?(saleUserId) } else { onShowMessage?("请扫描正确的获客员二维码") } return case "order_verify": await performOrderVerify(jsonBody: parsed.rawJSON, api: api) return case "check_in", "": await performCheckIn(scanResult: scanResult, api: api) return default: break } } await performCheckIn(scanResult: scanResult, api: api) } func fetchScenicSpotsAndShowSheet(lat: Double, lng: Double, api: OrderAPI) async { guard let data = multiVerifyPendingData else { return } do { let response = try await api.multiTravelScenicSpotList( orderNumber: data.orderNumber, lat: lat, lng: lng ) multiVerifyScenicSpotList = response.entityList selectedMultiVerifySpot = multiVerifyScenicSpotList.first notifyStateChange() if multiVerifyScenicSpotList.isEmpty { onShowMessage?("暂无可核销的打卡点") clearMultiVerifyPending() } } catch { onShowMessage?(error.localizedDescription) clearMultiVerifyPending() } } func onMultiVerifySpotSelected(_ spot: MultiTravelScenicSpotEntity) { selectedMultiVerifySpot = spot notifyStateChange() } func onMultiVerifyConfirm(api: OrderAPI) async { guard !abortIfScenicAdminReadOnlyMutation() else { return } guard let data = multiVerifyPendingData, let spot = selectedMultiVerifySpot else { onShowMessage?("请选择一个打卡点") return } do { let response = try await api.multiTravelOrderVerify( MultiTravelOrderVerifyRequest( orderNumber: data.orderNumber, userId: data.userId, time: data.time, checkSum: data.checkSum, scenicSpotId: spot.scenicSpotId ) ) signInResponse = response clearMultiVerifySheet() notifyStateChange() } catch { onShowMessage?(error.localizedDescription) clearMultiVerifyPending() } } func clearMultiVerifySheet() { multiVerifyScenicSpotList = [] selectedMultiVerifySpot = nil multiVerifyPendingData = nil notifyStateChange() } func clearMultiVerifyPending() { multiVerifyPendingData = nil notifyStateChange() } func clearSignInResponse() { signInResponse = nil notifyStateChange() } func clearWriteOffResponse() { writeOffResponse = nil notifyStateChange() } func shouldShowOrderSource(for order: OrderEntity) -> Bool { guard !isScenicAdminReadOnly else { return false } return order.orderType == OrderTypeCode.photographerFollow || order.orderType == OrderTypeCode.photographerFollowOfflineScan } func orderSourceSelection(for order: OrderEntity) -> OrderSourceSelection? { orderSourceSelection(orderNumber: order.orderNumber, referralOrder: order.referralOrder) } /// 按订单号读取当前绑定状态,供订单来源 Sheet 使用。 func orderSourceSelection(orderNumber: String, referralOrder: BoundReferralOrderEntity? = nil) -> OrderSourceSelection? { if let selection = orderSourceSelections[orderNumber] { return selection } if let referral = referralOrder ?? orderList.first(where: { $0.orderNumber == orderNumber })?.referralOrder { return OrderSourceMapping.selection(from: referral) } return nil } func onOrderSourceSheetOpen(api: OrderAPI) async { orderSourceLeads = [:] guard !isLoadingOrderSourcePeople else { return } isLoadingOrderSourcePeople = true notifyStateChange() defer { isLoadingOrderSourcePeople = false notifyStateChange() } let storeId = AppStore.shared.currentStoreId let storeIdParam = storeId > 0 ? String(storeId) : nil do { let response = try await api.cooperativeSalers(storeId: storeIdParam) orderSourcePeople = response.list.map { OrderSourcePerson(key: String($0.saleUserId), name: $0.displayName, phone: $0.displayPhone) } notifyStateChange() } catch { onShowMessage?(error.localizedDescription) } } func loadOrderSourceLeads(person: OrderSourcePerson, api: OrderAPI) async { guard loadingOrderSourceLeadsFor != person.key else { return } loadingOrderSourceLeadsFor = person.key notifyStateChange() defer { loadingOrderSourceLeadsFor = nil notifyStateChange() } do { let response = try await api.referralOrders(saleUserId: person.key) orderSourceLeads[person.key] = response.list.map(OrderSourceMapping.lead(from:)) notifyStateChange() } catch { onShowMessage?(error.localizedDescription) } } func bindOrderSource(orderNumber: String, selection: OrderSourceSelection, api: OrderAPI) async { guard !abortIfScenicAdminReadOnlyMutation() else { return } do { try await api.bindReferralOrder(orderNumber: orderNumber, referralOrderNo: selection.lead.key) orderSourceSelections[orderNumber] = selection onShowMessage?("绑定成功") await refreshOrderList(api: api) } catch { onShowMessage?(error.localizedDescription) } } private func loadOrderList(api: OrderAPI, append: Bool) async { let scenicId = AppStore.shared.currentScenicId guard scenicId > 0 else { finishLoading() onShowMessage?("当前景区信息缺失") return } let start = startTime.map { apiDateFormatter.string(from: $0) } let end = endTime.map { apiDateFormatter.string(from: $0) } let phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines) do { let response: OrderListResponse if isScenicAdminReadOnly { response = try await api.scenicAdminOrderList( scenicId: scenicId, page: currentPage, orderStatus: tabIndex, startTime: start, endTime: end, userPhone: phone.isEmpty ? nil : phone, isRefined: rTabIndex ) } else { response = try await api.photographerOrderList( scenicId: scenicId, page: currentPage, orderStatus: tabIndex, startTime: start, endTime: end, userPhone: phone.isEmpty ? nil : phone, isRefined: rTabIndex ) } totalCount = response.total if append { orderList.append(contentsOf: response.list) } else { orderList = response.list } syncOrderSourceSelections() canLoadMore = orderList.count < totalCount } catch is CancellationError { return } catch { onShowMessage?(error.localizedDescription) } finishLoading() } private func performCheckIn(scanResult: String, api: OrderAPI) async { guard let jsonBody = appendScenicId(to: scanResult) else { onShowMessage?("请扫描正确的二维码") return } do { signInResponse = try await api.orderCheckIn(jsonBody: jsonBody) notifyStateChange() } catch { onShowMessage?(error.localizedDescription) } } private func performOrderVerify(jsonBody: String, api: OrderAPI) async { do { writeOffResponse = try await api.orderVerify(jsonBody: jsonBody) notifyStateChange() } catch { onShowMessage?(error.localizedDescription) } } private func appendScenicId(to scanResult: String) -> String? { guard let data = scanResult.data(using: .utf8), var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } json["scenic_id"] = String(AppStore.shared.currentScenicId) guard let updated = try? JSONSerialization.data(withJSONObject: json), let string = String(data: updated, encoding: .utf8) else { return nil } return string } private func syncOrderSourceSelections() { var updated = orderSourceSelections for order in orderList { if let selection = orderSourceSelection(for: order) { updated[order.orderNumber] = selection } } orderSourceSelections = updated } @discardableResult private func abortIfScenicAdminReadOnlyMutation() -> Bool { guard isScenicAdminReadOnly else { return false } onShowMessage?("景区管理员仅可查看") return true } private func finishLoading() { isRefreshing = false isLoadingMore = false notifyStateChange() } private func notifyStateChange() { onStateChange?() } }