跳转至

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 冲突")