完善实名认证、钱包与打卡点模块 UI 与业务逻辑。

对齐 Android 实名认证流程与审核页、钱包提现/积分兑换界面,优化打卡点列表与详情交互,并补充相关资源、主题 token 与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 16:28:10 +08:00
parent 07b2b9d459
commit 5138c1c11a
63 changed files with 3101 additions and 745 deletions

View File

@ -39,8 +39,10 @@ final class PunchPointViewModelTests: XCTestCase {
let viewModel = PunchPointListViewModel(currentScenicIdProvider: { 9 })
var messages: [String] = []
var qrcode: String?
var removalID: Int64?
viewModel.onShowMessage = { messages.append($0) }
viewModel.onShowQRCode = { qrcode = $0 }
viewModel.onBeginDeleteAnimation = { removalID = $0 }
await viewModel.loadInitial(api: api)
viewModel.showQRCode(for: viewModel.items[1])
@ -50,6 +52,9 @@ final class PunchPointViewModelTests: XCTestCase {
XCTAssertEqual(messages, ["未审核通过不可查看二维码", "暂无二维码"])
XCTAssertEqual(qrcode, "https://cdn/qr.png")
XCTAssertEqual(removalID, 1)
XCTAssertEqual(viewModel.items.map(\.id), [1, 2])
viewModel.completeDeleteAnimation(id: 1)
XCTAssertEqual(viewModel.items.map(\.id), [2])
XCTAssertEqual(api.deletedIds, [1])
}
@ -140,6 +145,59 @@ final class PunchPointViewModelTests: XCTestCase {
XCTAssertTrue(api.addRequests.isEmpty)
}
func testImageLimitAcceptsNineAndRejectsFurtherSelection() async {
let uploader = MockPunchPointUploader()
let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 })
var messages: [String] = []
viewModel.onShowMessage = { messages.append($0) }
let images = (0 ..< 10).map {
PunchPointImageState(data: Data([UInt8($0)]), fileName: "\($0).jpg")
}
await viewModel.addLocalImages(images, uploader: uploader)
await viewModel.addLocalImages(
[PunchPointImageState(data: Data([99]), fileName: "extra.jpg")],
uploader: uploader
)
XCTAssertEqual(viewModel.images.count, 9)
XCTAssertEqual(uploader.uploads.count, 9)
XCTAssertEqual(messages.last, "最多只能选择9张图片")
}
func testFailedImageCanRetryAndBecomeUploaded() async {
let uploader = SequencedPunchPointUploader()
let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 })
await viewModel.addLocalImages(
[PunchPointImageState(data: Data([1]), fileName: "retry.jpg")],
uploader: uploader
)
XCTAssertNotNil(viewModel.images.first?.errorMessage)
await viewModel.retryUpload(at: 0, uploader: uploader)
XCTAssertNil(viewModel.images.first?.errorMessage)
XCTAssertEqual(viewModel.images.first?.uploadedURL, "https://cdn/retry.jpg")
XCTAssertEqual(uploader.callCount, 2)
}
func testAutoLocationFillsCoordinateAndAddress() async {
let location = MockPunchPointLocationProvider(
snapshot: HomeLocationSnapshot(latitude: 31.2, longitude: 121.5, address: "当前位置地址")
)
let viewModel = PunchPointFormViewModel(currentScenicIdProvider: { 7 }, locationProvider: location)
var changedCoordinate: CLLocationCoordinate2D?
viewModel.onCoordinateChange = { changedCoordinate = $0 }
await viewModel.autoLocation()
XCTAssertEqual(viewModel.coordinate?.latitude, 31.2)
XCTAssertEqual(viewModel.coordinate?.longitude, 121.5)
XCTAssertEqual(viewModel.address, "当前位置地址")
XCTAssertEqual(changedCoordinate?.latitude, 31.2)
}
private func item(id: Int64, status: Int = 1, mpQrcode: String? = nil) -> PunchPointItem {
PunchPointItem(
id: id,
@ -233,6 +291,25 @@ private final class SlowPunchPointUploader: PunchPointImageUploading {
}
}
@MainActor
private final class SequencedPunchPointUploader: PunchPointImageUploading {
var callCount = 0
func uploadPunchPointImage(
data: Data,
fileName: String,
scenicId: Int,
onProgress: @escaping (Int) -> Void
) async throws -> String {
callCount += 1
if callCount == 1 {
throw URLError(.cannotConnectToHost)
}
onProgress(100)
return "https://cdn/retry.jpg"
}
}
private final class MockPunchPointLocationProvider: LocationProviding, @unchecked Sendable {
let snapshot: HomeLocationSnapshot
let reverseAddress: String