初始化傻瓜比价正式版 iOS 客户端

对标安卓正式版、对接 app-server(8770)的 SwiftUI 客户端。iOS 无障碍受限、无比价核心,做自成一体:美团 CPS 导购 + 福利(签到/任务/金币)+ 查看(比价记录/省钱战绩)。
功能完整 M0-M5:脚手架、短信登录(JWT+Keychain+401 自动刷新)、首页券 feed 导购、比价记录+省钱战绩、福利、我的(资料/反馈/设置/注销)。
单 target、0 三方依赖、XcodeGen 生成工程;Debug/Release 双 plist 分流 ATS。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 00:54:11 +08:00
commit 98b7600797
60 changed files with 3112 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import Foundation
// ===== =====
struct SmsSendRequest: Codable {
let phone: String
}
struct SmsLoginRequest: Codable {
let phone: String
let code: String
}
struct RefreshRequest: Codable {
let refreshToken: String // refresh_token
}
struct ProfileUpdateRequest: Codable {
let nickname: String
}
// ===== =====
struct UserDTO: Codable, Identifiable, Hashable {
let id: Int
let phone: String
var nickname: String?
var avatarUrl: String?
let registerChannel: String
let status: String
let createdAt: String
let lastLoginAt: String
}
struct TokenPairDTO: Codable {
let accessToken: String
let refreshToken: String
let tokenType: String
let expiresIn: Int
let refreshExpiresIn: Int
}
struct TokenWithUserDTO: Codable {
let accessToken: String
let refreshToken: String
let tokenType: String
let expiresIn: Int
let refreshExpiresIn: Int
let user: UserDTO
}
struct SmsSendResponse: Codable {
let sent: Bool
let mock: Bool
let cooldownSec: Int
}
struct LogoutResponse: Codable {
let ok: Bool
}
struct FeedbackResultDTO: Codable {
let id: Int
let status: String
let createdAt: String?
}
+6
View File
@@ -0,0 +1,6 @@
import Foundation
/// {"ok": true}
struct OkResponse: Codable {
let ok: Bool
}
+91
View File
@@ -0,0 +1,91 @@
import Foundation
// () schemas/compare_record.py
// : *_cents (Int); price / couponSaved (Double)
// raw JSON , optional
struct ComparisonResultItemDTO: Codable, Hashable {
let platformId: String?
let platformName: String?
let `package`: String? // ,;Swift ,
let price: Double? // ;/ null
let isSource: Bool?
let rank: Int?
let couponSaved: Double? // ();/ null
}
struct ComparisonItemDTO: Codable, Hashable {
let name: String
var qty: Int = 1
let specs: [String]?
enum CodingKeys: String, CodingKey { case name, qty, specs }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
name = try c.decode(String.self, forKey: .name)
qty = (try? c.decode(Int.self, forKey: .qty)) ?? 1
specs = try? c.decode([String].self, forKey: .specs)
}
}
struct ComparisonRecordDTO: Codable, Identifiable, Hashable {
let id: Int
let businessType: String
let traceId: String
let sourcePlatformId: String?
let sourcePlatformName: String?
let sourcePackage: String?
let sourcePriceCents: Int?
let bestPlatformId: String?
let bestPlatformName: String?
let bestPriceCents: Int?
let savedAmountCents: Int?
let isSourceBest: Bool?
let storeName: String?
let totalDishCount: Int?
let skippedDishCount: Int?
let status: String
let information: String?
var items: [ComparisonItemDTO] = []
var comparisonResults: [ComparisonResultItemDTO] = []
var skippedDishNames: [String] = []
let createdAt: String
enum CodingKeys: String, CodingKey {
case id, businessType, traceId, sourcePlatformId, sourcePlatformName, sourcePackage
case sourcePriceCents, bestPlatformId, bestPlatformName, bestPriceCents, savedAmountCents
case isSourceBest, storeName, totalDishCount, skippedDishCount, status, information
case items, comparisonResults, skippedDishNames, createdAt
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(Int.self, forKey: .id)
businessType = (try? c.decode(String.self, forKey: .businessType)) ?? "food"
traceId = (try? c.decode(String.self, forKey: .traceId)) ?? ""
sourcePlatformId = try? c.decode(String.self, forKey: .sourcePlatformId)
sourcePlatformName = try? c.decode(String.self, forKey: .sourcePlatformName)
sourcePackage = try? c.decode(String.self, forKey: .sourcePackage)
sourcePriceCents = try? c.decode(Int.self, forKey: .sourcePriceCents)
bestPlatformId = try? c.decode(String.self, forKey: .bestPlatformId)
bestPlatformName = try? c.decode(String.self, forKey: .bestPlatformName)
bestPriceCents = try? c.decode(Int.self, forKey: .bestPriceCents)
savedAmountCents = try? c.decode(Int.self, forKey: .savedAmountCents)
isSourceBest = try? c.decode(Bool.self, forKey: .isSourceBest)
storeName = try? c.decode(String.self, forKey: .storeName)
totalDishCount = try? c.decode(Int.self, forKey: .totalDishCount)
skippedDishCount = try? c.decode(Int.self, forKey: .skippedDishCount)
status = (try? c.decode(String.self, forKey: .status)) ?? "unknown"
information = try? c.decode(String.self, forKey: .information)
items = (try? c.decode([ComparisonItemDTO].self, forKey: .items)) ?? []
comparisonResults = (try? c.decode([ComparisonResultItemDTO].self, forKey: .comparisonResults)) ?? []
skippedDishNames = (try? c.decode([String].self, forKey: .skippedDishNames)) ?? []
createdAt = (try? c.decode(String.self, forKey: .createdAt)) ?? ""
}
}
struct ComparisonRecordPageDTO: Codable {
let items: [ComparisonRecordDTO]
let nextCursor: Int?
}
+66
View File
@@ -0,0 +1,66 @@
import Foundation
// CPS (),
struct FeedRequest: Codable {
let longitude: Double
let latitude: Double
var page: Int = 1
var pageSize: Int = 20
}
struct FeedResponse: Codable {
let items: [CouponCard]
let hasNext: Bool
let page: Int
}
struct CouponCard: Codable, Identifiable, Hashable {
var id: String { productViewSign }
let productViewSign: String
let platform: Int
let bizLine: Int?
let name: String
let headImageUrl: String
let brandName: String?
let brandLogoUrl: String?
let sellPrice: String
let originalPrice: String
let discountAmount: String
let commissionRate: String
let commissionAmount: String?
let saleVolume: String?
let poiName: String?
let distanceText: String?
let distanceMeters: Double?
let availablePoiNum: Int?
let couponNum: Int?
let validDays: Int?
let priceLabel: String?
let rankLabel: String?
let ratingLabel: String?
}
struct ReferralLinkRequest: Codable {
let productViewSign: String
var platform: Int = 1
var bizLine: Int?
var linkTypeList: [Int]?
}
struct ReferralLinkResponse: Codable {
let link: String
var linkMap: [String: String] = [:]
enum CodingKeys: String, CodingKey {
case link
case linkMap // convertFromSnakeCase link_map linkMap
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
link = try c.decode(String.self, forKey: .link)
linkMap = (try? c.decode([String: String].self, forKey: .linkMap)) ?? [:]
}
}
+137
View File
@@ -0,0 +1,137 @@
import Foundation
// (///):=,=""""
// / 广 / , Withdraw / Ad / Exchange / Cash DTO
// ===== (: + )=====
struct CoinAccountDTO: Codable {
let coinBalance: Int
let cashBalanceCents: Int
let totalCoinEarned: Int
}
struct CoinTransactionDTO: Codable, Identifiable, Hashable {
let id: Int
let amount: Int
let balanceAfter: Int
let bizType: String
let refId: String?
let remark: String?
let createdAt: String
}
struct CoinTransactionPageDTO: Codable {
let items: [CoinTransactionDTO]
let nextCursor: Int?
}
// ===== =====
struct SigninStepDTO: Codable, Hashable {
let day: Int
let coin: Int
let status: String // claimed / today / locked
}
struct SigninStatusDTO: Codable {
let todaySigned: Bool
let consecutiveDays: Int
let todayCycleDay: Int
let todayCoin: Int
let canClaim: Bool
let steps: [SigninStepDTO]
}
struct SigninResultDTO: Codable {
let coinAwarded: Int
let cycleDay: Int
let streak: Int
let coinBalance: Int
}
// ===== =====
struct TaskDTO: Codable, Identifiable, Hashable {
var id: String { taskKey }
let taskKey: String
let coin: Int
let claimed: Bool
}
struct TaskListDTO: Codable {
let items: [TaskDTO]
}
struct TaskClaimResultDTO: Codable {
let taskKey: String
let coinAwarded: Int
let coinBalance: Int
}
// ===== ()=====
struct MilestoneStateDTO: Codable, Hashable {
let milestone: Int
let coin: Int
let state: String // claimed / active / locked
}
struct MilestoneStatusDTO: Codable {
let successCount: Int
let claimableCount: Int
let milestones: [MilestoneStateDTO]
}
// ===== (//)=====
struct SavingsSummaryDTO: Codable {
let totalSavedCents: Int
let orderCount: Int
let avgSavedCents: Int
}
struct SavingsBattleDTO: Codable {
let weekSavedCents: Int
let beatPercent: Int
let streakDays: Int
}
struct SavingsRecordDTO: Codable, Identifiable, Hashable {
let id: Int
let orderAmountCents: Int
let savedAmountCents: Int
let platform: String?
let title: String?
let shopName: String?
var dishes: [String] = []
let payChannel: String?
let sourcePlatformName: String?
let originalPriceCents: Int?
let createdAt: String
enum CodingKeys: String, CodingKey {
case id, orderAmountCents, savedAmountCents, platform, title
case shopName, dishes, payChannel, sourcePlatformName, originalPriceCents, createdAt
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(Int.self, forKey: .id)
orderAmountCents = try c.decode(Int.self, forKey: .orderAmountCents)
savedAmountCents = try c.decode(Int.self, forKey: .savedAmountCents)
platform = try? c.decode(String.self, forKey: .platform)
title = try? c.decode(String.self, forKey: .title)
shopName = try? c.decode(String.self, forKey: .shopName)
dishes = (try? c.decode([String].self, forKey: .dishes)) ?? []
payChannel = try? c.decode(String.self, forKey: .payChannel)
sourcePlatformName = try? c.decode(String.self, forKey: .sourcePlatformName)
originalPriceCents = try? c.decode(Int.self, forKey: .originalPriceCents)
createdAt = try c.decode(String.self, forKey: .createdAt)
}
}
struct SavingsRecordPageDTO: Codable {
let items: [SavingsRecordDTO]
let nextCursor: Int?
}