新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
300 lines
11 KiB
Swift
300 lines
11 KiB
Swift
//
|
||
// 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?
|
||
|
||
private var loadingOrderSourcePeople = false
|
||
|
||
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)
|
||
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)
|
||
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)
|
||
writeOffOrders = refreshed.list
|
||
writeOffTotal = refreshed.total
|
||
writeOffPage = 1
|
||
writeOffHasMore = writeOffOrders.count < refreshed.total && !refreshed.list.isEmpty
|
||
}
|
||
|
||
/// 判断订单卡片是否展示订单来源入口。
|
||
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
|
||
}()
|
||
}
|