Introduce real payment collection and wallet screens to replace home menu placeholders, wire APIs through RootView, and support bank card OSS uploads plus QR code saving to the photo library. Co-authored-by: Cursor <cursoragent@cursor.com>
414 lines
16 KiB
Swift
414 lines
16 KiB
Swift
//
|
||
// HomeMenuRouter.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/22.
|
||
//
|
||
|
||
import Foundation
|
||
import os
|
||
|
||
/// 首页菜单路由器,将后端权限 URI 映射为 iOS 内部路由。
|
||
enum HomeMenuRouter {
|
||
private static let titleMap: [String: String] = [
|
||
"basic_info": "基本信息",
|
||
"space_settings": "空间设置",
|
||
"album_list": "相册管理",
|
||
"album_trailer": "相册预览上传",
|
||
"photographer_stats": "数据统计",
|
||
"photographer_orders": "订单管理",
|
||
"/scenic-order-manage": "景区订单",
|
||
"verification_order": "核销订单",
|
||
"wallet": "我的钱包",
|
||
"cloud_management": "相册云盘",
|
||
"cloud_storage_transit": "传输管理",
|
||
"asset_management": "素材管理",
|
||
"material_upload": "上传素材",
|
||
"task_management": "任务管理",
|
||
"task_management_editor": "任务管理",
|
||
"task_create": "发布任务",
|
||
"schedule_management": "日程管理",
|
||
"system_settings": "设置中心",
|
||
"message_center": "消息中心",
|
||
"checkin_points": "打卡点管理",
|
||
"sample_management": "样片管理",
|
||
"sample_upload": "上传样片",
|
||
"live_stream_management": "直播管理",
|
||
"live_album": "直播相册",
|
||
"scenicselection": "景区选择",
|
||
"scenicapplication": "景区申请",
|
||
"permission_apply": "权限申请",
|
||
"permission_apply_status": "权限申请状态",
|
||
"scenic_settlement": "景区结算",
|
||
"scenic_settlement_review": "结算审核",
|
||
"pm": "项目管理",
|
||
"pm_manager": "项目管理",
|
||
"project_edit": "项目编辑",
|
||
"location_report": "位置上报",
|
||
"registration_invitation": "注册邀请",
|
||
"store": "店铺管理",
|
||
"fly": "飞行管理",
|
||
"pilot_cert": "飞手认证",
|
||
"pilot_controller": "飞控",
|
||
"/scenic-queue": "排队管理",
|
||
"queue_management": "排队管理",
|
||
"operating-area": "运营区域",
|
||
"payment_collection": "立即收款",
|
||
"payment_qr": "收款码",
|
||
"payment_code": "收款码",
|
||
"deposit_order_detail": "押金订单详情",
|
||
"deposit_order": "押金订单详情",
|
||
"deposit_order_shooting_info": "押金拍摄信息",
|
||
"withdrawal_audit": "提现审核",
|
||
"location_report_history": "定位上报历史",
|
||
"photographer_invite": "邀请摄影师",
|
||
"invite_record": "邀请记录",
|
||
"more_functions": "更多功能"
|
||
]
|
||
|
||
/// 根据权限 URI 解析跳转目标。
|
||
static func resolve(uri: String, title: String) -> HomeMenuResolvedRoute {
|
||
switch uri {
|
||
case "photographer_orders", "/scenic-order-manage":
|
||
return .orders(.storeOrders)
|
||
case "verification_order":
|
||
return .orders(.verificationOrders)
|
||
case "photographer_stats":
|
||
return .tab(.statistics)
|
||
case "space_settings", "basic_info":
|
||
return .destination(.profileSpace)
|
||
case "system_settings":
|
||
return .destination(.settings)
|
||
case "scenicselection":
|
||
return .destination(.scenicSelection)
|
||
case "wallet":
|
||
return .destination(.wallet)
|
||
case "payment_collection", "payment_qr", "payment_code":
|
||
return .destination(.paymentCollection)
|
||
case "store", "more_functions":
|
||
return .destination(.moreFunctions)
|
||
case "fly", "pilot_controller":
|
||
return .unsupported(
|
||
uri: uri,
|
||
title: title.isEmpty ? self.title(for: uri) : title,
|
||
reason: "DJI/飞控模块已明确不纳入 iOS 迁移和后续开发范围。"
|
||
)
|
||
case "message_center",
|
||
"/scenic-queue",
|
||
"queue_management",
|
||
"deposit_order_detail",
|
||
"deposit_order",
|
||
"deposit_order_shooting_info",
|
||
"withdrawal_audit",
|
||
"task_management",
|
||
"task_management_editor",
|
||
"task_create",
|
||
"schedule_management",
|
||
"checkin_points",
|
||
"pm",
|
||
"pm_manager",
|
||
"project_edit",
|
||
"cloud_management",
|
||
"cloud_storage_transit",
|
||
"album_list",
|
||
"album_trailer",
|
||
"asset_management",
|
||
"material_upload",
|
||
"sample_management",
|
||
"sample_upload",
|
||
"live_stream_management",
|
||
"live_album",
|
||
"scenicapplication",
|
||
"permission_apply",
|
||
"permission_apply_status",
|
||
"scenic_settlement",
|
||
"scenic_settlement_review",
|
||
"operating-area",
|
||
"location_report_history",
|
||
"location_report",
|
||
"registration_invitation",
|
||
"photographer_invite",
|
||
"invite_record",
|
||
"pilot_cert":
|
||
let resolvedTitle = title.isEmpty ? self.title(for: uri) : title
|
||
return .destination(.modulePlaceholder(uri: uri, title: resolvedTitle))
|
||
default:
|
||
return .placeholder(uri: uri, title: title.isEmpty ? self.title(for: uri) : title)
|
||
}
|
||
}
|
||
|
||
/// 返回指定 URI 的默认标题。
|
||
static func title(for uri: String) -> String {
|
||
titleMap[uri] ?? uri
|
||
}
|
||
|
||
/// 在可用 URI 集合中选取规范形式。
|
||
static func canonicalURI(for uri: String, availableURIs: Set<String>) -> String {
|
||
if availableURIs.contains(uri) {
|
||
return uri
|
||
}
|
||
switch uri {
|
||
case "task_management":
|
||
return availableURIs.contains("task_management_editor") ? "task_management_editor" : uri
|
||
case "task_management_editor":
|
||
return availableURIs.contains("task_management") ? "task_management" : uri
|
||
case "registration_invitation":
|
||
return availableURIs.contains("photographer_invite") ? "photographer_invite" : uri
|
||
case "photographer_invite":
|
||
return availableURIs.contains("registration_invitation") ? "registration_invitation" : uri
|
||
case "pm":
|
||
return availableURIs.contains("pm_manager") ? "pm_manager" : uri
|
||
case "pm_manager":
|
||
return availableURIs.contains("pm") ? "pm" : uri
|
||
case "payment_code":
|
||
return availableURIs.contains("payment_qr") ? "payment_qr" : uri
|
||
default:
|
||
return uri
|
||
}
|
||
}
|
||
|
||
/// 返回同义 URI 的去重键。
|
||
static func menuAliasKey(for uri: String) -> String {
|
||
switch uri {
|
||
case "task_management", "task_management_editor":
|
||
return "task_management"
|
||
case "registration_invitation", "photographer_invite":
|
||
return "registration_invitation"
|
||
case "pm", "pm_manager", "project_edit":
|
||
return "pm"
|
||
case "payment_collection", "payment_qr", "payment_code":
|
||
return "payment_collection"
|
||
case "deposit_order_detail", "deposit_order", "deposit_order_shooting_info":
|
||
return "deposit_order"
|
||
case "photographer_orders", "/scenic-order-manage":
|
||
return "photographer_orders"
|
||
case "/scenic-queue", "queue_management":
|
||
return "queue_management"
|
||
default:
|
||
return uri
|
||
}
|
||
}
|
||
|
||
/// 返回首页展示标题,部分同义入口统一文案。
|
||
static func displayTitle(for uri: String, fallback: String) -> String {
|
||
switch uri {
|
||
case "registration_invitation", "photographer_invite":
|
||
return "注册邀请"
|
||
case "location_report":
|
||
return "位置上报"
|
||
case "pm", "pm_manager":
|
||
return "项目管理"
|
||
case "space_settings":
|
||
return "空间设置"
|
||
case "task_management", "task_management_editor":
|
||
return "任务管理"
|
||
default:
|
||
return fallback
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 首页权限路由审计器,用于识别权限里哪些 URI 尚未映射。
|
||
enum HomePermissionRouteAuditor {
|
||
/// 扫描当前角色权限并分类统计可路由、已知不支持和未知 URI。
|
||
static func audit(permissions: [RolePermissionResponse], currentRoleId: Int?) -> HomePermissionRouteAudit {
|
||
let source: RolePermissionResponse?
|
||
if let currentRoleId {
|
||
source = permissions.first { $0.role.id == currentRoleId }
|
||
} else {
|
||
source = permissions.first
|
||
}
|
||
|
||
let entries = uniquePermissionItems(from: source?.role.permission ?? []).map { item in
|
||
let title = item.name.isEmpty ? HomeMenuRouter.title(for: item.uri) : item.name
|
||
return HomePermissionRouteAuditEntry(
|
||
uri: item.uri,
|
||
title: title,
|
||
route: HomeMenuRouter.resolve(uri: item.uri, title: title)
|
||
)
|
||
}
|
||
|
||
return HomePermissionRouteAudit(
|
||
routable: entries.filter { entry in
|
||
switch entry.route {
|
||
case .tab, .orders, .destination:
|
||
return true
|
||
case .unsupported, .placeholder:
|
||
return false
|
||
}
|
||
},
|
||
unsupported: entries.filter { entry in
|
||
if case .unsupported = entry.route { return true }
|
||
return false
|
||
},
|
||
unknown: entries.filter { entry in
|
||
if case .placeholder = entry.route { return true }
|
||
return false
|
||
}
|
||
)
|
||
}
|
||
|
||
/// 生成 Markdown 审计报告。
|
||
static func markdownReport(audit: HomePermissionRouteAudit, generatedAt: Date = Date()) -> String {
|
||
var lines = [
|
||
"# Home Permission Route Audit",
|
||
"",
|
||
"Generated: \(reportDateFormatter.string(from: generatedAt))",
|
||
"",
|
||
"Routable: \(audit.routable.count)",
|
||
"Unsupported: \(audit.unsupported.count)",
|
||
"Unknown: \(audit.unknown.count)",
|
||
""
|
||
]
|
||
|
||
if audit.unknown.isEmpty {
|
||
lines.append("No unknown permission routes.")
|
||
} else {
|
||
lines.append("## Unknown Routes")
|
||
lines.append("")
|
||
lines.append("| URI | Title |")
|
||
lines.append("| --- | --- |")
|
||
lines.append(contentsOf: audit.unknown.map { "| \(escape($0.uri)) | \(escape($0.title)) |" })
|
||
lines.append("")
|
||
lines.append("Action: map these URIs in `HomeMenuRouter`, confirm unsupported scope, or remove them before release.")
|
||
}
|
||
|
||
if !audit.unsupported.isEmpty {
|
||
lines.append("")
|
||
lines.append("## Known Unsupported Routes")
|
||
lines.append("")
|
||
lines.append("| URI | Title | Reason |")
|
||
lines.append("| --- | --- | --- |")
|
||
lines.append(contentsOf: audit.unsupported.map { entry in
|
||
let reason: String
|
||
if case let .unsupported(_, _, value) = entry.route {
|
||
reason = value
|
||
} else {
|
||
reason = ""
|
||
}
|
||
return "| \(escape(entry.uri)) | \(escape(entry.title)) | \(escape(reason)) |"
|
||
})
|
||
}
|
||
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
private static var reportDateFormatter: ISO8601DateFormatter {
|
||
let formatter = ISO8601DateFormatter()
|
||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||
return formatter
|
||
}
|
||
|
||
/// 从权限树提取非空 URI,并按同义 URI 去重。
|
||
private static func uniquePermissionItems(from items: [PermissionItem]) -> [PermissionItem] {
|
||
var seen = Set<String>()
|
||
return flatten(items).filter { item in
|
||
guard !item.uri.isEmpty else { return false }
|
||
return seen.insert(HomeMenuRouter.menuAliasKey(for: item.uri)).inserted
|
||
}
|
||
}
|
||
|
||
/// 递归展开权限树。
|
||
private static func flatten(_ items: [PermissionItem]) -> [PermissionItem] {
|
||
items.flatMap { item in
|
||
[item] + flatten(item.children)
|
||
}
|
||
}
|
||
|
||
/// 转义 Markdown 表格中的特殊字符。
|
||
private static func escape(_ value: String) -> String {
|
||
value
|
||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||
.replacingOccurrences(of: "|", with: "\\|")
|
||
.replacingOccurrences(of: "\n", with: " ")
|
||
}
|
||
}
|
||
|
||
/// 首页未知路由诊断工具,记录运行时无法识别的权限 URI。
|
||
enum HomeRouteDiagnostics {
|
||
private static let logger = Logger(subsystem: "com.yuanzhixiang.suixinkan", category: "HomeRouting")
|
||
private static let defaultsKey = "home.routing.unknown.records"
|
||
private static let dateFormatter: ISO8601DateFormatter = {
|
||
let formatter = ISO8601DateFormatter()
|
||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||
return formatter
|
||
}()
|
||
|
||
/// 记录一个未知首页 URI。
|
||
static func recordUnknown(uri: String, title: String) {
|
||
logger.warning("Unknown home menu uri: \(uri, privacy: .public), title: \(title, privacy: .public)")
|
||
var records = unknownRoutes()
|
||
let now = Date()
|
||
if let index = records.firstIndex(where: { $0.uri == uri }) {
|
||
let current = records[index]
|
||
records[index] = UnknownHomeRouteRecord(
|
||
uri: current.uri,
|
||
title: title.isEmpty ? current.title : title,
|
||
firstSeenAt: current.firstSeenAt,
|
||
lastSeenAt: now,
|
||
count: current.count + 1
|
||
)
|
||
} else {
|
||
records.append(UnknownHomeRouteRecord(uri: uri, title: title, firstSeenAt: now, lastSeenAt: now, count: 1))
|
||
}
|
||
save(records)
|
||
}
|
||
|
||
/// 读取所有未知首页路由记录。
|
||
static func unknownRoutes() -> [UnknownHomeRouteRecord] {
|
||
guard let data = UserDefaults.standard.data(forKey: defaultsKey),
|
||
let records = try? JSONDecoder().decode([UnknownHomeRouteRecord].self, from: data)
|
||
else { return [] }
|
||
return records
|
||
}
|
||
|
||
/// 生成未知首页路由 Markdown 报告。
|
||
static func unknownRouteReport(generatedAt: Date = Date()) -> String {
|
||
let records = unknownRoutes().sorted { lhs, rhs in
|
||
if lhs.count != rhs.count {
|
||
return lhs.count > rhs.count
|
||
}
|
||
return lhs.lastSeenAt > rhs.lastSeenAt
|
||
}
|
||
var lines = [
|
||
"# Home Route Diagnostics",
|
||
"",
|
||
"Generated: \(dateFormatter.string(from: generatedAt))",
|
||
""
|
||
]
|
||
|
||
guard !records.isEmpty else {
|
||
lines.append("No unknown home routes recorded.")
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
lines.append("| URI | Title | Count | First Seen | Last Seen |")
|
||
lines.append("| --- | --- | ---: | --- | --- |")
|
||
lines.append(contentsOf: records.map { record in
|
||
"| \(escape(record.uri)) | \(escape(record.title)) | \(record.count) | \(dateFormatter.string(from: record.firstSeenAt)) | \(dateFormatter.string(from: record.lastSeenAt)) |"
|
||
})
|
||
lines.append("")
|
||
lines.append("Action: each URI above must be mapped in `HomeMenuRouter`, confirmed as unsupported scope, or explicitly accepted before release.")
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
/// 清空未知首页路由记录。
|
||
static func resetUnknownRoutes() {
|
||
UserDefaults.standard.removeObject(forKey: defaultsKey)
|
||
}
|
||
|
||
/// 保存未知首页路由记录。
|
||
private static func save(_ records: [UnknownHomeRouteRecord]) {
|
||
guard let data = try? JSONEncoder().encode(records) else { return }
|
||
UserDefaults.standard.set(data, forKey: defaultsKey)
|
||
}
|
||
|
||
/// 转义 Markdown 表格中的特殊字符。
|
||
private static func escape(_ value: String) -> String {
|
||
value
|
||
.replacingOccurrences(of: "\\", with: "\\\\")
|
||
.replacingOccurrences(of: "|", with: "\\|")
|
||
.replacingOccurrences(of: "\n", with: " ")
|
||
}
|
||
}
|