迁移合作订单完整能力,并统一 AppRoleCode 角色权限与首页菜单展示。

新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 13:33:00 +08:00
parent d310d26293
commit 1970572edd
66 changed files with 4247 additions and 201 deletions

View File

@ -31,6 +31,16 @@ final class OrdersViewModel: ObservableObject {
@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)
@ -38,17 +48,17 @@ final class OrdersViewModel: ObservableObject {
}
///
func reload(api: OrderServing, scenicId: Int?, storeId: Int? = nil, roleId: Int?, showLoading: Bool = true) async throws {
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, roleId: roleId, showLoading: showLoading)
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?, roleId: Int?, showLoading: Bool = true) async throws {
func reloadStoreOrders(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?, showLoading: Bool = true) async throws {
guard let scenicId else {
storeOrders = []
storeTotal = 0
@ -69,16 +79,17 @@ final class OrdersViewModel: ObservableObject {
startTime: startTimeQuery,
endTime: endTimeQuery,
isRefined: refinedQuery,
isScenicAdmin: roleId == 53
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?, roleId: Int?) async throws {
func loadMoreStoreOrders(api: OrderServing, scenicId: Int?, appRole: AppRoleCode?) async throws {
guard !loadingMore, storeHasMore, let scenicId else { return }
loadingMore = true
defer { loadingMore = false }
@ -93,7 +104,7 @@ final class OrdersViewModel: ObservableObject {
startTime: startTimeQuery,
endTime: endTimeQuery,
isRefined: refinedQuery,
isScenicAdmin: roleId == 53
isScenicAdmin: appRole?.isScenicAdmin == true
)
storeOrders.append(contentsOf: result.list)
storeTotal = result.total
@ -154,6 +165,81 @@ final class OrdersViewModel: ObservableObject {
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 {