task(03452fc3-d6cb-4549-b79d-7d389c7e5e8e): Add M6 stats & upload module (2.13/2.10/2.11/2.12 + queue retransmit)

补偿提交:commitOnArchive 连续第四轮未落盘(M3/M4/M5 同款故障),由主控在子会话工作树补交。

M6 统计与数据上传模块交付:
- M6-02 (2.10) 项目监测计划统计列表(完成度进度条+百分比 Chip)
- M6-03 (2.11) / M6-04 (2.12) 测组统计与监测点统计两层下钻
- M6-01 (2.13) SAF 选原始文件 multipart 直传 AnalysisMonitorData + 解析结果反馈
- 上传队列补传闭环 UploadQueueSyncer(应用启动 + ConnectivityManager VALIDATED
  onAvailable 双触发,Mutex 串行,成功 SUCCESS/失败回退 PENDING 记 retry)
- Tab.STATS 业务化:挂载 StatsWorkspaceFragment,删除占位 StatsFragment
- 新增 StatApi/UploadApi/StatModels + StatRepository/UploadRepository
- 单测 StatsModelsTest/UploadPayloadWrapTest;静态核查 7 项 + 生产网关负向契约 4 端点
This commit is contained in:
阿猫
2026-09-04 02:59:47 +08:00
parent 65a693052e
commit b8e4c24fd2
30 changed files with 2729 additions and 8 deletions

View File

