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

@ -23,7 +23,8 @@ struct AccountContextLoader {
permissionContext: PermissionContext,
profileAPI: UserProfileServing,
accountContextAPI: AccountContextServing,
cachedCurrentRoleId: Int? = nil,
cachedCurrentRoleCode: String? = nil,
cachedLegacyRoleId: Int? = nil,
cachedCurrentScenicId: Int? = nil,
cachedCurrentStoreId: Int? = nil
) async throws {
@ -31,7 +32,11 @@ struct AccountContextLoader {
async let roleData = accountContextAPI.rolePermissions()
let (userInfo, rolePermissions) = try await (userData, roleData)
permissionContext.replaceRolePermissions(rolePermissions, currentRoleId: cachedCurrentRoleId)
permissionContext.replaceRolePermissions(
rolePermissions,
currentRoleCode: cachedCurrentRoleCode,
cachedLegacyRoleId: cachedLegacyRoleId
)
let scenicResponse = await loadScenicList(accountContextAPI: accountContextAPI, rolePermissions: rolePermissions)
let storesResponse = await loadStores(accountContextAPI: accountContextAPI)

View File

@ -73,6 +73,7 @@ final class AuthSessionCoordinator {
permissionContext: permissionContext,
profileAPI: profileAPI,
accountContextAPI: accountContextAPI,
cachedCurrentRoleCode: response.currentAccountRoleCode,
cachedCurrentScenicId: accountContext.currentScenic?.id,
cachedCurrentStoreId: accountContext.currentStore?.id
)
@ -114,7 +115,7 @@ final class AuthSessionCoordinator {
profile: profile,
accountType: snapshotStore.load()?.accountType,
businessUserId: snapshotStore.load()?.businessUserId,
currentRoleId: snapshotStore.load()?.currentRoleId,
currentRoleCode: snapshotStore.load()?.currentRoleCode,
accountContext: accountContext
)
}
@ -144,7 +145,7 @@ final class AuthSessionCoordinator {
accountType: String?,
businessUserId: Int?,
permissionContext: PermissionContext? = nil,
currentRoleId: Int? = nil,
currentRoleCode: String? = nil,
accountContext: AccountContext
) {
snapshotStore.save(
@ -152,7 +153,7 @@ final class AuthSessionCoordinator {
profile: profile,
accountType: accountType,
businessUserId: businessUserId,
currentRoleId: permissionContext?.currentRole?.id ?? currentRoleId,
currentRoleCode: permissionContext?.currentAppRole?.rawValue ?? currentRoleCode,
scenicScopes: accountContext.scenicScopes,
storeScopes: accountContext.storeScopes,
currentScenicId: accountContext.currentScenic?.id,

View File

@ -15,20 +15,94 @@ final class PermissionContext: ObservableObject {
@Published private(set) var permissionURIs: Set<String> = []
@Published var currentRole: RoleInfo?
/// ID
func replaceRolePermissions(_ rolePermissions: [RolePermissionResponse], currentRoleId: Int? = nil) {
/// App role_code legacy id
var currentAppRole: AppRoleCode? {
if let code = AppRoleCode.fromCode(currentRole?.roleCode) { return code }
if let roleId = currentRole?.id, let code = AppRoleCode.fromLegacyRoleId(roleId) { return code }
return AppRoleCode.fromRoleName(currentRole?.name)
}
/// role_code
func replaceRolePermissions(
_ rolePermissions: [RolePermissionResponse],
currentRoleCode: String? = nil,
cachedLegacyRoleId: Int? = nil,
roleName: String? = nil
) {
self.rolePermissions = rolePermissions
currentRole = currentRoleId.flatMap { id in
rolePermissions.first { $0.role.id == id }?.role
} ?? currentRole.flatMap { role in
rolePermissions.first { $0.role.id == role.id }?.role
} ?? rolePermissions.first?.role
let resolvedCode = RolePermissionMatching.resolveRoleCode(
roleCode: currentRoleCode,
roleId: cachedLegacyRoleId,
roleName: roleName,
permissions: rolePermissions
)?.rawValue ?? currentRoleCode
if let matched = RolePermissionMatching.match(
in: rolePermissions,
roleCode: resolvedCode,
roleId: cachedLegacyRoleId,
roleName: roleName ?? currentRole?.name
) {
currentRole = matched.role
} else if let currentRole,
let matched = rolePermissions.first(where: { $0.role.id == currentRole.id }) {
self.currentRole = matched.role
} else {
currentRole = rolePermissions.first?.role
}
permissionURIs = Set(Self.flattenPermissions(currentRole?.permission ?? []))
}
///
func selectRole(code: AppRoleCode, accountContext: AccountContext) {
guard let rolePermission = rolePermissions.first(where: {
AppRoleCode.fromCode($0.role.roleCode) == code
}) else { return }
applyRoleSelection(rolePermission, accountContext: accountContext)
}
/// legacy id使 API role id
func selectRole(id roleId: Int, accountContext: AccountContext) {
guard let rolePermission = rolePermissions.first(where: { $0.role.id == roleId }) else { return }
applyRoleSelection(rolePermission, accountContext: accountContext)
}
/// URI
func canAccess(_ uri: String) -> Bool {
permissionURIs.contains(uri.trimmingCharacters(in: .whitespacesAndNewlines))
}
/// URI API
func topLevelPermissionURIs(for roleCode: String?) -> [String] {
guard let roleCode,
let rolePermission = matchedRolePermission(roleCode: roleCode) else {
return []
}
return rolePermission.role.permission.compactMap { item in
let uri = item.uri.trimmingCharacters(in: .whitespacesAndNewlines)
return uri.isEmpty ? nil : uri
}
}
///
func currentRoleScenicScopes() -> [BusinessScope] {
guard let currentRole else { return [] }
guard let rolePermission = matchedRolePermission(roleCode: currentRole.roleCode)
?? rolePermissions.first(where: { $0.role.id == currentRole.id }) else {
return []
}
return Self.uniqueScenicScopes(from: rolePermission.scenic)
}
/// 退
func reset() {
rolePermissions = []
permissionURIs = []
currentRole = nil
}
///
private func applyRoleSelection(_ rolePermission: RolePermissionResponse, accountContext: AccountContext) {
currentRole = rolePermission.role
permissionURIs = Set(Self.flattenPermissions(rolePermission.role.permission))
@ -41,25 +115,14 @@ final class PermissionContext: ObservableObject {
)
}
/// URI
func canAccess(_ uri: String) -> Bool {
permissionURIs.contains(uri.trimmingCharacters(in: .whitespacesAndNewlines))
}
///
func currentRoleScenicScopes() -> [BusinessScope] {
guard let currentRole else { return [] }
guard let rolePermission = rolePermissions.first(where: { $0.role.id == currentRole.id }) else {
return []
}
return Self.uniqueScenicScopes(from: rolePermission.scenic)
}
/// 退
func reset() {
rolePermissions = []
permissionURIs = []
currentRole = nil
/// role_code
private func matchedRolePermission(roleCode: String) -> RolePermissionResponse? {
RolePermissionMatching.match(
in: rolePermissions,
roleCode: roleCode,
roleId: nil,
roleName: nil
)
}
/// URI

View File

@ -58,7 +58,8 @@ final class SessionBootstrapper {
permissionContext: permissionContext,
profileAPI: profileAPI,
accountContextAPI: accountContextAPI,
cachedCurrentRoleId: cachedSnapshot?.currentRoleId,
cachedCurrentRoleCode: cachedSnapshot?.currentRoleCode,
cachedLegacyRoleId: cachedSnapshot?.currentRoleId,
cachedCurrentScenicId: cachedSnapshot?.currentScenicId,
cachedCurrentStoreId: cachedSnapshot?.currentStoreId
)
@ -100,7 +101,7 @@ final class SessionBootstrapper {
profile: accountContext.profile,
accountType: existing?.accountType,
businessUserId: existing?.businessUserId,
currentRoleId: permissionContext.currentRole?.id ?? existing?.currentRoleId,
currentRoleCode: permissionContext.currentAppRole?.rawValue ?? existing?.currentRoleCode,
scenicScopes: accountContext.scenicScopes,
storeScopes: accountContext.storeScopes,
currentScenicId: accountContext.currentScenic?.id,