Files
suixinkan_uikit/suixinkan/Features/Orders/ViewModels/OrderListViewModel.swift
T
lujiuyinandCursor 005349f8e6 模块化 AppStore 并完善素材管理与个人空间设置。
将会话、权限、位置、收款与排队存储拆分为独立模块,同步更新各 ViewModel 与单元测试,并补充素材管理 UI 与个人空间设置交互。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 11:01:08 +08:00

517 lines
18 KiB
Swift

//
// 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.session.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.session.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.session.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<OrderEntity>
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.session.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?()
}
}