Files
biz_xfj_cloud/lib/common/services/database/database_migration.dart
T
2026-09-04 13:15:55 +08:00

431 lines
16 KiB
Dart

import 'package:floor/floor.dart';
/// 收银终端 SQLite 连续版本迁移清单,禁止跳过中间版本或在原生层重复执行结构 SQL。
abstract class DatabaseMigration {
/// 从版本1到版本2的迁移:添加productNamePy字段
static Migration migration1To2 = Migration(1, 2, (database) async {
// 先检查列是否存在,不存在才添加
final result = await database.rawQuery(
"PRAGMA table_info(supermarket_spu)",
);
bool columnExists = false;
for (var column in result) {
if (column['name'] == 'productNamePy') {
columnExists = true;
break;
}
}
if (!columnExists) {
await database
.execute('ALTER TABLE supermarket_spu ADD COLUMN productNamePy TEXT');
}
});
/// 从版本2到版本3的迁移:添加orderListJson字段
static Migration migration2To3 = Migration(2, 3, (database) async {
// 如果表不存在则创建,存在则校验列并补齐缺失项
final tables = await database.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='order_hold_bill'",
);
if (tables.isEmpty) {
await database.execute('''
CREATE TABLE IF NOT EXISTS order_hold_bill (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sysType TEXT NOT NULL,
carListJson TEXT,
userJson TEXT,
price INTEGER DEFAULT 0 NOT NULL,
extraPrice INTEGER DEFAULT 0 NOT NULL,
payPrice INTEGER DEFAULT 0 NOT NULL,
createTime TEXT NOT NULL
)
''');
await database.execute(
"CREATE INDEX IF NOT EXISTS idx_order_hold_bill_sysType ON order_hold_bill(sysType)",
);
await database.execute(
"CREATE INDEX IF NOT EXISTS idx_order_hold_bill_createTime ON order_hold_bill(createTime)",
);
} else {
final columns =
await database.rawQuery("PRAGMA table_info(order_hold_bill)");
bool hasSysType = false;
bool hasCarListJson = false;
bool hasOrderListJson = false;
bool hasUserJson = false;
bool hasPrice = false;
bool hasExtraPrice = false;
bool hasPayPrice = false;
bool hasCreateTime = false;
for (final c in columns) {
final name = c['name'];
if (name == 'sysType') hasSysType = true;
if (name == 'carListJson') hasCarListJson = true;
if (name == 'orderListJson') hasOrderListJson = true;
if (name == 'userJson') hasUserJson = true;
if (name == 'price') hasPrice = true;
if (name == 'extraPrice') hasExtraPrice = true;
if (name == 'payPrice') hasPayPrice = true;
if (name == 'createTime') hasCreateTime = true;
}
if (!hasSysType) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN sysType TEXT',
);
}
if (!hasCarListJson) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN carListJson TEXT',
);
if (hasOrderListJson) {
// 旧列迁移到新列
await database.execute(
"UPDATE order_hold_bill SET carListJson = orderListJson WHERE carListJson IS NULL",
);
}
}
if (!hasUserJson) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN userJson TEXT',
);
}
if (!hasPrice) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN price INTEGER DEFAULT 0 NOT NULL',
);
}
if (!hasExtraPrice) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN extraPrice INTEGER DEFAULT 0 NOT NULL',
);
}
if (!hasPayPrice) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN payPrice INTEGER DEFAULT 0 NOT NULL',
);
}
if (!hasCreateTime) {
await database.execute(
'ALTER TABLE order_hold_bill ADD COLUMN createTime TEXT DEFAULT "" NOT NULL',
);
}
await database.execute(
"CREATE INDEX IF NOT EXISTS idx_order_hold_bill_sysType ON order_hold_bill(sysType)",
);
await database.execute(
"CREATE INDEX IF NOT EXISTS idx_order_hold_bill_createTime ON order_hold_bill(createTime)",
);
}
});
/// 从版本3到版本4的迁移:添加为user_record表添加discountsList字段
static Migration migration3To4 = Migration(3, 4, (database) async {
final columns = await database.rawQuery("PRAGMA table_info(user_record)");
bool hasDiscountsList = false;
for (final c in columns) {
if (c['name'] == 'discountsList') {
hasDiscountsList = true;
break;
}
}
if (!hasDiscountsList) {
await database
.execute("ALTER TABLE user_record ADD COLUMN discountsList TEXT");
await database.execute(
"UPDATE user_record SET discountsList = '[]' WHERE discountsList IS NULL",
);
}
});
static Migration migration4To5 = Migration(4, 5, (database) async {
final columns = await database.rawQuery("PRAGMA table_info(user_record)");
bool hasDiscountsUpdateTime = false;
for (final c in columns) {
if (c['name'] == 'discountsUpdateTime') {
hasDiscountsUpdateTime = true;
break;
}
}
if (!hasDiscountsUpdateTime) {
await database.execute(
"ALTER TABLE user_record ADD COLUMN discountsUpdateTime TEXT");
}
});
static Migration migration5To6 = Migration(5, 6, (database) async {
final columns = await database.rawQuery("PRAGMA table_info(canteen_spu)");
bool hasIsPriceIncreased = false;
for (final c in columns) {
if (c['name'] == 'isPriceIncreased') {
hasIsPriceIncreased = true;
break;
}
}
if (!hasIsPriceIncreased) {
await database.execute(
"ALTER TABLE canteen_spu ADD COLUMN isPriceIncreased INTEGER NOT NULL DEFAULT 0",
);
}
});
static Migration migration6To7 = Migration(6, 7, (database) async {
final columns = await database.rawQuery("PRAGMA table_info(shop_card)");
bool hasIsPriceIncreased = false;
for (final c in columns) {
if (c['name'] == 'isPriceIncreased') {
hasIsPriceIncreased = true;
break;
}
}
if (!hasIsPriceIncreased) {
await database.execute(
"ALTER TABLE shop_card ADD COLUMN isPriceIncreased INTEGER NOT NULL DEFAULT 0",
);
}
});
/// 版本8将调度配置、执行日志和文件上传状态从 sysJson 收敛为可查询的结构化表。
static Migration migration7To8 = Migration(7, 8, (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)',
);
});
/// 版本9增加商品类型并将套餐组成拆分为可查询的一对多明细表。
static Migration migration8To9 = Migration(8, 9, (database) async {
final columns = await database.rawQuery("PRAGMA table_info(canteen_spu)");
final hasSpuType = columns.any((column) => column['name'] == 'spuType');
if (!hasSpuType) {
await database.execute(
'ALTER TABLE canteen_spu ADD COLUMN spuType INTEGER NOT NULL DEFAULT 1',
);
}
await database.execute('''
CREATE TABLE IF NOT EXISTS canteen_spu_combo_item (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parentServiceUniqueCode TEXT NOT NULL,
saleDate TEXT NOT NULL,
childServiceUniqueCode TEXT NOT NULL,
name TEXT NOT NULL,
image TEXT,
quantity INTEGER NOT NULL,
sort INTEGER NOT NULL,
price INTEGER NOT NULL
)
''');
await database.execute(
'CREATE INDEX IF NOT EXISTS index_canteen_spu_combo_item_parentServiceUniqueCode_saleDate '
'ON canteen_spu_combo_item(parentServiceUniqueCode, saleDate)',
);
await database.execute(
'CREATE UNIQUE INDEX IF NOT EXISTS index_canteen_spu_combo_item_parentServiceUniqueCode_saleDate_childServiceUniqueCode '
'ON canteen_spu_combo_item(parentServiceUniqueCode, saleDate, childServiceUniqueCode)',
);
});
/// 版本10新增订单识别证据补传队列,长期证据仍由服务端从表保存。
static Migration migration9To10 = Migration(9, 10, (database) async {
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)',
);
});
/// 版本11为食堂每日菜谱快照增加可空库存,旧记录库存未知且不会阻断售卖。
static Migration migration10To11 = Migration(10, 11, (database) async {
final columns = await database.rawQuery("PRAGMA table_info(canteen_spu)");
final hasStock = columns.any((column) => column['name'] == 'stock');
if (!hasStock) {
await database.execute(
'ALTER TABLE canteen_spu ADD COLUMN stock INTEGER',
);
}
});
/// 版本12为本地人脸增加质量诊断结果;全部字段可空或带默认值,不影响历史识别数据。
static Migration migration11To12 = Migration(11, 12, (database) async {
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)',
);
});
/// 版本13把旧发票记录升级为可幂等恢复、可补报结果的打印任务队列。
static Migration migration12To13 = Migration(12, 13, (database) async {
await database.execute(
"ALTER TABLE print_history ADD COLUMN printType TEXT NOT NULL DEFAULT '1'");
await database.execute(
'ALTER TABLE print_history ADD COLUMN printNo INTEGER NOT NULL DEFAULT 1');
await database.execute(
'ALTER TABLE print_history ADD COLUMN retryVersion INTEGER NOT NULL DEFAULT 0');
await database.execute(
'ALTER TABLE print_history ADD COLUMN attemptCount INTEGER NOT NULL DEFAULT 0');
await database
.execute('ALTER TABLE print_history ADD COLUMN lastError TEXT');
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)',
);
});
/// 按起始版本升序返回全部迁移,Floor 打开旧数据库时据此连续升级到当前版本。
static List<Migration> getDatabaseMigrations() {
return [
migration1To2,
migration2To3,
migration3To4,
migration4To5,
migration5To6,
migration6To7,
migration7To8,
migration8To9,
migration9To10,
migration10To11,
migration11To12,
migration12To13,
];
}
}