This commit is contained in:
阿猫
2026-09-03 12:38:16 +08:00
parent 09669fe4a2
commit e96e92d7ab
68 changed files with 2810 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
// core:database 本地存储Room AppDatabase + 上传队列(离线优先参考实现)
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
}
android {
namespace = "com.stec.cmd.core.database"
compileSdk = 34
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(project(":core:common"))
api(libs.androidx.room.runtime)
api(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
testImplementation(libs.junit)
}

View File

@@ -0,0 +1,27 @@
package com.stec.cmd.core.database
import androidx.room.Database
import androidx.room.RoomDatabase
/**
* 应用数据库骨架。
*
* S0 仅注册上传队列一张表作为离线优先参考实现;
* S1+ 新业务实体(任务缓存、测点、统计数据等)在 entities 中追加并升 version。
* 骨架期 exportSchema=false无迁移需求开启迁移管理时再导出 schema。
*/
@Database(
entities = [
UploadQueueEntity::class,
],
version = 1,
exportSchema = false,
)
abstract class AppDatabase : RoomDatabase() {
abstract fun uploadQueueDao(): UploadQueueDao
companion object {
const val DATABASE_NAME = "stec-cmd.db"
}
}

View File

@@ -0,0 +1,32 @@
package com.stec.cmd.core.database
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/** 数据库 Hilt 装配:全工程共享一个 AppDatabase。 */
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideAppDatabase(
@ApplicationContext context: Context,
): AppDatabase = Room.databaseBuilder(
context,
AppDatabase::class.java,
AppDatabase.DATABASE_NAME,
).build()
@Provides
@Singleton
fun provideUploadQueueDao(
database: AppDatabase,
): UploadQueueDao = database.uploadQueueDao()
}

View File

@@ -0,0 +1,54 @@
package com.stec.cmd.core.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
/**
* 上传队列 DAO演示离线优先的入队/取待传/回写状态闭环。
* S1+ 各数据类型 DAO 照此挂载到 [AppDatabase]。
*/
@Dao
interface UploadQueueDao {
/** 入队一条待上传数据。 */
@Insert
suspend fun enqueue(entity: UploadQueueEntity): Long
/** 按入队顺序观察待上传(含失败待重试)队列。 */
@Query(
"SELECT * FROM upload_queue " +
"WHERE status IN (:statuses) ORDER BY created_at ASC",
)
fun observeByStatuses(vararg statuses: Int): Flow<List<UploadQueueEntity>>
/** 一次性取待上传列表(上传调度器使用)。 */
@Query(
"SELECT * FROM upload_queue " +
"WHERE status IN (:statuses) ORDER BY created_at ASC LIMIT :limit",
)
suspend fun takePending(statuses: List<Int>, limit: Int = DEFAULT_BATCH): List<UploadQueueEntity>
/** 标记上传中。 */
@Query("UPDATE upload_queue SET status = :status WHERE id = :id")
suspend fun updateStatus(id: Long, status: Int)
/** 标记成功并记录时间。 */
@Query("UPDATE upload_queue SET status = :status, uploaded_at = :uploadedAt WHERE id = :id")
suspend fun markSuccess(id: Long, status: Int, uploadedAt: Long)
/** 失败重试:状态回待传并累计重试次数。 */
@Query(
"UPDATE upload_queue SET status = :status, retry_count = retry_count + 1 WHERE id = :id",
)
suspend fun markRetry(id: Long, status: Int)
/** 清理已成功且早于截止时间的记录(队列瘦身)。 */
@Query("DELETE FROM upload_queue WHERE status = :status AND uploaded_at IS NOT NULL AND uploaded_at < :before")
suspend fun purgeSuccessBefore(status: Int, before: Long): Int
companion object {
private const val DEFAULT_BATCH = 50
}
}

View File

@@ -0,0 +1,67 @@
package com.stec.cmd.core.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* 采集数据上传队列(离线优先参考实现,对应功能明细 M4-07
*
* 设计口径:所有采集数据先入本队列再尝试上传,网络失败留在队列内自动补传;
* S1+ 各数据类型(水平位移/测斜/轴力/水位…)照此模式挂载自己的实体,
* payloadJson 统一存接口文档约定的上传 JSON 结构。
*/
@Entity(tableName = "upload_queue")
data class UploadQueueEntity(
/** 自增主键。 */
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Long = 0L,
/** 设备类型IE1000 / VM208见 core:ble DeviceType。 */
@ColumnInfo(name = "device_type")
val deviceType: String,
/** 设备蓝牙标识(蓝牙名或 MAC。 */
@ColumnInfo(name = "device_id")
val deviceId: String,
/** 业务数据类型(水平位移/测斜/轴力/水位等S1+ 扩展枚举)。 */
@ColumnInfo(name = "data_type")
val dataType: String,
/** 按接口文档组装好的上传 JSON。 */
@ColumnInfo(name = "payload_json")
val payloadJson: String,
/** 队列状态,见 [UploadStatus]。 */
@ColumnInfo(name = "status")
val status: Int = UploadStatus.PENDING,
/** 已重试次数429/403 限流退避依据)。 */
@ColumnInfo(name = "retry_count")
val retryCount: Int = 0,
/** 入队时间epoch 毫秒)。 */
@ColumnInfo(name = "created_at")
val createdAt: Long,
/** 上传成功时间epoch 毫秒),未上传为 null。 */
@ColumnInfo(name = "uploaded_at")
val uploadedAt: Long? = null,
)
/** 队列状态常量S0 用 Int 避免 Room TypeConverter 样板S1 可按需替换为枚举)。 */
object UploadStatus {
/** 待上传。 */
const val PENDING = 0
/** 上传中。 */
const val UPLOADING = 1
/** 上传成功(保留一段时间后由清理任务删除)。 */
const val SUCCESS = 2
/** 失败待重试。 */
const val FAILED = 3
}