迁移合作订单完整能力,并统一 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

@ -0,0 +1,93 @@
//
// AppRoleCodeTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/29.
//
import XCTest
@testable import suixinkan
final class AppRoleCodeTests: XCTestCase {
func testFromCodeParsesKnownRoles() {
XCTAssertEqual(AppRoleCode.fromCode("store_admin"), .storeAdmin)
XCTAssertEqual(AppRoleCode.fromCode("scenic_admin"), .scenicAdmin)
XCTAssertEqual(AppRoleCode.fromCode("photographer"), .photographer)
}
func testFromLegacyRoleIdMapsKnownIds() {
XCTAssertEqual(AppRoleCode.fromLegacyRoleId(46), .storeAdmin)
XCTAssertEqual(AppRoleCode.fromLegacyRoleId(53), .scenicAdmin)
XCTAssertEqual(AppRoleCode.fromLegacyRoleId(41), .photographer)
}
func testFromRoleNameAvoidsAssistantMisclassification() {
XCTAssertEqual(AppRoleCode.fromRoleName("摄影师助理"), .photographerAssistant)
XCTAssertEqual(AppRoleCode.fromRoleName("资深摄影师"), .photographer)
}
func testHomeMinimalTopRolesContainsManagementRoles() {
XCTAssertTrue(AppRoleCode.homeMinimalTopRoles.contains(.storeAdmin))
XCTAssertTrue(AppRoleCode.homeMinimalTopRoles.contains(.scenicAdmin))
XCTAssertFalse(AppRoleCode.homeMinimalTopRoles.contains(.photographer))
}
}
final class RolePermissionMatchingTests: XCTestCase {
func testMatchPrefersRoleCodeOverLegacyId() {
let permissions = [
RolePermissionResponse(
role: RoleInfo(id: 10, name: "摄影师", roleCode: "photographer", notes: nil, permission: []),
scenic: []
),
RolePermissionResponse(
role: RoleInfo(id: 53, name: "景区管理员", roleCode: "scenic_admin", notes: nil, permission: []),
scenic: []
)
]
let matched = RolePermissionMatching.match(
in: permissions,
roleCode: "scenic_admin",
roleId: 10,
roleName: "摄影师"
)
XCTAssertEqual(matched?.role.id, 53)
}
func testResolveRoleCodeMigratesLegacySession() {
let resolved = RolePermissionMatching.resolveRoleCode(
roleCode: nil,
roleId: 46,
roleName: nil,
permissions: []
)
XCTAssertEqual(resolved, .storeAdmin)
}
}
final class AccountSnapshotMigrationTests: XCTestCase {
func testMigrateLegacyRoleIdToRoleCode() {
var snapshot = AccountSnapshot(currentRoleId: 53)
snapshot.migrateLegacyRoleIfNeeded()
XCTAssertEqual(snapshot.currentRoleCode, "scenic_admin")
XCTAssertNil(snapshot.currentRoleId)
}
func testLoadMigratesAndPersistsLegacySnapshot() {
let defaults = UserDefaults(suiteName: "AccountSnapshotMigrationTests")!
defaults.removePersistentDomain(forName: "AccountSnapshotMigrationTests")
let store = AccountSnapshotStore(defaults: defaults, key: "test.snapshot")
store.save(AccountSnapshot(currentRoleId: 46))
let loaded = store.load()
XCTAssertEqual(loaded?.currentRoleCode, "store_admin")
XCTAssertNil(loaded?.currentRoleId)
let persisted = store.load()
XCTAssertEqual(persisted?.currentRoleCode, "store_admin")
XCTAssertNil(persisted?.currentRoleId)
}
}

View File

