541 lines
19 KiB
Swift
541 lines
19 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 let appStore: AppStore
|
|
|
|
/// 注入账号上下文,使订单查询与测试使用各自独立的会话。
|
|
init(appStore: AppStore = .shared) {
|
|
self.appStore = appStore
|
|
}
|
|
|
|
|
|
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 listRequestID = UUID()
|
|
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.session.currentAppRole == .scenicAdmin
|
|
}
|
|
|
|
var listAPIPath: String {
|
|
isScenicAdminReadOnly
|
|
? "/api/app/scenic-admin/order/list"
|
|
: "/api/yf-handset-app/photog/order/listv2"
|
|
}
|
|
|
|
/// 清除通知进入前的查询条件,由页面随后发起第一页刷新。
|
|
func resetFiltersForOrderNotification() {
|
|
searchText = ""
|
|
filterLabel = "全部"
|
|
tabIndex = 0
|
|
rTabIndex = 0
|
|
startTime = nil
|
|
endTime = nil
|
|
notifyStateChange()
|
|
}
|
|
|
|
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.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 requestID = UUID()
|
|
listRequestID = requestID
|
|
let scenicId = appStore.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
|
|
)
|
|
}
|
|
guard listRequestID == requestID else { return }
|
|
totalCount = response.total
|
|
if append {
|
|
orderList.append(contentsOf: response.list)
|
|
} else {
|
|
orderList = response.list
|
|
}
|
|
syncOrderSourceSelections()
|
|
canLoadMore = orderList.count < totalCount
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
guard listRequestID == requestID else { return }
|
|
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.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?()
|
|
}
|
|
}
|