Implement statistics tab aligned with Android and fix summary decode.
Add role-based analyse APIs, summary/daily UI with date range sheet, and flexible parsing for string refund amounts from the backend. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
210
suixinkanTests/StatisticsViewModelTests.swift
Normal file
210
suixinkanTests/StatisticsViewModelTests.swift
Normal file
@ -0,0 +1,210 @@
|
||||
//
|
||||
// StatisticsViewModelTests.swift
|
||||
// suixinkanTests
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
@MainActor
|
||||
/// 数据统计 ViewModel 测试。
|
||||
final class StatisticsViewModelTests: XCTestCase {
|
||||
|
||||
override func tearDown() {
|
||||
AppStore.shared.roleCode = ""
|
||||
AppStore.shared.roleName = ""
|
||||
AppStore.shared.currentScenicId = 0
|
||||
AppStore.shared.currentStoreId = 0
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testStatisticsSummaryDecodesRefundAmountAsString() throws {
|
||||
let json = """
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"order_amount_sum": "0.00",
|
||||
"order_count": 0,
|
||||
"order_price_avg": "0.00",
|
||||
"received_amount_sum": "0.00",
|
||||
"refund_amount_sum": "0.00"
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let envelope = try JSONDecoder().decode(APIEnvelope<StatisticsSummaryResponse>.self, from: json)
|
||||
XCTAssertEqual(envelope.data?.refundAmountSum, "0.00")
|
||||
XCTAssertEqual(envelope.data?.orderAmountSum, "0.00")
|
||||
}
|
||||
|
||||
func testRangeValueMapping() {
|
||||
let viewModel = StatisticsViewModel()
|
||||
XCTAssertEqual(viewModel.rangeValue(for: "今日"), "1")
|
||||
XCTAssertEqual(viewModel.rangeValue(for: "昨日"), "2")
|
||||
XCTAssertEqual(viewModel.rangeValue(for: "7日"), "3")
|
||||
XCTAssertEqual(viewModel.rangeValue(for: "本月"), "4")
|
||||
}
|
||||
|
||||
func testFormattedSelectedPeriodTimeForToday() {
|
||||
let viewModel = StatisticsViewModel()
|
||||
let text = viewModel.formattedSelectedPeriodTime(for: "今日")
|
||||
let today = formattedToday()
|
||||
XCTAssertEqual(text, "\(today)至\(today)")
|
||||
}
|
||||
|
||||
func testFormattedSelectedPeriodTimeForThisMonth() {
|
||||
let viewModel = StatisticsViewModel()
|
||||
let text = viewModel.formattedSelectedPeriodTime(for: "本月")
|
||||
XCTAssertTrue(text.contains("至"))
|
||||
XCTAssertTrue(text.hasPrefix(firstDayOfMonth()))
|
||||
}
|
||||
|
||||
func testUpdateRangeByShortcutAllClearsTimes() async {
|
||||
let summaryJSON = """
|
||||
{"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}}
|
||||
""".data(using: .utf8)!
|
||||
let listJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
AppStore.shared.currentScenicId = 10
|
||||
let session = MockURLSession(responses: [listJSON])
|
||||
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = StatisticsViewModel()
|
||||
|
||||
await viewModel.updateRangeByShortcut("全部", api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.statisticsRangeText, "全部")
|
||||
XCTAssertEqual(viewModel.listStartTime, "")
|
||||
XCTAssertEqual(viewModel.listEndTime, "")
|
||||
XCTAssertEqual(session.requests.count, 1)
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/yf-handset-app/photog/analyse/user/daily")
|
||||
let queryItems = URLComponents(url: session.requests[0].url!, resolvingAgainstBaseURL: false)?.queryItems ?? []
|
||||
XCTAssertEqual(queryItems.first(where: { $0.name == "start_time" })?.value, "")
|
||||
XCTAssertEqual(queryItems.first(where: { $0.name == "end_time" })?.value, "")
|
||||
}
|
||||
|
||||
func testPhotographerSummaryAndDailyRequests() async throws {
|
||||
let summaryJSON = """
|
||||
{"code":100000,"msg":"success","data":{"order_amount_sum":"100","order_count":2,"order_price_avg":"50","received_amount_sum":"90","refund_amount_sum":10}}
|
||||
""".data(using: .utf8)!
|
||||
let listJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":2,"order_price":"50","refund":"10","received":"90"}]}}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
AppStore.shared.currentScenicId = 10
|
||||
let session = MockURLSession(responses: [summaryJSON, listJSON])
|
||||
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = StatisticsViewModel()
|
||||
|
||||
await viewModel.loadStatistics("7日", api: api)
|
||||
await viewModel.updateRangeByShortcut("今日", api: api)
|
||||
|
||||
XCTAssertEqual(viewModel.statistics?.orderAmountSum, "100")
|
||||
XCTAssertEqual(viewModel.statisticsList.count, 1)
|
||||
XCTAssertEqual(session.requests.map { $0.url?.path }, [
|
||||
"/api/yf-handset-app/photog/analyse/user",
|
||||
"/api/yf-handset-app/photog/analyse/user/daily",
|
||||
])
|
||||
XCTAssertTrue(session.requests[0].url?.query?.contains("range=3") == true)
|
||||
XCTAssertTrue(session.requests[1].url?.query?.contains("paeg_size=15") == true)
|
||||
XCTAssertTrue(session.requests[1].url?.query?.contains("scenic_id=10") == true)
|
||||
}
|
||||
|
||||
func testScenicAdminUsesScenicAdminEndpoints() async {
|
||||
let summaryJSON = """
|
||||
{"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}}
|
||||
""".data(using: .utf8)!
|
||||
let listJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
AppStore.shared.roleCode = AppRoleCode.scenicAdmin.rawValue
|
||||
AppStore.shared.currentScenicId = 20
|
||||
let session = MockURLSession(responses: [summaryJSON, listJSON])
|
||||
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = StatisticsViewModel()
|
||||
|
||||
await viewModel.loadStatistics("今日", api: api)
|
||||
await viewModel.updateRangeByShortcut("今日", api: api)
|
||||
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/scenic-admin/analyse")
|
||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/scenic-admin/analyse/daily")
|
||||
}
|
||||
|
||||
func testStoreAdminUsesStoreEndpoints() async {
|
||||
let summaryJSON = """
|
||||
{"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}}
|
||||
""".data(using: .utf8)!
|
||||
let listJSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":0,"data":[]}}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
AppStore.shared.roleCode = AppRoleCode.storeAdmin.rawValue
|
||||
AppStore.shared.currentStoreId = 30
|
||||
let session = MockURLSession(responses: [summaryJSON, listJSON])
|
||||
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = StatisticsViewModel()
|
||||
|
||||
await viewModel.loadStatistics("今日", api: api)
|
||||
await viewModel.updateRangeByShortcut("今日", api: api)
|
||||
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/store/analyse")
|
||||
XCTAssertTrue(session.requests[0].url?.query?.contains("store_id=30") == true)
|
||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/store/analyse/daily")
|
||||
}
|
||||
|
||||
func testStoreAdminWithoutStoreIdDoesNotRequest() async {
|
||||
AppStore.shared.roleCode = AppRoleCode.storeAdmin.rawValue
|
||||
AppStore.shared.currentStoreId = 0
|
||||
let session = MockURLSession(responses: [])
|
||||
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = StatisticsViewModel()
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.loadStatistics("今日", api: api)
|
||||
|
||||
XCTAssertEqual(message, "请先选择店铺")
|
||||
XCTAssertEqual(session.requests.count, 0)
|
||||
}
|
||||
|
||||
func testLoadMoreStopsWhenReachedTotal() async {
|
||||
let page1JSON = """
|
||||
{"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":1,"order_price":"10","refund":"0","received":"10"}]}}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
AppStore.shared.currentScenicId = 10
|
||||
let session = MockURLSession(responses: [page1JSON])
|
||||
let api = StatisticsAPI(client: APIClient(environment: .testing, session: session))
|
||||
let viewModel = StatisticsViewModel()
|
||||
|
||||
await viewModel.updateRangeByShortcut("今日", api: api)
|
||||
XCTAssertEqual(viewModel.statisticsList.count, 1)
|
||||
XCTAssertFalse(viewModel.canLoadMore)
|
||||
|
||||
await viewModel.loadMore(api: api)
|
||||
XCTAssertEqual(viewModel.listCurrentPage, 1)
|
||||
XCTAssertEqual(session.requests.count, 1)
|
||||
}
|
||||
|
||||
private func formattedToday() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "zh_CN")
|
||||
formatter.dateFormat = "yyyy/MM/dd"
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
private func firstDayOfMonth() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.calendar = Calendar(identifier: .gregorian)
|
||||
formatter.locale = Locale(identifier: "zh_CN")
|
||||
formatter.dateFormat = "yyyy/MM/dd"
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
let components = calendar.dateComponents([.year, .month], from: Date())
|
||||
let firstDay = calendar.date(from: components) ?? Date()
|
||||
return formatter.string(from: firstDay)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user