@ -0,0 +1,196 @@
//
// CooperationOrderAPITests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/29.
//
import XCTest
@testable import suixinkan
@MainActor
/// API
final class CooperationOrderAPITests: XCTestCase {
///
func testCooperativeSalersDecodesFixtureAndUsesExpectedQuery() async throws {
let session = CooperationRecordingURLSession(data: try TestFixture.data(named: "cooperative_salers_success"))
let api = CooperationOrderAPI(client: APIClient(session: session))
let response = try await api.cooperativeSalers(storeId: 66, page: 0, pageSize: 0)
XCTAssertEqual(response.total, 1)
XCTAssertEqual(response.page, 1)
XCTAssertEqual(response.pageSize, 50)
XCTAssertEqual(response.list.first?.displayId, 101)
XCTAssertEqual(response.list.first?.displayName, "管铜彬")
XCTAssertEqual(response.list.first?.displayPhone, "19281871413")
XCTAssertEqual(response.list.first?.bindingId, 5)
XCTAssertEqual(response.list.first?.shareCode, "INV-000101")
let request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.httpMethod, "GET")
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/cooperative-salers")
let query = cooperationQueryItems(from: request)
XCTAssertEqual(query["store_id"], "66")
XCTAssertEqual(query["page"], "1")
XCTAssertEqual(query["page_size"], "1")
}
///
func testSaleUserInviteInfoDecodesFixture() async throws {
let session = CooperationRecordingURLSession(data: try TestFixture.data(named: "sale_user_invite_info_success"))
let api = CooperationOrderAPI(client: APIClient(session: session))
let info = try await api.saleUserInviteInfo(saleUserId: 101)
XCTAssertEqual(info.saleUserId, 101)
XCTAssertFalse(info.isBound)
XCTAssertEqual(info.photographerName, "陆久银")
XCTAssertEqual(info.photographerPhone, "18651857230")
XCTAssertEqual(info.salerName, "管铜彬")
XCTAssertEqual(info.salerPhone, "13222319413")
XCTAssertEqual(info.shareCode, "INV-000101")
XCTAssertEqual(info.subtitle, "确认与获客员绑定,可以绑定多个获客员")
XCTAssertEqual(info.title, "绑定获客员")
let request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/invite-info")
XCTAssertEqual(cooperationQueryItems(from: request)["sale_user_id"], "101")
}
/// 线
func testSaleUserReferralLeadsDecodesFixture() async throws {
let session = CooperationRecordingURLSession(data: try TestFixture.data(named: "sale_user_referral_leads_success"))
let api = CooperationOrderAPI(client: APIClient(session: session))
let response = try await api.saleUserReferralLeads(page: 2, pageSize: 20)
XCTAssertEqual(response.total, 1)
XCTAssertEqual(response.list.first?.displayId, "501")
XCTAssertEqual(response.list.first?.displayPhone, "13900000002")
XCTAssertEqual(response.list.first?.remark, "景区门口获客")
XCTAssertEqual(response.list.first?.displayImages.first, "https://example.com/lead.jpg")
let request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/referral-leads")
let query = cooperationQueryItems(from: request)
XCTAssertEqual(query["page"], "2")
XCTAssertEqual(query["page_size"], "20")
}
/// keyword
func testSaleUserAcquisitionOrdersDecodesFixtureWithKeyword() async throws {
let session = CooperationRecordingURLSession(data: try TestFixture.data(named: "sale_user_acquisition_orders_success"))
let api = CooperationOrderAPI(client: APIClient(session: session))
let response = try await api.saleUserAcquisitionOrders(keyword: "138", page: 1, pageSize: 10)
XCTAssertEqual(response.total, 1)
XCTAssertEqual(response.list.first?.orderNumber, "ACQ001")
XCTAssertEqual(response.list.first?.displayAmount, "299.00")
XCTAssertEqual(response.list.first?.displayCustomerPhone, "13700000003")
XCTAssertEqual(response.list.first?.displayPartner, "张三")
let request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/acquisition-orders")
let query = cooperationQueryItems(from: request)
XCTAssertEqual(query["keyword"], "138")
}
///
func testSaleUserReferralOrdersDecodesFixture() async throws {
let session = CooperationRecordingURLSession(data: try TestFixture.data(named: "sale_user_referral_orders_success"))
let api = CooperationOrderAPI(client: APIClient(session: session))
let response = try await api.saleUserReferralOrders(saleUserId: 101, page: 1, pageSize: 10)
XCTAssertEqual(response.total, 1)
XCTAssertEqual(response.page, 1)
XCTAssertEqual(response.pageSize, 50)
XCTAssertEqual(response.saler?.saleUserId, 101)
XCTAssertEqual(response.saler?.salerName, "管铜彬")
XCTAssertEqual(response.list.first?.id, 54)
XCTAssertEqual(response.list.first?.displayReferralOrderNo, "LD202606250009")
XCTAssertEqual(response.list.first?.userMobile, "13322222223")
XCTAssertEqual(response.list.first?.remark, "带客备注")
XCTAssertEqual(response.list.first?.statusLabel, "未完成")
XCTAssertEqual(response.list.first?.images.count, 1)
let request = try XCTUnwrap(session.requests.last)
XCTAssertEqual(request.url?.path, "/api/yf-handset-app/photog/sale-user/referral-orders")
let query = cooperationQueryItems(from: request)
XCTAssertEqual(query["sale_user_id"], "101")
}
///
func testSaleUserAcceptBindAndBindReferralOrderUsePostQuery() async throws {
let emptyData = try TestFixture.data(named: "empty_success")
let acceptSession = CooperationRecordingURLSession(data: emptyData)
let acceptAPI = CooperationOrderAPI(client: APIClient(session: acceptSession))
try await acceptAPI.saleUserAcceptBind(saleUserId: 101)
let acceptRequest = try XCTUnwrap(acceptSession.requests.last)
XCTAssertEqual(acceptRequest.httpMethod, "POST")
XCTAssertEqual(acceptRequest.url?.path, "/api/yf-handset-app/photog/sale-user/accept-bind")
XCTAssertEqual(cooperationQueryItems(from: acceptRequest)["sale_user_id"], "101")
XCTAssertNil(acceptRequest.httpBody)
let bindSession = CooperationRecordingURLSession(data: emptyData)
let bindAPI = CooperationOrderAPI(client: APIClient(session: bindSession))
try await bindAPI.saleUserBindReferralOrder(orderNumber: "ORD001", referralOrderNo: "REF001")
let bindRequest = try XCTUnwrap(bindSession.requests.last)
XCTAssertEqual(bindRequest.httpMethod, "POST")
XCTAssertEqual(bindRequest.url?.path, "/api/yf-handset-app/photog/sale-user/bind-referral-order")
let bindQuery = cooperationQueryItems(from: bindRequest)
XCTAssertEqual(bindQuery["order_number"], "ORD001")
XCTAssertEqual(bindQuery["referral_order_no"], "REF001")
XCTAssertNil(bindRequest.httpBody)
let statusSession = CooperationRecordingURLSession(data: emptyData)
let statusAPI = CooperationOrderAPI(client: APIClient(session: statusSession))
try await statusAPI.saleUserUpdateReferralOrderStatus(id: 15, status: 3)
let statusRequest = try XCTUnwrap(statusSession.requests.last)
XCTAssertEqual(statusRequest.httpMethod, "POST")
XCTAssertEqual(
statusRequest.url?.path,
"/api/yf-handset-app/photog/sale-user/referral-orders/15/status"
)
let statusBody = try XCTUnwrap(statusRequest.httpBody)
let statusJSON = try XCTUnwrap(JSONSerialization.jsonObject(with: statusBody) as? [String: Any])
XCTAssertEqual(statusJSON["status"] as? Int, 3)
}
}
/// URLSession API
private final class CooperationRecordingURLSession: URLSessionProtocol {
let data: Data
var statusCode: Int
private(set) var requests: [URLRequest] = []
init(data: Data, statusCode: Int = 200) {
self.data = data
self.statusCode = statusCode
}
/// HTTP
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
requests.append(request)
return (
data,
HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
)
}
}
private func cooperationQueryItems(from request: URLRequest) -> [String: String] {
guard let url = request.url,
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return [:]
}
return Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in
item.value.map { (item.name, $0) }
})
}

