From 34c118394e999cbca009fa90222b8b3fd0f7fc58 Mon Sep 17 00:00:00 2001 From: mxhsnd <2049295946@qq.com> Date: Mon, 24 Aug 2026 00:57:09 +0800 Subject: [PATCH] feat: define infrared key contracts --- .../domain/infrared/InfraredKeyFactory.kt | 46 +++++++++++++++++++ .../domain/infrared/InfraredKeyModels.kt | 26 +++++++++++ .../domain/infrared/InfraredKeyRepository.kt | 20 ++++++++ .../domain/infrared/InfraredKeyFactoryTest.kt | 38 +++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt create mode 100644 domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyModels.kt create mode 100644 domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyRepository.kt create mode 100644 domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt new file mode 100644 index 0000000..1ee845f --- /dev/null +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactory.kt @@ -0,0 +1,46 @@ +package com.flagship.abox.manager.domain.infrared + +import com.flagship.abox.manager.domain.DeviceName +import com.flagship.abox.manager.domain.DomainError +import com.flagship.abox.manager.domain.DomainResult +import com.flagship.abox.manager.domain.ProviderProfileId + +fun interface InfraredKeyIdGenerator { fun nextId(): InfraredKeyId } +fun interface EpochMillisClock { fun now(): Long } + +class InfraredKeyFactory( + private val idGenerator: InfraredKeyIdGenerator, + private val clock: EpochMillisClock, +) { + fun create( + profileId: ProviderProfileId, + deviceName: DeviceName, + displayName: String, + ): DomainResult { + val normalizedName = displayName.trim() + if (profileId.value.isBlank() || deviceName.value.isBlank() || normalizedName.isBlank()) { + return DomainResult.Failure( + DomainError.InvalidRequest(detail = "Profile, device name, and display name are required"), + ) + } + val id = idGenerator.nextId() + val normalizedId = id.value.lowercase().filter { it in 'a'..'z' || it in '0'..'9' } + if (normalizedId.isBlank()) { + return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key id is invalid")) + } + val now = clock.now() + return DomainResult.Success( + SavedInfraredKey( + id = id, + profileId = profileId, + deviceName = deviceName, + displayName = normalizedName, + sdkKey = "app_ir_${normalizedId.take(40)}", + status = InfraredKeyStatus.LEARNING, + lastErrorCode = null, + createdAtEpochMillis = now, + updatedAtEpochMillis = now, + ), + ) + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyModels.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyModels.kt new file mode 100644 index 0000000..1e84c29 --- /dev/null +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyModels.kt @@ -0,0 +1,26 @@ +package com.flagship.abox.manager.domain.infrared + +import com.flagship.abox.manager.domain.DeviceName +import com.flagship.abox.manager.domain.ProviderProfileId + +@JvmInline +value class InfraredKeyId(val value: String) + +data class InfraredKeyRef( + val profileId: ProviderProfileId, + val keyId: InfraredKeyId, +) + +enum class InfraredKeyStatus { LEARNING, READY, FAILED } + +data class SavedInfraredKey( + val id: InfraredKeyId, + val profileId: ProviderProfileId, + val deviceName: DeviceName, + val displayName: String, + val sdkKey: String, + val status: InfraredKeyStatus, + val lastErrorCode: String?, + val createdAtEpochMillis: Long, + val updatedAtEpochMillis: Long, +) \ No newline at end of file diff --git a/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyRepository.kt b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyRepository.kt new file mode 100644 index 0000000..9d7d38d --- /dev/null +++ b/domain/src/main/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyRepository.kt @@ -0,0 +1,20 @@ +package com.flagship.abox.manager.domain.infrared + +import com.flagship.abox.manager.domain.DeviceName +import com.flagship.abox.manager.domain.DomainResult +import com.flagship.abox.manager.domain.ProviderProfileId +import kotlinx.coroutines.flow.Flow + +interface InfraredKeyRepository { + fun observeKeys(profileId: ProviderProfileId, deviceName: DeviceName): Flow>> + suspend fun getKey(ref: InfraredKeyRef): DomainResult + suspend fun insert(key: SavedInfraredKey): DomainResult + suspend fun update(key: SavedInfraredKey): DomainResult + suspend fun delete(ref: InfraredKeyRef): DomainResult + suspend fun findByStatus(profileId: ProviderProfileId, status: InfraredKeyStatus): DomainResult> +} + +interface InfraredDeviceConfigurationRepository { + fun observeDeviceName(profileId: ProviderProfileId): Flow> + suspend fun setDeviceName(profileId: ProviderProfileId, deviceName: DeviceName): DomainResult +} \ No newline at end of file diff --git a/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt b/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt new file mode 100644 index 0000000..acb8fad --- /dev/null +++ b/domain/src/test/kotlin/com/flagship/abox/manager/domain/infrared/InfraredKeyFactoryTest.kt @@ -0,0 +1,38 @@ +package com.flagship.abox.manager.domain.infrared + +import com.flagship.abox.manager.domain.DeviceName +import com.flagship.abox.manager.domain.DomainResult +import com.flagship.abox.manager.domain.ProviderProfileId +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class InfraredKeyFactoryTest { + private val id = InfraredKeyId("01J5MOLERABC123") + private val factory = InfraredKeyFactory( + idGenerator = InfraredKeyIdGenerator { id }, + clock = EpochMillisClock { 1_724_435_200_000L }, + ) + + @Test + fun createsLearningKeyWithStableAsciiSdkKey() { + val result = factory.create( + ProviderProfileId("abox"), + DeviceName("ir-1"), + "空调开关", + ) as DomainResult.Success + + assertEquals(id, result.value.id) + assertEquals("app_ir_01j5molerabc123", result.value.sdkKey) + assertEquals(InfraredKeyStatus.LEARNING, result.value.status) + assertEquals("空调开关", result.value.displayName) + assertEquals(1_724_435_200_000L, result.value.createdAtEpochMillis) + assertEquals(result.value.createdAtEpochMillis, result.value.updatedAtEpochMillis) + } + + @Test + fun rejectsBlankDeviceOrDisplayName() { + assertTrue(factory.create(ProviderProfileId("abox"), DeviceName(""), "Power") is DomainResult.Failure) + assertTrue(factory.create(ProviderProfileId("abox"), DeviceName("ir-1"), " ") is DomainResult.Failure) + } +} \ No newline at end of file