11
This commit is contained in:
1 parent
2e0036af45
commit
c75b441dea
37 files changed
+1894
-271
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:common_utils/common_utils.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -168,11 +170,12 @@ abstract class SyncProductTool {
|
||||
// 同步商品分类 spu end
|
||||
// **************************************************************************
|
||||
|
||||
/// 同步超市所有数据
|
||||
/// 1-删除所有商品分类
|
||||
/// 2-删除所有商品
|
||||
/// 3-同步商品分类
|
||||
/// 4-同步商品
|
||||
/// 同步超市所有数据(全量 diff)
|
||||
/// 1-同步商品分类
|
||||
/// 2-分页拉取全部商品,与本地快照比对后只写入变化部分
|
||||
/// 3-清理服务端已不存在的商品
|
||||
///
|
||||
/// 不再先清空本地表:未变化的商品直接跳过,拼音等派生数据全部沿用本地旧值。
|
||||
static Future<void> syncSupermarket() async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
EasyLoading.show(
|
||||
@@ -180,11 +183,17 @@ abstract class SyncProductTool {
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
try {
|
||||
// 删除
|
||||
await delDatabase();
|
||||
// 同步
|
||||
await syncClassify();
|
||||
await syncProduct();
|
||||
final success = await _syncSupermarketProduct(
|
||||
mode: _SupermarketSyncMode.full,
|
||||
);
|
||||
if (success) {
|
||||
// 全量同步成功才推进水位,失败保留完整窗口下次重试
|
||||
final sysSetting = await AppSettingTool.getSysSetting();
|
||||
sysSetting.supermarketProductSyncTime = DateTime.now();
|
||||
await AppSettingTool.setSysSetting(sysSetting);
|
||||
}
|
||||
} catch (error, stack) {
|
||||
logger.e(
|
||||
"|$_tag|同步|${SysType.SUPERMARKET.title}|商品信息意外的错误error:|$error|,stack:|$stack|");
|
||||
@@ -212,36 +221,13 @@ abstract class SyncProductTool {
|
||||
);
|
||||
if (!success) return;
|
||||
} else if (sysType == SysType.SUPERMARKET) {
|
||||
// 超市
|
||||
final app = Get.find<AppControl>();
|
||||
final dataBase = app.dataBase;
|
||||
final supermarketSpuDao = dataBase.getSupermarketSpuDao;
|
||||
final supermarketSkuDao = dataBase.getSupermarketSkuDao;
|
||||
final supermarketCodeDao = dataBase.getSupermarketCodeDao;
|
||||
int currentPage = 1;
|
||||
Set<String> serviceCodes = {};
|
||||
while (true) {
|
||||
final pa = {"current": currentPage, "size": 100};
|
||||
final supermarket = await ProductApi.supermarketSpu(pa);
|
||||
if (supermarket == null) break;
|
||||
if (ObjectUtil.isEmptyList(supermarket.records)) break;
|
||||
final ids = await _batchUpdateSupermarketProduct(
|
||||
list: supermarket.records!,
|
||||
);
|
||||
currentPage += 1;
|
||||
serviceCodes.addAll(ids);
|
||||
}
|
||||
// 处理删除
|
||||
final productList = await supermarketSpuDao.getAll(
|
||||
"-1",
|
||||
);
|
||||
final dbCodes = productList.map((e) => e.serviceUniqueCode).toSet();
|
||||
final deleteCodes = dbCodes.difference(serviceCodes).toList();
|
||||
if (ObjectUtil.isNotEmpty(deleteCodes)) {
|
||||
await supermarketSpuDao.delByIds(deleteCodes);
|
||||
await supermarketSkuDao.delBySpuIds(deleteCodes);
|
||||
await supermarketCodeDao.delBySpuIds(deleteCodes);
|
||||
}
|
||||
// 超市:已有同步水位时走增量(只请求上次同步后变化的商品,含删除墓碑),
|
||||
// 首次同步退化为全量 diff。
|
||||
final mode = sysSetting.supermarketProductSyncTime == null
|
||||
? _SupermarketSyncMode.full
|
||||
: _SupermarketSyncMode.incremental;
|
||||
final success = await _syncSupermarketProduct(mode: mode);
|
||||
if (!success) return;
|
||||
}
|
||||
// 更新同步时间
|
||||
if (sysType == SysType.CANTEEN) {
|
||||
@@ -448,59 +434,101 @@ abstract class SyncProductTool {
|
||||
// **************************************************************************
|
||||
|
||||
// **************************************************************************
|
||||
// 同步食堂商品 spu start
|
||||
// 同步超市商品 spu start
|
||||
// **************************************************************************
|
||||
|
||||
static Future<List<String>> _batchUpdateSupermarketProduct({
|
||||
required List<ServiceSupermarketSpu> list,
|
||||
/// 按服务端推送的编号定向同步超市商品。
|
||||
///
|
||||
/// MQTT SUPERMARKET_PRODUCT 消息携带 spuIds 时走此入口,
|
||||
/// 只拉取变化的商品;接口未返回的编号视为服务端已删除。
|
||||
static Future<bool> syncSupermarketProductByIds(Set<String> spuIds) async {
|
||||
final sysSetting = await AppSettingTool.getSysSetting();
|
||||
if (sysSetting.sysType != SysType.SUPERMARKET) return false;
|
||||
if (ObjectUtil.isEmpty(spuIds)) return false;
|
||||
try {
|
||||
return await _syncSupermarketProduct(
|
||||
mode: _SupermarketSyncMode.surgical,
|
||||
onlySpuIds: spuIds,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
logger.e("|$_tag|定向同步|超市|商品信息意外的错误error:|$e|,stack:|$stack|");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 超市商品同步主体,返回 false 表示接口无有效响应,调用方不应推进同步水位。
|
||||
static Future<bool> _syncSupermarketProduct({
|
||||
required _SupermarketSyncMode mode,
|
||||
Set<String>? onlySpuIds,
|
||||
}) async {
|
||||
final app = Get.find<AppControl>();
|
||||
final dataBase = app.dataBase;
|
||||
final supermarketSpuDao = dataBase.getSupermarketSpuDao;
|
||||
final productList = await supermarketSpuDao.getAll(
|
||||
"-1",
|
||||
);
|
||||
if (ObjectUtil.isEmpty(productList)) {
|
||||
await _batchAddOrUpdateSupermarketProduct(
|
||||
list: list,
|
||||
isUpdate: false,
|
||||
);
|
||||
await _batchAddOrUpdateSupermarketSpuAndCode(list);
|
||||
return list.map((e) => e.id.toString()).toSet().toList();
|
||||
}
|
||||
final apiServiceCodes = list.map((e) => e.id.toString()).toSet();
|
||||
final dbServiceCodes = productList.map((e) => e.serviceUniqueCode).toSet();
|
||||
final supermarketSkuDao = dataBase.getSupermarketSkuDao;
|
||||
final supermarketCodeDao = dataBase.getSupermarketCodeDao;
|
||||
|
||||
final addCodes = apiServiceCodes.difference(dbServiceCodes).toList();
|
||||
final updateCodes = apiServiceCodes.intersection(dbServiceCodes).toList();
|
||||
if (ObjectUtil.isNotEmpty(addCodes)) {
|
||||
await _batchAddOrUpdateSupermarketProduct(
|
||||
list: _filterCaneenSupermarketByCodes(list, addCodes),
|
||||
isUpdate: false,
|
||||
);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(updateCodes)) {
|
||||
await _batchAddOrUpdateSupermarketProduct(
|
||||
list: _filterCaneenSupermarketByCodes(list, updateCodes),
|
||||
isUpdate: true,
|
||||
);
|
||||
}
|
||||
await _batchAddOrUpdateSupermarketSpuAndCode(list);
|
||||
return apiServiceCodes.toList();
|
||||
}
|
||||
// 本地快照只取一次,分页循环内全部走内存比对,替代逐页全表查询和逐条 getByCode。
|
||||
final productList = await supermarketSpuDao.getAll("-1");
|
||||
final dbMap = {
|
||||
for (final v in productList) v.serviceUniqueCode: v,
|
||||
};
|
||||
|
||||
static Future<void> _batchAddOrUpdateSupermarketProduct({
|
||||
required List<ServiceSupermarketSpu> list,
|
||||
required bool isUpdate,
|
||||
}) async {
|
||||
if (ObjectUtil.isEmpty(list)) return;
|
||||
final app = Get.find<AppControl>();
|
||||
final dataBase = app.dataBase;
|
||||
final productDao = dataBase.getSupermarketSpuDao;
|
||||
List<SupermarketSpuEntity> changeList = [];
|
||||
for (final v in list) {
|
||||
SupermarketSpuEntity item = SupermarketSpuEntity(
|
||||
serviceUniqueCode: v.id ?? "-1",
|
||||
// 增量模式多回看几分钟容忍终端与服务端时钟偏差,重复拉取是幂等的。
|
||||
String? updateTime;
|
||||
if (mode == _SupermarketSyncMode.incremental) {
|
||||
final sysSetting = await AppSettingTool.getSysSetting();
|
||||
final last = sysSetting.supermarketProductSyncTime;
|
||||
if (last != null) {
|
||||
updateTime = DateUtil.formatDate(
|
||||
last.subtract(const Duration(minutes: 5)),
|
||||
format: "yyyy-MM-dd HH:mm:ss",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final serviceCodes = <String>{};
|
||||
final tombstoneCodes = <String>{};
|
||||
final apiList = <ServiceSupermarketSpu>[];
|
||||
int currentPage = 1;
|
||||
while (true) {
|
||||
final pa = <String, dynamic>{"current": currentPage, "size": 100};
|
||||
if (updateTime != null) pa["updateTime"] = updateTime;
|
||||
if (onlySpuIds != null) pa["ids"] = onlySpuIds.toList();
|
||||
final supermarket = await ProductApi.supermarketSpu(pa);
|
||||
if (supermarket == null) return false;
|
||||
if (ObjectUtil.isEmptyList(supermarket.records)) break;
|
||||
for (final v in supermarket.records!) {
|
||||
final code = v.id.toString();
|
||||
if (v.delFlag == "1") {
|
||||
// 服务端逻辑删除的墓碑行,本地有才需要清理
|
||||
tombstoneCodes.add(code);
|
||||
continue;
|
||||
}
|
||||
serviceCodes.add(code);
|
||||
apiList.add(v);
|
||||
}
|
||||
currentPage += 1;
|
||||
}
|
||||
|
||||
// 拼音只对新增或改名的商品计算,名称未变的沿用本地旧值。
|
||||
final pendingNames = <String>{};
|
||||
for (final v in apiList) {
|
||||
final old = dbMap[v.id.toString()];
|
||||
final name = v.productName ?? "";
|
||||
if (old == null || old.productName != name) {
|
||||
pendingNames.add(name);
|
||||
}
|
||||
}
|
||||
final pyMap = await _batchTransitionPy(pendingNames.toList());
|
||||
|
||||
final addList = <SupermarketSpuEntity>[];
|
||||
final editList = <SupermarketSpuEntity>[];
|
||||
final detailList = <ServiceSupermarketSpu>[];
|
||||
for (final v in apiList) {
|
||||
final code = v.id.toString();
|
||||
final old = dbMap[code];
|
||||
final item = SupermarketSpuEntity(
|
||||
serviceUniqueCode: code,
|
||||
classifyServiceUniqueCode: v.categoryId ?? "-1",
|
||||
productCode: v.productCode ?? "-1",
|
||||
productName: v.productName ?? "",
|
||||
@@ -518,44 +546,78 @@ abstract class SyncProductTool {
|
||||
retailPrice: v.retailPrice ?? 0,
|
||||
promotionPrice: v.promotionPrice ?? 0,
|
||||
sort: 0,
|
||||
createTime: DateTime.now(),
|
||||
createTime: old?.createTime ?? DateTime.now(),
|
||||
updateTime: DateTime.now(),
|
||||
delFlag: false,
|
||||
);
|
||||
// 计算一下拼音
|
||||
final productNamePy = await transitionPy(item.productName);
|
||||
if (ObjectUtil.isNotEmpty(productNamePy)) {
|
||||
item.productNamePy = productNamePy;
|
||||
if (old != null && old.productName == item.productName) {
|
||||
item.productNamePy = old.productNamePy;
|
||||
} else {
|
||||
item.productNamePy = pyMap[item.productName] ?? "";
|
||||
}
|
||||
if (isUpdate) {
|
||||
final detail = await productDao.getByCode(item.serviceUniqueCode);
|
||||
if (ObjectUtil.isEmpty(detail)) continue;
|
||||
if (detail == item) continue;
|
||||
item.id = detail?.id;
|
||||
if (old == null) {
|
||||
addList.add(item);
|
||||
detailList.add(v);
|
||||
continue;
|
||||
}
|
||||
changeList.add(item);
|
||||
// Equatable 只比较业务字段:未变化的商品跳过写库与详情请求
|
||||
if (old == item) continue;
|
||||
item.id = old.id;
|
||||
editList.add(item);
|
||||
detailList.add(v);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(changeList)) {
|
||||
isUpdate
|
||||
? await productDao.batchEdit(changeList)
|
||||
: await productDao.batchSave(changeList);
|
||||
if (ObjectUtil.isNotEmpty(addList)) {
|
||||
await supermarketSpuDao.batchSave(addList);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(editList)) {
|
||||
await supermarketSpuDao.batchEdit(editList);
|
||||
}
|
||||
// sku 与辅助条码只对变化的商品重建
|
||||
await _batchSaveSupermarketSkuAndCode(detailList);
|
||||
|
||||
// 删除:增量模式看服务端墓碑,全量/定向模式比对差集
|
||||
List<String> deleteCodes;
|
||||
if (mode == _SupermarketSyncMode.incremental) {
|
||||
deleteCodes = tombstoneCodes
|
||||
.where((code) => dbMap.containsKey(code))
|
||||
.toList();
|
||||
} else if (mode == _SupermarketSyncMode.surgical) {
|
||||
deleteCodes = onlySpuIds!
|
||||
.where((code) =>
|
||||
!serviceCodes.contains(code) && dbMap.containsKey(code))
|
||||
.toList();
|
||||
} else {
|
||||
deleteCodes = dbMap.keys
|
||||
.where((code) => !serviceCodes.contains(code))
|
||||
.toList();
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(deleteCodes)) {
|
||||
// 分批删除,避免 IN 占位符超出 SQLite 变量数上限
|
||||
for (final codes in _chunked(deleteCodes, 500)) {
|
||||
await supermarketSpuDao.delByIds(codes);
|
||||
await supermarketSkuDao.delBySpuIds(codes);
|
||||
await supermarketCodeDao.delBySpuIds(codes);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static List<ServiceSupermarketSpu> _filterCaneenSupermarketByCodes(
|
||||
List<ServiceSupermarketSpu> sourceList,
|
||||
List<String> codes,
|
||||
) {
|
||||
return sourceList
|
||||
.where((item) => codes.contains(item.id.toString()))
|
||||
.toList();
|
||||
/// 按固定大小切分列表。
|
||||
static List<List<T>> _chunked<T>(List<T> source, int size) {
|
||||
final result = <List<T>>[];
|
||||
for (var i = 0; i < source.length; i += size) {
|
||||
result.add(
|
||||
source.sublist(i, i + size > source.length ? source.length : i + size),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 更新辅助条码和spu
|
||||
static Future<void> _batchAddOrUpdateSupermarketSpuAndCode(
|
||||
// 重建指定商品的 sku 与辅助条码
|
||||
static Future<void> _batchSaveSupermarketSkuAndCode(
|
||||
List<ServiceSupermarketSpu> list,
|
||||
) async {
|
||||
if (list.isEmpty) return;
|
||||
if (ObjectUtil.isEmpty(list)) return;
|
||||
final app = Get.find<AppControl>();
|
||||
final dataBase = app.dataBase;
|
||||
final skuDao = dataBase.getSupermarketSkuDao;
|
||||
@@ -564,53 +626,56 @@ abstract class SyncProductTool {
|
||||
final spuIds =
|
||||
list.where((v) => ObjectUtil.isNotEmpty(v)).map((v) => v.id!).toList();
|
||||
try {
|
||||
final pa = {"ids": spuIds};
|
||||
final skuDetailList = await ProductApi.supermarketSupDetial(pa);
|
||||
if (skuDetailList == null) {
|
||||
return;
|
||||
}
|
||||
// 删除 sku
|
||||
skuDao.delBySpuIds(spuIds);
|
||||
// 删除辅助条码
|
||||
codeDao.delBySpuIds(spuIds);
|
||||
List<SupermarketCodeEntity> codeList = [];
|
||||
List<SupermarketSkuEntity> skuList = [];
|
||||
for (final v in skuDetailList) {
|
||||
// 辅助条码
|
||||
if (v.supermarketAuxiliaryCodeEntityList != null) {
|
||||
for (final i in v.supermarketAuxiliaryCodeEntityList!) {
|
||||
SupermarketCodeEntity item = SupermarketCodeEntity(
|
||||
serviceUniqueCode: i.id ?? "-1",
|
||||
spuId: i.spuId ?? "-1",
|
||||
unitId: i.unitId ?? "",
|
||||
barCode: i.barCode ?? "",
|
||||
);
|
||||
codeList.add(item);
|
||||
// 详情接口的 ids 拼在 GET URL 上,分批请求避免超出网关 URL 长度限制
|
||||
for (final chunkIds in _chunked(spuIds, 100)) {
|
||||
final pa = {"ids": chunkIds};
|
||||
final skuDetailList = await ProductApi.supermarketSupDetial(pa);
|
||||
if (skuDetailList == null) {
|
||||
return;
|
||||
}
|
||||
// 删除 sku
|
||||
await skuDao.delBySpuIds(chunkIds);
|
||||
// 删除辅助条码
|
||||
await codeDao.delBySpuIds(chunkIds);
|
||||
List<SupermarketCodeEntity> codeList = [];
|
||||
List<SupermarketSkuEntity> skuList = [];
|
||||
for (final v in skuDetailList) {
|
||||
// 辅助条码
|
||||
if (v.supermarketAuxiliaryCodeEntityList != null) {
|
||||
for (final i in v.supermarketAuxiliaryCodeEntityList!) {
|
||||
SupermarketCodeEntity item = SupermarketCodeEntity(
|
||||
serviceUniqueCode: i.id ?? "-1",
|
||||
spuId: i.spuId ?? "-1",
|
||||
unitId: i.unitId ?? "",
|
||||
barCode: i.barCode ?? "",
|
||||
);
|
||||
codeList.add(item);
|
||||
}
|
||||
}
|
||||
// sku
|
||||
if (v.supermarketUnitEntityList != null) {
|
||||
for (final i in v.supermarketUnitEntityList!) {
|
||||
SupermarketSkuEntity item = SupermarketSkuEntity(
|
||||
serviceUniqueCode: i.id ?? "-1",
|
||||
spuId: i.spuId ?? "-1",
|
||||
barCode: i.barCode ?? "",
|
||||
spec: i.spec ?? "",
|
||||
unit: i.unit ?? "",
|
||||
packageNum: i.packageNum ?? 0,
|
||||
purchasePrice: i.purchasePrice ?? 0,
|
||||
retailPrice: i.retailPrice ?? 0,
|
||||
promotionPrice: i.promotionPrice ?? 0,
|
||||
);
|
||||
skuList.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
// sku
|
||||
if (v.supermarketUnitEntityList != null) {
|
||||
for (final i in v.supermarketUnitEntityList!) {
|
||||
SupermarketSkuEntity item = SupermarketSkuEntity(
|
||||
serviceUniqueCode: i.id ?? "-1",
|
||||
spuId: i.spuId ?? "-1",
|
||||
barCode: i.barCode ?? "",
|
||||
spec: i.spec ?? "",
|
||||
unit: i.unit ?? "",
|
||||
packageNum: i.packageNum ?? 0,
|
||||
purchasePrice: i.purchasePrice ?? 0,
|
||||
retailPrice: i.retailPrice ?? 0,
|
||||
promotionPrice: i.promotionPrice ?? 0,
|
||||
);
|
||||
skuList.add(item);
|
||||
}
|
||||
if (codeList.isNotEmpty) {
|
||||
await codeDao.batchSave(codeList);
|
||||
}
|
||||
if (skuList.isNotEmpty) {
|
||||
await skuDao.batchSave(skuList);
|
||||
}
|
||||
}
|
||||
if (codeList.isNotEmpty) {
|
||||
await codeDao.batchSave(codeList);
|
||||
}
|
||||
if (skuList.isNotEmpty) {
|
||||
await skuDao.batchSave(skuList);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logger.e("|$_tag|添加超市spu detail 错误error:|$e|stack:|$stack|");
|
||||
@@ -618,22 +683,41 @@ abstract class SyncProductTool {
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// 同步食堂商品 spu end
|
||||
// 同步超市商品 spu end
|
||||
// **************************************************************************
|
||||
|
||||
// 中文转拼音
|
||||
static Future<String?> transitionPy(String str) async {
|
||||
// 中文转短拼音;纯数字内容返回空串
|
||||
static String _shortPinyin(String str) {
|
||||
if (ObjectUtil.isEmpty(str)) return "";
|
||||
if (double.tryParse(str) != null) return "";
|
||||
return PinyinHelper.getShortPinyin(str).replaceAll(" ", "");
|
||||
}
|
||||
|
||||
/// 批量中文转拼音。计算量随商品数增长,放后台 isolate 避免卡住收银界面;
|
||||
/// isolate 异常时退回主 isolate 逐个计算,保证同步流程不中断。
|
||||
static Future<Map<String, String>> _batchTransitionPy(
|
||||
List<String> names,
|
||||
) async {
|
||||
if (ObjectUtil.isEmpty(names)) return {};
|
||||
try {
|
||||
if (ObjectUtil.isEmpty(str)) return "";
|
||||
// 尝试转换为数字,如果是数字,返回空
|
||||
if (double.tryParse(str) != null) return "";
|
||||
final py = PinyinHelper.getShortPinyin(str);
|
||||
logger.d("|$_tag|中文转拼音|中文:|$str|转换到拼音:|$py|");
|
||||
// 去除所有空格
|
||||
final pyNoSpace = py.replaceAll(" ", "");
|
||||
return pyNoSpace;
|
||||
} catch (error, stack) {
|
||||
logger.e("|$_tag|添加超市spu detail 错误error:|$error|stack:|$stack|");
|
||||
return await Isolate.run(() {
|
||||
return {for (final name in names) name: _shortPinyin(name)};
|
||||
});
|
||||
} catch (e, stack) {
|
||||
logger.e("|$_tag|批量中文转拼音isolate错误error:|$e|stack:|$stack|");
|
||||
return {for (final name in names) name: _shortPinyin(name)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 超市商品同步模式。
|
||||
enum _SupermarketSyncMode {
|
||||
/// 全量分页拉取,并清理服务端已不存在的商品。
|
||||
full,
|
||||
|
||||
/// 只拉取上次同步时间之后变化的商品(含删除墓碑),不做全量差集清理。
|
||||
incremental,
|
||||
|
||||
/// 只同步指定的商品编号,接口未返回的编号按已删除处理。
|
||||
surgical,
|
||||
}
|
||||
@@ -1,9 +1,34 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// 一次离线人员同步的不可变结果汇总。
|
||||
class OfflineUserSyncSummary {
|
||||
/// 创建一次同步的统计结果。
|
||||
const OfflineUserSyncSummary({
|
||||
required this.added,
|
||||
required this.updated,
|
||||
required this.unchanged,
|
||||
});
|
||||
|
||||
/// 本机新增的人员数量。
|
||||
final int added;
|
||||
|
||||
/// 更新钱包等快照字段的人员数量。
|
||||
final int updated;
|
||||
|
||||
/// 与本地数据一致而跳过写入的人员数量。
|
||||
final int unchanged;
|
||||
}
|
||||
|
||||
abstract class SyncUserTool {
|
||||
static const _tag = "sync商品工具";
|
||||
|
||||
/// 离线人员快照单页数量,与服务端默认值保持一致。
|
||||
static const int _offlineSyncPageSize = 200;
|
||||
|
||||
/// 游标保护页数,避免异常服务端持续返回 hasMore 造成无限循环。
|
||||
static const int _maxOfflineSyncPages = 10000;
|
||||
|
||||
// 获取存储用户
|
||||
static Future<UserEntity?> storageUserByCarNumber(String cardNumber) async {
|
||||
final app = Get.find<AppControl>();
|
||||
@@ -82,4 +107,91 @@ abstract class SyncUserTool {
|
||||
}
|
||||
return userEntity;
|
||||
}
|
||||
|
||||
/// 按设备绑定部门树(或全租户)同步脱敏后的离线人员到本机缓存。
|
||||
///
|
||||
/// [isAll] 只允许非生产调试使用;生产调用即使误传也会被服务端收敛为本部门。
|
||||
/// 服务端返回的记录姓名已脱敏且不含手机号头像;本地已有的完整消费缓存
|
||||
/// (真实姓名、手机号)优先保留,同步只补齐缺失人员和刷新钱包快照字段。
|
||||
/// 分页期间快照上界变化会立即中断,已写入的本地数据保持有效。
|
||||
static Future<OfflineUserSyncSummary> syncOfflineUsers({bool isAll = false}) async {
|
||||
if (!AppNetworkTool.isAutoNetwork()) {
|
||||
throw StateError('网络不可用,无法同步离线人员');
|
||||
}
|
||||
final app = Get.find<AppControl>();
|
||||
final userDao = app.dataBase.getUserDao;
|
||||
var afterId = '0';
|
||||
String? snapshotMaxId;
|
||||
var added = 0;
|
||||
var updated = 0;
|
||||
var unchanged = 0;
|
||||
for (var page = 0; page < _maxOfflineSyncPages; page++) {
|
||||
final snapshot = await HardApi.getOfflineUserSyncSnapshot(
|
||||
afterId: afterId,
|
||||
limit: _offlineSyncPageSize,
|
||||
snapshotMaxId: snapshotMaxId,
|
||||
allTenant: isAll,
|
||||
);
|
||||
snapshotMaxId ??= snapshot.snapshotMaxId;
|
||||
if (snapshot.snapshotMaxId != snapshotMaxId) {
|
||||
throw StateError('离线人员快照上界在分页期间发生变化');
|
||||
}
|
||||
for (final record in snapshot.records) {
|
||||
final existing = await userDao.findByUserId(record.userId);
|
||||
if (existing == null) {
|
||||
final now = DateTime.now();
|
||||
await userDao.save(
|
||||
UserEntity(
|
||||
userId: record.userId,
|
||||
nickname: record.username,
|
||||
walletCardNumber: record.walletCardNumber,
|
||||
walletId: record.walletId,
|
||||
walletTypeId: record.walletTypeId,
|
||||
walletBalance: record.walletBalance,
|
||||
canteenId: record.canteenId?.toString(),
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
),
|
||||
);
|
||||
added++;
|
||||
continue;
|
||||
}
|
||||
// 姓名、手机号、头像和优惠保留本地消费缓存的真实数据,仅刷新钱包快照。
|
||||
final merged = UserEntity(
|
||||
id: existing.id,
|
||||
userId: existing.userId,
|
||||
nickname: existing.nickname,
|
||||
mobile: existing.mobile,
|
||||
avatar: existing.avatar,
|
||||
walletCardNumber: record.walletCardNumber,
|
||||
walletId: record.walletId,
|
||||
walletTypeId: record.walletTypeId,
|
||||
walletBalance: record.walletBalance,
|
||||
canteenId: record.canteenId?.toString() ?? existing.canteenId,
|
||||
discountsList: existing.discountsList,
|
||||
discountsUpdateTime: existing.discountsUpdateTime,
|
||||
createTime: existing.createTime,
|
||||
updateTime: DateTime.now(),
|
||||
);
|
||||
// UserEntity 等值比较不含时间戳;完全一致时避免无谓写库。
|
||||
if (merged == existing) {
|
||||
unchanged++;
|
||||
} else {
|
||||
await userDao.edit(merged);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
if (!snapshot.hasMore) break;
|
||||
afterId = snapshot.nextAfterId;
|
||||
}
|
||||
final summary = OfflineUserSyncSummary(
|
||||
added: added,
|
||||
updated: updated,
|
||||
unchanged: unchanged,
|
||||
);
|
||||
logger.i(
|
||||
'|$_tag|离线人员同步完成|新增:$added|更新:$updated|无变化:$unchanged|',
|
||||
);
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user