View File

@ -0,0 +1,24 @@
{
"code": 100000,
"msg": "success",
"data": {
"total": 1,
"page": 1,
"page_size": 50,
"list": [
{
"bind_time": "2026-06-25 17:11:01",
"binding_id": 5,
"bound_at": "2026-06-25 17:11:01",
"name": "管铜彬",
"phone": "19281871413",
"phone_masked": "192****1413",
"sale_user_id": 101,
"saler_name": "管铜彬",
"saler_phone": "19281871413",
"share_code": "INV-000101",
"store_id": 37
}
]
}
}

View File

@ -0,0 +1,20 @@
{
"code": 100000,
"msg": "success",
"data": {
"total": "1",
"list": [
{
"id": 801,
"order_number": "ACQ001",
"project_name": "亲子跟拍",
"order_type": 11,
"actual_pay_amount": "299.00",
"customer_phone": "13700000003",
"order_status_name": "已完成",
"sale_user_name": "张三",
"created_at": "2026-05-22 16:00:00"
}
]
}
}

View File

@ -0,0 +1,15 @@
{
"code": 100000,
"msg": "success",
"data": {
"is_bound": false,
"photographer_name": "陆久银",
"photographer_phone": "18651857230",
"sale_user_id": 101,
"saler_name": "管铜彬",
"saler_phone": "13222319413",
"share_code": "INV-000101",
"subtitle": "确认与获客员绑定,可以绑定多个获客员",
"title": "绑定获客员"
}
}

