feat(nfc-task): NFC 任务打卡模块(单个+批量+硬件开锁)

REQ-20260506-0024

新增 NfcTaskManager:
- startTaskPunch(taskId):任务详情页单个 NFC 打卡
- startActivePunch():返回键触发主动打卡
  → checkNfcType 判断:hardwareNfcFlag=true 硬件开锁 / false 批量打卡
- NFC 超时自动关闭 + 震动/音效反馈
- isScanning 标记防重复触发

TaskApi 增加 3 个接口:
- POST watchTask/nfcToBeginTask(单个打卡)
- POST watchTask/nfcBatchBeginTask(批量打卡)
- GET nfcInfo/nfcOpenLock(类型判断)

返回键逻辑修正:
- 原:返回键→考勤打卡
- 现:返回键→主动打卡(批量任务 or 硬件开锁)
- 考勤打卡只从下拉面板触发
- onBackKeyPressed 改为返回 Boolean(true=已处理)

TaskDetailFragment:
- "开启打卡"按钮填充 NFC 打卡逻辑(替换 TODO)
- 扫描中按钮变灰禁用,成功/失败用 QuTipDialog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-05-06 12:05:40 +09:30
parent 70da49f650
commit 1b24e2d044
7 changed files with 392 additions and 25 deletions

View File

