feat:消费机更新
feat:人员调用钱包部门不调动的bug
This commit is contained in:
1 parent
842402e364
commit
60d9380a65
144 files changed
+27832
-1666
No files matched your search
@@ -5,6 +5,7 @@ import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
// face
|
||||
@dao
|
||||
abstract class FaceDao {
|
||||
/// 保存一条已完成文件和原生特征校验的人脸记录。
|
||||
@insert
|
||||
Future<int?> save(FaceEntity entity);
|
||||
|
||||
@@ -48,6 +49,59 @@ abstract class FaceDao {
|
||||
""")
|
||||
Future<int?> findCount(String userName);
|
||||
|
||||
/// 统计已完成质量检测且低于指定阈值的人脸数量,阈值不写入样本以支持即时重分类。
|
||||
@Query('''
|
||||
SELECT COUNT(0) FROM face
|
||||
WHERE qualityStatus = 'detected'
|
||||
AND qualityScore IS NOT NULL
|
||||
AND qualityScore < :threshold
|
||||
AND (:userName = '-1' OR userName LIKE '%' || :userName || '%')
|
||||
''')
|
||||
Future<int?> findLowQualityCount(String userName, double threshold);
|
||||
|
||||
/// 分页查询已检测且低于阈值的人脸,结果按本地自增编号稳定排序。
|
||||
@Query('''
|
||||
SELECT * FROM face
|
||||
WHERE qualityStatus = 'detected'
|
||||
AND qualityScore IS NOT NULL
|
||||
AND qualityScore < :threshold
|
||||
AND (:userName = '-1' OR userName LIKE '%' || :userName || '%')
|
||||
ORDER BY id ASC
|
||||
LIMIT :pageSize OFFSET :offset
|
||||
''')
|
||||
Future<List<FaceEntity>> findLowQuality(
|
||||
String userName,
|
||||
double threshold,
|
||||
int pageSize,
|
||||
int offset,
|
||||
);
|
||||
|
||||
/// 查询全部已检测且低于阈值的人脸,用于一键删除前获取完整服务端删除集合。
|
||||
@Query('''
|
||||
SELECT * FROM face
|
||||
WHERE qualityStatus = 'detected'
|
||||
AND qualityScore IS NOT NULL
|
||||
AND qualityScore < :threshold
|
||||
ORDER BY id ASC
|
||||
''')
|
||||
Future<List<FaceEntity>> findAllLowQuality(double threshold);
|
||||
|
||||
/// 统计尚未完成质量检测的历史样本数量。
|
||||
@Query("SELECT COUNT(0) FROM face WHERE qualityStatus = 'pending'")
|
||||
Future<int?> findPendingQualityCount();
|
||||
|
||||
/// 统计最近一次质量检测失败的样本数量。
|
||||
@Query("SELECT COUNT(0) FROM face WHERE qualityStatus = 'failed'")
|
||||
Future<int?> findFailedQualityCount();
|
||||
|
||||
/// 查询待检测或失败后重试的本地样本,按编号稳定排序。
|
||||
@Query("""
|
||||
SELECT * FROM face
|
||||
WHERE qualityStatus IN ('pending', 'failed')
|
||||
ORDER BY id ASC
|
||||
""")
|
||||
Future<List<FaceEntity>> findQualityAuditCandidates();
|
||||
|
||||
@Query('''
|
||||
SELECT * FROM face WHERE id = :id
|
||||
''')
|
||||
@@ -75,6 +129,12 @@ abstract class FaceDao {
|
||||
int offset,
|
||||
);
|
||||
|
||||
/// 查询本机全部人脸,用于完整部门快照计算集合差异。
|
||||
@Query('''
|
||||
SELECT * FROM face ORDER BY sampleId ASC
|
||||
''')
|
||||
Future<List<FaceEntity>> getAll();
|
||||
|
||||
@Query('''
|
||||
SELECT DISTINCT a.deptId FROM face a WHERE a.deptId IS NOT NULL
|
||||
''')
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `file_upload_record` 本机文件上传状态的数据访问接口。
|
||||
@dao
|
||||
abstract class FileUploadRecordDao {
|
||||
/// 保存文件状态并返回主键;相同主键时替换原记录以保留同一路径的最新状态。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<int> save(FileUploadRecordEntity entity);
|
||||
|
||||
/// 按主键更新已经存在的文件上传状态。
|
||||
@update
|
||||
Future<void> edit(FileUploadRecordEntity entity);
|
||||
|
||||
/// 查询指定设备与业务类型的全部文件状态,并按最近更新时间倒序返回。
|
||||
@Query('''
|
||||
SELECT * FROM file_upload_record
|
||||
WHERE hardId = :hardId AND bizType = :bizType
|
||||
ORDER BY updateTime DESC
|
||||
''')
|
||||
Future<List<FileUploadRecordEntity>> getAll(
|
||||
String hardId,
|
||||
String bizType,
|
||||
);
|
||||
|
||||
/// 按设备、业务类型和相对路径查询唯一状态记录;不存在时返回空。
|
||||
@Query('''
|
||||
SELECT * FROM file_upload_record
|
||||
WHERE hardId = :hardId
|
||||
AND bizType = :bizType
|
||||
AND relativePath = :relativePath
|
||||
LIMIT 1
|
||||
''')
|
||||
Future<FileUploadRecordEntity?> getByPath(
|
||||
String hardId,
|
||||
String bizType,
|
||||
String relativePath,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `order_evidence_upload_task` 本地补传队列的数据访问接口。
|
||||
@dao
|
||||
abstract class OrderEvidenceUploadTaskDao {
|
||||
/// 保存任务并返回主键;同一主键冲突时以最新状态整体替换。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<int> save(OrderEvidenceUploadTaskEntity entity);
|
||||
|
||||
/// 按主键更新已经存在的任务,不改变证据位置唯一键。
|
||||
@update
|
||||
Future<void> edit(OrderEvidenceUploadTaskEntity entity);
|
||||
|
||||
/// 按订单号和证据位置查询唯一任务;不存在时返回空。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE androidOrderCode = :androidOrderCode
|
||||
AND evidenceType = :evidenceType
|
||||
AND evidenceRole = :evidenceRole
|
||||
AND sequenceNo = :sequenceNo
|
||||
LIMIT 1
|
||||
''')
|
||||
Future<OrderEvidenceUploadTaskEntity?> getByPosition(
|
||||
String androidOrderCode,
|
||||
String evidenceType,
|
||||
String evidenceRole,
|
||||
int sequenceNo,
|
||||
);
|
||||
|
||||
/// 按服务端订单 ID 和证据位置查询预约核销任务;不存在时返回空。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE serverOrderId = :serverOrderId
|
||||
AND evidenceType = :evidenceType
|
||||
AND evidenceRole = :evidenceRole
|
||||
AND sequenceNo = :sequenceNo
|
||||
LIMIT 1
|
||||
''')
|
||||
Future<OrderEvidenceUploadTaskEntity?> getByServerOrderPosition(
|
||||
String serverOrderId,
|
||||
String evidenceType,
|
||||
String evidenceRole,
|
||||
int sequenceNo,
|
||||
);
|
||||
|
||||
/// 查询同一终端订单的全部任务,按类型、角色和序号稳定排序。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE androidOrderCode = :androidOrderCode
|
||||
ORDER BY evidenceType, evidenceRole, sequenceNo
|
||||
''')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getByOrderCode(
|
||||
String androidOrderCode,
|
||||
);
|
||||
|
||||
/// 查询同一识别会话创建的参考图与现场图任务,用于支付前绑定订单或取消清理。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE recognitionSessionId = :recognitionSessionId
|
||||
ORDER BY evidenceRole, sequenceNo
|
||||
''')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getByRecognitionSession(
|
||||
String recognitionSessionId,
|
||||
);
|
||||
|
||||
/// 查询到达重试时间的活动任务,每轮最多由调用方传入 20 条并按创建时间串行处理。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE status IN ('WAITING_ORDER', 'READY', 'UPLOADING', 'RETRY_WAIT')
|
||||
AND (androidOrderCode IS NOT NULL OR serverOrderId IS NOT NULL)
|
||||
AND (nextRetryAt IS NULL OR nextRetryAt <= :now)
|
||||
ORDER BY createTime ASC, id ASC
|
||||
LIMIT :limit
|
||||
''')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getDueTasks(
|
||||
DateTime now,
|
||||
int limit,
|
||||
);
|
||||
|
||||
/// 查询需要向服务端报告永久缺失的已绑定终态任务,已报告记录不会重复返回。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE status IN ('PERMANENT_FAILED', 'EXPIRED')
|
||||
AND (androidOrderCode IS NOT NULL OR serverOrderId IS NOT NULL)
|
||||
AND unavailableReportedAt IS NULL
|
||||
AND (nextRetryAt IS NULL OR nextRetryAt <= :now)
|
||||
ORDER BY updateTime ASC, id ASC
|
||||
LIMIT :limit
|
||||
''')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getUnavailableReportTasks(
|
||||
DateTime now,
|
||||
int limit,
|
||||
);
|
||||
|
||||
/// 查询已超过 24 小时绑定期限的等待订单任务,调用方删除图片后转为 EXPIRED。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE status = 'WAITING_ORDER' AND expiresAt <= :now
|
||||
ORDER BY expiresAt ASC
|
||||
''')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getExpiredWaitingTasks(
|
||||
DateTime now,
|
||||
);
|
||||
|
||||
/// 查询已超过 30 天图片保留期且仍持有现场文件的非成功任务。
|
||||
@Query('''
|
||||
SELECT * FROM order_evidence_upload_task
|
||||
WHERE localFilePath IS NOT NULL
|
||||
AND status NOT IN ('SUCCESS', 'CANCELLED')
|
||||
AND expiresAt <= :now
|
||||
ORDER BY expiresAt ASC
|
||||
''')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getExpiredImageTasks(
|
||||
DateTime now,
|
||||
);
|
||||
|
||||
/// 删除完成时间早于指定时间的成功任务元数据,其他失败终态不自动删除。
|
||||
@Query('''
|
||||
DELETE FROM order_evidence_upload_task
|
||||
WHERE status = 'SUCCESS'
|
||||
AND completedAt IS NOT NULL
|
||||
AND completedAt < :before
|
||||
''')
|
||||
Future<void> deleteSuccessfulBefore(DateTime before);
|
||||
|
||||
/// 查询全部任务,仅用于迁移验证和针对性测试,不参与生产批量处理。
|
||||
@Query('SELECT * FROM order_evidence_upload_task ORDER BY id ASC')
|
||||
Future<List<OrderEvidenceUploadTaskEntity>> getAll();
|
||||
}
|
||||
@@ -31,4 +31,24 @@ abstract class PrintHistoryDao {
|
||||
(:status = '-1' OR status = :status)
|
||||
''')
|
||||
Future<List<PrintHistoryEntity>> getAll(String status);
|
||||
|
||||
/// 以服务端任务 ID 唯一定位本地记录,作为 MQTT/HTTP 双通道去重依据。
|
||||
@Query(
|
||||
'SELECT * FROM print_history WHERE servicePrintId = :servicePrintId LIMIT 1')
|
||||
Future<PrintHistoryEntity?> findByServicePrintId(String servicePrintId);
|
||||
|
||||
/// 查询等待补报成功或失败结果的任务。
|
||||
@Query(
|
||||
'SELECT * FROM print_history WHERE resultPending = 1 ORDER BY createTime ASC')
|
||||
Future<List<PrintHistoryEntity>> getPendingResults();
|
||||
|
||||
@Query("SELECT COUNT(*) FROM print_history WHERE status = 'NONE'")
|
||||
Future<int?> countPending();
|
||||
|
||||
@Query("SELECT COUNT(*) FROM print_history WHERE status = 'ERROR'")
|
||||
Future<int?> countFailed();
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM print_history WHERE lastError IS NOT NULL AND lastError != '' ORDER BY id DESC LIMIT 1")
|
||||
Future<PrintHistoryEntity?> findLatestError();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `scheduled_task` 配置和最近执行快照的数据访问接口。
|
||||
@dao
|
||||
abstract class ScheduledTaskDao {
|
||||
/// 按 taskCode 保存单条任务;主键冲突时以新配置和执行快照整体替换旧记录。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> save(ScheduledTaskEntity entity);
|
||||
|
||||
/// 批量补齐默认任务;每条记录均按 taskCode 使用替换冲突策略。
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> batchSave(List<ScheduledTaskEntity> entities);
|
||||
|
||||
/// 按实体主键更新一条已经存在的任务记录。
|
||||
@update
|
||||
Future<void> edit(ScheduledTaskEntity entity);
|
||||
|
||||
/// 查询全部任务并按稳定 taskCode 升序返回,便于启动时构建确定性配置映射。
|
||||
@Query('SELECT * FROM scheduled_task ORDER BY taskCode')
|
||||
Future<List<ScheduledTaskEntity>> getAll();
|
||||
|
||||
/// 按稳定 taskCode 查询单条任务;不存在时返回空。
|
||||
@Query('SELECT * FROM scheduled_task WHERE taskCode = :taskCode LIMIT 1')
|
||||
Future<ScheduledTaskEntity?> getByCode(String taskCode);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// `scheduled_task_log` 结构化执行日志的数据访问接口。
|
||||
@dao
|
||||
abstract class ScheduledTaskLogDao {
|
||||
/// 新增任务开始日志并返回 SQLite 自增主键。
|
||||
@insert
|
||||
Future<int> save(ScheduledTaskLogEntity entity);
|
||||
|
||||
/// 按日志主键写回结束状态、耗时、错误和统计字段。
|
||||
@update
|
||||
Future<void> edit(ScheduledTaskLogEntity entity);
|
||||
|
||||
/// 按任务编码查询最近日志,可选依赖类型过滤,并按主键倒序限制返回数量。
|
||||
@Query('''
|
||||
SELECT * FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
AND (:sourceType = '' OR sourceType = :sourceType)
|
||||
ORDER BY id DESC
|
||||
LIMIT :limit
|
||||
''')
|
||||
Future<List<ScheduledTaskLogEntity>> getByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
int limit,
|
||||
);
|
||||
|
||||
/// 按任务编码和可选来源分页查询日志,使用主键倒序保证翻页期间最新记录始终位于前页。
|
||||
@Query('''
|
||||
SELECT * FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
AND (:sourceType = '' OR sourceType = :sourceType)
|
||||
ORDER BY id DESC
|
||||
LIMIT :pageSize OFFSET :offset
|
||||
''')
|
||||
Future<List<ScheduledTaskLogEntity>> getPageByTask(
|
||||
String taskCode,
|
||||
String sourceType,
|
||||
int pageSize,
|
||||
int offset,
|
||||
);
|
||||
|
||||
/// 统计指定任务和可选来源下的日志总数,为分页组件提供准确的总记录数。
|
||||
@Query('''
|
||||
SELECT COUNT(0) FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
AND (:sourceType = '' OR sourceType = :sourceType)
|
||||
''')
|
||||
Future<int?> countByTask(String taskCode, String sourceType);
|
||||
|
||||
/// 查询指定任务最近一条日志;从未执行时返回空。
|
||||
@Query('''
|
||||
SELECT * FROM scheduled_task_log
|
||||
WHERE taskCode = :taskCode
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
''')
|
||||
Future<ScheduledTaskLogEntity?> getLatest(String taskCode);
|
||||
|
||||
/// 终端长期常驻,执行日志只保留最近 2000 条,避免数据库无限增长。
|
||||
@Query('''
|
||||
DELETE FROM scheduled_task_log
|
||||
WHERE id NOT IN (
|
||||
SELECT id FROM scheduled_task_log ORDER BY id DESC LIMIT 2000
|
||||
)
|
||||
''')
|
||||
Future<void> trimHistory();
|
||||
}
|
||||
@@ -22,6 +22,10 @@ const entities = <Type>[
|
||||
UserEntity,
|
||||
FaceEntity,
|
||||
SettingEntity,
|
||||
ScheduledTaskEntity,
|
||||
ScheduledTaskLogEntity,
|
||||
FileUploadRecordEntity,
|
||||
OrderEvidenceUploadTaskEntity,
|
||||
];
|
||||
|
||||
const typeConverters = <Type>[
|
||||
@@ -40,7 +44,7 @@ const typeConverters = <Type>[
|
||||
BoolConverter,
|
||||
];
|
||||
|
||||
@Database(version: 9, entities: entities)
|
||||
@Database(version: 10, entities: entities)
|
||||
@TypeConverters(typeConverters)
|
||||
abstract class AppDatabase extends FloorDatabase {
|
||||
BroadcastDao get getBroadcastDao;
|
||||
@@ -56,6 +60,10 @@ abstract class AppDatabase extends FloorDatabase {
|
||||
UserDao get getUserDao;
|
||||
FaceDao get getFaceDao;
|
||||
SettingDao get getSettingDao;
|
||||
ScheduledTaskDao get getScheduledTaskDao;
|
||||
ScheduledTaskLogDao get getScheduledTaskLogDao;
|
||||
FileUploadRecordDao get getFileUploadRecordDao;
|
||||
OrderEvidenceUploadTaskDao get getOrderEvidenceUploadTaskDao;
|
||||
|
||||
static Future<AppDatabase> getDataBase() async {
|
||||
final dbPath = await AppFileTool.getDatabasePath();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -234,6 +234,195 @@ abstract class DatabaseMigration {
|
||||
''');
|
||||
});
|
||||
|
||||
|
||||
/// 从版本9到版本10的迁移(迁移自 biz_xfj_cloud):新增定时任务/任务日志/文件上传
|
||||
/// 记录/订单证据上传队列表,并为 face 表补充在库质量检测列。
|
||||
static Migration migration9To10 = Migration(9, 10, (database) async {
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS scheduled_task (
|
||||
taskCode TEXT NOT NULL PRIMARY KEY,
|
||||
enabled INTEGER NOT NULL,
|
||||
cronExpression TEXT NOT NULL,
|
||||
idleOnly INTEGER NOT NULL,
|
||||
lastRunAt TEXT,
|
||||
lastSuccessAt TEXT,
|
||||
lastStatus TEXT NOT NULL,
|
||||
lastMessage TEXT NOT NULL,
|
||||
createTime TEXT NOT NULL,
|
||||
updateTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS scheduled_task_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
taskCode TEXT NOT NULL,
|
||||
taskName TEXT NOT NULL,
|
||||
triggerType TEXT NOT NULL,
|
||||
sourceType TEXT NOT NULL,
|
||||
sourceName TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
errorType TEXT,
|
||||
errorMessage TEXT,
|
||||
errorStack TEXT,
|
||||
startedAt TEXT NOT NULL,
|
||||
finishedAt TEXT,
|
||||
durationMs INTEGER NOT NULL,
|
||||
scannedCount INTEGER,
|
||||
skippedCount INTEGER,
|
||||
successCount INTEGER,
|
||||
failureCount INTEGER
|
||||
)
|
||||
''');
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_scheduled_task_log_taskCode_startedAt '
|
||||
'ON scheduled_task_log(taskCode, startedAt)',
|
||||
);
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_scheduled_task_log_sourceType_status_startedAt '
|
||||
'ON scheduled_task_log(sourceType, status, startedAt)',
|
||||
);
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS file_upload_record (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
hardId TEXT NOT NULL,
|
||||
bizType TEXT NOT NULL,
|
||||
relativePath TEXT NOT NULL,
|
||||
originalName TEXT NOT NULL,
|
||||
sourceSize INTEGER NOT NULL,
|
||||
sourceModifiedTime TEXT NOT NULL,
|
||||
sanitizedHash TEXT NOT NULL,
|
||||
sanitizedSize INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
serverFileId TEXT,
|
||||
attemptCount INTEGER NOT NULL,
|
||||
lastAttemptAt TEXT,
|
||||
uploadedAt TEXT,
|
||||
lastError TEXT,
|
||||
createTime TEXT NOT NULL,
|
||||
updateTime TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await database.execute(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS index_file_upload_record_hardId_bizType_relativePath '
|
||||
'ON file_upload_record(hardId, bizType, relativePath)',
|
||||
);
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_file_upload_record_bizType_status_updateTime '
|
||||
'ON file_upload_record(bizType, status, updateTime)',
|
||||
);
|
||||
await database.execute('''
|
||||
CREATE TABLE IF NOT EXISTS order_evidence_upload_task (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
androidOrderCode TEXT,
|
||||
serverOrderId TEXT,
|
||||
recognitionSessionId TEXT NOT NULL,
|
||||
evidenceType TEXT NOT NULL,
|
||||
evidenceRole TEXT NOT NULL,
|
||||
sequenceNo INTEGER NOT NULL,
|
||||
userId TEXT NOT NULL,
|
||||
faceSampleId TEXT,
|
||||
referenceImageUrl TEXT,
|
||||
referenceImageVersion TEXT,
|
||||
localFilePath TEXT,
|
||||
imageHash TEXT,
|
||||
imageSize INTEGER,
|
||||
imageWidth INTEGER,
|
||||
imageHeight INTEGER,
|
||||
similarity REAL,
|
||||
similarityThreshold REAL,
|
||||
capturedAt TEXT NOT NULL,
|
||||
appVersion TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
attemptCount INTEGER NOT NULL,
|
||||
nextRetryAt TEXT,
|
||||
serverEvidenceId TEXT,
|
||||
serverImageUrl TEXT,
|
||||
lastError TEXT,
|
||||
unavailableReportedAt TEXT,
|
||||
completedAt TEXT,
|
||||
createTime TEXT NOT NULL,
|
||||
updateTime TEXT NOT NULL,
|
||||
expiresAt TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await database.execute(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS index_order_evidence_upload_task_position '
|
||||
'ON order_evidence_upload_task(androidOrderCode, evidenceType, evidenceRole, sequenceNo)',
|
||||
);
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_order_evidence_upload_task_status_nextRetryAt_createTime '
|
||||
'ON order_evidence_upload_task(status, nextRetryAt, createTime)',
|
||||
);
|
||||
|
||||
// face 表补充在库质量检测列(迁移自 biz_xfj_cloud 版本11到12迁移)。
|
||||
final columns = await database.rawQuery('PRAGMA table_info(face)');
|
||||
final names = columns.map((column) => column['name']).toSet();
|
||||
if (!names.contains('qualityScore')) {
|
||||
await database.execute('ALTER TABLE face ADD COLUMN qualityScore REAL');
|
||||
}
|
||||
if (!names.contains('qualityStatus')) {
|
||||
await database.execute(
|
||||
"ALTER TABLE face ADD COLUMN qualityStatus TEXT NOT NULL DEFAULT 'pending'",
|
||||
);
|
||||
}
|
||||
if (!names.contains('qualityCheckedAt')) {
|
||||
await database.execute(
|
||||
'ALTER TABLE face ADD COLUMN qualityCheckedAt TEXT',
|
||||
);
|
||||
}
|
||||
if (!names.contains('qualityErrorCode')) {
|
||||
await database.execute(
|
||||
'ALTER TABLE face ADD COLUMN qualityErrorCode INTEGER',
|
||||
);
|
||||
}
|
||||
await database.execute(
|
||||
"UPDATE face SET qualityStatus = 'pending' WHERE qualityStatus IS NULL OR qualityStatus NOT IN ('pending', 'detected', 'failed')",
|
||||
);
|
||||
// 项目现有 DateTimeEmptyConverter 以空字符串表示业务空值;迁移旧记录时同步规范化,
|
||||
// 避免 Floor 生成代码把 SQLite NULL 强转为 String 后读取失败。
|
||||
await database.execute(
|
||||
"UPDATE face SET qualityCheckedAt = '' WHERE qualityCheckedAt IS NULL",
|
||||
);
|
||||
await database.execute(
|
||||
'CREATE INDEX IF NOT EXISTS index_face_qualityStatus_qualityScore ON face(qualityStatus, qualityScore)',
|
||||
);
|
||||
|
||||
// print_history 升级为幂等打印任务队列(迁移自 biz_xfj_cloud 版本12到13迁移)。
|
||||
final printColumns = await database.rawQuery('PRAGMA table_info(print_history)');
|
||||
final printColumnNames = printColumns.map((column) => column['name']).toSet();
|
||||
if (!printColumnNames.contains('printType')) {
|
||||
await database.execute(
|
||||
"ALTER TABLE print_history ADD COLUMN printType TEXT NOT NULL DEFAULT '1'");
|
||||
}
|
||||
if (!printColumnNames.contains('printNo')) {
|
||||
await database.execute(
|
||||
'ALTER TABLE print_history ADD COLUMN printNo INTEGER NOT NULL DEFAULT 1');
|
||||
}
|
||||
if (!printColumnNames.contains('retryVersion')) {
|
||||
await database.execute(
|
||||
'ALTER TABLE print_history ADD COLUMN retryVersion INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
if (!printColumnNames.contains('attemptCount')) {
|
||||
await database.execute(
|
||||
'ALTER TABLE print_history ADD COLUMN attemptCount INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
if (!printColumnNames.contains('lastError')) {
|
||||
await database.execute('ALTER TABLE print_history ADD COLUMN lastError TEXT');
|
||||
}
|
||||
if (!printColumnNames.contains('resultPending')) {
|
||||
await database.execute(
|
||||
'ALTER TABLE print_history ADD COLUMN resultPending INTEGER NOT NULL DEFAULT 0');
|
||||
}
|
||||
await database.execute('''
|
||||
DELETE FROM print_history
|
||||
WHERE id NOT IN (SELECT MAX(id) FROM print_history GROUP BY servicePrintId)
|
||||
''');
|
||||
await database.execute(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS index_print_history_servicePrintId ON print_history(servicePrintId)',
|
||||
);
|
||||
});
|
||||
|
||||
static List<Migration> getDatabaseMigrations() {
|
||||
return [
|
||||
migration1To2,
|
||||
@@ -244,6 +433,7 @@ abstract class DatabaseMigration {
|
||||
migration6To7,
|
||||
migration7To8,
|
||||
migration8To9,
|
||||
migration9To10,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,18 @@ class FaceEntity extends Equatable {
|
||||
/// 同步时间
|
||||
DateTime? updateTime;
|
||||
|
||||
/// 最近一次在库图片质量分,范围为0到1;未检测或检测失败时为空。
|
||||
double? qualityScore;
|
||||
|
||||
/// 质量检测状态,持久化值为pending、detected或failed。
|
||||
String qualityStatus;
|
||||
|
||||
/// 最近一次质量检测完成时间;从未检测时为空。
|
||||
DateTime? qualityCheckedAt;
|
||||
|
||||
/// 最近一次质量检测的虹软或插件错误码;成功时为空。
|
||||
int? qualityErrorCode;
|
||||
|
||||
FaceEntity({
|
||||
this.id,
|
||||
required this.userId,
|
||||
@@ -53,6 +65,10 @@ class FaceEntity extends Equatable {
|
||||
this.faceModel,
|
||||
this.createTime,
|
||||
this.updateTime,
|
||||
this.qualityScore,
|
||||
this.qualityStatus = 'pending',
|
||||
this.qualityCheckedAt,
|
||||
this.qualityErrorCode,
|
||||
});
|
||||
|
||||
factory FaceEntity.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -22,6 +22,12 @@ FaceEntity _$FaceEntityFromJson(Map<String, dynamic> json) => FaceEntity(
|
||||
updateTime: json['updateTime'] == null
|
||||
? null
|
||||
: DateTime.parse(json['updateTime'] as String),
|
||||
qualityScore: (json['qualityScore'] as num?)?.toDouble(),
|
||||
qualityStatus: json['qualityStatus'] as String? ?? 'pending',
|
||||
qualityCheckedAt: json['qualityCheckedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['qualityCheckedAt'] as String),
|
||||
qualityErrorCode: (json['qualityErrorCode'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FaceEntityToJson(FaceEntity instance) =>
|
||||
@@ -37,4 +43,8 @@ Map<String, dynamic> _$FaceEntityToJson(FaceEntity instance) =>
|
||||
'faceModel': instance.faceModel,
|
||||
'createTime': instance.createTime?.toIso8601String(),
|
||||
'updateTime': instance.updateTime?.toIso8601String(),
|
||||
'qualityScore': instance.qualityScore,
|
||||
'qualityStatus': instance.qualityStatus,
|
||||
'qualityCheckedAt': instance.qualityCheckedAt?.toIso8601String(),
|
||||
'qualityErrorCode': instance.qualityErrorCode,
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'file_upload_record_entity.g.dart';
|
||||
|
||||
/// 本机文件上传记录。
|
||||
///
|
||||
/// 目前 [bizType] 使用 `terminal_log`,保留业务类型后可复用到数据库备份等文件任务。
|
||||
@JsonSerializable()
|
||||
@Entity(
|
||||
tableName: 'file_upload_record',
|
||||
indices: [
|
||||
Index(value: ['hardId', 'bizType', 'relativePath'], unique: true),
|
||||
Index(value: ['bizType', 'status', 'updateTime']),
|
||||
],
|
||||
)
|
||||
class FileUploadRecordEntity {
|
||||
/// SQLite 自增主键;首次保存前为空。
|
||||
@PrimaryKey(autoGenerate: true)
|
||||
final int? id;
|
||||
|
||||
/// 终端绑定的设备 ID,用于隔离设备重新绑定前后的文件状态。
|
||||
final String hardId;
|
||||
|
||||
/// 文件业务类型;终端日志固定使用 `terminal_log`。
|
||||
final String bizType;
|
||||
|
||||
/// 文件在业务根目录下使用正斜杠的相对路径。
|
||||
final String relativePath;
|
||||
|
||||
/// 原始文件名,不包含目录。
|
||||
final String originalName;
|
||||
|
||||
/// 生成脱敏副本时读取的原文件快照大小,单位为字节。
|
||||
final int sourceSize;
|
||||
|
||||
/// 生成脱敏副本时原文件的最后修改时间。
|
||||
final DateTime sourceModifiedTime;
|
||||
|
||||
/// 脱敏后文件内容的 MD5 哈希;首次尝试尚未完成脱敏时可为空字符串。
|
||||
final String sanitizedHash;
|
||||
|
||||
/// 脱敏后文件大小,单位为字节;尚未完成脱敏时为零。
|
||||
final int sanitizedSize;
|
||||
|
||||
/// 最近一次上传尝试状态,保存 [FileUploadStatus.name]。
|
||||
final String status;
|
||||
|
||||
/// 后端 hard_terminal_log_file 记录 ID;预检判定无需上传且本机无旧记录时可为空。
|
||||
final String? serverFileId;
|
||||
|
||||
/// 对该路径发起过的上传尝试次数,预检前即递增。
|
||||
final int attemptCount;
|
||||
|
||||
/// 最近一次开始上传尝试的本地时间;从未尝试时为空。
|
||||
final DateTime? lastAttemptAt;
|
||||
|
||||
/// 最近一次被服务端确认成功或无需上传的本地时间。
|
||||
final DateTime? uploadedAt;
|
||||
|
||||
/// 最近一次失败的截断错误摘要;成功记录为空。
|
||||
final String? lastError;
|
||||
|
||||
/// 当前设备与业务类型下首次创建该路径记录的本地时间。
|
||||
final DateTime createTime;
|
||||
|
||||
/// 状态、快照或服务端 ID 最近一次更新的本地时间。
|
||||
final DateTime updateTime;
|
||||
|
||||
/// 创建一条文件上传状态记录。
|
||||
const FileUploadRecordEntity({
|
||||
this.id,
|
||||
required this.hardId,
|
||||
required this.bizType,
|
||||
required this.relativePath,
|
||||
required this.originalName,
|
||||
required this.sourceSize,
|
||||
required this.sourceModifiedTime,
|
||||
required this.sanitizedHash,
|
||||
required this.sanitizedSize,
|
||||
required this.status,
|
||||
this.serverFileId,
|
||||
this.attemptCount = 0,
|
||||
this.lastAttemptAt,
|
||||
this.uploadedAt,
|
||||
this.lastError,
|
||||
required this.createTime,
|
||||
required this.updateTime,
|
||||
});
|
||||
|
||||
/// 从 JSON 数据恢复上传记录,仅供调试序列化使用。
|
||||
factory FileUploadRecordEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$FileUploadRecordEntityFromJson(json);
|
||||
|
||||
/// 将上传记录转换为 JSON,仅供调试序列化使用。
|
||||
Map<String, dynamic> toJson() => _$FileUploadRecordEntityToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'file_upload_record_entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
FileUploadRecordEntity _$FileUploadRecordEntityFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
FileUploadRecordEntity(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
hardId: json['hardId'] as String,
|
||||
bizType: json['bizType'] as String,
|
||||
relativePath: json['relativePath'] as String,
|
||||
originalName: json['originalName'] as String,
|
||||
sourceSize: (json['sourceSize'] as num).toInt(),
|
||||
sourceModifiedTime: DateTime.parse(json['sourceModifiedTime'] as String),
|
||||
sanitizedHash: json['sanitizedHash'] as String,
|
||||
sanitizedSize: (json['sanitizedSize'] as num).toInt(),
|
||||
status: json['status'] as String,
|
||||
serverFileId: json['serverFileId'] as String?,
|
||||
attemptCount: (json['attemptCount'] as num?)?.toInt() ?? 0,
|
||||
lastAttemptAt: json['lastAttemptAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastAttemptAt'] as String),
|
||||
uploadedAt: json['uploadedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['uploadedAt'] as String),
|
||||
lastError: json['lastError'] as String?,
|
||||
createTime: DateTime.parse(json['createTime'] as String),
|
||||
updateTime: DateTime.parse(json['updateTime'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$FileUploadRecordEntityToJson(
|
||||
FileUploadRecordEntity instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'hardId': instance.hardId,
|
||||
'bizType': instance.bizType,
|
||||
'relativePath': instance.relativePath,
|
||||
'originalName': instance.originalName,
|
||||
'sourceSize': instance.sourceSize,
|
||||
'sourceModifiedTime': instance.sourceModifiedTime.toIso8601String(),
|
||||
'sanitizedHash': instance.sanitizedHash,
|
||||
'sanitizedSize': instance.sanitizedSize,
|
||||
'status': instance.status,
|
||||
'serverFileId': instance.serverFileId,
|
||||
'attemptCount': instance.attemptCount,
|
||||
'lastAttemptAt': instance.lastAttemptAt?.toIso8601String(),
|
||||
'uploadedAt': instance.uploadedAt?.toIso8601String(),
|
||||
'lastError': instance.lastError,
|
||||
'createTime': instance.createTime.toIso8601String(),
|
||||
'updateTime': instance.updateTime.toIso8601String(),
|
||||
};
|
||||
@@ -82,6 +82,7 @@ const _$OrderPayTypeEnumMap = {
|
||||
OrderPayType.WALLET_DEFAULT: 'WALLET_DEFAULT',
|
||||
OrderPayType.WALLET_QR_CODE: 'WALLET_QR_CODE',
|
||||
OrderPayType.WALLET_QR_CODE_SCAN: 'WALLET_QR_CODE_SCAN',
|
||||
OrderPayType.AGGREGATE_ORDER_PAY: 'AGGREGATE_ORDER_PAY',
|
||||
OrderPayType.WALLET_CARD: 'WALLET_CARD',
|
||||
OrderPayType.FACE: 'FACE',
|
||||
OrderPayType.STRIDE_DEPT_QR_CODE: 'STRIDE_DEPT_QR_CODE',
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'package:floor/floor.dart';
|
||||
|
||||
/// 终端订单识别证据补传任务。
|
||||
///
|
||||
/// 本表是断网、崩溃和接口超时后的本地待办队列,不是最终证据库。服务端确认成功后立即删除
|
||||
/// [localFilePath] 对应的现场图片;失败图片最多保留 30 天,任务终态元数据按业务规则保留。
|
||||
@Entity(
|
||||
tableName: 'order_evidence_upload_task',
|
||||
indices: [
|
||||
Index(
|
||||
value: [
|
||||
'androidOrderCode',
|
||||
'evidenceType',
|
||||
'evidenceRole',
|
||||
'sequenceNo',
|
||||
],
|
||||
unique: true,
|
||||
),
|
||||
Index(value: ['status', 'nextRetryAt', 'createTime']),
|
||||
],
|
||||
)
|
||||
class OrderEvidenceUploadTaskEntity {
|
||||
/// SQLite 自增主键,首次保存前为空。
|
||||
@PrimaryKey(autoGenerate: true)
|
||||
final int? id;
|
||||
|
||||
/// 终端生成的稳定订单号,用于响应超时后与服务端对账。
|
||||
final String? androidOrderCode;
|
||||
|
||||
/// 对账成功后的服务端订单主键;尚未落单或请求结果不确定时为空。
|
||||
final String? serverOrderId;
|
||||
|
||||
/// 产生现场图片的人脸识别业务会话编号,用于诊断但不作为服务端幂等键。
|
||||
final String recognitionSessionId;
|
||||
|
||||
/// 证据类型,保存 `OrderEvidenceType.code`。
|
||||
final String evidenceType;
|
||||
|
||||
/// 图片角色,保存 `OrderEvidenceRole.code`。
|
||||
final String evidenceRole;
|
||||
|
||||
/// 同一订单、类型和角色下的稳定序号,从 0 开始。
|
||||
final int sequenceNo;
|
||||
|
||||
/// 识别用户主键快照,来源于最终用于下单的人员。
|
||||
final String userId;
|
||||
|
||||
/// 虹软样本编号快照;非人脸证据允许为空。
|
||||
final String? faceSampleId;
|
||||
|
||||
/// 识别时本地人脸记录保存的后台参考图片地址,仅由服务端校验和复制。
|
||||
final String? referenceImageUrl;
|
||||
|
||||
/// 识别时参考图片的版本摘要,当前使用本地人脸记录更新时间 ISO 字符串。
|
||||
final String? referenceImageVersion;
|
||||
|
||||
/// 终端私有目录中的现场 JPEG 路径;参考图、已成功或图片到期任务为空。
|
||||
final String? localFilePath;
|
||||
|
||||
/// 现场图片或已知参考图内容的 SHA-256;参考图未知内容时允许为空。
|
||||
final String? imageHash;
|
||||
|
||||
/// 现场图片大小,单位字节;参考图未下载时允许为空。
|
||||
final int? imageSize;
|
||||
|
||||
/// 现场图片宽度,单位像素;参考图未下载时允许为空。
|
||||
final int? imageWidth;
|
||||
|
||||
/// 现场图片高度,单位像素;参考图未下载时允许为空。
|
||||
final int? imageHeight;
|
||||
|
||||
/// 人脸比对相似度快照,范围 0 到 1;菜谱识别允许为空。
|
||||
final double? similarity;
|
||||
|
||||
/// 人脸比对通过时使用的阈值快照,范围 0 到 1;菜谱识别允许为空。
|
||||
final double? similarityThreshold;
|
||||
|
||||
/// 真正比对成功帧的终端采集时间。
|
||||
final DateTime capturedAt;
|
||||
|
||||
/// 产生证据时的终端应用展示版本与内部版本组合。
|
||||
final String appVersion;
|
||||
|
||||
/// 当前任务状态,保存 `OrderEvidenceTaskStatus.code`。
|
||||
final String status;
|
||||
|
||||
/// 已发起的服务端对账、提交或上传尝试次数,用于计算退避间隔。
|
||||
final int attemptCount;
|
||||
|
||||
/// 下一次允许自动处理的本地时间;立即可处理时为空。
|
||||
final DateTime? nextRetryAt;
|
||||
|
||||
/// 服务端证据记录主键;尚未创建或永久缺失上报未返回时为空。
|
||||
final String? serverEvidenceId;
|
||||
|
||||
/// 服务端最终证据图片地址;非 AVAILABLE 或参考复制失败时为空。
|
||||
final String? serverImageUrl;
|
||||
|
||||
/// 最近一次失败的单行截断摘要,不包含 Token、原始帧、完整路径或图片内容。
|
||||
final String? lastError;
|
||||
|
||||
/// 终端已向服务端成功报告 UNAVAILABLE 的时间;未报告或无订单时为空。
|
||||
final DateTime? unavailableReportedAt;
|
||||
|
||||
/// 服务端确认 AVAILABLE 的完成时间;其他状态为空。
|
||||
final DateTime? completedAt;
|
||||
|
||||
/// 任务首次创建时间,用于 24 小时未绑定订单和终态保留策略。
|
||||
final DateTime createTime;
|
||||
|
||||
/// 状态、重试时间或服务端结果最近一次更新时间。
|
||||
final DateTime updateTime;
|
||||
|
||||
/// 当前图片或未绑定任务的本地到期时间;绑定订单后延长为现场图片 30 天。
|
||||
final DateTime expiresAt;
|
||||
|
||||
/// 创建一条可持久化的订单证据补传任务。
|
||||
const OrderEvidenceUploadTaskEntity({
|
||||
this.id,
|
||||
this.androidOrderCode,
|
||||
this.serverOrderId,
|
||||
required this.recognitionSessionId,
|
||||
required this.evidenceType,
|
||||
required this.evidenceRole,
|
||||
required this.sequenceNo,
|
||||
required this.userId,
|
||||
this.faceSampleId,
|
||||
this.referenceImageUrl,
|
||||
this.referenceImageVersion,
|
||||
this.localFilePath,
|
||||
this.imageHash,
|
||||
this.imageSize,
|
||||
this.imageWidth,
|
||||
this.imageHeight,
|
||||
this.similarity,
|
||||
this.similarityThreshold,
|
||||
required this.capturedAt,
|
||||
required this.appVersion,
|
||||
required this.status,
|
||||
this.attemptCount = 0,
|
||||
this.nextRetryAt,
|
||||
this.serverEvidenceId,
|
||||
this.serverImageUrl,
|
||||
this.lastError,
|
||||
this.unavailableReportedAt,
|
||||
this.completedAt,
|
||||
required this.createTime,
|
||||
required this.updateTime,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
/// 保留证据位置和识别快照,生成包含指定状态变化的新任务。
|
||||
OrderEvidenceUploadTaskEntity copyWith({
|
||||
String? androidOrderCode,
|
||||
String? serverOrderId,
|
||||
String? localFilePath,
|
||||
bool clearLocalFilePath = false,
|
||||
String? imageHash,
|
||||
int? imageSize,
|
||||
int? imageWidth,
|
||||
int? imageHeight,
|
||||
String? status,
|
||||
int? attemptCount,
|
||||
DateTime? nextRetryAt,
|
||||
bool clearNextRetryAt = false,
|
||||
String? serverEvidenceId,
|
||||
String? serverImageUrl,
|
||||
String? lastError,
|
||||
bool clearLastError = false,
|
||||
DateTime? unavailableReportedAt,
|
||||
DateTime? completedAt,
|
||||
DateTime? updateTime,
|
||||
DateTime? expiresAt,
|
||||
}) {
|
||||
return OrderEvidenceUploadTaskEntity(
|
||||
id: id,
|
||||
androidOrderCode: androidOrderCode ?? this.androidOrderCode,
|
||||
serverOrderId: serverOrderId ?? this.serverOrderId,
|
||||
recognitionSessionId: recognitionSessionId,
|
||||
evidenceType: evidenceType,
|
||||
evidenceRole: evidenceRole,
|
||||
sequenceNo: sequenceNo,
|
||||
userId: userId,
|
||||
faceSampleId: faceSampleId,
|
||||
referenceImageUrl: referenceImageUrl,
|
||||
referenceImageVersion: referenceImageVersion,
|
||||
localFilePath:
|
||||
clearLocalFilePath ? null : localFilePath ?? this.localFilePath,
|
||||
imageHash: imageHash ?? this.imageHash,
|
||||
imageSize: imageSize ?? this.imageSize,
|
||||
imageWidth: imageWidth ?? this.imageWidth,
|
||||
imageHeight: imageHeight ?? this.imageHeight,
|
||||
similarity: similarity,
|
||||
similarityThreshold: similarityThreshold,
|
||||
capturedAt: capturedAt,
|
||||
appVersion: appVersion,
|
||||
status: status ?? this.status,
|
||||
attemptCount: attemptCount ?? this.attemptCount,
|
||||
nextRetryAt: clearNextRetryAt ? null : nextRetryAt ?? this.nextRetryAt,
|
||||
serverEvidenceId: serverEvidenceId ?? this.serverEvidenceId,
|
||||
serverImageUrl: serverImageUrl ?? this.serverImageUrl,
|
||||
lastError: clearLastError ? null : lastError ?? this.lastError,
|
||||
unavailableReportedAt:
|
||||
unavailableReportedAt ?? this.unavailableReportedAt,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
createTime: createTime,
|
||||
updateTime: updateTime ?? this.updateTime,
|
||||
expiresAt: expiresAt ?? this.expiresAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,12 @@ import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
part 'print_history_entity.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@Entity(tableName: "print_history")
|
||||
@Entity(
|
||||
tableName: "print_history",
|
||||
indices: [
|
||||
Index(value: ['servicePrintId'], unique: true)
|
||||
],
|
||||
)
|
||||
class PrintHistoryEntity {
|
||||
/// 自增ID
|
||||
@PrimaryKey(autoGenerate: true)
|
||||
@@ -24,6 +29,18 @@ class PrintHistoryEntity {
|
||||
/// 发票图片地址
|
||||
String invoiceUrl;
|
||||
|
||||
/// 服务端凭证类型、打印序号与重试版本。
|
||||
String printType;
|
||||
int printNo;
|
||||
int retryVersion;
|
||||
|
||||
/// 当前重试版本实际执行的 USB 尝试次数和最近错误。
|
||||
int attemptCount;
|
||||
String? lastError;
|
||||
|
||||
/// 打印结果尚未可靠提交到服务端时保持为 true,网络恢复后继续补报。
|
||||
bool resultPending;
|
||||
|
||||
/// 打印状态
|
||||
PrintStatus status;
|
||||
|
||||
@@ -42,6 +59,12 @@ class PrintHistoryEntity {
|
||||
required this.serviceOrderId,
|
||||
required this.servicePrintId,
|
||||
required this.invoiceUrl,
|
||||
this.printType = '1',
|
||||
this.printNo = 1,
|
||||
this.retryVersion = 0,
|
||||
this.attemptCount = 0,
|
||||
this.lastError,
|
||||
this.resultPending = false,
|
||||
required this.status,
|
||||
this.printTime,
|
||||
this.syncTime,
|
||||
|
||||
@@ -13,6 +13,12 @@ PrintHistoryEntity _$PrintHistoryEntityFromJson(Map<String, dynamic> json) =>
|
||||
serviceOrderId: json['serviceOrderId'] as String,
|
||||
servicePrintId: json['servicePrintId'] as String,
|
||||
invoiceUrl: json['invoiceUrl'] as String,
|
||||
printType: json['printType'] as String? ?? '1',
|
||||
printNo: (json['printNo'] as num?)?.toInt() ?? 1,
|
||||
retryVersion: (json['retryVersion'] as num?)?.toInt() ?? 0,
|
||||
attemptCount: (json['attemptCount'] as num?)?.toInt() ?? 0,
|
||||
lastError: json['lastError'] as String?,
|
||||
resultPending: json['resultPending'] as bool? ?? false,
|
||||
status: $enumDecode(_$PrintStatusEnumMap, json['status']),
|
||||
printTime: json['printTime'] as String?,
|
||||
syncTime: json['syncTime'] == null
|
||||
@@ -28,6 +34,12 @@ Map<String, dynamic> _$PrintHistoryEntityToJson(PrintHistoryEntity instance) =>
|
||||
'serviceOrderId': instance.serviceOrderId,
|
||||
'servicePrintId': instance.servicePrintId,
|
||||
'invoiceUrl': instance.invoiceUrl,
|
||||
'printType': instance.printType,
|
||||
'printNo': instance.printNo,
|
||||
'retryVersion': instance.retryVersion,
|
||||
'attemptCount': instance.attemptCount,
|
||||
'lastError': instance.lastError,
|
||||
'resultPending': instance.resultPending,
|
||||
'status': _$PrintStatusEnumMap[instance.status]!,
|
||||
'printTime': instance.printTime,
|
||||
'syncTime': instance.syncTime?.toIso8601String(),
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'scheduled_task_entity.g.dart';
|
||||
|
||||
/// 终端定时任务配置及最近一次执行快照。
|
||||
///
|
||||
/// [taskCode] 使用代码枚举名作为稳定主键,界面文案调整时不会产生重复任务。
|
||||
@JsonSerializable()
|
||||
@Entity(tableName: 'scheduled_task')
|
||||
class ScheduledTaskEntity {
|
||||
/// 稳定任务编码,取 [ScheduledTaskType.name],作为表主键且不能随展示文案变化。
|
||||
@PrimaryKey()
|
||||
final String taskCode;
|
||||
|
||||
/// 是否启用 Cron 自动调度;关闭时仍可通过设置页手动触发。
|
||||
final bool enabled;
|
||||
|
||||
/// cron 0.6.2 使用的六段表达式,字段顺序为秒、分、时、日、月、周。
|
||||
final String cronExpression;
|
||||
|
||||
/// 自动任务是否必须等待连续 60 秒无人操作的完整空闲窗口。
|
||||
final bool idleOnly;
|
||||
|
||||
/// 最近一次任务开始执行的本地时间;从未执行时为空。
|
||||
final DateTime? lastRunAt;
|
||||
|
||||
/// 最近一次成功完成的本地时间;尚无成功记录时为空。
|
||||
final DateTime? lastSuccessAt;
|
||||
|
||||
/// 最近一次运行状态,保存 [ScheduledTaskRunStatus.name]。
|
||||
final String lastStatus;
|
||||
|
||||
/// 最近一次运行结果的简短中文摘要。
|
||||
final String lastMessage;
|
||||
|
||||
/// 任务配置首次写入 SQLite 的本地时间。
|
||||
final DateTime createTime;
|
||||
|
||||
/// 配置或执行快照最近一次更新的本地时间。
|
||||
final DateTime updateTime;
|
||||
|
||||
/// 创建一条定时任务配置与最近执行快照。
|
||||
const ScheduledTaskEntity({
|
||||
required this.taskCode,
|
||||
required this.enabled,
|
||||
required this.cronExpression,
|
||||
required this.idleOnly,
|
||||
this.lastRunAt,
|
||||
this.lastSuccessAt,
|
||||
required this.lastStatus,
|
||||
required this.lastMessage,
|
||||
required this.createTime,
|
||||
required this.updateTime,
|
||||
});
|
||||
|
||||
/// 从 JSON 数据恢复实体,仅供兼容迁移和调试序列化使用。
|
||||
factory ScheduledTaskEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScheduledTaskEntityFromJson(json);
|
||||
|
||||
/// 将实体转换为 JSON,仅供兼容迁移和调试序列化使用。
|
||||
Map<String, dynamic> toJson() => _$ScheduledTaskEntityToJson(this);
|
||||
|
||||
/// 保留主键与创建时间,生成包含配置或最近执行结果变更的新实体。
|
||||
ScheduledTaskEntity copyWith({
|
||||
bool? enabled,
|
||||
String? cronExpression,
|
||||
bool? idleOnly,
|
||||
DateTime? lastRunAt,
|
||||
DateTime? lastSuccessAt,
|
||||
String? lastStatus,
|
||||
String? lastMessage,
|
||||
DateTime? updateTime,
|
||||
}) =>
|
||||
ScheduledTaskEntity(
|
||||
taskCode: taskCode,
|
||||
enabled: enabled ?? this.enabled,
|
||||
cronExpression: cronExpression ?? this.cronExpression,
|
||||
idleOnly: idleOnly ?? this.idleOnly,
|
||||
lastRunAt: lastRunAt ?? this.lastRunAt,
|
||||
lastSuccessAt: lastSuccessAt ?? this.lastSuccessAt,
|
||||
lastStatus: lastStatus ?? this.lastStatus,
|
||||
lastMessage: lastMessage ?? this.lastMessage,
|
||||
createTime: createTime,
|
||||
updateTime: updateTime ?? this.updateTime,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scheduled_task_entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ScheduledTaskEntity _$ScheduledTaskEntityFromJson(Map<String, dynamic> json) =>
|
||||
ScheduledTaskEntity(
|
||||
taskCode: json['taskCode'] as String,
|
||||
enabled: json['enabled'] as bool,
|
||||
cronExpression: json['cronExpression'] as String,
|
||||
idleOnly: json['idleOnly'] as bool,
|
||||
lastRunAt: json['lastRunAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastRunAt'] as String),
|
||||
lastSuccessAt: json['lastSuccessAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['lastSuccessAt'] as String),
|
||||
lastStatus: json['lastStatus'] as String,
|
||||
lastMessage: json['lastMessage'] as String,
|
||||
createTime: DateTime.parse(json['createTime'] as String),
|
||||
updateTime: DateTime.parse(json['updateTime'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScheduledTaskEntityToJson(
|
||||
ScheduledTaskEntity instance) =>
|
||||
<String, dynamic>{
|
||||
'taskCode': instance.taskCode,
|
||||
'enabled': instance.enabled,
|
||||
'cronExpression': instance.cronExpression,
|
||||
'idleOnly': instance.idleOnly,
|
||||
'lastRunAt': instance.lastRunAt?.toIso8601String(),
|
||||
'lastSuccessAt': instance.lastSuccessAt?.toIso8601String(),
|
||||
'lastStatus': instance.lastStatus,
|
||||
'lastMessage': instance.lastMessage,
|
||||
'createTime': instance.createTime.toIso8601String(),
|
||||
'updateTime': instance.updateTime.toIso8601String(),
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'scheduled_task_log_entity.g.dart';
|
||||
|
||||
/// 定时任务的结构化执行日志。
|
||||
///
|
||||
/// 来源、触发方式、状态和统计值使用独立列,日志页面无需解析拼接文本即可筛选。
|
||||
@JsonSerializable()
|
||||
@Entity(
|
||||
tableName: 'scheduled_task_log',
|
||||
indices: [
|
||||
Index(value: ['taskCode', 'startedAt']),
|
||||
Index(value: ['sourceType', 'status', 'startedAt']),
|
||||
],
|
||||
)
|
||||
class ScheduledTaskLogEntity {
|
||||
/// SQLite 自增主键;日志尚未插入数据库时为空。
|
||||
@PrimaryKey(autoGenerate: true)
|
||||
final int? id;
|
||||
|
||||
/// 任务稳定编码,关联 scheduled_task.taskCode,但不建立外键以兼容任务版本演进。
|
||||
final String taskCode;
|
||||
|
||||
/// 执行发生时的任务展示名称快照。
|
||||
final String taskName;
|
||||
|
||||
/// 触发来源,保存 [ScheduledTaskTriggerType.name]。
|
||||
final String triggerType;
|
||||
|
||||
/// 主要依赖类型,保存 [ScheduledTaskSourceType.name]。
|
||||
final String sourceType;
|
||||
|
||||
/// 执行发生时的依赖名称快照,例如 MQTT 消息服务或 HTTP 商品接口。
|
||||
final String sourceName;
|
||||
|
||||
/// 本次执行状态,保存 [ScheduledTaskRunStatus.name]。
|
||||
final String status;
|
||||
|
||||
/// 本次执行结果的简短中文摘要。
|
||||
final String message;
|
||||
|
||||
/// 异常运行时的 Dart 异常类型;成功或跳过时为空。
|
||||
final String? errorType;
|
||||
|
||||
/// 截断并脱敏后的异常摘要;成功或跳过时为空。
|
||||
final String? errorMessage;
|
||||
|
||||
/// 截断后的异常堆栈,最多保存调度器允许的诊断长度。
|
||||
final String? errorStack;
|
||||
|
||||
/// 任务开始执行的本地时间。
|
||||
final DateTime startedAt;
|
||||
|
||||
/// 任务结束的本地时间;执行中日志为空。
|
||||
final DateTime? finishedAt;
|
||||
|
||||
/// 从开始到结束的耗时,单位为毫秒;执行中默认为零。
|
||||
final int durationMs;
|
||||
|
||||
/// 文件类任务扫描到的文件数量;不适用的任务为空。
|
||||
final int? scannedCount;
|
||||
|
||||
/// 文件类任务因未变化或服务端已存在而跳过的数量;不适用时为空。
|
||||
final int? skippedCount;
|
||||
|
||||
/// 文件类任务实际上传成功的数量;不适用时为空。
|
||||
final int? successCount;
|
||||
|
||||
/// 文件类任务处理失败的数量;不适用时为空。
|
||||
final int? failureCount;
|
||||
|
||||
/// 创建一条任务执行日志;开始记录尚未结束时不填写结束信息。
|
||||
const ScheduledTaskLogEntity({
|
||||
this.id,
|
||||
required this.taskCode,
|
||||
required this.taskName,
|
||||
required this.triggerType,
|
||||
required this.sourceType,
|
||||
required this.sourceName,
|
||||
required this.status,
|
||||
required this.message,
|
||||
this.errorType,
|
||||
this.errorMessage,
|
||||
this.errorStack,
|
||||
required this.startedAt,
|
||||
this.finishedAt,
|
||||
this.durationMs = 0,
|
||||
this.scannedCount,
|
||||
this.skippedCount,
|
||||
this.successCount,
|
||||
this.failureCount,
|
||||
});
|
||||
|
||||
/// 从 JSON 数据恢复任务日志,仅供调试序列化使用。
|
||||
factory ScheduledTaskLogEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$ScheduledTaskLogEntityFromJson(json);
|
||||
|
||||
/// 将任务日志转换为 JSON,仅供调试序列化使用。
|
||||
Map<String, dynamic> toJson() => _$ScheduledTaskLogEntityToJson(this);
|
||||
|
||||
/// 使用最终状态、错误和统计值生成同一条日志的完成快照。
|
||||
ScheduledTaskLogEntity finish({
|
||||
required String nextStatus,
|
||||
required String nextMessage,
|
||||
required DateTime at,
|
||||
String? nextErrorType,
|
||||
String? nextErrorMessage,
|
||||
String? nextErrorStack,
|
||||
int? scanned,
|
||||
int? skipped,
|
||||
int? succeeded,
|
||||
int? failed,
|
||||
}) =>
|
||||
ScheduledTaskLogEntity(
|
||||
id: id,
|
||||
taskCode: taskCode,
|
||||
taskName: taskName,
|
||||
triggerType: triggerType,
|
||||
sourceType: sourceType,
|
||||
sourceName: sourceName,
|
||||
status: nextStatus,
|
||||
message: nextMessage,
|
||||
errorType: nextErrorType,
|
||||
errorMessage: nextErrorMessage,
|
||||
errorStack: nextErrorStack,
|
||||
startedAt: startedAt,
|
||||
finishedAt: at,
|
||||
durationMs: at.difference(startedAt).inMilliseconds,
|
||||
scannedCount: scanned,
|
||||
skippedCount: skipped,
|
||||
successCount: succeeded,
|
||||
failureCount: failed,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'scheduled_task_log_entity.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ScheduledTaskLogEntity _$ScheduledTaskLogEntityFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
ScheduledTaskLogEntity(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
taskCode: json['taskCode'] as String,
|
||||
taskName: json['taskName'] as String,
|
||||
triggerType: json['triggerType'] as String,
|
||||
sourceType: json['sourceType'] as String,
|
||||
sourceName: json['sourceName'] as String,
|
||||
status: json['status'] as String,
|
||||
message: json['message'] as String,
|
||||
errorType: json['errorType'] as String?,
|
||||
errorMessage: json['errorMessage'] as String?,
|
||||
errorStack: json['errorStack'] as String?,
|
||||
startedAt: DateTime.parse(json['startedAt'] as String),
|
||||
finishedAt: json['finishedAt'] == null
|
||||
? null
|
||||
: DateTime.parse(json['finishedAt'] as String),
|
||||
durationMs: (json['durationMs'] as num?)?.toInt() ?? 0,
|
||||
scannedCount: (json['scannedCount'] as num?)?.toInt(),
|
||||
skippedCount: (json['skippedCount'] as num?)?.toInt(),
|
||||
successCount: (json['successCount'] as num?)?.toInt(),
|
||||
failureCount: (json['failureCount'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ScheduledTaskLogEntityToJson(
|
||||
ScheduledTaskLogEntity instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'taskCode': instance.taskCode,
|
||||
'taskName': instance.taskName,
|
||||
'triggerType': instance.triggerType,
|
||||
'sourceType': instance.sourceType,
|
||||
'sourceName': instance.sourceName,
|
||||
'status': instance.status,
|
||||
'message': instance.message,
|
||||
'errorType': instance.errorType,
|
||||
'errorMessage': instance.errorMessage,
|
||||
'errorStack': instance.errorStack,
|
||||
'startedAt': instance.startedAt.toIso8601String(),
|
||||
'finishedAt': instance.finishedAt?.toIso8601String(),
|
||||
'durationMs': instance.durationMs,
|
||||
'scannedCount': instance.scannedCount,
|
||||
'skippedCount': instance.skippedCount,
|
||||
'successCount': instance.successCount,
|
||||
'failureCount': instance.failureCount,
|
||||
};
|
||||
Reference in new issue
Block a user