View File

@ -0,0 +1,16 @@
{
"code": 100000,
"msg": "success",
"data": {
"total": "1",
"list": [
{
"lead_id": 501,
"user_phone": "13900000002",
"remark": "景区门口获客",
"created_at": "2026-05-21 14:00:00",
"image_urls": ["https://example.com/lead.jpg"]
}
]
}
}

View File

@ -0,0 +1,29 @@
{
"code": 100000,
"msg": "success",
"data": {
"total": 1,
"page": 1,
"page_size": 50,
"saler": {
"sale_user_id": 101,
"saler_name": "管铜彬",
"saler_phone": "19281871413"
},
"list": [
{
"id": 54,
"referral_order_no": "LD202606250009",
"order_no": "LD202606250009",
"user_mobile": "13322222223",
"remark": "带客备注",
"created_at": "2026-06-25 17:18:52",
"status": 1,
"status_label": "未完成",
"images": [
"https://vipsky.oss-cn-shanghai.aliyuncs.com/sales/referral-order/create/6/1782379129957_hd2z9x.jpg"
]
}
]
}
}

View File

@ -10,38 +10,117 @@ import XCTest
///
final class HomeCommonMenuStoreTests: XCTestCase {
///
func testLoadCreatesDefaultCommonMenusWhenBaselineIsMissing() {
///
func testLoadCreatesDefaultCommonMenusFromTopLevelPermissions() {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("registration_invitation"),
menu("location_report"),
menu("pm_manager")
menu("space_settings"),
menu("wallet"),
menu("cloud_management"),
menu("message_center")
]
let uris = store.load(menuItems: menus)
let uris = store.load(
menuItems: menus,
roleCode: "photographer",
topLevelPermissionURIs: ["space_settings", "wallet", "cloud_management", "message_center"]
)
XCTAssertEqual(uris, ["registration_invitation", "location_report", "pm_manager"])
XCTAssertTrue(defaults.bool(forKey: HomeCommonMenuStore.defaultBaselineKey))
XCTAssertEqual(uris, ["space_settings", "wallet", "cloud_management", "message_center"])
XCTAssertEqual(
defaults.stringArray(forKey: HomeCommonMenuStore.storageKeyPrefix + "photographer"),
["space_settings", "wallet", "cloud_management", "message_center"]
)
}
/// 4 4
func testDefaultCommonURIsTakeFirstFourWhenMoreThanFourPermissions() {
let availableURIs: Set<String> = [
"space_settings",
"wallet",
"cloud_management",
"message_center",
"task_management"
]
let uris = HomeCommonMenuStore.defaultCommonURIs(
orderedTopLevelURIs: [
"space_settings",
"wallet",
"cloud_management",
"message_center",
"task_management"
],
availableURIs: availableURIs
)
XCTAssertEqual(uris, ["space_settings", "wallet", "cloud_management", "message_center"])
}
/// URI
func testLoadCreatesDefaultsWithoutLegacyPreferredURIs() {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("space_settings"),
menu("wallet"),
menu("cloud_management")
]
let uris = store.load(
menuItems: menus,
roleCode: "editor",
topLevelPermissionURIs: ["space_settings", "wallet", "cloud_management"]
)
XCTAssertEqual(uris, ["space_settings", "wallet", "cloud_management"])
}
/// URI URI
func testLoadFiltersSavedUrisByCurrentPermissions() {
let defaults = makeIsolatedDefaults()
defaults.set(true, forKey: HomeCommonMenuStore.defaultBaselineKey)
defaults.set(["task_management", "unknown", "pm"], forKey: HomeCommonMenuStore.defaultStorageKey)
defaults.set(["task_management", "unknown", "pm"], forKey: HomeCommonMenuStore.storageKeyPrefix + "photographer")
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("task_management_editor"),
menu("project_edit")
]
let uris = store.load(menuItems: menus)
let uris = store.load(
menuItems: menus,
roleCode: "photographer",
topLevelPermissionURIs: ["task_management_editor", "project_edit"]
)
XCTAssertEqual(uris, ["task_management_editor", "project_edit"])
}
/// 使 key
func testLoadUsesRoleScopedStorageKeys() {
let defaults = makeIsolatedDefaults()
defaults.set(["wallet"], forKey: HomeCommonMenuStore.storageKeyPrefix + "photographer")
defaults.set(["cloud_management"], forKey: HomeCommonMenuStore.storageKeyPrefix + "editor")
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [
menu("wallet"),
menu("cloud_management")
]
let roleOne = store.load(
menuItems: menus,
roleCode: "photographer",
topLevelPermissionURIs: ["wallet", "cloud_management"]
)
let roleTwo = store.load(
menuItems: menus,
roleCode: "editor",
topLevelPermissionURIs: ["wallet", "cloud_management"]
)
XCTAssertEqual(roleOne, ["wallet"])
XCTAssertEqual(roleTwo, ["cloud_management"])
}
/// URI
func testAddDeduplicatesAliases() {
let defaults = makeIsolatedDefaults()
@ -50,7 +129,12 @@ final class HomeCommonMenuStoreTests: XCTestCase {
menu("task_management_editor")
]
let uris = store.add("task_management", current: ["task_management_editor"], menuItems: menus)
let uris = store.add(
"task_management",
current: ["task_management_editor"],
menuItems: menus,
roleCode: "photographer"
)
XCTAssertEqual(uris, ["task_management_editor"])
}
@ -60,11 +144,45 @@ final class HomeCommonMenuStoreTests: XCTestCase {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let uris = store.remove("pm", current: ["project_edit", "pm_manager", "location_report"])
let uris = store.remove("pm", current: ["project_edit", "pm_manager", "location_report"], roleCode: "photographer")
XCTAssertEqual(uris, ["pm_manager", "location_report"])
}
/// role_code
func testLoadReturnsEmptyWhenRoleCodeIsMissing() {
let defaults = makeIsolatedDefaults()
let store = HomeCommonMenuStore(defaults: defaults)
let uris = store.load(
menuItems: [menu("wallet")],
roleCode: nil,
topLevelPermissionURIs: ["wallet"]
)
XCTAssertTrue(uris.isEmpty)
XCTAssertNil(defaults.stringArray(forKey: HomeCommonMenuStore.storageKeyPrefix + "photographer"))
}
/// legacy role id key
func testLoadMigratesLegacyRoleIdStorageKey() {
let defaults = makeIsolatedDefaults()
defaults.set(["wallet", "cloud_management"], forKey: HomeCommonMenuStore.storageKeyPrefix + "46")
let store = HomeCommonMenuStore(defaults: defaults)
let menus = [menu("wallet"), menu("cloud_management")]
let uris = store.load(
menuItems: menus,
roleCode: "store_admin",
legacyRoleId: 46,
topLevelPermissionURIs: ["wallet", "cloud_management"]
)
XCTAssertEqual(uris, ["wallet", "cloud_management"])
XCTAssertEqual(defaults.stringArray(forKey: HomeCommonMenuStore.storageKeyPrefix + "store_admin"), ["wallet", "cloud_management"])
XCTAssertNil(defaults.stringArray(forKey: HomeCommonMenuStore.storageKeyPrefix + "46"))
}
///
private func menu(_ uri: String) -> HomeMenuItem {
HomeMenuItem(title: HomeMenuRouter.title(for: uri), uri: uri, iconSrc: nil)

View File

@ -57,6 +57,7 @@ final class HomeMenuRouterTests: XCTestCase {
XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order", title: ""), .destination(.depositOrders))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "deposit_order_shooting_info", title: ""), .destination(.depositOrders))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "withdrawal_audit", title: ""), .destination(.withdrawalAudit))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "cooperation_order", title: ""), .destination(.cooperationOrder))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenic_settlement", title: ""), .destination(.scenicSettlement))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "scenic_settlement_review", title: ""), .destination(.scenicSettlementReview))
XCTAssertEqual(HomeMenuRouter.resolve(uri: "message_center", title: ""), .destination(.messageCenter))
@ -150,6 +151,7 @@ final class HomeMenuRouterTests: XCTestCase {
func testPermissionRouteAuditSeparatesRoutableUnsupportedAndUnknownEntries() {
let permissions = [
rolePermission(
roleCode: "photographer",
roleId: 1,
permissions: [
permission(id: 1, name: "任务管理", uri: "task_management", children: [
@ -162,7 +164,7 @@ final class HomeMenuRouterTests: XCTestCase {
)
]
let audit = HomePermissionRouteAuditor.audit(permissions: permissions, currentRoleId: 1)
let audit = HomePermissionRouteAuditor.audit(permissions: permissions, currentRoleCode: "photographer")
XCTAssertEqual(audit.routable.map(\.uri), ["task_management", "task_create"])
XCTAssertEqual(audit.unsupported.map(\.uri), ["pilot_controller"])
@ -214,9 +216,9 @@ final class HomeMenuRouterTests: XCTestCase {
#endif
///
private func rolePermission(roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse {
private func rolePermission(roleCode: String, roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse {
RolePermissionResponse(
role: RoleInfo(id: roleId, name: "角色\(roleId)", permission: permissions),
role: RoleInfo(id: roleId, name: "角色\(roleId)", roleCode: roleCode, permission: permissions),
scenic: []
)
}

View File

@ -15,15 +15,15 @@ final class HomeViewModelTests: XCTestCase {
func testBuildMenusClearsStaleMenusWhenCurrentRoleIsMissing() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
rolePermission(roleCode: "photographer", roleId: 1, permissions: [
permission(id: 1, name: "消息中心", uri: "message_center")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
viewModel.buildMenus(from: permissions, currentRoleCode: "photographer")
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["message_center"])
viewModel.buildMenus(from: permissions, currentRoleId: 99)
viewModel.buildMenus(from: permissions, currentRoleCode: "unknown_role")
XCTAssertTrue(viewModel.menuItems.isEmpty)
}
@ -32,15 +32,15 @@ final class HomeViewModelTests: XCTestCase {
func testBuildMenusFallsBackToFirstRoleOnlyWhenCurrentRoleIsNil() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
rolePermission(roleCode: "photographer", roleId: 1, permissions: [
permission(id: 1, name: "任务管理", uri: "task_management")
]),
rolePermission(roleId: 2, permissions: [
rolePermission(roleCode: "editor", roleId: 2, permissions: [
permission(id: 2, name: "消息中心", uri: "message_center")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: nil)
viewModel.buildMenus(from: permissions, currentRoleCode: nil)
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["task_management"])
}
@ -49,14 +49,14 @@ final class HomeViewModelTests: XCTestCase {
func testBuildMenusFlattensPermissionTree() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
rolePermission(roleCode: "photographer", roleId: 1, permissions: [
permission(id: 1, name: "任务管理", uri: "task_management", children: [
permission(id: 2, name: "消息中心", uri: "message_center")
])
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
viewModel.buildMenus(from: permissions, currentRoleCode: "photographer")
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["task_management", "message_center"])
}
@ -65,7 +65,7 @@ final class HomeViewModelTests: XCTestCase {
func testBuildMenusDeduplicatesKnownAndroidAliases() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
rolePermission(roleCode: "photographer", roleId: 1, permissions: [
permission(id: 1, name: "项目管理", uri: "pm_manager"),
permission(id: 2, name: "项目管理旧入口", uri: "pm"),
permission(id: 3, name: "收款码", uri: "payment_code"),
@ -75,7 +75,7 @@ final class HomeViewModelTests: XCTestCase {
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
viewModel.buildMenus(from: permissions, currentRoleCode: "photographer")
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["pm", "deposit_order_detail", "pm_manager", "payment_code"])
}
@ -84,22 +84,22 @@ final class HomeViewModelTests: XCTestCase {
func testBuildMenusSortsByPreferredOrder() {
let viewModel = HomeViewModel()
let permissions = [
rolePermission(roleId: 1, permissions: [
rolePermission(roleCode: "photographer", roleId: 1, permissions: [
permission(id: 1, name: "消息中心", uri: "message_center"),
permission(id: 2, name: "空间设置", uri: "space_settings"),
permission(id: 3, name: "钱包", uri: "wallet")
])
]
viewModel.buildMenus(from: permissions, currentRoleId: 1)
viewModel.buildMenus(from: permissions, currentRoleCode: "photographer")
XCTAssertEqual(viewModel.menuItems.map(\.uri), ["space_settings", "wallet", "message_center"])
}
///
private func rolePermission(roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse {
private func rolePermission(roleCode: String, roleId: Int, permissions: [PermissionItem]) -> RolePermissionResponse {
RolePermissionResponse(
role: RoleInfo(id: roleId, name: "角色\(roleId)", permission: permissions),
role: RoleInfo(id: roleId, name: "角色\(roleId)", roleCode: roleCode, permission: permissions),
scenic: []
)
}

View File

@ -156,8 +156,17 @@ private func accountContext(scenicId: Int? = 88, storeId: Int? = 101) -> Account
@MainActor
private func permissionContext(roleName: String) -> PermissionContext {
let context = PermissionContext()
let role = RoleInfo(id: 1, name: roleName, permission: [PermissionItem(id: 1, name: "运营区域", uri: "operating-area")])
context.replaceRolePermissions([RolePermissionResponse(role: role, scenic: [])], currentRoleId: 1)
let roleCode = AppRoleCode.fromRoleName(roleName)?.rawValue ?? ""
let role = RoleInfo(
id: 1,
name: roleName,
roleCode: roleCode,
permission: [PermissionItem(id: 1, name: "运营区域", uri: "operating-area")]
)
context.replaceRolePermissions(
[RolePermissionResponse(role: role, scenic: [])],
currentRoleCode: roleCode
)
return context
}

View File

@ -17,10 +17,10 @@ final class OrdersViewModelTests: XCTestCase {
api.storeResponses = [try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: nil)
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, appRole: nil)
XCTAssertFalse(viewModel.storeOrders.isEmpty)
try await viewModel.reloadStoreOrders(api: api, scenicId: nil, roleId: nil)
try await viewModel.reloadStoreOrders(api: api, scenicId: nil, appRole: nil)
XCTAssertTrue(viewModel.storeOrders.isEmpty)
XCTAssertEqual(viewModel.storeTotal, 0)
@ -35,8 +35,8 @@ final class OrdersViewModelTests: XCTestCase {
api.storeResponses = [try TestFixture.payload(ListPayload<OrderEntity>.self, named: "order_v2_success")]
let viewModel = OrdersViewModel()
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: nil)
try await viewModel.loadMoreStoreOrders(api: api, scenicId: 88, roleId: nil)
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, appRole: nil)
try await viewModel.loadMoreStoreOrders(api: api, scenicId: 88, appRole: nil)
XCTAssertEqual(viewModel.storeOrders.count, 2)
XCTAssertEqual(viewModel.storeTotal, 2)
@ -58,13 +58,13 @@ final class OrdersViewModelTests: XCTestCase {
viewModel.filterStartDate = Self.date("2026-05-01")
viewModel.filterEndDate = Self.date("2026-05-31")
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: nil)
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, appRole: nil)
viewModel.selectedStatus = 30
viewModel.searchPhone = " "
viewModel.filterStartDate = nil
viewModel.filterEndDate = nil
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, roleId: 53)
try await viewModel.reloadStoreOrders(api: api, scenicId: 88, appRole: .scenicAdmin)
XCTAssertEqual(api.orderListCalls.count, 2)
XCTAssertEqual(api.orderListCalls[0].page, 1)

View File

@ -0,0 +1,42 @@
//
// SaleUserQrParserTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/29.
//
import XCTest
@testable import suixinkan
final class SaleUserQrParserTests: XCTestCase {
///
func testParsePlainNumber() {
XCTAssertEqual(SaleUserQrParser.parseSaleUserId("12345"), 12345)
}
/// JSON
func testParseLegacyJSONWithoutMethod() {
let json = #"{"sale_user_id":88}"#
XCTAssertEqual(SaleUserQrParser.parseSaleUserId(json), 88)
}
/// sale_user_bind
func testParseStandardSaleUserBindQR() {
let json = #"{"method":"sale_user_bind","sale_user_id":2}"#
XCTAssertEqual(SaleUserQrParser.parseSaleUserId(json), 2)
XCTAssertTrue(SaleUserQrParser.isSaleUserBindQR(json))
}
/// URL query sale_user_id
func testParseFromURLQuery() {
let url = "https://example.com/bind?sale_user_id=99"
XCTAssertEqual(SaleUserQrParser.parseSaleUserId(url), 99)
}
/// nil
func testParseInvalidContentReturnsNil() {
XCTAssertNil(SaleUserQrParser.parseSaleUserId("not-a-code"))
XCTAssertNil(SaleUserQrParser.parseSaleUserId(""))
XCTAssertFalse(SaleUserQrParser.isSaleUserBindQR("12345"))
}
}

View File

@ -101,7 +101,9 @@ final class ScenicSelectionViewModelTests: XCTestCase {
currentScenicId: 1,
currentStoreId: 10
)
permissionContext.replaceRolePermissions([RolePermissionResponse(role: RoleInfo(id: 7, name: "运营"), scenic: [])])
permissionContext.replaceRolePermissions([
RolePermissionResponse(role: RoleInfo(id: 7, name: "运营", roleCode: "scenic_operation"), scenic: [])
], currentRoleCode: "scenic_operation")
ScenicSelectionViewModel().select(
scenicId: 2,
@ -114,6 +116,6 @@ final class ScenicSelectionViewModelTests: XCTestCase {
XCTAssertEqual(accountContext.currentStore?.id, 20)
XCTAssertEqual(snapshotStore.load()?.currentScenicId, 2)
XCTAssertEqual(snapshotStore.load()?.currentStoreId, 20)
XCTAssertEqual(snapshotStore.load()?.currentRoleId, 7)
XCTAssertEqual(snapshotStore.load()?.currentRoleCode, "scenic_operation")
}
}

View File

@ -18,7 +18,7 @@ final class StatisticsViewModelTests: XCTestCase {
api.dailyResponses = [try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")]
let viewModel = StatisticsViewModel()
try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
try await viewModel.reload(api: api, scenicId: 88, appRole: nil)
XCTAssertEqual(viewModel.summary.orderCount, 18)
XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23"])
@ -44,11 +44,11 @@ final class StatisticsViewModelTests: XCTestCase {
]
let viewModel = StatisticsViewModel()
try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
try await viewModel.loadMore(api: api, scenicId: 88, roleId: nil)
try await viewModel.reload(api: api, scenicId: 88, appRole: nil)
try await viewModel.loadMore(api: api, scenicId: 88, appRole: nil)
XCTAssertEqual(viewModel.dailyItems.count, 3)
try await viewModel.selectPeriod(.sevenDays, api: api, scenicId: 88, roleId: nil)
try await viewModel.selectPeriod(.sevenDays, api: api, scenicId: 88, appRole: nil)
XCTAssertEqual(viewModel.selectedPeriod, .sevenDays)
XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23"])
@ -70,8 +70,8 @@ final class StatisticsViewModelTests: XCTestCase {
]
let viewModel = StatisticsViewModel()
try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
try await viewModel.loadMore(api: api, scenicId: 88, roleId: nil)
try await viewModel.reload(api: api, scenicId: 88, appRole: nil)
try await viewModel.loadMore(api: api, scenicId: 88, appRole: nil)
XCTAssertEqual(viewModel.dailyItems.map(\.date), ["2026-05-23", "2026-05-22", "2026-05-21"])
XCTAssertFalse(viewModel.hasMore)
@ -86,11 +86,11 @@ final class StatisticsViewModelTests: XCTestCase {
api.dailyResponses = [try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")]
let viewModel = StatisticsViewModel()
try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
try await viewModel.reload(api: api, scenicId: 88, appRole: nil)
api.nextDailyError = APIError.httpStatus(500, "server error")
do {
try await viewModel.loadMore(api: api, scenicId: 88, roleId: nil)
try await viewModel.loadMore(api: api, scenicId: 88, appRole: nil)
XCTFail("Expected load more to fail")
} catch APIError.httpStatus(let statusCode, _) {
XCTAssertEqual(statusCode, 500)
@ -110,10 +110,10 @@ final class StatisticsViewModelTests: XCTestCase {
api.dailyResponses = [try TestFixture.payload(DataListPayload<StatisticsDailyItem>.self, named: "statistics_daily_page1_total3")]
let viewModel = StatisticsViewModel()
try await viewModel.reload(api: api, scenicId: 88, roleId: nil)
try await viewModel.reload(api: api, scenicId: 88, appRole: nil)
XCTAssertEqual(viewModel.summary.orderCount, 18)
try await viewModel.reload(api: api, scenicId: nil, roleId: nil)
try await viewModel.reload(api: api, scenicId: nil, appRole: nil)
XCTAssertEqual(api.summaryCalls.count, 1)
XCTAssertEqual(viewModel.summary.orderCount, 0)