feat: 新增 Fake 仓库实现(含可提交的打卡/计划与会话)

This commit is contained in:
AndyChen1024
2026-09-15 14:35:52 +08:00
parent ec1eed8f04
commit c39a324534
5 changed files with 316 additions and 12 deletions

View File

@@ -0,0 +1,64 @@
import Combine
import CoreDomain
import CoreModel
/// Fake 打卡仓库(骨架阶段返回假数据,可提交)。
public final class FakeCheckinRepository: CheckinRepository, @unchecked Sendable {
private let record: CurrentValueSubject<CheckinRecord, Never>
public init() {
record = CurrentValueSubject(Self.seed())
}
public func observeCheckin() -> AsyncThrowingStream<CheckinRecord, Error> {
Self.stream(record)
}
public func submitCheckin(status: CheckinStatus) async throws {
try await Task.sleep(for: .milliseconds(300))
var current = record.value
current = CheckinRecord(
classDate: current.classDate,
weekday: current.weekday,
dayNum: current.dayNum,
checked: true,
status: status,
groupName: current.groupName,
groupMembers: current.groupMembers.map { member in
guard member.id == "me" else { return member }
return GroupMember(id: member.id, name: member.name, checkedIn: true, checkinTime: "现在", status: status)
},
personalHistory: current.personalHistory
)
record.value = current
}
private static func stream<T>(_ subject: CurrentValueSubject<T, Never>) -> AsyncThrowingStream<T, Error> {
AsyncThrowingStream { continuation in
let cancellable = subject.sink { continuation.yield($0) }
continuation.onTermination = { _ in cancellable.cancel() }
}
}
private static func seed() -> CheckinRecord {
CheckinRecord(
classDate: "2026-09-09",
weekday: "周三",
dayNum: 9,
checked: false,
status: nil,
groupName: "进阶一组",
groupMembers: [
GroupMember(id: "m1", name: "张三", checkedIn: true, checkinTime: "19:02", status: .normal),
GroupMember(id: "m2", name: "李四", checkedIn: true, checkinTime: "19:30", status: .late),
GroupMember(id: "me", name: "我", checkedIn: false, checkinTime: nil, status: nil),
GroupMember(id: "m3", name: "王五", checkedIn: true, checkinTime: "09:10", status: .leave),
GroupMember(id: "m4", name: "赵六", checkedIn: false, checkinTime: nil, status: nil),
],
personalHistory: [
PersonalCheckinRecord(date: "2026-09-06", status: .normal, time: "19:05"),
PersonalCheckinRecord(date: "2026-09-02", status: .late, time: "19:32"),
]
)
}
}

View File

