431 lines
16 KiB
Dart
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,
|
|
];
|
|
}
|
|
}
|