diff --git a/README.md b/README.md index 618dafa..07f196d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ flutter pub run build_runner build --delete-conflicting-outputs~ ## flutter打包 ``` -flutter build apk --build-name 1.4.5 --build-number 145 +flutter build apk --build-name 1.4.6 --build-number 146 --build-name为版本号(展示) --build-number为版本号(内部比较) ``` diff --git a/lib/common/api/product_api.dart b/lib/common/api/product_api.dart index 8e4d927..6804051 100644 --- a/lib/common/api/product_api.dart +++ b/lib/common/api/product_api.dart @@ -15,7 +15,7 @@ abstract class ProductApi { .toList(); } - /// 菜品商品 + /// 查询指定单日的食堂菜谱商品。 static Future?> canteenProduct( dynamic pa, ) async { @@ -29,6 +29,20 @@ abstract class ProductApi { .toList(); } + /// 按闭区间查询多日食堂菜谱商品,响应结构与单日接口保持一致。 + static Future?> canteenProductsByDateRange( + Map requestBody, + ) async { + final res = await HttpManage().dio.post( + "/canteen/canteenDailyMenu/ad/getCanteenListByDateRange", + data: requestBody, + ); + if (res.data == null) return null; + return res.data + ?.map((e) => ServiceProduct.fromJson(e)) + .toList(); + } + /// 超市分类 static Future?> supermarketClassify( dynamic pa, diff --git a/lib/common/models/api/product_api_models.dart b/lib/common/models/api/product_api_models.dart index 259848b..957c93f 100644 --- a/lib/common/models/api/product_api_models.dart +++ b/lib/common/models/api/product_api_models.dart @@ -77,7 +77,8 @@ class ServiceProduct { /// 分类附带的餐段类型;为空时不参与本地餐段筛选。 String? categoryType; - /// 本次同步写入的供应日期,接口未返回时由同步任务补齐。 + /// 菜谱供应日期,来源于单日和范围接口共同返回的 `startTime` 字段。 + @JsonKey(name: 'startTime') DateTime? saleDate; /// 是否涨价 diff --git a/lib/common/models/api/product_api_models.g.dart b/lib/common/models/api/product_api_models.g.dart index 23d0931..0df7b38 100644 --- a/lib/common/models/api/product_api_models.g.dart +++ b/lib/common/models/api/product_api_models.g.dart @@ -40,9 +40,9 @@ ServiceProduct _$ServiceProductFromJson(Map json) => stock: JsonTool.anyToNullableInt(json['stock']), sort: (json['sort'] as num?)?.toInt(), categoryType: json['categoryType'] as String?, - saleDate: json['saleDate'] == null + saleDate: json['startTime'] == null ? null - : DateTime.parse(json['saleDate'] as String), + : DateTime.parse(json['startTime'] as String), isPriceIncreased: JsonTool.stringToBoolean(json['isPriceIncreased'] as String?), spuType: (json['spuType'] as num?)?.toInt(), @@ -64,7 +64,7 @@ Map _$ServiceProductToJson(ServiceProduct instance) => 'stock': instance.stock, 'sort': instance.sort, 'categoryType': instance.categoryType, - 'saleDate': instance.saleDate?.toIso8601String(), + 'startTime': instance.saleDate?.toIso8601String(), 'isPriceIncreased': JsonTool.booleanToString(instance.isPriceIncreased), 'spuType': instance.spuType, 'comboItems': instance.comboItems, diff --git a/lib/common/utils/sync/scheduled_task_storage_tool.dart b/lib/common/utils/sync/scheduled_task_storage_tool.dart index 9fc852f..ca1e080 100644 --- a/lib/common/utils/sync/scheduled_task_storage_tool.dart +++ b/lib/common/utils/sync/scheduled_task_storage_tool.dart @@ -4,8 +4,11 @@ import 'package:pj_supermarket_cloud/common/index.dart'; /// 定时任务结构化存储入口,隔离调度器与 Floor DAO 的实体转换细节。 abstract final class ScheduledTaskStorageTool { - /// 旧版本商品同步任务的默认 Cron,仅用于识别尚未升级的默认配置。 - static const String _legacyProductSyncCron = '0 25 0 * * *'; + /// 最早版本商品同步任务的默认 Cron,仅用于识别尚未升级的关闭配置。 + static const String _legacyDisabledProductSyncCron = '0 25 0 * * *'; + + /// 上一版本商品同步任务的默认 Cron,仅用于把未自定义配置升级为每 3 小时。 + static const String _legacyFiveMinuteProductSyncCron = '0 */5 * * * *'; /// 当前应用数据库中的任务配置 DAO;数据库由 AppControl 在调度器启动前完成初始化。 static ScheduledTaskDao get _taskDao => @@ -89,9 +92,9 @@ abstract final class ScheduledTaskStorageTool { required bool idleOnly, }) => type == ScheduledTaskType.productSync && - !enabled && - cron == _legacyProductSyncCron && - idleOnly; + idleOnly && + ((!enabled && cron == _legacyDisabledProductSyncCron) || + (enabled && cron == _legacyFiveMinuteProductSyncCron)); /// 从 scheduled_task 读取有效任务,并转换为调度器使用的配置和最近执行快照。 /// diff --git a/lib/common/utils/sync/sync_check_tool.dart b/lib/common/utils/sync/sync_check_tool.dart index 0aeff10..ac40cf1 100644 --- a/lib/common/utils/sync/sync_check_tool.dart +++ b/lib/common/utils/sync/sync_check_tool.dart @@ -304,7 +304,7 @@ class SyncCheckTool { ScheduledTaskConfig( type: ScheduledTaskType.productSync, enabled: true, - cron: '0 */5 * * * *', + cron: '0 0 */3 * * *', idleOnly: true, ), ScheduledTaskConfig( diff --git a/lib/common/utils/sync/sync_product_tool.dart b/lib/common/utils/sync/sync_product_tool.dart index 272fb6b..cc75c62 100644 --- a/lib/common/utils/sync/sync_product_tool.dart +++ b/lib/common/utils/sync/sync_product_tool.dart @@ -209,22 +209,20 @@ abstract class SyncProductTool { /// 按终端业态执行常规商品同步。 /// - /// 食堂常规同步保留今天起连续七天的缓存范围;MQTT 食堂变更必须改用 [syncTodayCanteenProduct]。 + /// 食堂常规同步保留今天起连续七天的缓存范围。 + /// MQTT 食堂变更和进入食堂收银页只刷新当天,使用 [syncTodayCanteenProduct]。 static Future syncProduct() async { final sysSetting = await AppSettingTool.getSysSetting(); final sysType = sysSetting.sysType; try { if (sysType == SysType.CANTEEN) { - // 获取从今天开始的所有日期时间 - final DateTime now = DateTime.now(); - for (int i = 0; i < 7; i++) { - final DateTime date = now.add(Duration(days: i)); - final success = await _syncCanteenProductForDay( - date: date, - merchantId: sysSetting.serviceMerchantId, - ); - if (!success) return; - } + final startDate = DateTime.now(); + final success = await _syncCanteenProductForDateRange( + startDate: startDate, + endDate: startDate.add(const Duration(days: 6)), + merchantId: sysSetting.serviceMerchantId, + ); + if (!success) return; } else if (sysType == SysType.SUPERMARKET) { // 超市 final app = Get.find(); @@ -275,7 +273,7 @@ abstract class SyncProductTool { /// 仅同步终端本地时区的当天食堂菜品,并在成功后记录食堂商品同步时间。 /// - /// MQTT 菜品变更使用此入口,确保请求和本地替换都不会扩展到未来一周。 + /// MQTT 菜品变更和进入食堂收银页使用此入口,避免刷新当天时额外拉取未来六天。 static Future syncTodayCanteenProduct() async { final today = DateTime.now(); final sysSetting = await AppSettingTool.getSysSetting(); @@ -329,6 +327,75 @@ abstract class SyncProductTool { return true; } + /// 一次请求指定闭区间的食堂菜谱,并按接口返回的供应日期逐日原子替换本地快照。 + /// + /// 返回 `false` 表示接口无有效响应或任一记录缺少合法供应日期,此时不会修改任何日期的本地数据。 + static Future _syncCanteenProductForDateRange({ + required DateTime startDate, + required DateTime endDate, + required String? merchantId, + }) async { + final startDay = DateUtil.formatDate( + startDate, + format: DateFormats.y_mo_d, + ); + final endDay = DateUtil.formatDate( + endDate, + format: DateFormats.y_mo_d, + ); + final requestMerchantId = int.tryParse(merchantId ?? ''); + if (requestMerchantId == null) { + logger.w('|$_tag|范围菜谱同步缺少有效商户 ID,保留本地旧快照'); + return false; + } + final canteenList = await ProductApi.canteenProductsByDateRange({ + "startDate": startDay, + "endDate": endDay, + "hourType": null, + "categoryId": null, + "merchantId": requestMerchantId, + }); + if (canteenList == null) return false; + + final productsByDay = >{}; + var currentDate = DateTime.parse(startDay); + final lastDate = DateTime.parse(endDay); + while (!currentDate.isAfter(lastDate)) { + productsByDay[DateUtil.formatDate( + currentDate, + format: DateFormats.y_mo_d, + )] = []; + currentDate = currentDate.add(const Duration(days: 1)); + } + + for (final product in canteenList) { + final saleDate = product.saleDate; + if (saleDate == null) { + logger.w('|$_tag|范围菜谱响应缺少供应日期,保留本地旧快照:${product.toJson()}'); + return false; + } + final saleDay = DateUtil.formatDate( + saleDate, + format: DateFormats.y_mo_d, + ); + final dayProducts = productsByDay[saleDay]; + if (dayProducts == null) { + logger + .w('|$_tag|范围菜谱响应日期超出请求区间,保留本地旧快照:请求$startDay至$endDay,响应$saleDay'); + return false; + } + dayProducts.add(product); + } + + for (final entry in productsByDay.entries) { + await _batchUpdateCaneenProduct( + list: entry.value, + dayStr: entry.key, + ); + } + return true; + } + // ************************************************************************** // 同步食堂商品 spu start // ************************************************************************** diff --git a/lib/common/values/constants.dart b/lib/common/values/constants.dart index e22da94..72c24a3 100644 --- a/lib/common/values/constants.dart +++ b/lib/common/values/constants.dart @@ -15,8 +15,8 @@ class Constants { static const pwdEncKey = 'pigxpigxpigxpigx'; /// 服务 url 线上 - // static const apiUrl = 'https://dzc-test-1.fy.takin.cc'; - static const apiUrl = 'https://www.zianxin.com/api'; + static const apiUrl = 'https://dzc-test-1.fy.takin.cc'; + // static const apiUrl = 'https://www.zianxin.com/api'; // static const apiUrl = 'https://dzc-test-1.fy.takin.cc'; diff --git a/lib/pages/terminal/canteen_product/controller.dart b/lib/pages/terminal/canteen_product/controller.dart index f984e8d..4bd23aa 100644 --- a/lib/pages/terminal/canteen_product/controller.dart +++ b/lib/pages/terminal/canteen_product/controller.dart @@ -181,7 +181,10 @@ class CanteenProductController extends GetxController with OrderMixin { } sysType = SysType.CANTEEN; _listenPayTypeSelectCallback(); - // 收银页只读取本地商品快照;定时和 MQTT 商品更新统一由 productSync 在安全窗口执行。 + // 进入收银页时在线刷新当天菜谱;接口失败或离线仍读取本地快照,不能阻断收银。 + if (AppNetworkTool.isAutoNetwork()) { + await SyncProductTool.syncTodayCanteenProduct(); + } try { await _getMchRule(); } catch (e, stack) { @@ -260,7 +263,7 @@ class CanteenProductController extends GetxController with OrderMixin { /// 在结算弹框中按商品编码修改真实购物车数量,并返回包含最新金额与库存的展示快照。 /// /// 弹框只负责触发操作,购物车、餐费和副屏仍由当前页面 Controller 统一维护;本地库存 - /// 可能因五分钟后台同步而滞后,因此这里不按库存上限拦截,最终以服务端下单校验为准。 + /// 仍可能在后台同步间隔内变化,因此这里不按库存上限拦截,最终以服务端下单校验为准。 Future _changeCheckoutQuantity( String productServiceUniqueCode, int quantity, diff --git a/pubspec.yaml b/pubspec.yaml index 713e268..890c786 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pj_supermarket_cloud description: "资安信智慧后勤-测试" publish_to: "none" -version: 1.4.5+145 +version: 1.4.6+146 environment: sdk: ^3.6.0 diff --git a/test/common/product_api_models_test.dart b/test/common/product_api_models_test.dart new file mode 100644 index 0000000..b78d982 --- /dev/null +++ b/test/common/product_api_models_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +/// 验证每日菜谱单日与范围接口共用的日期字段能够稳定映射到终端商品模型。 +void main() { + test('每日菜谱 startTime 映射为本地供应日期', () { + final product = ServiceProduct.fromJson({ + 'spuId': '1001', + 'startTime': '2026-09-02', + }); + + expect(product.saleDate, DateTime(2026, 9, 2)); + expect(product.toJson()['startTime'], '2026-09-02T00:00:00.000'); + expect(product.toJson().containsKey('saleDate'), isFalse); + }); +} diff --git a/test/common/product_sync_config_test.dart b/test/common/product_sync_config_test.dart index 5f76f6b..aa8b651 100644 --- a/test/common/product_sync_config_test.dart +++ b/test/common/product_sync_config_test.dart @@ -2,15 +2,15 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; -/// 验证商品同步任务使用既有调度器,并按五分钟周期等待终端空闲后自动执行。 +/// 验证商品同步任务使用既有调度器,并按三小时周期等待终端空闲后自动执行。 void main() { - test('商品同步默认开启且每五分钟仅空闲执行', () { + test('商品同步默认开启且每三小时仅空闲执行', () { final config = SyncCheckTool.defaultConfigs.singleWhere( (item) => item.type == ScheduledTaskType.productSync, ); expect(config.enabled, isTrue); - expect(config.cron, '0 */5 * * * *'); + expect(config.cron, '0 0 */3 * * *'); expect(config.idleOnly, isTrue); }); }