@@ -0,0 +1,276 @@
package com.xiaoqu.watch.service.manager
import com.xiaoqu.watch.device.nfc.NfcController
import com.xiaoqu.watch.device.sensor.VibrationController
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* NFC 任务打卡管理器
*
* 统一处理三种 NFC 打卡场景:
* 1. 任务单个打卡(任务详情页"开启打卡"按钮)
* 2. 主动批量打卡返回键触发checkNfcType → nfcBatchBeginTask
* 3. 硬件开锁返回键触发checkNfcType → 根据 status 播放语音)
*
* 与 PunchViewModel考勤打卡独立互不干扰。
* 通过 isScanning 防止两者同时操作 NFC。
*/
@Singleton
class NfcTaskManager @Inject constructor(
private val nfcController: NfcController,
private val vibrationController: VibrationController,
private val taskApi: TaskApi
) {
companion object {
/** 默认 NFC 超时(毫秒),可被 MQTT type=4 的 nfcOpenTime 覆盖 */
private const val DEFAULT_NFC_TIMEOUT_MS = 20_000L
// 震动方案 planId
private const val PLAN_PUNCH_SUCCESS = 4
private const val PLAN_PUNCH_FAIL = 7
private const val PLAN_NFC_OPEN = 8
private const val PLAN_NFC_CLOSE = 9
private const val PLAN_OFFLINE = 10
private const val PLAN_OPEN_FAILED = 11
private const val PLAN_NO_AUTH = 12
private const val PLAN_OPENING = 13
}
/** NFC 是否正在扫描(供外部互斥检查) */
@Volatile
var isScanning = false
private set
/** NFC 超时时间(毫秒),通过 MQTT type=4 更新 */
var nfcTimeoutMs: Long = DEFAULT_NFC_TIMEOUT_MS
/** 协程作用域 */
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
/** 超时 Job */
private var timeoutJob: Job? = null
/**
* 场景 1任务单个打卡
* 开启 NFC → 读卡 → POST nfcToBeginTask → 回调结果
*
* @param taskId 任务 ID
* @param onResult 结果回调主线程success + message
*/
fun startTaskPunch(taskId: Long, onResult: (success: Boolean, message: String) -> Unit) {
if (isScanning) {
Timber.d("NFC任务打卡: 已在扫描中,忽略")
return
}
isScanning = true
Timber.d("NFC任<EFBFBD><EFBFBD>打卡: 开启单个打卡, taskId=%d", taskId)
// 开启 NFC + 音效
vibrationController.executeByPlanId(PLAN_NFC_OPEN)
nfcController.open()
// 开始扫描
nfcController.startScan { nfcId ->
Timber.d("NFC任务打卡: 读到卡号 %s", nfcId)
cancelTimeout()
nfcController.stopScan()
nfcController.close()
// 调用打卡 API
scope.launch {
val params = hashMapOf<String, Any>("id" to taskId, "nfcId" to nfcId)
val result = safeApiCall { taskApi.nfcToBeginTask(params) }
when (result) {
is ApiResult.Success -> {
Timber.d("NFC任务打卡: 打卡成功")
vibrationController.executeByPlanId(PLAN_PUNCH_SUCCESS)
onResult(true, "打卡成功")
}
is ApiResult.Error -> {
Timber.w("NFC任务打卡: 打卡失败 - %s", result.message)
vibrationController.executeByPlanId(PLAN_PUNCH_FAIL)
onResult(false, result.message ?: "<EFBFBD><EFBFBD>卡失败")
}
is ApiResult.NetworkError -> {
Timber.w(result.exception, "NFC任务打卡: 网络异常")
vibrationController.executeByPlanId(PLAN_PUNCH_FAIL)
onResult(false, "网络异常")
}
}
isScanning = false
}
}
// 启动超时
startTimeout {
Timber.d("NFC任务打卡: 超时自动关闭")
closeNfc()
onResult(false, "<EFBFBD><EFBFBD><EFBFBD>")
isScanning = false
}
}
/**
* 场景 2+3主动打卡<E58DA1><EFBC88><EFBFBD>回键触发
* 开启 NFC → 读卡 → checkNfcType → 批量打卡 or 硬件开锁
*
* @param onResult 结果回调主线程success + message
*/
fun startActivePunch(onResult: (success: Boolean, message: String) -> Unit) {
if (isScanning) {
Timber.d("NFC主动打卡: 已在扫<E59CA8><E689AB>忽略")
return
}
isScanning = true
Timber.d("NFC主动<EFBFBD><EFBFBD>卡: 开启(返回键触发)")
// 开启 NFC + <20><>
vibrationController.executeByPlanId(PLAN_NFC_OPEN)
nfcController.open()
// 开始扫描
nfcController.startScan { nfcId ->
Timber.d("NFC主动打卡: 读到<E8AFBB><E588B0><EFBFBD>号 %s", nfcId)
cancelTimeout()
nfcController.stopScan()
nfcController.close()
vibrationController.executeByPlanId(PLAN_NFC_CLOSE)
// 判断类型:批量打卡 or 硬件开锁
scope.launch {
handleNfcType(nfcId, onResult)
isScanning = false
}
}
// 启动超时
startTimeout {
Timber.d("NFC主动打卡: 超时自动关闭")
closeNfc()
vibrationController.executeByPlanId(PLAN_NFC_CLOSE)
onResult(false, "")
isScanning = false
}
}
/** 取消 NFC 扫描 */
fun cancel() {
if (!isScanning) return
Timber.d("NFC任务: 手动取消")
cancelTimeout()
closeNfc()
vibrationController.executeByPlanId(PLAN_NFC_CLOSE)
isScanning = false
}
/**
* 判断 NFC 类型并处理
* hardwareNfcFlag=true → 硬件开锁<EFBC88><E692AD><EFBFBD>语音
* hardwareNfcFlag=false → 批量任务打卡(调 API
*/
private suspend fun handleNfcType(nfcId: String, onResult: (Boolean, String) -> Unit) {
val result = safeApiCall { taskApi.checkNfcType(nfcId) }
when (result) {
is ApiResult.Success -> {
val data = result.data
if (data == null) {
onResult(false, "数据异常")
return
}
if (data.hardwareNfcFlag) {
// 硬件开锁
handleHardwareUnlock(data.status, onResult)
} else {
// 批量任务打卡
handleBatchPunch(nfcId, onResult)
}
}
is ApiResult.Error -> {
Timber.w("NFC主动打卡: checkNfcType 失败 - %s", result.message)
vibrationController.executeByPlanId(PLAN_PUNCH_FAIL)
onResult(false, result.message ?: "判断失败")
}
is ApiResult.NetworkError -> {
Timber.w(result.exception, "NFC主动打卡: 网络异常")
vibrationController.executeByPlanId(PLAN_PUNCH_FAIL)
onResult(false, "网络异常")
}
}
}
/** 硬件开锁:根据 status 播放对应语音 */
private fun handleHardwareUnlock(status: Int, onResult: (Boolean, String) -> Unit) {
Timber.d("NFC<EFBFBD><EFBFBD>动打卡: 硬件开锁, status=%d", status)
when (status) {
0 -> {
vibrationController.executeByPlanId(PLAN_OPENING)
onResult(true, "正在开锁")
}
1 -> {
vibrationController.executeByPlanId(PLAN_OFFLINE)
onResult(false, "设备离线")
}
2 -> {
vibrationController.executeByPlanId(PLAN_OPEN_FAILED)
onResult(false, "开锁失败")
}
3 -> {
vibrationController.executeByPlanId(PLAN_NO_AUTH)
onResult(false, "无开锁权限")
}
else -> {
onResult(false, "<EFBFBD><EFBFBD>知状态")
}
}
}
/** 批量任务打卡 */
private suspend fun handleBatchPunch(nfcId: String, onResult: (Boolean, String) -> Unit) {
Timber.d("NFC主动打卡: 批量打卡, nfcId=%s", nfcId)
val params = hashMapOf<String, Any>("nfcId" to nfcId)
val result = safeApiCall { taskApi.nfcBatchBeginTask(params) }
when (result) {
is ApiResult.Success -> {
Timber.d("NFC主动打卡: 批量打卡成功")
vibrationController.executeByPlanId(PLAN_PUNCH_SUCCESS)
onResult(true, "打卡成功")
}
is ApiResult.Error -> {
Timber.w("NFC主动打卡: 批量打卡失败 - %s", result.message)
vibrationController.executeByPlanId(PLAN_PUNCH_FAIL)
onResult(false, result.message ?: "打卡失败")
}
is ApiResult.NetworkError -> {
Timber.w(result.exception, "NFC主动打卡: 网络<E7BD91><E7BB9C>")
vibrationController.executeByPlanId(PLAN_PUNCH_FAIL)
onResult(false, "网络异常")
}
}
}
/** 关闭 NFC 硬件 */
private fun closeNfc() {
nfcController.stopScan()
nfcController.close()
}
/** 启动超时协程 */
private fun startTimeout(onTimeout: () -> Unit) {
timeoutJob = scope.launch {
delay(nfcTimeoutMs)
onTimeout()
}
}
/** 取消超时协程 */
private fun cancelTimeout() {
timeoutJob?.cancel()
timeoutJob = null
}
}