rsf-server/src/main/java/com/vincent/rsf/server/manager/service/CusBarcodeSyncMatnrApplyService.java
@@ -2,6 +2,7 @@ import com.vincent.rsf.server.api.controller.erp.params.SyncOrdersItem; import com.vincent.rsf.server.manager.entity.Matnr; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -19,6 +20,7 @@ /** * 主库 man_matnr 按视图结果写入;独立事务,与副库视图查询分离 */ @Slf4j @Service public class CusBarcodeSyncMatnrApplyService { @@ -55,6 +57,7 @@ .setCreateTime(new Date()) .setUpdateTime(new Date()); matnrService.save(matnr); log.info("[cus_barcode_sync] man_matnr 新增 code={}", matCode); continue; } boolean nameDiff = incomingName != null @@ -64,6 +67,7 @@ && (!StringUtils.equals(StringUtils.trimToEmpty(local.getUnit()), viewUnit) || !StringUtils.equals(StringUtils.trimToEmpty(local.getStockUnit()), viewUnit)); if (!nameDiff && !specDiff && !unitDiff) { log.debug("[cus_barcode_sync] man_matnr 已存在且无变更 code={} id={}", matCode, local.getId()); continue; } Matnr update = new Matnr(); @@ -79,6 +83,8 @@ } update.setUpdateBy(loginUserId).setUpdateTime(new Date()); matnrService.updateById(update); log.info("[cus_barcode_sync] man_matnr 更新 code={} id={} nameDiff={} specDiff={} unitDiff={}", matCode, local.getId(), nameDiff, specDiff, unitDiff); } } rsf-server/src/main/java/com/vincent/rsf/server/manager/service/CusBarcodeSyncMatnrService.java
@@ -1,6 +1,5 @@ package com.vincent.rsf.server.manager.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.api.controller.erp.params.SyncOrdersItem; import com.vincent.rsf.server.manager.entity.Matnr; @@ -23,6 +22,7 @@ /** * cus_barcode_sync_view 与 man_matnr 同步(云仓通知单、无订单组托等共用)。 * 分支仅由 {@link GlobalConfigCode#CUS_ITEM_SYNC_MODE}(sys_config.val)解析为 {@link CusItemSyncMode} 决定。 * 副库读无事务({@link CusBarcodeSyncViewQueryService} {@code NOT_SUPPORTED}),主库写 {@link CusBarcodeSyncMatnrApplyService} {@code REQUIRES_NEW}。 */ @Service @@ -89,54 +89,118 @@ return map; } private CusItemSyncMode resolveCusItemSyncMode() { Config c = configService.getOne(new LambdaQueryWrapper<Config>() .eq(Config::getFlag, GlobalConfigCode.CUS_ITEM_SYNC_MODE) .eq(Config::getDeleted, 0), false); /** 与分支解析共用同一份 Config(来自 ConfigService 全局缓存,避免每次同步打库) */ private CusItemSyncConfigSnapshot resolveCusItemSyncConfig() { Config c = configService.getCachedOrLoad(GlobalConfigCode.CUS_ITEM_SYNC_MODE); if (c == null) { return CusItemSyncMode.NONE; return new CusItemSyncConfigSnapshot(CusItemSyncMode.NONE, null); } return CusItemSyncMode.fromConfig(c.getVal()); return new CusItemSyncConfigSnapshot(CusItemSyncMode.fromConfig(c.getVal()), c.getVal()); } private static String formatCfgVal(String rawVal) { if (rawVal == null) { return "(无配置)"; } String t = rawVal.trim(); return t.isEmpty() ? "(空)" : t; } private void syncAlignedWithBarcodeView(List<String> matnrCodes, Map<String, SyncOrdersItem> orderItemByCode, Long loginUserId) { CusItemSyncMode mode = resolveCusItemSyncMode(); if (mode == CusItemSyncMode.NONE) { syncMatnrNonForceFromView(matnrCodes, orderItemByCode, loginUserId); CusItemSyncConfigSnapshot cfg = resolveCusItemSyncConfig(); log.info("[cus_barcode_sync] 同步入口 CUS_ITEM_SYNC_MODE.val={} resolved={} ds={} matnrCount={} matnrs=[{}]", formatCfgVal(cfg.rawVal), cfg.mode, cusBarcodeSyncViewQueryService.effectiveDataSourceLabel(), matnrCodes.size(), joinCodesForLog(matnrCodes)); if (cfg.mode == CusItemSyncMode.NONE) { syncMatnrNonForceFromView(cfg, matnrCodes, orderItemByCode, loginUserId); return; } List<Map<String, Object>> viewItems = cusBarcodeSyncViewQueryService.listByItemNos(matnrCodes); log.info("[cus_barcode_sync] FORCE_VIEW 分支 CUS_ITEM_SYNC_MODE.val={} viewRows={} viewBarcodes=[{}]", formatCfgVal(cfg.rawVal), viewItems == null ? 0 : viewItems.size(), summarizeViewBarcodes(viewItems)); for (String code : matnrCodes) { if (!CusBarcodeSyncViewQueryService.orderMatnrHitsBarcodeView(code, viewItems)) { boolean hit = CusBarcodeSyncViewQueryService.orderMatnrHitsBarcodeView(code, viewItems); log.info("[cus_barcode_sync] 强制校验 code={} viewHit={}", code, hit); if (!hit) { throw new CoolException("物料未在视图 cus_barcode_sync_view 中:" + code); } } cusBarcodeSyncMatnrApplyService.applyFromViewRows(viewItems, orderItemByCode, loginUserId); } private void syncMatnrNonForceFromView(List<String> matnrCodes, Map<String, SyncOrdersItem> orderItemByCode, Long loginUserId) { private void syncMatnrNonForceFromView(CusItemSyncConfigSnapshot cfg, List<String> matnrCodes, Map<String, SyncOrdersItem> orderItemByCode, Long loginUserId) { List<Map<String, Object>> viewItems = null; try { viewItems = cusBarcodeSyncViewQueryService.listByItemNos(matnrCodes); } catch (Exception ex) { log.warn("查询 cus_barcode_sync_view 失败,将仅按物料表校验:{}", ex.getMessage()); log.warn("[cus_barcode_sync] 查询 cus_barcode_sync_view 异常,将仅按物料表校验", ex); } log.info("[cus_barcode_sync] NONE 分支 CUS_ITEM_SYNC_MODE.val={} 副库视图 rows={} barcodesInView=[{}]", formatCfgVal(cfg.rawVal), viewItems == null ? 0 : viewItems.size(), summarizeViewBarcodes(viewItems)); if (viewItems != null && !viewItems.isEmpty()) { try { cusBarcodeSyncMatnrApplyService.applyFromViewRows(viewItems, orderItemByCode, loginUserId); } catch (Exception ex) { log.warn("按条码视图写入物料主数据失败:{}", ex.getMessage()); log.warn("[cus_barcode_sync] 批量 applyFromViewRows 失败", ex); } } // 视图有条码但本地仍无:按行补建档 for (String code : matnrCodes) { if (CusBarcodeSyncViewQueryService.orderMatnrHitsBarcodeView(code, viewItems)) { Matnr m = findLocalMatnrForOrderCode(code); if (m != null) { log.info("[cus_barcode_sync] 校验通过 code={} localMatnrId={}", code, m.getId()); continue; } Matnr m = findLocalMatnrForOrderCode(code); boolean viewHit = viewItems != null && CusBarcodeSyncViewQueryService.orderMatnrHitsBarcodeView(code, viewItems); log.info("[cus_barcode_sync] 本地无记录 code={} viewHit={} viewRowCount={}", code, viewHit, viewItems == null ? 0 : viewItems.size()); if (viewHit && viewItems != null) { List<Map<String, Object>> rowsForCode = viewItems.stream() .filter(r -> CusBarcodeSyncViewQueryService.rowMatchesOrderMatnr(code, Objects.toString(r.get("barcode"), null))) .collect(Collectors.toList()); if (!rowsForCode.isEmpty()) { try { log.info("[cus_barcode_sync] 按条码补档 apply rows={} code={}", rowsForCode.size(), code); cusBarcodeSyncMatnrApplyService.applyFromViewRows(rowsForCode, orderItemByCode, loginUserId); } catch (Exception ex) { log.warn("[cus_barcode_sync] 按视图补全物料失败 code={}", code, ex); } m = findLocalMatnrForOrderCode(code); } } if (m == null) { log.warn("[cus_barcode_sync] 仍无法落地 man_matnr code={} viewHit={} viewSample=[{}]", code, viewHit, summarizeViewBarcodes(viewItems)); throw new CoolException("物料不存在:" + code); } } } private static String joinCodesForLog(List<String> matnrCodes) { if (matnrCodes == null || matnrCodes.isEmpty()) { return ""; } String s = String.join(",", matnrCodes); return s.length() > 1200 ? s.substring(0, 1200) + "..." : s; } private static String summarizeViewBarcodes(List<Map<String, Object>> viewItems) { if (viewItems == null || viewItems.isEmpty()) { return ""; } String s = viewItems.stream() .map(r -> Objects.toString(r.get("barcode"), "")) .filter(StringUtils::isNotBlank) .distinct() .collect(Collectors.joining(",")); return s.length() > 1200 ? s.substring(0, 1200) + "..." : s; } private Matnr findLocalMatnrForOrderCode(String orderMatnr) { @@ -147,4 +211,15 @@ return matnrService.getOneByCodeAndBatch(t, ""); } private static final class CusItemSyncConfigSnapshot { final CusItemSyncMode mode; /** sys_config.CUS_ITEM_SYNC_MODE 的 val,无配置行为 null */ final String rawVal; CusItemSyncConfigSnapshot(CusItemSyncMode mode, String rawVal) { this.mode = mode; this.rawVal = rawVal; } } } rsf-server/src/main/java/com/vincent/rsf/server/system/controller/ConfigController.java
@@ -1,12 +1,10 @@ package com.vincent.rsf.server.system.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.vincent.rsf.framework.common.Cools; import com.vincent.rsf.framework.common.R; import com.vincent.rsf.framework.common.SnowflakeIdWorker; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.common.annotation.OperationLog; import com.vincent.rsf.server.common.domain.BaseParam; import com.vincent.rsf.server.common.domain.KeyValVo; @@ -79,9 +77,9 @@ config.setUpdateTime(new Date()); if (!configService.save(config)) { return R.error("Save Fail"); } else { ConfigServiceImpl.CONFIG_CACHE.put(config.getFlag(), config); } ConfigServiceImpl.CONFIG_CACHE.put(config.getFlag(), config); configService.evictSysConfigRedis(config.getFlag()); return R.ok("Save Success").add(config); } @@ -98,9 +96,9 @@ config.setUpdateTime(new Date()); if (!configService.updateById(config)) { return R.error("Update Fail"); } else { ConfigServiceImpl.CONFIG_CACHE.put(config.getFlag(), config); } ConfigServiceImpl.CONFIG_CACHE.put(config.getFlag(), config); configService.evictSysConfigRedis(config.getFlag()); return R.ok("Update Success").add(config); } @@ -125,10 +123,10 @@ } if (!configService.removeByIds(Arrays.asList(ids))) { return R.error("Delete Fail"); } else { for (String flag : flagList) { ConfigServiceImpl.CONFIG_CACHE.remove(flag); } } for (String flag : flagList) { ConfigServiceImpl.CONFIG_CACHE.remove(flag); configService.evictSysConfigRedis(flag); } return R.ok("Delete Success").add(ids); } rsf-server/src/main/java/com/vincent/rsf/server/system/service/ConfigService.java
@@ -6,6 +6,15 @@ public interface ConfigService extends IService<Config> { /** * 优先 JVM 内全局缓存(启动预载 + 后台增改删时维护),未命中或缓存已失效再查库并回填。 * 若启用 Redis:先读带 TTL 的副本,过期或缺失则读库并以 setex 回写;不使用永久 key。 */ Config getCachedOrLoad(String flag); /** 配置变更后剔除 Redis 中的副本,下次读取从库加载并重新 setex */ void evictSysConfigRedis(String flag); <T> T getVal(String key, Class<T> clazz); <T> boolean setVal(String key, T val); rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/ConfigServiceImpl.java
@@ -10,9 +10,13 @@ import com.vincent.rsf.server.system.entity.Config; import com.vincent.rsf.server.system.enums.ConfigType; import com.vincent.rsf.server.system.enums.StatusType; import com.vincent.rsf.server.common.service.RedisService; import com.vincent.rsf.server.system.config.ConfigCacheProperties; import com.vincent.rsf.server.system.mapper.ConfigMapper; import com.vincent.rsf.server.system.service.ConfigService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; @@ -28,7 +32,15 @@ @Slf4j public class ConfigServiceImpl extends ServiceImpl<ConfigMapper, Config> implements ConfigService { /** 与 {@link RedisService#set(String, String, Object, Integer)} 的 flag 段一致:sys_config 非永久 key,见 {@link ConfigCacheProperties} */ private static final String REDIS_FLAG_SYS_CONFIG = "SYS_CONFIG"; public static final Map<String, Config> CONFIG_CACHE = new ConcurrentHashMap<>(); @Autowired(required = false) private RedisService redisService; @Autowired private ConfigCacheProperties configCacheProperties; @PostConstruct public void init() { @@ -41,6 +53,82 @@ } catch (Exception e) { log.warn("配置缓存初始化失败,跳过配置表加载,后续按默认流程处理", e); } } private boolean redisReady() { return redisService != null && Boolean.TRUE.equals(redisService.initialize); } private static boolean isEffectiveConfig(Config c) { return c != null && (c.getDeleted() == null || c.getDeleted() == 0); } private Config loadConfigFromDb(String flag) { return getOne(new LambdaQueryWrapper<Config>() .eq(Config::getFlag, flag) .eq(Config::getDeleted, 0), false); } private Config tryRedisGetConfig(String flag) { try { return redisService.get(REDIS_FLAG_SYS_CONFIG, flag); } catch (Exception e) { log.debug("sys_config Redis get flag={}", flag, e); return null; } } private void tryRedisSetexConfig(String flag, Config loaded) { try { int ttl = Math.max(1, configCacheProperties.getRedisTtlSeconds()); redisService.set(REDIS_FLAG_SYS_CONFIG, flag, loaded, ttl); } catch (Exception e) { log.warn("sys_config Redis setex flag={}", flag, e); } } @Override public void evictSysConfigRedis(String flag) { if (!redisReady() || StringUtils.isBlank(flag)) { return; } try { redisService.delete(REDIS_FLAG_SYS_CONFIG, flag); } catch (Exception e) { log.warn("sys_config Redis evict flag={}", flag, e); } } @Override public Config getCachedOrLoad(String flag) { if (StringUtils.isBlank(flag)) { return null; } if (redisReady()) { Config fromRedis = tryRedisGetConfig(flag); if (isEffectiveConfig(fromRedis)) { CONFIG_CACHE.put(flag, fromRedis); return fromRedis; } Config loaded = loadConfigFromDb(flag); if (loaded != null) { CONFIG_CACHE.put(flag, loaded); tryRedisSetexConfig(flag, loaded); } return loaded; } Config cached = CONFIG_CACHE.get(flag); if (isEffectiveConfig(cached)) { return cached; } if (cached != null) { CONFIG_CACHE.remove(flag); } Config loaded = loadConfigFromDb(flag); if (loaded != null) { CONFIG_CACHE.put(flag, loaded); } return loaded; } @Override @@ -130,7 +218,11 @@ throw new UnsupportedOperationException("Unsupported ConfigType: " + configType); } return this.updateById(config); boolean ok = this.updateById(config); if (ok) { evictSysConfigRedis(key); } return ok; } private List<Config> safeList(LambdaQueryWrapper<Config> wrapper) { @@ -152,6 +244,15 @@ if (!this.update(new LambdaUpdateWrapper<Config>().set(Config::getVal, config.getVal()).eq(Config::getFlag, config.getFlag()))) { throw new CoolException("修改失败!!"); } Config fresh = getOne(new LambdaQueryWrapper<Config>() .eq(Config::getFlag, config.getFlag()) .eq(Config::getDeleted, 0), false); if (fresh != null) { CONFIG_CACHE.put(fresh.getFlag(), fresh); } else { CONFIG_CACHE.remove(config.getFlag()); } evictSysConfigRedis(config.getFlag()); return R.ok(); } rsf-server/src/main/resources/application.yml
@@ -6,6 +6,9 @@ system-name: @pom.artifactId@ system-version: @pom.version@ system-mode: OFFLINE cache: # sys_config 写入 Redis 的过期秒数(setex,不用永久 key);到期后下次读取从库刷新(4 小时) redis-ttl-seconds: 14400 token-key: KUHSMcYQ4lePt3r6bckz0P13cBJyoonYqInThvQlUnbsFCIcCcZZAbWZ6UNFztYNYPhGdy6eyb8WdIz8FU2Cz396TyTJk3NI2rtXMHBOehRb4WWJ4MdYVVg2oWPyqRQ2 super-username: root code-length: 6