474 lines
16 KiB
Swift
474 lines
16 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 displayText: String
|
||
let userMobile: String
|
||
let createdAt: String
|
||
}
|
||
|
||
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?
|
||
|
||
var onStateChange: (() -> Void)?
|
||
var onShowMessage: ((String) -> Void)?
|
||
|
||
private var currentPage = 1
|
||
private var totalCount = 0
|
||
private var loadingOrderSourcePeople = false
|
||
|
||
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 {
|
||
onShowMessage?("获客员绑定功能待接入")
|
||
} 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? {
|
||
if let selection = orderSourceSelections[order.orderNumber] { return selection }
|
||
guard let referral = order.referralOrder else { return nil }
|
||
let person = OrderSourcePerson(
|
||
key: String(referral.saleUserId),
|
||
name: referral.displaySalerName,
|
||
phone: referral.salerPhone
|
||
)
|
||
let lead = OrderSourceLead(
|
||
key: referral.orderNo,
|
||
displayText: referral.displayText.isEmpty ? referral.orderNo : referral.displayText,
|
||
userMobile: referral.userMobile,
|
||
createdAt: referral.createdAt
|
||
)
|
||
return OrderSourceSelection(person: person, lead: lead)
|
||
}
|
||
|
||
func onOrderSourceSheetOpen(api: OrderAPI) async {
|
||
orderSourceLeads = [:]
|
||
guard !loadingOrderSourcePeople else { return }
|
||
loadingOrderSourcePeople = true
|
||
defer { loadingOrderSourcePeople = false }
|
||
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 == nil else { return }
|
||
loadingOrderSourceLeadsFor = person.key
|
||
notifyStateChange()
|
||
defer {
|
||
loadingOrderSourceLeadsFor = nil
|
||
notifyStateChange()
|
||
}
|
||
do {
|
||
let response = try await api.referralOrders(saleUserId: person.key)
|
||
let leads = response.list.map {
|
||
OrderSourceLead(
|
||
key: $0.displayReferralOrderNo,
|
||
displayText: $0.displayReferralOrderNo,
|
||
userMobile: $0.userMobile,
|
||
createdAt: $0.createdAt
|
||
)
|
||
}
|
||
orderSourceLeads[person.key] = leads
|
||
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<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.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?()
|
||
}
|
||
}
|