初始化傻瓜比价正式版 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:
@@ -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?
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
/// 通用响应。后端不少接口返 {"ok": true}。
|
||||
struct OkResponse: Codable {
|
||||
let ok: Bool
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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)) ?? [:]
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
Reference in New Issue
Block a user