fix: route payment pushes to orders and prevent refresh crash
This commit is contained in:
@@ -2,7 +2,7 @@ import Foundation
|
||||
|
||||
/// 推送点击后可到达的业务页面。
|
||||
enum PushDestination: Sendable, Equatable {
|
||||
case paymentRecord
|
||||
case orders
|
||||
case paymentDetails
|
||||
case messageCenter
|
||||
case aiRetouchTaskList
|
||||
@@ -36,8 +36,8 @@ struct PushPayload: Sendable, Equatable {
|
||||
/// 仅根据后端业务消息类型解析目标页面。
|
||||
nonisolated var destination: PushDestination {
|
||||
switch type {
|
||||
case "1":
|
||||
return .paymentRecord
|
||||
case "1", "5":
|
||||
return .orders
|
||||
case "6":
|
||||
return .paymentDetails
|
||||
case "14":
|
||||
|
||||
@@ -8,6 +8,14 @@ import Foundation
|
||||
/// 店铺管理员押金单列表 ViewModel,对齐 Android `DepositOrderListViewModel`。
|
||||
final class DepositOrderListViewModel {
|
||||
|
||||
private let appStore: AppStore
|
||||
|
||||
/// 注入账号上下文,使订单查询与测试使用各自独立的会话。
|
||||
init(appStore: AppStore = .shared) {
|
||||
self.appStore = appStore
|
||||
}
|
||||
|
||||
|
||||
private(set) var searchText = ""
|
||||
private(set) var filterLabel = "全部"
|
||||
private(set) var tabIndex = 0
|
||||
@@ -22,6 +30,7 @@ final class DepositOrderListViewModel {
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
|
||||
private var listRequestID = UUID()
|
||||
private var currentPage = 1
|
||||
private var totalCount = 0
|
||||
private var searchDebounceTask: Task<Void, Never>?
|
||||
@@ -34,6 +43,18 @@ final class DepositOrderListViewModel {
|
||||
return formatter
|
||||
}()
|
||||
|
||||
/// 清除通知进入前的查询条件,由页面随后发起第一页刷新。
|
||||
func resetFiltersForOrderNotification() {
|
||||
searchDebounceTask?.cancel()
|
||||
searchDebounceTask = nil
|
||||
searchText = ""
|
||||
filterLabel = "全部"
|
||||
tabIndex = 0
|
||||
startTime = nil
|
||||
endTime = nil
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
func refreshOrderList(api: OrderAPI) async {
|
||||
currentPage = 1
|
||||
isRefreshing = true
|
||||
@@ -102,7 +123,7 @@ final class DepositOrderListViewModel {
|
||||
onShowMessage?("核销码内容不完整")
|
||||
return
|
||||
}
|
||||
let storeId = AppStore.shared.session.currentStoreId
|
||||
let storeId = appStore.session.currentStoreId
|
||||
guard storeId > 0 else {
|
||||
onShowMessage?("店铺信息异常,请先选择店铺")
|
||||
return
|
||||
@@ -125,7 +146,7 @@ final class DepositOrderListViewModel {
|
||||
refundReason: String,
|
||||
api: OrderAPI
|
||||
) async {
|
||||
let storeId = AppStore.shared.session.currentStoreId
|
||||
let storeId = appStore.session.currentStoreId
|
||||
guard storeId > 0 else {
|
||||
onShowMessage?("店铺信息异常")
|
||||
return
|
||||
@@ -163,10 +184,12 @@ final class DepositOrderListViewModel {
|
||||
}
|
||||
|
||||
private func loadOrderList(refresh: Bool, api: OrderAPI) async {
|
||||
let requestID = UUID()
|
||||
listRequestID = requestID
|
||||
let phone = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let isPhoneSearch = !phone.isEmpty && Self.isValidPhone(phone)
|
||||
|
||||
let storeId = AppStore.shared.session.currentStoreId
|
||||
let storeId = appStore.session.currentStoreId
|
||||
if storeId <= 0 {
|
||||
finishLoading()
|
||||
orderList = []
|
||||
@@ -201,6 +224,7 @@ final class DepositOrderListViewModel {
|
||||
startTime: start,
|
||||
endTime: end
|
||||
)
|
||||
guard listRequestID == requestID else { return }
|
||||
totalCount = response.total
|
||||
if refresh || currentPage == 1 {
|
||||
orderList = response.list
|
||||
@@ -213,6 +237,7 @@ final class DepositOrderListViewModel {
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
guard listRequestID == requestID else { return }
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
finishLoading()
|
||||
|
||||
@@ -83,6 +83,14 @@ struct OrderSourceSelection: Equatable {
|
||||
/// 摄影师/景区管理员订单列表 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 = "全部"
|
||||
@@ -110,6 +118,7 @@ final class OrderListViewModel {
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onNavigateBindAcquirer: ((Int) -> Void)?
|
||||
|
||||
private var listRequestID = UUID()
|
||||
private var currentPage = 1
|
||||
private var totalCount = 0
|
||||
|
||||
@@ -122,7 +131,7 @@ final class OrderListViewModel {
|
||||
}()
|
||||
|
||||
var isScenicAdminReadOnly: Bool {
|
||||
AppStore.shared.session.currentAppRole == .scenicAdmin
|
||||
appStore.session.currentAppRole == .scenicAdmin
|
||||
}
|
||||
|
||||
var listAPIPath: String {
|
||||
@@ -131,6 +140,17 @@ final class OrderListViewModel {
|
||||
: "/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
|
||||
@@ -359,7 +379,7 @@ final class OrderListViewModel {
|
||||
isLoadingOrderSourcePeople = false
|
||||
notifyStateChange()
|
||||
}
|
||||
let storeId = AppStore.shared.session.currentStoreId
|
||||
let storeId = appStore.session.currentStoreId
|
||||
let storeIdParam = storeId > 0 ? String(storeId) : nil
|
||||
do {
|
||||
let response = try await api.cooperativeSalers(storeId: storeIdParam)
|
||||
@@ -402,7 +422,9 @@ final class OrderListViewModel {
|
||||
}
|
||||
|
||||
private func loadOrderList(api: OrderAPI, append: Bool) async {
|
||||
let scenicId = AppStore.shared.session.currentScenicId
|
||||
let requestID = UUID()
|
||||
listRequestID = requestID
|
||||
let scenicId = appStore.session.currentScenicId
|
||||
guard scenicId > 0 else {
|
||||
finishLoading()
|
||||
onShowMessage?("当前景区信息缺失")
|
||||
@@ -436,6 +458,7 @@ final class OrderListViewModel {
|
||||
isRefined: rTabIndex
|
||||
)
|
||||
}
|
||||
guard listRequestID == requestID else { return }
|
||||
totalCount = response.total
|
||||
if append {
|
||||
orderList.append(contentsOf: response.list)
|
||||
@@ -447,6 +470,7 @@ final class OrderListViewModel {
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
guard listRequestID == requestID else { return }
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
finishLoading()
|
||||
@@ -479,7 +503,7 @@ final class OrderListViewModel {
|
||||
var json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return nil
|
||||
}
|
||||
json["scenic_id"] = String(AppStore.shared.session.currentScenicId)
|
||||
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
|
||||
|
||||
@@ -67,16 +67,20 @@ final class MainTabBarController: UITabBarController {
|
||||
|
||||
/// 响应推送点击,切换到目标 Tab 并打开对应业务页面。
|
||||
func openPushDestination(_ destination: PushDestination) {
|
||||
let targetTab = AppTab.home
|
||||
loadViewIfNeeded()
|
||||
let targetTab: AppTab = destination == .orders ? .orders : .home
|
||||
|
||||
let wasAlreadyOnOrders = targetTab == .orders && selectedViewController === tabNavigationControllers[.orders]
|
||||
selectTab(targetTab)
|
||||
guard let navigationController = tabNavigationControllers[targetTab] else { return }
|
||||
_ = navigationController.popToRootViewController(animated: false)
|
||||
|
||||
let controller: UIViewController?
|
||||
switch destination {
|
||||
case .paymentRecord:
|
||||
controller = PaymentCollectionRecordViewController()
|
||||
case .orders:
|
||||
(navigationController.viewControllers.first as? OrdersViewController)?.openFromOrderNotification()
|
||||
if wasAlreadyOnOrders { badgeViewModel.refreshPendingWriteOffCount() }
|
||||
controller = nil
|
||||
case .paymentDetails:
|
||||
controller = PaymentCollectionDetailsViewController()
|
||||
case .messageCenter:
|
||||
|
||||
@@ -8,17 +8,64 @@ import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 订单 Tab 根页面,按角色分流摄影师/景区管理员/店铺管理员。
|
||||
final class OrdersViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate {
|
||||
final class OrdersViewController: BaseViewController, UITableViewDelegate {
|
||||
|
||||
private let orderAPI = NetworkServices.shared.orderAPI
|
||||
private let orderListViewModel = OrderListViewModel()
|
||||
private let depositViewModel = DepositOrderListViewModel()
|
||||
private let appStore: AppStore
|
||||
private let orderAPI: OrderAPI
|
||||
private let orderListViewModel: OrderListViewModel
|
||||
private let depositViewModel: DepositOrderListViewModel
|
||||
|
||||
/// 表格快照持有完整行数据,避免刷新清空 ViewModel 后仍按旧行号取值。
|
||||
private struct OrderRow: Hashable {
|
||||
/// 同一快照内按位置区分行,兼容接口返回重复订单号。
|
||||
let position: Int
|
||||
let content: Content
|
||||
|
||||
/// 摄影师和店铺订单的不可变展示数据。
|
||||
enum Content: Equatable {
|
||||
case photographer(OrderEntity, source: OrderSourceSelection?, readOnly: Bool, showSource: Bool)
|
||||
case deposit(StoreOrderItem)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(position)
|
||||
switch content {
|
||||
case .photographer(let order, _, _, _):
|
||||
hasher.combine(0)
|
||||
hasher.combine(order.orderNumber)
|
||||
case .deposit(let order):
|
||||
hasher.combine(1)
|
||||
hasher.combine(order.orderNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, OrderRow>?
|
||||
|
||||
/// 注入订单数据依赖;默认使用应用共享 API 和独立的页面 ViewModel。
|
||||
init(
|
||||
orderAPI: OrderAPI? = nil,
|
||||
orderListViewModel: OrderListViewModel? = nil,
|
||||
depositViewModel: DepositOrderListViewModel? = nil,
|
||||
appStore: AppStore = .shared
|
||||
) {
|
||||
self.appStore = appStore
|
||||
self.orderAPI = orderAPI ?? NetworkServices.shared.orderAPI
|
||||
self.orderListViewModel = orderListViewModel ?? OrderListViewModel(appStore: appStore)
|
||||
self.depositViewModel = depositViewModel ?? DepositOrderListViewModel(appStore: appStore)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private let filterBar = OrderFilterBarView()
|
||||
private let listView = OrderListView()
|
||||
|
||||
private var hasInitialized = false
|
||||
private var isStoreMode: Bool { AppStore.shared.session.currentAppRole == .storeAdmin }
|
||||
private var isStoreMode: Bool { appStore.session.currentAppRole == .storeAdmin }
|
||||
private var pendingOrderSourceNumber: String?
|
||||
private weak var orderSourcePicker: OrderSourcePickerViewController?
|
||||
private var pendingGiftOrderNumber: String?
|
||||
@@ -32,10 +79,10 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
override func setupUI() {
|
||||
view.addSubview(filterBar)
|
||||
view.addSubview(listView)
|
||||
listView.tableView.dataSource = self
|
||||
listView.tableView.delegate = self
|
||||
listView.tableView.register(OrderCardCell.self, forCellReuseIdentifier: OrderCardCell.reuseIdentifier)
|
||||
listView.tableView.register(DepositOrderCardCell.self, forCellReuseIdentifier: DepositOrderCardCell.reuseIdentifier)
|
||||
configureDataSource()
|
||||
}
|
||||
|
||||
override func setupConstraints() {
|
||||
@@ -133,6 +180,21 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
Task { await initializeList() }
|
||||
}
|
||||
|
||||
/// 从订单通知进入时恢复默认查询,并刷新已有或首次打开的列表。
|
||||
func openFromOrderNotification() {
|
||||
loadViewIfNeeded()
|
||||
hasInitialized = true
|
||||
if isStoreMode {
|
||||
depositViewModel.resetFiltersForOrderNotification()
|
||||
} else {
|
||||
orderListViewModel.resetFiltersForOrderNotification()
|
||||
}
|
||||
applyViewModel()
|
||||
Task { @MainActor [weak self] in
|
||||
await self?.initializeList()
|
||||
}
|
||||
}
|
||||
|
||||
private func initializeList() async {
|
||||
if isStoreMode {
|
||||
await depositViewModel.refreshOrderList(api: orderAPI)
|
||||
@@ -142,6 +204,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
applyOrderSnapshot()
|
||||
if isStoreMode {
|
||||
filterBar.configure(
|
||||
isStoreMode: true,
|
||||
@@ -191,7 +254,6 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
presentMultiVerifySheet()
|
||||
}
|
||||
}
|
||||
listView.tableView.reloadData()
|
||||
}
|
||||
|
||||
private func dateRangeText(start: Date?, end: Date?) -> String {
|
||||
@@ -314,31 +376,60 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
|
||||
|
||||
// MARK: - UITableView
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
isStoreMode ? depositViewModel.orderList.count : orderListViewModel.orderList.count
|
||||
private func configureDataSource() {
|
||||
dataSource = UITableViewDiffableDataSource<Int, OrderRow>(tableView: listView.tableView) { [weak self] tableView, _, row in
|
||||
guard let self else { return nil }
|
||||
switch row.content {
|
||||
case .deposit(let item):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier) as! DepositOrderCardCell
|
||||
cell.configure(item: item, actions: self.makeDepositActions(for: item))
|
||||
return cell
|
||||
case .photographer(let order, let source, let readOnly, let showSource):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier) as! OrderCardCell
|
||||
cell.configure(
|
||||
order: order,
|
||||
hideWriteActions: readOnly,
|
||||
actions: self.makeOrderActions(for: order),
|
||||
showOrderSource: showSource,
|
||||
sourceSelection: source
|
||||
)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
private func applyOrderSnapshot() {
|
||||
let rows: [OrderRow]
|
||||
if isStoreMode {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DepositOrderCardCell.reuseIdentifier, for: indexPath) as! DepositOrderCardCell
|
||||
let item = depositViewModel.orderList[indexPath.row]
|
||||
cell.configure(item: item, actions: makeDepositActions(for: item))
|
||||
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: depositViewModel.shouldLoadMore(at: indexPath.row))
|
||||
return cell
|
||||
rows = depositViewModel.orderList.enumerated().map {
|
||||
OrderRow(position: $0.offset, content: .deposit($0.element))
|
||||
}
|
||||
} else {
|
||||
rows = orderListViewModel.orderList.enumerated().map { index, order in
|
||||
OrderRow(position: index, content: .photographer(
|
||||
order,
|
||||
source: orderListViewModel.orderSourceSelection(for: order),
|
||||
readOnly: orderListViewModel.isScenicAdminReadOnly,
|
||||
showSource: orderListViewModel.shouldShowOrderSource(for: order)
|
||||
))
|
||||
}
|
||||
}
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, OrderRow>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(rows)
|
||||
dataSource?.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: OrderCardCell.reuseIdentifier, for: indexPath) as! OrderCardCell
|
||||
let order = orderListViewModel.orderList[indexPath.row]
|
||||
let source = orderListViewModel.orderSourceSelection(for: order)
|
||||
cell.configure(
|
||||
order: order,
|
||||
hideWriteActions: orderListViewModel.isScenicAdminReadOnly,
|
||||
actions: makeOrderActions(for: order),
|
||||
showOrderSource: orderListViewModel.shouldShowOrderSource(for: order),
|
||||
sourceSelection: source
|
||||
)
|
||||
listView.notifyLoadMoreIfNeeded(at: indexPath.row, shouldLoad: orderListViewModel.shouldLoadMore(at: indexPath.row))
|
||||
return cell
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
guard let row = dataSource?.itemIdentifier(for: indexPath) else { return }
|
||||
let shouldLoad: Bool
|
||||
switch row.content {
|
||||
case .deposit:
|
||||
shouldLoad = isStoreMode && depositViewModel.shouldLoadMore(at: row.position)
|
||||
case .photographer:
|
||||
shouldLoad = !isStoreMode && orderListViewModel.shouldLoadMore(at: row.position)
|
||||
}
|
||||
listView.notifyLoadMoreIfNeeded(at: row.position, shouldLoad: shouldLoad)
|
||||
}
|
||||
|
||||
private func makeOrderActions(for order: OrderEntity) -> OrderCardActions {
|
||||
|
||||
Reference in New Issue
Block a user