@@ -3,7 +3,9 @@ package com.stec.cmd.core.network
import com.stec.cmd.core.network.api.AuthApi
import com.stec.cmd.core.network.api.PointApi
import com.stec.cmd.core.network.api.ProjectApi
import com.stec.cmd.core.network.api.StatApi
import com.stec.cmd.core.network.api.TaskApi
import com.stec.cmd.core.network.api.UploadApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -90,6 +92,16 @@ object NetworkModule {
@Singleton
fun providePointApi(retrofit: Retrofit): PointApi = retrofit.create(PointApi::class.java)
/** 统计接口M62.10/2.11/2.12)。 */
@Provides
@Singleton
fun provideStatApi(retrofit: Retrofit): StatApi = retrofit.create(StatApi::class.java)
/** 数据上传接口M62.8.2 队列补传 / 2.13 原始文件解析)。 */
@Provides
@Singleton
fun provideUploadApi(retrofit: Retrofit): UploadApi = retrofit.create(UploadApi::class.java)
/** 仅满足 Retrofit 构造约束;真实地址请求期经 DynamicBaseUrlInterceptor 改写。 */
private const val PLACEHOLDER_BASE_URL = "https://placeholder.stec.invalid/"

View File

@@ -0,0 +1,52 @@
package com.stec.cmd.core.network.api
import com.stec.cmd.core.network.ApiEnvelope
import kotlinx.serialization.json.JsonElement
import retrofit2.http.GET
import retrofit2.http.Query
/**
* 统计接口(接口管理规范 V4.5
* - 2.10 获取项目监测计划统计表M6-02P0
* - 2.11 获取指定监测计划的测组统计表M6-03P1
* - 2.12 监测点统计表M6-04P1历史数据
*
* 三要素请求头SecretKey/SystemCode/Token由 AuthInterceptor 统一注入。
*
* 契约宽容:文档约定 data 为 {"PlanList"/"GroupList"/"PointsList":[…]}
* 与项目/任务/测点列表M2/M3/M5 先例)同理保留形态偏差可能,故以 [JsonElement]
* 宽容承载,由 app 层 StatRepository 归一提取。
*/
interface StatApi {
/** 2.10 项目监测计划统计表:[projectId] 必填。 */
@GET(PATH_GET_PLAN_STATISTICAL)
suspend fun getPlanStatistical(
@Query(QUERY_PROJECT_ID) projectId: String,
): ApiEnvelope<JsonElement>
/** 2.11 指定监测计划的测组统计表:[planId] 必填。 */
@GET(PATH_GET_SURVEY_GROUP_LIST)
suspend fun getSurveyGroupList(
@Query(QUERY_PLAN_ID) planId: String,
): ApiEnvelope<JsonElement>
/** 2.12 监测点统计表:[planId]/[groupId] 均必填。 */
@GET(PATH_GET_POINT_STATISTICS)
suspend fun getPointStatistics(
@Query(QUERY_PLAN_ID) planId: String,
@Query(QUERY_GROUP_ID) groupId: String,
): ApiEnvelope<JsonElement>
companion object {
// ---- 接口管理规范 V4.5 · 章节 2.1 所有 API 目录 ----
const val PATH_GET_PLAN_STATISTICAL = "OutWebApi/api/GetPlanStatistical"
const val PATH_GET_SURVEY_GROUP_LIST = "OutWebApi/api/GetSurveyGroupList"
const val PATH_GET_POINT_STATISTICS = "OutWebApi/api/GetPointStatistics"
/** 查询参数名2.12 复用 PlanID2.10/2.11/2.12 各取所需)。 */
const val QUERY_PROJECT_ID = "ProjectID"
const val QUERY_PLAN_ID = "PlanID"
const val QUERY_GROUP_ID = "GroupID"
}
}

View File

@@ -0,0 +1,54 @@
package com.stec.cmd.core.network.api
import kotlinx.serialization.Serializable
/**
* 统计 DTO接口管理规范 V4.5 · 2.10/2.11/2.12 返回值示例)。
*
* 字段名与平台契约一致PascalCase文档未给类型全 `String?` 容错,
* 响应侧配合 Json.ignoreUnknownKeys 容忍字段演进。
* 2.10 仅统计含「APP上传」测组的监测计划非 APP 上传测组不计入完成度。
*/
/** 2.10 监测计划统计条目data.PlanList。 */
@Serializable
data class PlanStatItem(
/** 监测计划 ID2.11/2.12 下钻与 2.13 上传 PlanID 的取值来源)。 */
val PlanID: String? = null,
/** 期数。 */
val PeriodsNum: String? = null,
/** 截止时间。 */
val EndTime: String? = null,
/** 施工工况。 */
val Working: String? = null,
/** 完成度(未开始上传的计划为 0。 */
val CompleteDegree: String? = null,
/** 类型:常规 | 加测。 */
val Mold: String? = null,
)
/** 2.11 测组统计条目data.GroupList。 */
@Serializable
data class GroupStatItem(
/** 测组 ID2.12 下钻 GroupID 取值来源)。 */
val GroupID: String? = null,
/** 测组名称。 */
val GroupName: String? = null,
/** 责任人。 */
val DutyName: String? = null,
/** 完成度。 */
val CompleteDegree: String? = null,
/** 监测类型:水平位移/竖向位移/测斜/水位/轴力等。 */
val MonitoringType: String? = null,
)
/** 2.12 监测点统计条目data.PointsList历史数据。 */
@Serializable
data class PointStatItem(
/** 点名。 */
val PointName: String? = null,
/** 上传时间。 */
val ImportTime: String? = null,
/** 设备。 */
val MonitorEqu: String? = null,
)

View File

@@ -0,0 +1,66 @@
package com.stec.cmd.core.network.api
import com.stec.cmd.core.network.ApiEnvelope
import kotlinx.serialization.json.JsonElement
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
/**
* 数据上传接口(接口管理规范 V4.5
* - 2.8.2 上传本次监测值M4 蓝牙采集 / M5 手动录入 Room 队列的统一补传通道)
* - 2.13 数据解析·原始文件上传M6-01采集原始文件兜底通道全程序唯一 multipart 端点)
*
* 三要素请求头SecretKey/SystemCode/Token由 AuthInterceptor 统一注入multipart 同样生效),
* 真实目标地址由 DynamicBaseUrlInterceptor 请求期改写。
*
* 契约宽容:两端点成功均返回 data:null仍以 [JsonElement] 承载以复用
* ApiEnvelope/ApiCaller 统一错误解包链HTTP500+ExceptionMessage 内嵌信封)。
*/
interface UploadApi {
/**
* 2.8.2 上传本次监测值文档「Post 请求传递 Json 参数」,参数名为
* MonitorValue故 body 为 `{"MonitorValue":{…}}` 包装对象
* (队列 payloadJson 为 MonitorValue 对象本身,由 app 层包装)。
*/
@POST(PATH_UPLOAD_MONITOR_VALUE)
suspend fun uploadMonitorValue(
@Body body: JsonElement,
): ApiEnvelope<JsonElement>
/**
* 2.13 数据解析原始文件上传multipart/form-data。
* 必填 ProjectID/WorkPointID/SurveyGroupID/PlanID[monitorDate] 可选(传 null 省略);
* 文档约定 Files 参数名可省(服务端自动识别文件),此处显式命名并携带原始文件名。
*/
@Multipart
@POST(PATH_ANALYSIS_MONITOR_DATA)
suspend fun analysisMonitorData(
@Part(PROJECT_ID_PART) projectId: RequestBody,
@Part(WORK_POINT_ID_PART) workPointId: RequestBody,
@Part(SURVEY_GROUP_ID_PART) surveyGroupId: RequestBody,
@Part(PLAN_ID_PART) planId: RequestBody,
@Part(MONITOR_DATE_PART) monitorDate: RequestBody?,
@Part file: MultipartBody.Part,
): ApiEnvelope<JsonElement>
companion object {
// ---- 接口管理规范 V4.5 · 章节 2.1 所有 API 目录 ----
const val PATH_UPLOAD_MONITOR_VALUE = "OutWebApi/api/UploadMonitorValue"
const val PATH_ANALYSIS_MONITOR_DATA = "OutWebApi/api/AnalysisMonitorData"
/** 2.13 multipart 字段名(文档 2.13.4 接口参数表)。 */
const val PROJECT_ID_PART = "ProjectID"
const val WORK_POINT_ID_PART = "WorkPointID"
const val SURVEY_GROUP_ID_PART = "SurveyGroupID"
const val PLAN_ID_PART = "PlanID"
const val MONITOR_DATE_PART = "MonitorDate"
/** 2.13 文件字段名(服务端自动识别,命名仅作自描述)。 */
const val FILE_PART = "Files"
}
}