207 lines
7.0 KiB
Swift
207 lines
7.0 KiB
Swift
//
|
||
// StatisticsModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/22.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 数据统计时间段实体,表示看板顶部的快捷日期范围。
|
||
enum StatisticsPeriod: String, CaseIterable, Identifiable {
|
||
case today = "今日"
|
||
case yesterday = "昨日"
|
||
case sevenDays = "7日"
|
||
case thisMonth = "本月"
|
||
|
||
var id: String { rawValue }
|
||
|
||
/// 后端汇总接口需要的范围标识。
|
||
var apiRange: String {
|
||
switch self {
|
||
case .today:
|
||
return "1"
|
||
case .yesterday:
|
||
return "2"
|
||
case .sevenDays:
|
||
return "3"
|
||
case .thisMonth:
|
||
return "4"
|
||
}
|
||
}
|
||
|
||
/// 当前时间段对应的本地日期区间。
|
||
var interval: (start: Date, end: Date) {
|
||
let calendar = Calendar.current
|
||
let today = calendar.startOfDay(for: Date())
|
||
switch self {
|
||
case .today:
|
||
return (today, today)
|
||
case .yesterday:
|
||
let day = calendar.date(byAdding: .day, value: -1, to: today) ?? today
|
||
return (day, day)
|
||
case .sevenDays:
|
||
return (calendar.date(byAdding: .day, value: -6, to: today) ?? today, today)
|
||
case .thisMonth:
|
||
let components = calendar.dateComponents([.year, .month], from: today)
|
||
return (calendar.date(from: components) ?? today, today)
|
||
}
|
||
}
|
||
|
||
/// 页面展示的时间范围文案。
|
||
var selectedTimeText: String {
|
||
let dates = interval
|
||
return "\(Self.uiDateFormatter.string(from: dates.start))至\(Self.uiDateFormatter.string(from: dates.end))"
|
||
}
|
||
|
||
/// 后端列表接口开始日期。
|
||
var apiStartTime: String {
|
||
Self.apiDateFormatter.string(from: interval.start)
|
||
}
|
||
|
||
/// 后端列表接口结束日期。
|
||
var apiEndTime: String {
|
||
Self.apiDateFormatter.string(from: interval.end)
|
||
}
|
||
|
||
private static let uiDateFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "zh_CN")
|
||
formatter.dateFormat = "yyyy/MM/dd"
|
||
return formatter
|
||
}()
|
||
|
||
private static let apiDateFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
formatter.dateFormat = "yyyy-MM-dd"
|
||
return formatter
|
||
}()
|
||
}
|
||
|
||
/// 数据统计汇总实体,表示订单金额、数量、客单价、实收和退款。
|
||
struct StatisticsSummaryResponse: Decodable, Equatable {
|
||
let orderAmountSum: String
|
||
let orderCount: Int
|
||
let orderPriceAverage: String
|
||
let receivedAmountSum: String
|
||
let refundAmountSum: String
|
||
|
||
var orderAmountValue: Double { Self.parseAmount(orderAmountSum) }
|
||
var orderPriceAverageValue: Double { Self.parseAmount(orderPriceAverage) }
|
||
var receivedAmountValue: Double { Self.parseAmount(receivedAmountSum) }
|
||
var refundAmountValue: Double { Self.parseAmount(refundAmountSum) }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case orderAmountSum = "order_amount_sum"
|
||
case orderCount = "order_count"
|
||
case orderPriceAverage = "order_price_avg"
|
||
case receivedAmountSum = "received_amount_sum"
|
||
case refundAmountSum = "refund_amount_sum"
|
||
}
|
||
|
||
/// 创建统计汇总实体,主要用于初始状态和测试。
|
||
init(
|
||
orderAmountSum: String = "",
|
||
orderCount: Int = 0,
|
||
orderPriceAverage: String = "",
|
||
receivedAmountSum: String = "",
|
||
refundAmountSum: String = ""
|
||
) {
|
||
self.orderAmountSum = orderAmountSum
|
||
self.orderCount = orderCount
|
||
self.orderPriceAverage = orderPriceAverage
|
||
self.receivedAmountSum = receivedAmountSum
|
||
self.refundAmountSum = refundAmountSum
|
||
}
|
||
|
||
/// 宽松解码统计汇总字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
orderAmountSum = try container.decodeLossyString(forKey: .orderAmountSum)
|
||
orderCount = try container.decodeLossyInt(forKey: .orderCount) ?? 0
|
||
orderPriceAverage = try container.decodeLossyString(forKey: .orderPriceAverage)
|
||
receivedAmountSum = try container.decodeLossyString(forKey: .receivedAmountSum)
|
||
refundAmountSum = try container.decodeLossyString(forKey: .refundAmountSum)
|
||
}
|
||
|
||
private static func parseAmount(_ text: String) -> Double {
|
||
let normalized = text.filter { "0123456789.-".contains($0) }
|
||
return Double(normalized) ?? 0
|
||
}
|
||
}
|
||
|
||
/// 每日统计实体,表示某天订单数、客单价、退款和实收。
|
||
struct StatisticsDailyItem: Decodable, Identifiable, Equatable {
|
||
var id: String { date }
|
||
|
||
let date: String
|
||
let orderCount: Int
|
||
let orderPrice: String
|
||
let refund: String
|
||
let received: String
|
||
|
||
var orderPriceValue: Double { Self.parseAmount(orderPrice) }
|
||
var refundValue: Double { Self.parseAmount(refund) }
|
||
var receivedValue: Double { Self.parseAmount(received) }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case date
|
||
case orderCount = "order_count"
|
||
case orderPrice = "order_price"
|
||
case refund
|
||
case received
|
||
}
|
||
|
||
/// 宽松解码每日统计字段。
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
date = try container.decodeLossyString(forKey: .date)
|
||
orderCount = try container.decodeLossyInt(forKey: .orderCount) ?? 0
|
||
orderPrice = try container.decodeLossyString(forKey: .orderPrice)
|
||
refund = try container.decodeLossyString(forKey: .refund)
|
||
received = try container.decodeLossyString(forKey: .received)
|
||
}
|
||
|
||
private static func parseAmount(_ text: String) -> Double {
|
||
let normalized = text.filter { "0123456789.-".contains($0) }
|
||
return Double(normalized) ?? 0
|
||
}
|
||
}
|
||
|
||
private extension KeyedDecodingContainer {
|
||
/// 将任意常见 JSON 值宽松解码成字符串。
|
||
func decodeLossyString(forKey key: Key) throws -> String {
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
return value
|
||
}
|
||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||
return String(value)
|
||
}
|
||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||
return String(value)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
/// 将 Int、Double 或数字字符串宽松解码成整数。
|
||
func decodeLossyInt(forKey key: Key) throws -> Int? {
|
||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||
return value
|
||
}
|
||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if let intValue = Int(text) {
|
||
return intValue
|
||
}
|
||
if let doubleValue = Double(text) {
|
||
return Int(doubleValue)
|
||
}
|
||
}
|
||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||
return Int(value)
|
||
}
|
||
return nil
|
||
}
|
||
}
|