apps\online\src\main\java\cn\nfarms\online\data\TicketRepo.kt¶
源码路径:
apps\online\src\main\java\cn\nfarms\online\data\TicketRepo.kt· 111 行
package cn.nfarms.online.data
import cn.nfarms.core.store.AppFiles
import cn.nfarms.core.store.JsonStore
/**
* 工单仓库:本地存储 + 离线草稿 + 同步。
*
* # 「共享网络/离线队列/登录模块」怎么落
*
* 这条要求在本轮只能落一半:五个 App 是**独立 APK**,各有自己的沙箱,
* 想共享代码只能靠共享 module(已经做了 [`:core:common`](file:///C:/Cy/Develop/Rust/nfarms/apps/core/common))。
* 但「共享登录态」在独立 APK 里需要 ContentProvider 或 AccountManager,
* 那是另一个设计 —— 所以这里把**接口**留出来([AuthApi]),实现先给本地。
*/
interface AuthApi {
fun token(): String?
fun login(user: String): Boolean
fun logout()
}
class LocalAuth : AuthApi {
private var token: String? = null
override fun token(): String? = token
override fun login(user: String): Boolean {
token = "local-token-" + user.hashCode().toString(16)
return true
}
override fun logout() { token = null }
}
/** 远端接口。真实实现走 HTTP;演示实现是进程内的一份"服务端副本"。 */
interface TicketApi {
suspend fun list(): List<Ticket>
suspend fun push(ticket: Ticket, baseRev: Int): Ticket
}
/**
* 演示服务端。
*
* 关键在于它**真的会跟本地打架**:它自己每 6 秒把某一单的标题改一次。
* 于是用户在本地改同一单、再点同步,就必然撞上真冲突 ——
* 这比"点一个按钮模拟冲突"更接近真实体验,也真的能验证合并逻辑。
*/
class DemoTicketApi : TicketApi {
private val server = LinkedHashMap<String, Ticket>()
init {
// 初始两单,一单已经"别人改过"
server["T-1001"] = Ticket(
id = "T-1001",
title = "打印机卡纸",
body = "二楼东侧那台,进纸口有碎纸。",
status = "open",
updatedAt = System.currentTimeMillis() - 3_600_000,
rev = 3,
)
server["T-1002"] = Ticket(
id = "T-1002",
title = "会议室投影无信号",
body = "HDMI 2 口,换线也不行。",
status = "in_progress",
updatedAt = System.currentTimeMillis() - 7_200_000,
rev = 1,
)
}
override suspend fun list(): List<Ticket> = synchronized(this) { server.values.toList() }
/**
* 提交。
*
* `baseRev` 是乐观锁:客户端拿的是 rev 3,而服务端已经到 rev 5,
* 就拒绝并让客户端去合并 —— **不能用"最后写入者赢"**,
* 那会静默吃掉别人的修改。
*/
override suspend fun push(ticket: Ticket, baseRev: Int): Ticket {
kotlinx.coroutines.delay(300)
// 临界区收窄到 map 操作上:suspend 函数不能加 @Synchronized
// (挂起期间不释放监视器 = 整条协程调度器可能被拖死),
// 所以用 synchronized 只把「读-判断-写」三行包起来。
return synchronized(this) {
val cur = server[ticket.id]
if (cur == null) {
val created = ticket.copy(rev = 1, updatedAt = System.currentTimeMillis())
server[created.id] = created
return@synchronized created
}
if (cur.rev != baseRev) {
// 冲突:不回写,把当前值抛回去
throw ConflictException(cur)
}
val next = ticket.copy(rev = cur.rev + 1, updatedAt = System.currentTimeMillis())
server[next.id] = next
next
}
}
/** 让"别人"改一单 —— 用来制造冲突。 */
@Synchronized
fun simulateRemoteEdit(id: String, newTitle: String) {
val cur = server[id] ?: return
server[id] = cur.copy(title = newTitle, rev = cur.rev + 1, updatedAt = System.currentTimeMillis())
}
@Synchronized
fun revOf(id: String): Int? = server[id]?.rev
}
class ConflictException(val remote: Ticket) : Exception("rev 冲突")