|
|
@@ -345,6 +345,285 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
|
|
|
return diff;
|
|
|
}
|
|
|
|
|
|
+ /** 主表免审字段(与配置 productColumns 取并集) */
|
|
|
+ private static final Set<String> DEFAULT_PRODUCT_EXEMPT_AUDIT_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
+ "agentPrice", "cost", "otPrice", "stock", "integral",
|
|
|
+ "isHot", "isGood", "isDisplay", "isBest", "isNew",
|
|
|
+ "giveIntegral", "sort", "sales"
|
|
|
+ )));
|
|
|
+
|
|
|
+ /** 规格免审字段 */
|
|
|
+ private static final Set<String> DEFAULT_ATTR_EXEMPT_AUDIT_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
+ "agentPrice", "cost", "otPrice", "stock", "groupBarCode",
|
|
|
+ "weight", "volume", "integral",
|
|
|
+ "brokerage", "brokerageTwo", "brokerageThree",
|
|
|
+ "sales", "giveIntegral"
|
|
|
+ )));
|
|
|
+
|
|
|
+ /** 对比忽略字段 */
|
|
|
+ private static final Set<String> PRODUCT_AUDIT_IGNORE_FIELDS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
+ "isAudit", "reviewAudit", "commentContent", "complaint",
|
|
|
+ "storeIds", "serialVersionUID", "browse", "views",
|
|
|
+ "codePath", "codeUrl", "transactionStatus", "platformProductId",
|
|
|
+ "updateTime", "createTime", "params", "searchValue",
|
|
|
+ "beginTime", "endTime", "createBy", "updateBy", "remark",
|
|
|
+ "id", "productId", "storeId", "detail"
|
|
|
+ )));
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 已审通过商品编辑时是否需要重新待审。
|
|
|
+ * 规则:商品状态下架不触发审核;上架时再按字段差异判定(免审字段除外)。
|
|
|
+ */
|
|
|
+ private boolean needReAuditOnEdit(FsStoreProductScrm oldProduct, FsStoreProductScrm newProduct,
|
|
|
+ FsStoreProductAddEditParam param) {
|
|
|
+ if (oldProduct == null || newProduct == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!"1".equals(oldProduct.getIsAudit())) {
|
|
|
+ log.info("product skip re-audit, not audited passed, productId={}, oldIsAudit={}",
|
|
|
+ oldProduct.getProductId(), oldProduct.getIsAudit());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 下架不需要审核;仅上架才进入字段差异审核
|
|
|
+ Integer newShow = newProduct.getIsShow();
|
|
|
+ if (newShow == null || newShow.intValue() != 1) {
|
|
|
+ log.info("product skip re-audit, offline, productId={}, newIsShow={}",
|
|
|
+ oldProduct.getProductId(), newShow);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!isProductAuditSwitchOn()) {
|
|
|
+ log.info("product skip re-audit, audit switch off, productId={}", oldProduct.getProductId());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ Set<String> exemptFields = resolveProductExemptAuditFields();
|
|
|
+ try {
|
|
|
+ for (String fieldName : collectChangedFields(oldProduct, newProduct)) {
|
|
|
+ if (PRODUCT_AUDIT_IGNORE_FIELDS.contains(fieldName) || exemptFields.contains(fieldName)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ log.info("product re-audit by main field, productId={}, field={}", oldProduct.getProductId(), fieldName);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ boolean attrChanged = hasSensitiveAttrChange(oldProduct.getProductId(), param == null ? null : param.getValues());
|
|
|
+ if (attrChanged) {
|
|
|
+ log.info("product re-audit by attr change, productId={}", oldProduct.getProductId());
|
|
|
+ } else {
|
|
|
+ log.info("product no re-audit, no sensitive change, productId={}", oldProduct.getProductId());
|
|
|
+ }
|
|
|
+ return attrChanged;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("product audit diff failed, productId={}", oldProduct.getProductId(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isProductAuditSwitchOn() {
|
|
|
+ try {
|
|
|
+ JSONObject switchConfig = configUtil.generateConfigByKey("medicalMall.func.switch");
|
|
|
+ if (switchConfig == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return Boolean.TRUE.equals(switchConfig.getBoolean("isAudit"));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("read medicalMall.func.switch.isAudit failed", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> resolveProductExemptAuditFields() {
|
|
|
+ Set<String> exempt = new HashSet<>(DEFAULT_PRODUCT_EXEMPT_AUDIT_FIELDS);
|
|
|
+ try {
|
|
|
+ JSONObject switchConfig = configUtil.generateConfigByKey("medicalMall.func.switch");
|
|
|
+ if (switchConfig == null) {
|
|
|
+ return exempt;
|
|
|
+ }
|
|
|
+ JSONArray productColumns = switchConfig.getJSONArray("productColumns");
|
|
|
+ if (productColumns == null || productColumns.isEmpty()) {
|
|
|
+ return exempt;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < productColumns.size(); i++) {
|
|
|
+ String column = productColumns.getString(i);
|
|
|
+ if (com.fs.common.utils.StringUtils.isNotEmpty(column)) {
|
|
|
+ exempt.add(column.trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("read productColumns failed, use default exempt fields", e);
|
|
|
+ }
|
|
|
+ return exempt;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Set<String> collectChangedFields(Object oldObj, Object newObj) throws IllegalAccessException {
|
|
|
+ Set<String> changed = new HashSet<>();
|
|
|
+ if (oldObj == null || newObj == null || !oldObj.getClass().equals(newObj.getClass())) {
|
|
|
+ return changed;
|
|
|
+ }
|
|
|
+ for (Field field : oldObj.getClass().getDeclaredFields()) {
|
|
|
+ if (PRODUCT_AUDIT_IGNORE_FIELDS.contains(field.getName())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ field.setAccessible(true);
|
|
|
+ Object oldVal = field.get(oldObj);
|
|
|
+ Object newVal = field.get(newObj);
|
|
|
+ if (!isAuditValueEqual(oldVal, newVal)) {
|
|
|
+ changed.add(field.getName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return changed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isAuditValueEqual(Object oldVal, Object newVal) {
|
|
|
+ if (Objects.equals(oldVal, newVal)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (newVal == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (isBlankAuditValue(oldVal) && isBlankAuditValue(newVal)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (oldVal instanceof Number || newVal instanceof Number || oldVal instanceof BigDecimal || newVal instanceof BigDecimal) {
|
|
|
+ BigDecimal oldBd = toAuditBigDecimal(oldVal);
|
|
|
+ BigDecimal newBd = toAuditBigDecimal(newVal);
|
|
|
+ if (oldBd != null && newBd != null) {
|
|
|
+ return oldBd.compareTo(newBd) == 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (oldVal instanceof Boolean || newVal instanceof Boolean || oldVal instanceof Byte || newVal instanceof Byte) {
|
|
|
+ Integer oldNum = toAuditInteger(oldVal);
|
|
|
+ Integer newNum = toAuditInteger(newVal);
|
|
|
+ if (oldNum != null && newNum != null) {
|
|
|
+ return oldNum.equals(newNum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Objects.equals(String.valueOf(oldVal).trim(), String.valueOf(newVal).trim());
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isBlankAuditValue(Object val) {
|
|
|
+ if (val == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (val instanceof CharSequence) {
|
|
|
+ return val.toString().trim().isEmpty();
|
|
|
+ }
|
|
|
+ if (val instanceof Map) {
|
|
|
+ return ((Map<?, ?>) val).isEmpty();
|
|
|
+ }
|
|
|
+ if (val instanceof Collection) {
|
|
|
+ return ((Collection<?>) val).isEmpty();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal toAuditBigDecimal(Object val) {
|
|
|
+ if (val == null || isBlankAuditValue(val)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ if (val instanceof BigDecimal) {
|
|
|
+ return (BigDecimal) val;
|
|
|
+ }
|
|
|
+ return new BigDecimal(String.valueOf(val).trim());
|
|
|
+ } catch (Exception e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Integer toAuditInteger(Object val) {
|
|
|
+ if (val == null || isBlankAuditValue(val)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (val instanceof Boolean) {
|
|
|
+ return Boolean.TRUE.equals(val) ? 1 : 0;
|
|
|
+ }
|
|
|
+ if (val instanceof Number) {
|
|
|
+ return ((Number) val).intValue();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return new BigDecimal(String.valueOf(val).trim()).intValue();
|
|
|
+ } catch (Exception e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasSensitiveAttrChange(Long productId, List<FsStoreProductAttrValueScrm> newValues) {
|
|
|
+ if (productId == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ List<FsStoreProductAttrValueScrm> oldValues =
|
|
|
+ fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(productId);
|
|
|
+ List<FsStoreProductAttrValueScrm> safeNew = newValues == null ? Collections.emptyList() : newValues;
|
|
|
+ List<FsStoreProductAttrValueScrm> safeOld = oldValues == null ? Collections.emptyList() : oldValues;
|
|
|
+ if (safeNew.isEmpty() && safeOld.isEmpty()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<Long, FsStoreProductAttrValueScrm> oldById = new HashMap<>();
|
|
|
+ Map<String, FsStoreProductAttrValueScrm> oldBySku = new HashMap<>();
|
|
|
+ for (FsStoreProductAttrValueScrm oldVal : safeOld) {
|
|
|
+ if (oldVal == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (oldVal.getId() != null) {
|
|
|
+ oldById.put(oldVal.getId(), oldVal);
|
|
|
+ }
|
|
|
+ if (com.fs.common.utils.StringUtils.isNotEmpty(oldVal.getSku())) {
|
|
|
+ oldBySku.put(oldVal.getSku(), oldVal);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<Long> matchedOldIds = new HashSet<>();
|
|
|
+ boolean newHasAnyId = false;
|
|
|
+ for (FsStoreProductAttrValueScrm newVal : safeNew) {
|
|
|
+ if (newVal == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (newVal.getId() != null) {
|
|
|
+ newHasAnyId = true;
|
|
|
+ }
|
|
|
+ FsStoreProductAttrValueScrm oldVal = null;
|
|
|
+ if (newVal.getId() != null) {
|
|
|
+ oldVal = oldById.get(newVal.getId());
|
|
|
+ }
|
|
|
+ if (oldVal == null && com.fs.common.utils.StringUtils.isNotEmpty(newVal.getSku())) {
|
|
|
+ oldVal = oldBySku.get(newVal.getSku());
|
|
|
+ }
|
|
|
+ if (oldVal == null) {
|
|
|
+ if (newVal.getId() == null && com.fs.common.utils.StringUtils.isEmpty(newVal.getSku())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ log.info("product re-audit by new attr, productId={}, sku={}", productId, newVal.getSku());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (oldVal.getId() != null) {
|
|
|
+ matchedOldIds.add(oldVal.getId());
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ for (String fieldName : collectChangedFields(oldVal, newVal)) {
|
|
|
+ if (PRODUCT_AUDIT_IGNORE_FIELDS.contains(fieldName)
|
|
|
+ || DEFAULT_ATTR_EXEMPT_AUDIT_FIELDS.contains(fieldName)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ log.info("product re-audit by attr field, productId={}, attrId={}, field={}", productId, oldVal.getId(), fieldName);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("attr audit diff failed, productId={}", productId, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (newHasAnyId) {
|
|
|
+ for (FsStoreProductAttrValueScrm oldVal : safeOld) {
|
|
|
+ if (oldVal != null && oldVal.getId() != null && !matchedOldIds.contains(oldVal.getId())) {
|
|
|
+ log.info("product re-audit by deleted attr, productId={}, attrId={}", productId, oldVal.getId());
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 批量删除商品
|
|
|
*
|
|
|
@@ -546,6 +825,9 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
|
|
|
}
|
|
|
|
|
|
//基础信息
|
|
|
+ if (param.getValues() == null || param.getValues().isEmpty()) {
|
|
|
+ return R.error("请至少添加一个商品规格");
|
|
|
+ }
|
|
|
ProductAttrCountDto countDto = computedProductCount(param.getValues());
|
|
|
FsStoreProductScrm product = new FsStoreProductScrm();
|
|
|
BeanUtils.copyProperties(param, product);
|
|
|
@@ -555,15 +837,14 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
|
|
|
populateDateRanges(product, param);
|
|
|
setDefaultBarcodeIfAvailable(product, param);
|
|
|
product.setBusinessLink(param.getBusinessLink());
|
|
|
- product.setCreateTime(new Date());
|
|
|
|
|
|
- //判断是否是纯正堂非药品商品不审核
|
|
|
+ // 判断是否是纯正堂非药品商品不审核
|
|
|
if(CZT.equals(cloudHostProper.getCompanyName()) || param.getIsDrug() != 1){
|
|
|
product.setIsAudit("1");
|
|
|
}
|
|
|
|
|
|
- //校验是否药品网络销售禁止清单
|
|
|
- if(product.getIsDrug().equals("1")){//药品进入校验
|
|
|
+ // 校验是否药品网络销售禁止清单
|
|
|
+ if("1".equals(product.getIsDrug())){//药品进入校验
|
|
|
String checkStr = checkProductInfo(product);
|
|
|
if(checkStr != null){
|
|
|
return R.error(checkStr);
|
|
|
@@ -572,39 +853,31 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
|
|
|
|
|
|
if(param.getProductId() != null && param.getProductId() > 0){
|
|
|
FsStoreProductScrm oldFsStoreProduct = fsStoreProductMapper.selectFsStoreProductById(product.getProductId());
|
|
|
- Boolean isAudit = configUtil.generateConfigByKey("medicalMall.func.switch").getBoolean("isAudit");
|
|
|
- try {
|
|
|
- if (isAudit != null && isAudit && param.getIsDrug() != 1) {
|
|
|
- if (oldFsStoreProduct.getIsAudit() != null && "1".equals(oldFsStoreProduct.getIsAudit())) {
|
|
|
- Map<String, Object> diff = getDiff(oldFsStoreProduct, product);
|
|
|
- Set<String> diff_columns = diff.keySet();
|
|
|
- JSONArray productColumns = configUtil.generateConfigByKey("medicalMall.func.switch").getJSONArray("productColumns");
|
|
|
- if(com.fs.common.utils.StringUtils.isNotEmpty(productColumns)){
|
|
|
- //判断diff_columns是否在productColumns中,不是则将isAudit设置为0
|
|
|
- for (String column : diff_columns) {
|
|
|
- if (!productColumns.contains(column)) {
|
|
|
- product.setIsAudit("0");
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }else{
|
|
|
- product.setIsAudit("0");
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (IllegalAccessException e) {
|
|
|
- log.error("获取diff出错", e);
|
|
|
+ if (oldFsStoreProduct == null) {
|
|
|
+ return R.error("商品不存在");
|
|
|
}
|
|
|
- //只要是编辑,都待审核,通过审核sql 去处理audit和show字段。
|
|
|
- if(oldFsStoreProduct.getIsAudit() != null){
|
|
|
+ // 编辑:保留原创建时间
|
|
|
+ product.setCreateTime(oldFsStoreProduct.getCreateTime());
|
|
|
+ product.setUpdateTime(DateUtils.getNowDate());
|
|
|
+ // 先继承旧审核状态,再按规则决定是否进入审核中
|
|
|
+ if (oldFsStoreProduct.getIsAudit() != null) {
|
|
|
+ product.setIsAudit(oldFsStoreProduct.getIsAudit());
|
|
|
+ }
|
|
|
+ boolean offlineToOnline = !Integer.valueOf(1).equals(oldFsStoreProduct.getIsShow())
|
|
|
+ && Integer.valueOf(1).equals(product.getIsShow());
|
|
|
+ if (offlineToOnline) {
|
|
|
+ // 下架改上架:强制进入审核中,不依赖旧审核状态与字段差异
|
|
|
+ log.info("product re-audit by offlineToOnline, productId={}, oldIsShow={}, newIsShow={}",
|
|
|
+ product.getProductId(), oldFsStoreProduct.getIsShow(), product.getIsShow());
|
|
|
+ product.setIsAudit("0");
|
|
|
+ } else if (needReAuditOnEdit(oldFsStoreProduct, product, param)) {
|
|
|
product.setIsAudit("0");
|
|
|
}
|
|
|
fsStoreProductMapper.updateFsStoreProductNullable(product);
|
|
|
-// else外面也会调用这个方法,为啥会在if中调用执行handleProductAttributes
|
|
|
-// handleProductAttributes(param, product, storeId);
|
|
|
} else{
|
|
|
- //新添加的商品默认都应该不是上架状态,流程都是需要先审核。即使你新增时候选择了上架状态,也应该是上架并待审核才对。
|
|
|
- if(product.getIsShow() == 1){
|
|
|
+ // 新增:上架需待审核;下架不强制进审
|
|
|
+ product.setCreateTime(new Date());
|
|
|
+ if (Integer.valueOf(1).equals(product.getIsShow())) {
|
|
|
product.setIsAudit("0");
|
|
|
}
|
|
|
fsStoreProductMapper.insertFsStoreProduct(product);
|
|
|
@@ -1334,9 +1607,8 @@ private void addProductAttr(Long productId, List<ProductArrtDTO> items, List<FsS
|
|
|
@Override
|
|
|
@Transactional
|
|
|
public void batchAudit(ProductAuditDTO auditDTO) {
|
|
|
- //isAudit = 1 通过,如果通过了就需要将商品同步到产品库去,做一个id绑定。
|
|
|
- //如果审核通过就是上架,如果审核不通过就是处于下架状态,为了清晰这里可以添加枚举(0未审核1审核通过2审核退回),
|
|
|
- // 正常情况是退回后可以申请重新上架的如果不通过2,那么show就变回0(直接在sql中去做的),按照原有逻辑去做
|
|
|
+ // isAudit = 1 通过;通过后 is_show=1。若审核前商品状态已是上架,则清空 offline_remark
|
|
|
+ // isAudit != 1 退回;is_show=0,保留下架备注
|
|
|
fsStoreProductMapper.batchAudit(auditDTO);
|
|
|
storeAuditLogUtil.addBatchAuditList(auditDTO.getProductIds(),auditDTO.getReason(),auditDTO.getAttachImage());
|
|
|
//审核通过进入、平台商品库
|
|
|
@@ -1511,6 +1783,77 @@ private void addProductAttr(Long productId, List<ProductArrtDTO> items, List<FsS
|
|
|
return fsStoreProductMapper.getProductNoticeInfo(storeId);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 资质过期自动下架。
|
|
|
+ * 规则:上架中的药品,营业执照/生产许可证/注册证任一到期且非长期有效,则下架并写入下架备注。
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public int offlineExpiredQualificationProducts() {
|
|
|
+ List<FsStoreProductScrm> expiredProducts = fsStoreProductMapper.selectExpiredQualificationProducts();
|
|
|
+ if (expiredProducts == null || expiredProducts.isEmpty()) {
|
|
|
+ log.info("资质过期自动下架:无待处理商品");
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ int successCount = 0;
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ for (FsStoreProductScrm product : expiredProducts) {
|
|
|
+ if (product == null || product.getProductId() == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String offlineRemark = buildExpiredQualificationRemark(product, today);
|
|
|
+ if (com.fs.common.utils.StringUtils.isEmpty(offlineRemark)) {
|
|
|
+ log.warn("资质过期自动下架跳过:未拼出有效备注, productId={}", product.getProductId());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ int rows = fsStoreProductMapper.offlineProductByQualification(product.getProductId(), offlineRemark);
|
|
|
+ if (rows > 0) {
|
|
|
+ successCount++;
|
|
|
+ log.info("资质过期自动下架成功, productId={}, productName={}, remark={}",
|
|
|
+ product.getProductId(), product.getProductName(), offlineRemark);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.info("资质过期自动下架完成, 待处理={}, 成功={}", expiredProducts.size(), successCount);
|
|
|
+ return successCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组装资质过期下架备注(仅包含实际已过期且非长期有效的证件)
|
|
|
+ */
|
|
|
+ private String buildExpiredQualificationRemark(FsStoreProductScrm product, LocalDate today) {
|
|
|
+ List<String> reasons = new ArrayList<>();
|
|
|
+ if (isQualificationExpired(product.getBusinessEnd(), product.getIsBusinessPermanent(), today)) {
|
|
|
+ reasons.add("生产企业营业执照已过期");
|
|
|
+ }
|
|
|
+ if (isQualificationExpired(product.getLicenseEnd(), product.getIsLicensePermanent(), today)) {
|
|
|
+ reasons.add("生产企业的生产许可证/备案凭证已过期");
|
|
|
+ }
|
|
|
+ if (isQualificationExpired(product.getCertificateEnd(), product.getIsCertificatePermanent(), today)) {
|
|
|
+ reasons.add("商品注册证/备案凭证已过期");
|
|
|
+ }
|
|
|
+ if (reasons.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return "资质过期自动下架(" + String.join("、", reasons) + ")";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 证件是否已过期:到期日早于当天,且未标记长期有效
|
|
|
+ */
|
|
|
+ private boolean isQualificationExpired(LocalDate endDate, Byte permanentFlag, LocalDate today) {
|
|
|
+ if (endDate == null || today == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (isQualificationPermanent(permanentFlag)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return endDate.isBefore(today);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isQualificationPermanent(Byte permanentFlag) {
|
|
|
+ return permanentFlag != null && permanentFlag.byteValue() == 1;
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public R copyProduct(FsStoreProductScrm product) {
|