@@ -2,11 +2,19 @@ import CoreCommon
import CoreDomain import CoreDomain
import CoreModel import CoreModel
/// Fake 仓库实现(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。 /// Fake 首页仓库(骨架阶段返回假数据,后续替换为 OfflineFirst 实现)。
public struct FakeHomeRepository: HomeRepository { public struct FakeHomeRepository: HomeRepository {
public init() {} public init() {}
public func observeHome() -> AsyncThrowingStream<HomeData, Error> { public func observeHome() -> AsyncThrowingStream<HomeData, Error> {
stream(Self.loggedInData)
}
public func observeGuestHome() -> AsyncThrowingStream<HomeData, Error> {
stream(Self.guestData)
}
private func stream(_ data: HomeData) -> AsyncThrowingStream<HomeData, Error> {
AsyncThrowingStream { continuation in AsyncThrowingStream { continuation in
let task = Task { let task = Task {
do { do {
@@ -14,7 +22,7 @@ public struct FakeHomeRepository: HomeRepository {
try await Task.sleep(for: .milliseconds(400)) try await Task.sleep(for: .milliseconds(400))
try Task.checkCancellation() try Task.checkCancellation()
Logger.info("FakeHomeRepository 返回假数据") Logger.info("FakeHomeRepository 返回假数据")
continuation.yield(Self.fakeData) continuation.yield(data)
continuation.finish() continuation.finish()
} catch { } catch {
continuation.finish(throwing: error) continuation.finish(throwing: error)
@@ -26,15 +34,32 @@ public struct FakeHomeRepository: HomeRepository {
} }
} }
private static let fakeData = HomeData( private static let modules = [
id: "home-1", HomeModule(id: "checkin", emoji: "📋", title: "打卡介绍", description: "每日打卡记录", destination: .checkin),
greeting: "早上好,Andy", HomeModule(id: "plan", emoji: "📝", title: "计划介绍", description: "周/月/年计划", destination: .plan),
summary: "今日已完成 2 / 4", HomeModule(id: "about", emoji: "ℹ️", title: "关于我们", description: "了解明缘云道场", destination: .about),
habits: [
Habit(id: "habit-1", title: "晨跑 5 公里", detail: "保持运动习惯", isCheckedInToday: true, streakDays: 12),
Habit(id: "habit-2", title: "阅读 30 分钟", detail: "每天进步一点", isCheckedInToday: true, streakDays: 8),
Habit(id: "habit-3", title: "喝水 8 杯", detail: "健康补水", isCheckedInToday: false, streakDays: 0),
Habit(id: "habit-4", title: "写日记", detail: "记录今日所思", isCheckedInToday: false, streakDays: 3),
] ]
private static let intros = [
IntroCard(title: "📚 学习成长工具", body: "明缘云道场是一款专注于个人成长的效率工具,通过每周打卡与计划管理,帮助你养成良好的学习习惯,实现持续进步。好好学习,天天向上。"),
IntroCard(title: "✅ 周打卡", body: "每周打卡,支持多种状态记录(正常、迟到、请假、早退、不参加讨论等)。查看小组打卡情况,与同学们互相监督、共同进步。"),
IntroCard(title: "📝 计划管理", body: "提供周计划、月计划、年计划三级管理,支持文字与图片上传。清晰追踪每个周期的目标完成情况,让成长有迹可循。"),
IntroCard(title: "👥 小组共学", body: "加入学习小组,查看组员的打卡与计划完成情况。在集体的氛围中保持学习动力,与志同道合的伙伴一起成长。"),
]
private static let guestData = HomeData(
greeting: "欢迎来到明缘云道场",
displayName: "游客",
encouragement: "登录后即可打卡、提交计划",
modules: modules,
intros: intros
)
private static let loggedInData = HomeData(
greeting: "晚上好",
displayName: "张三",
encouragement: "坚持每周打卡,与小组共同进步。",
modules: modules,
intros: intros
) )
} }

View File

@@ -0,0 +1,136 @@
import Combine
import CoreDomain
import CoreModel
import Foundation
/// Fake 计划仓库(骨架阶段返回假数据,可提交)。
public final class FakePlanRepository: PlanRepository, @unchecked Sendable {
private let plans: [PlanType: CurrentValueSubject<PlanRecord, Never>]
private var submits: [String: CurrentValueSubject<PlanSubmitData, Never>] = [:]
private let lock = NSLock()
public init() {
var dict: [PlanType: CurrentValueSubject<PlanRecord, Never>] = [:]
for type in PlanType.allCases {
dict[type] = CurrentValueSubject<PlanRecord, Never>(Self.seedPlan(type))
}
plans = dict
}
public func observePlan(type: PlanType) -> AsyncThrowingStream<PlanRecord, Error> {
Self.stream(plans[type]!)
}
public func observePlanSubmit(type: PlanType, periodKey: String) -> AsyncThrowingStream<PlanSubmitData, Error> {
Self.stream(submitSubject(type: type, periodKey: periodKey))
}
public func savePlan(type: PlanType, periodKey: String, content: String, attachments: [String]) async throws {
try await Task.sleep(for: .milliseconds(300))
let submit = submitSubject(type: type, periodKey: periodKey)
let currentSubmit = submit.value
submit.value = PlanSubmitData(
content: content,
attachments: attachments,
checked: true,
canSubmit: currentSubmit.canSubmit,
message: currentSubmit.message,
history: currentSubmit.history
)
let plan = plans[type]!
let currentPlan = plan.value
plan.value = PlanRecord(
type: currentPlan.type,
periodKey: currentPlan.periodKey,
canSubmit: currentPlan.canSubmit,
message: currentPlan.message,
myChecked: true,
myRecord: MyPlanRecord(status: "已完成", submittedTime: "现在", content: content, attachments: attachments),
groups: currentPlan.groups,
unassignedCount: currentPlan.unassignedCount
)
}
private func submitSubject(type: PlanType, periodKey: String) -> CurrentValueSubject<PlanSubmitData, Never> {
let key = "\(type.rawValue):\(periodKey)"
lock.lock()
defer { lock.unlock() }
if let existing = submits[key] {
return existing
}
let subject = CurrentValueSubject<PlanSubmitData, Never>(Self.seedSubmit(type: type))
submits[key] = subject
return subject
}
private static func stream<T>(_ subject: CurrentValueSubject<T, Never>) -> AsyncThrowingStream<T, Error> {
AsyncThrowingStream { continuation in
let cancellable = subject.sink { continuation.yield($0) }
continuation.onTermination = { _ in cancellable.cancel() }
}
}
private static func seedPlan(_ type: PlanType) -> PlanRecord {
PlanRecord(
type: type,
periodKey: periodKey(for: type),
canSubmit: true,
message: type == .weekly ? "本周计划提交截止:周日晚 23:59" : nil,
myChecked: type == .weekly,
myRecord: type == .weekly
? MyPlanRecord(status: "已完成", submittedTime: "今天 18:30", content: "本周完成两章学习,整理笔记并复习错题。", attachments: ["图1", "图2"])
: nil,
groups: seedGroups(),
unassignedCount: 2
)
}
private static func seedSubmit(type: PlanType) -> PlanSubmitData {
if type == .weekly {
return PlanSubmitData(
content: "本周完成两章学习,整理笔记并复习错题。",
attachments: ["图1", "图2"],
checked: true,
canSubmit: true,
message: nil,
history: []
)
}
return PlanSubmitData(
content: "",
attachments: [],
checked: false,
canSubmit: true,
message: nil,
history: [
MyPlanRecord(status: "已完成", submittedTime: "2026-08 完成", content: "上月计划内容……", attachments: []),
]
)
}
private static func periodKey(for type: PlanType) -> String {
switch type {
case .weekly: return "2026 第 37 周"
case .monthly: return "2026 年 9 月"
case .yearly: return "2026 年"
}
}
private static func seedGroups() -> [PlanGroup] {
[
PlanGroup(id: "g1", name: "进阶一组", checkedCount: 4, memberCount: 6, members: seedMembers()),
PlanGroup(id: "g2", name: "进阶二组", checkedCount: 5, memberCount: 5, members: seedMembers()),
PlanGroup(id: "g3", name: "进阶三组", checkedCount: 2, memberCount: 7, members: seedMembers()),
]
}
private static func seedMembers() -> [PlanMember] {
[
PlanMember(id: "p1", name: "张三", filled: true),
PlanMember(id: "p2", name: "李四", filled: true),
PlanMember(id: "p3", name: "王五", filled: false),
]
}
}

View File

@@ -0,0 +1,40 @@
import CoreCommon
import CoreDomain
import CoreModel
/// Fake 个人资料仓库(骨架阶段返回假数据)。
public struct FakeProfileRepository: ProfileRepository {
public init() {}
public func observeProfile() -> AsyncThrowingStream<UserProfile, Error> {
AsyncThrowingStream { continuation in
let task = Task {
do {
try await Task.sleep(for: .milliseconds(400))
try Task.checkCancellation()
Logger.info("FakeProfileRepository 返回假数据")
continuation.yield(Self.fakeData)
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in task.cancel() }
}
}
private static let fakeData = UserProfile(
displayName: "张三",
name: "张三",
studentId: "20260001",
phone: "138****1234",
role: "成员",
group: "进阶一组",
loginType: "手机号",
avatarUrl: nil,
classGroups: [
ClassGroup(period: "2026 秋季", group: "进阶一组", role: "成员"),
ClassGroup(period: "2026 春季", group: "进阶二组", role: "组长"),
]
)
}

View File

@@ -0,0 +1,39 @@
import Combine
import CoreDomain
import Foundation
/// Fake 会话仓库(登录态在进程内共享,默认未登录)。
public final class FakeSessionRepository: SessionRepository, @unchecked Sendable {
private let isLoggedIn = CurrentValueSubject<Bool, Never>(false)
public init() {}
public func observeIsLoggedIn() -> AsyncThrowingStream<Bool, Error> {
AsyncThrowingStream { continuation in
let cancellable = self.isLoggedIn.sink { continuation.yield($0) }
continuation.onTermination = { _ in cancellable.cancel() }
}
}
public func requestSmsCode(phone: String) async throws {
try await Task.sleep(for: .milliseconds(500))
guard phone.range(of: #"^1\d{10}$"#, options: .regularExpression) != nil else {
throw SessionError.invalidPhone
}
}
public func login(phone: String, code: String) async throws {
try await Task.sleep(for: .milliseconds(500))
guard code == "123456" else {
throw SessionError.wrongCode
}
guard phone == "13812341234" else {
throw SessionError.phoneNotMatched
}
isLoggedIn.value = true
}
public func logout() async throws {
isLoggedIn.value = false
}
}