原材料入库结存

This commit is contained in:
2026-05-15 11:37:52 +08:00
839 changed files with 41718 additions and 425 deletions

View File

@@ -18,5 +18,11 @@
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- 复用打印模板模块打印机枚举、业务绑定、PDF 提交队列 -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-system-biz</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,164 @@
package org.jeecg.modules.xslmes.bootstrap;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.print.entity.PrintBizPermEntity;
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
import org.jeecg.modules.print.util.PrintBizDetailPropertyScanner;
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecg.modules.system.entity.SysPermission;
import org.jeecg.modules.system.service.ISysPermissionService;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail;
import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 启动后异步:遍历 {@code print_biz_perm_entity} 中已配置实体类的业务,反射主表与明细槽位字段并写入 {@code mes_xsl_biz_entity_field_*}
* 供「业务打印绑定」弹窗读取(避免运行时频繁反射)。
*
* <p>关闭:{@code jeecg.print.biz-entity-field-catalog-sync=false}
*
* <p>建议在 {@link org.jeecg.modules.print.bootstrap.PrintBizPermEntityWarmupRunner}Order 2000之后执行。
*/
@Slf4j
@Component
@Order(2100)
public class BizEntityFieldCatalogSyncRunner implements ApplicationRunner {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Resource
private IPrintBizPermEntityService printBizPermEntityService;
@Resource
private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService;
@Resource
private ISysPermissionService sysPermissionService;
@Value("${jeecg.print.biz-entity-field-catalog-sync:true}")
private boolean syncEnabled;
/** 延迟执行秒数,便于晚于「打印菜单实体映射预热」写完 print_biz_perm_entity */
@Value("${jeecg.print.biz-entity-field-catalog-sync-delay-seconds:10}")
private int catalogSyncDelaySeconds;
@Override
public void run(ApplicationArguments args) {
if (!syncEnabled) {
log.info("业务实体字段缓存同步已关闭jeecg.print.biz-entity-field-catalog-sync=false");
return;
}
log.info(
"业务实体字段缓存:将在 {} 秒后异步同步(来源 print_biz_perm_entity → mes_xsl_biz_entity_field_*",
Math.max(0, catalogSyncDelaySeconds));
CompletableFuture.runAsync(
this::doSync,
CompletableFuture.delayedExecutor(
Math.max(0, catalogSyncDelaySeconds),
TimeUnit.SECONDS,
ForkJoinPool.commonPool()));
}
private void doSync() {
try {
log.info("业务实体字段缓存:开始异步同步(来源 print_biz_perm_entity");
List<PrintBizPermEntity> rows = printBizPermEntityService.list();
if (rows == null || rows.isEmpty()) {
log.info("业务实体字段缓存print_biz_perm_entity 无数据,跳过");
return;
}
int ok = 0;
int skip = 0;
for (PrintBizPermEntity row : rows) {
if (row == null || StringUtils.isBlank(row.getPermId())) {
skip++;
continue;
}
String permId = row.getPermId().trim();
String entityFqn = StringUtils.trimToNull(row.getEntityClass());
if (entityFqn == null) {
skip++;
continue;
}
Class<?> clazz = PrintBizEntityFieldIntrospector.tryLoadClass(entityFqn);
if (clazz == null) {
skip++;
continue;
}
try {
syncOne(permId, clazz, entityFqn);
ok++;
} catch (Exception ex) {
log.warn("业务实体字段缓存同步失败 permId={} entity={}", permId, entityFqn, ex);
skip++;
}
}
log.info("业务实体字段缓存同步完成:成功 {} 条,跳过 {} 条", ok, skip);
} catch (Exception e) {
log.warn("业务实体字段缓存同步异常(不影响系统启动)", e);
}
}
private void syncOne(String permId, Class<?> clazz, String entityFqn) throws Exception {
List<PrintBizFieldItemVO> mainFields = PrintBizEntityFieldIntrospector.listFields(clazz);
String mainJson = OBJECT_MAPPER.writeValueAsString(mainFields);
List<PrintBizDetailSlotVO> slots = PrintBizDetailPropertyScanner.listSlots(clazz);
List<MesXslBizEntityFieldDetail> detailRows = new ArrayList<>();
int sort = 0;
Date now = new Date();
for (PrintBizDetailSlotVO slot : slots) {
Class<?> itemClazz =
PrintBizDetailPropertyScanner.resolveItemClassForSlot(
clazz, slot.getPropertyName(), slot.getSlotKind());
if (itemClazz == null) {
continue;
}
List<PrintBizFieldItemVO> itemFields = PrintBizEntityFieldIntrospector.listFields(itemClazz);
MesXslBizEntityFieldDetail d = new MesXslBizEntityFieldDetail();
d.setDetailPropertyName(slot.getPropertyName());
d.setDetailSlotKind(slot.getSlotKind());
d.setDetailName(slot.getLabel());
d.setDetailEntityClassName(fqn(itemClazz));
d.setDetailFieldsJson(OBJECT_MAPPER.writeValueAsString(itemFields));
d.setSortNo(sort++);
d.setCreateTime(now);
d.setUpdateTime(now);
detailRows.add(d);
}
String bizName = resolveMenuName(permId);
bizEntityFieldProfileService.upsertScannedProfile(permId, bizName, entityFqn, mainJson, detailRows);
}
private static String fqn(Class<?> c) {
if (c == null) {
return null;
}
String cn = c.getCanonicalName();
return cn != null ? cn : c.getName();
}
private String resolveMenuName(String permId) {
SysPermission p = sysPermissionService.getById(permId);
if (p != null && StringUtils.isNotBlank(p.getName())) {
return p.getName().trim();
}
return permId;
}
}

View File

@@ -0,0 +1,16 @@
package org.jeecg.modules.xslmes.capacity;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import java.util.List;
/**
* 库区展示用「实际存放量」数据源贡献者(可多实现并按 Spring Order 编排)
*/
public interface WarehouseAreaActualCapacityContribution {
/**
* @param areas 通常为列表/分页的一段记录(内存回填,不写库)
*/
void contribute(List<MesXslWarehouseArea> areas);
}

View File

@@ -0,0 +1,31 @@
package org.jeecg.modules.xslmes.capacity;
import lombok.RequiredArgsConstructor;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 编排库区「实际存放量」展示回填(可多 {@link WarehouseAreaActualCapacityContribution} 扩展)。
*/
@Component
@RequiredArgsConstructor
public class WarehouseAreaDisplayedActualCapacityMediator {
private final List<WarehouseAreaActualCapacityContribution> contributions;
public void apply(Collection<MesXslWarehouseArea> areas) {
if (areas == null || areas.isEmpty()) {
return;
}
List<MesXslWarehouseArea> list = new ArrayList<>(areas);
List<WarehouseAreaActualCapacityContribution> ordered = contributions.stream().sorted(AnnotationAwareOrderComparator.INSTANCE).toList();
for (WarehouseAreaActualCapacityContribution c : ordered) {
c.contribute(list);
}
}
}

View File

@@ -0,0 +1,134 @@
package org.jeecg.modules.xslmes.capacity.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.xslmes.capacity.WarehouseAreaActualCapacityContribution;
import org.jeecg.modules.xslmes.config.XslMesWarehouseAreaCapacityProperties;
import org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO;
import org.jeecg.modules.system.service.ISysCategoryService;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 用「原材料卡片」按库区(trim)汇总 remaining_quantity回填展示字段 actual_capacity不写库
*/
@Slf4j
@Component
@Order(100)
@RequiredArgsConstructor
public class RawMaterialCardRemainQtyCapacityContribution implements WarehouseAreaActualCapacityContribution {
private final XslMesWarehouseAreaCapacityProperties properties;
private final MesXslRawMaterialCardMapper mesXslRawMaterialCardMapper;
private final ISysCategoryService sysCategoryService;
@Override
public void contribute(List<MesXslWarehouseArea> areas) {
if (areas == null || areas.isEmpty()) {
return;
}
if (!properties.isEnabled()) {
return;
}
Set<String> allowedCategories = resolveAllowedWarehouseCategoryIds();
if (allowedCategories.isEmpty()) {
log.debug("[WarehouseAreaActualCapacity] 原材料库分类编码/ID 均未解析到有效 warehouse_category跳过 RawMaterialCard 策略");
return;
}
List<MesXslWarehouseArea> targets =
areas.stream().filter(a -> a != null && allowedCategories.contains(normId(a.getWarehouseCategory())) && StringUtils.isNotBlank(a.getAreaCode()))
.collect(Collectors.toList());
if (targets.isEmpty()) {
return;
}
Collection<String> codes =
targets.stream().map(MesXslWarehouseArea::getAreaCode).map(String::trim).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
Map<String, Long> sumMap = loadSumMap(codes);
for (MesXslWarehouseArea a : targets) {
String ck = trimKey(a.getAreaCode());
long sum = sumMap.getOrDefault(ck, 0L);
int display;
try {
display = Math.toIntExact(Math.min(sum, Integer.MAX_VALUE));
} catch (ArithmeticException ex) {
display = Integer.MAX_VALUE;
}
a.setActualCapacity(display);
}
}
private Map<String, Long> loadSumMap(Collection<String> codes) {
if (codes.isEmpty()) {
return Collections.emptyMap();
}
List<MesXslAreaRemainQtySumDTO> rows = mesXslRawMaterialCardMapper.sumRemainingQtyByTrimmedWarehouseArea(codes);
if (rows == null || rows.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Long> map = new HashMap<>(Math.max(16, rows.size() * 2));
for (MesXslAreaRemainQtySumDTO row : rows) {
if (row == null || row.getAreaCode() == null) {
continue;
}
String k = row.getAreaCode().trim();
if (StringUtils.isBlank(k)) {
continue;
}
long v = row.getQtySum() == null ? 0L : row.getQtySum();
map.put(k, v);
}
return map;
}
private static String trimKey(String code) {
return code == null ? "" : code.trim();
}
private static String normId(String id) {
return StringUtils.trimToEmpty(id);
}
private static Set<String> normalizeIdSet(Collection<String> raw) {
if (raw == null) {
return Collections.emptySet();
}
return raw.stream().filter(StringUtils::isNotBlank).map(String::trim).collect(Collectors.toSet());
}
/** 配置的 id + 配置的 code 解析出的 id 合并 */
private Set<String> resolveAllowedWarehouseCategoryIds() {
Set<String> set = new HashSet<>(normalizeIdSet(properties.getRawMaterialWarehouseCategoryIds()));
Collection<String> codes = properties.getRawMaterialWarehouseCategoryCodes();
if (codes != null) {
for (String c : codes) {
if (StringUtils.isBlank(c)) {
continue;
}
try {
String id = sysCategoryService.queryIdByCode(c.trim());
if (StringUtils.isNotBlank(id)) {
set.add(id.trim());
}
} catch (Exception ex) {
log.warn("[WarehouseAreaActualCapacity] 解析原材料库分类编码失败 code={}: {}", c, ex.getMessage());
}
}
}
return set;
}
}

View File

@@ -0,0 +1,43 @@
package org.jeecg.modules.xslmes.config;
import lombok.Data;
import org.jeecg.modules.xslmes.constant.MesXslWarehouseCategory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 库区列表「实际存放量」展示口径配置(不写库)
* <p>
* 推荐使用 {@link #rawMaterialWarehouseCategoryCodes},启动时解析为 sys_category.id
* 避免在配置里维护易变的雪花 id也可用 {@link #rawMaterialWarehouseCategoryIds} 直接指定 id。
*/
@Data
@Component
@ConfigurationProperties(prefix = "xslmes.warehouse-area.display-actual-capacity")
public class XslMesWarehouseAreaCapacityProperties {
/**

View File

@@ -0,0 +1,15 @@
package org.jeecg.modules.xslmes.constant;
/**
* 打印业务绑定 biz_code 使用菜单 permission id与 print_biz_perm_entity、Flyway 中原材料卡片菜单一致。
*/
public final class MesXslPrintConstants {
/** 原材料卡片页面菜单sys_permission.id */
public static final String RAW_MATERIAL_CARD_PERM_ID = "1900000000000000540";
/** 原料入场记录页面菜单sys_permission.id与 Flyway 中 parent 菜单一致) */
public static final String RAW_MATERIAL_ENTRY_PERM_ID = "1900000000000000530";
private MesXslPrintConstants() {}
}

View File

@@ -1,7 +1,19 @@
package org.jeecg.modules.xslmes.constant;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* MES 仓库分类(分类字典 sys_category根编码 {@link #ROOT_CODE}
*
* <p>说明:历史版本曾在编码上使用 F1/F2 后缀;当前业务树已不再按楼层区分,
* 「F1」「F2」仅可能作为编码后缀保留请以 {@link #RAW_MATERIAL_CATEGORY_CODES}、
* 客户/供应商库编码集合为准做兼容。</p>
*/
public final class MesXslWarehouseCategory {
@@ -10,8 +22,37 @@ public final class MesXslWarehouseCategory {
/** 根节点编码,与 JCategorySelect 的 pcode 一致 */
public static final String ROOT_CODE = "XSLMES_WH";
/** 客户库(二楼):须关联客户 */
public static final String CUSTOMER_CATEGORY_CODE = "XSLMES_WH_F2_KH";
/** 供应商库(二楼):须关联供应商 */
public static final String SUPPLIER_CATEGORY_CODE = "XSLMES_WH_F2_GYS";
/** 兼容历史:原材料库在库中可能出现的分类编码(新环境多为 F1老数据可能仍为 F2 */
public static final List<String> RAW_MATERIAL_CATEGORY_CODES =
Collections.unmodifiableList(Arrays.asList("XSLMES_WH_F1_YCL", "XSLMES_WH_F2_YCL"));
/** 兼容历史编号的「客户库」分类编码集合 */
private static final Set<String> CUSTOMER_CATEGORY_CODES =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("XSLMES_WH_F1_KH", "XSLMES_WH_F2_KH")));
/** 兼容历史编号的「供应商库」分类编码集合 */
private static final Set<String> SUPPLIER_CATEGORY_CODES =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("XSLMES_WH_F1_GYS", "XSLMES_WH_F2_GYS")));
/**
* @deprecated 请改用 {@link #isCustomerWarehouseCategoryCode(String)}
*/
@Deprecated
public static final String CUSTOMER_CATEGORY_CODE = "XSLMES_WH_F1_KH";
/**
* @deprecated 请改用 {@link #isSupplierWarehouseCategoryCode(String)}
*/
@Deprecated
public static final String SUPPLIER_CATEGORY_CODE = "XSLMES_WH_F1_GYS";
/** 是否为「客户库」分类编码(兼容 XSLMES_WH_F1_KH / XSLMES_WH_F2_KH */
public static boolean isCustomerWarehouseCategoryCode(String code) {
return CUSTOMER_CATEGORY_CODES.contains(StringUtils.trimToEmpty(code));
}
/** 是否为「供应商库」分类编码(兼容 XSLMES_WH_F1_GYS / XSLMES_WH_F2_GYS */
public static boolean isSupplierWarehouseCategoryCode(String code) {
return SUPPLIER_CATEGORY_CODES.contains(StringUtils.trimToEmpty(code));
}
}

View File

@@ -0,0 +1,128 @@
package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile;
import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* 业务实体字段配置:主表存业务名称与主实体字段 JSON子表存各明细表的字段 JSON。
*/
@Tag(name = "业务实体字段配置")
@RestController
@RequestMapping("/xslmes/mesXslBizEntityFieldProfile")
@Slf4j
public class MesXslBizEntityFieldProfileController extends JeecgController<MesXslBizEntityFieldProfile, IMesXslBizEntityFieldProfileService> {
@Autowired
private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService;
@Operation(summary = "分页列表(不含明细,减轻负载)")
@GetMapping(value = "/list")
public Result<IPage<MesXslBizEntityFieldProfile>> queryPageList(
MesXslBizEntityFieldProfile query,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslBizEntityFieldProfile> qw = QueryGenerator.initQueryWrapper(query, req.getParameterMap());
Page<MesXslBizEntityFieldProfile> page = new Page<>(pageNo, pageSize);
IPage<MesXslBizEntityFieldProfile> pageList = bizEntityFieldProfileService.page(page, qw);
return Result.OK(pageList);
}
@AutoLog(value = "业务实体字段配置-添加")
@Operation(summary = "添加(请求体可含 detailList")
@RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody MesXslBizEntityFieldProfile entity) {
if (oConvertUtils.isEmpty(entity.getBusinessName())) {
return Result.error("业务名称不能为空");
}
if (oConvertUtils.isEmpty(entity.getBusinessCode())) {
return Result.error("业务编码不能为空(建议填写菜单 permission id");
}
bizEntityFieldProfileService.saveWithDetails(entity);
return Result.OK("添加成功");
}
@AutoLog(value = "业务实体字段配置-编辑")
@Operation(summary = "编辑(明细全量替换)")
@RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslBizEntityFieldProfile entity) {
if (oConvertUtils.isEmpty(entity.getId())) {
return Result.error("主键不能为空");
}
if (oConvertUtils.isEmpty(entity.getBusinessName())) {
return Result.error("业务名称不能为空");
}
if (oConvertUtils.isEmpty(entity.getBusinessCode())) {
return Result.error("业务编码不能为空(建议填写菜单 permission id");
}
bizEntityFieldProfileService.updateWithDetails(entity);
return Result.OK("编辑成功");
}
@AutoLog(value = "业务实体字段配置-删除")
@Operation(summary = "删除")
@RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
bizEntityFieldProfileService.removeWithDetails(id);
return Result.OK("删除成功");
}
@AutoLog(value = "业务实体字段配置-批量删除")
@Operation(summary = "批量删除")
@RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
bizEntityFieldProfileService.removeBatchWithDetails(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功");
}
@Operation(summary = "按 id 查询(含 detailList")
@GetMapping(value = "/queryById")
public Result<MesXslBizEntityFieldProfile> queryById(@RequestParam(name = "id", required = true) String id) {
MesXslBizEntityFieldProfile entity = bizEntityFieldProfileService.getByIdWithDetails(id);
if (entity == null) {
return Result.error("未找到对应数据");
}
return Result.OK(entity);
}
@RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslBizEntityFieldProfile query) {
return super.exportXls(request, query, MesXslBizEntityFieldProfile.class, "业务实体字段配置");
}
@RequiresPermissions("xslmes:mes_xsl_biz_entity_field_profile:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, MesXslBizEntityFieldProfile.class);
}
}

View File

@@ -9,10 +9,20 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
import org.jeecg.modules.print.service.IPrintTemplateService;
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
import org.jeecg.modules.xslmes.constant.MesXslPrintConstants;
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
@@ -33,6 +43,9 @@ import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
@@ -59,6 +72,9 @@ public class MesXslDesktopAnonController {
private final IMesXslWarehouseService warehouseService;
private final IMesXslWarehouseAreaService warehouseAreaService;
private final MesXslStompNotifyService stompNotify;
private final IPrintBizTemplateBindService printBizTemplateBindService;
private final IPrintTemplateService printTemplateService;
private final ObjectMapper objectMapper;
// ═══════════════════════════ 车辆管理 ═══════════════════════════
@@ -606,6 +622,61 @@ public class MesXslDesktopAnonController {
return Result.OK((int) count);
}
@Operation(summary = "原材料卡片-免密准备原生打印数据(桌面端用)")
@GetMapping("/xslmes/mesXslRawMaterialCard/anon/prepareNativePrint")
public Result<Map<String, Object>> rawMaterialCardAnonPrepareNativePrint(@RequestParam(name = "id") String id) {
try {
MesXslRawMaterialCard card = rawMaterialCardService.getById(id);
if (card == null) return Result.error("未找到原材料卡片");
PrintBizTemplateBind bind =
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID);
if (bind == null) return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
if (tpl == null) return Result.error("绑定的打印模板不存在");
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(card);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8);
out.put("cardId", card.getId());
out.put("templateCode", bind.getTemplateCode());
out.put("templateJson", tpl.getTemplateJson());
out.put("printData", objectMapper.convertValue(printData, Map.class));
return Result.OK(out);
} catch (Exception e) {
log.error("原材料卡片-免密准备打印数据失败 id={}", id, e);
return Result.error("准备打印数据失败: " + e.getMessage());
}
}
@Operation(summary = "原料入场记录-免密准备原生打印数据(桌面端用)")
@GetMapping("/xslmes/mesXslRawMaterialEntry/anon/prepareNativePrint")
public Result<Map<String, Object>> rawMaterialEntryAnonPrepareNativePrint(@RequestParam(name = "id") String id) {
try {
MesXslRawMaterialEntry entry = rawMaterialEntryService.getById(id);
if (entry == null) return Result.error("未找到原料入场记录");
PrintBizTemplateBind bind =
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID);
if (bind == null) return Result.error("请先在「业务打印绑定」中配置原料入场记录与打印模板");
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
if (tpl == null) return Result.error("绑定的打印模板不存在");
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(entry);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8);
out.put("entryId", entry.getId());
out.put("templateCode", bind.getTemplateCode());
out.put("templateJson", tpl.getTemplateJson());
out.put("printData", objectMapper.convertValue(printData, Map.class));
return Result.OK(out);
} catch (Exception e) {
log.error("原料入场记录-免密准备打印数据失败 id={}", id, e);
return Result.error("准备打印数据失败: " + e.getMessage());
}
}
// ═══════════════════════════ 仓库管理(只读,供桌面端下拉选取) ═══════════════════════════
@Operation(summary = "仓库-免密分页列表查询(供桌面端筛选使用)")
@@ -632,6 +703,7 @@ public class MesXslDesktopAnonController {
HttpServletRequest req) {
QueryWrapper<MesXslWarehouseArea> qw = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
IPage<MesXslWarehouseArea> page = warehouseAreaService.page(new Page<>(pageNo, pageSize), qw);
warehouseAreaService.enrichDisplayedActualCapacity(page.getRecords());
return Result.OK(page);
}
@@ -639,7 +711,11 @@ public class MesXslDesktopAnonController {
@GetMapping("/xslmes/mesXslWarehouseArea/anon/queryById")
public Result<MesXslWarehouseArea> warehouseAreaAnonQueryById(@RequestParam(name = "id") String id) {
MesXslWarehouseArea entity = warehouseAreaService.getById(id);
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
if (entity == null) {
return Result.error("未找到对应数据");
}
warehouseAreaService.enrichDisplayedActualCapacity(Collections.singletonList(entity));
return Result.OK(entity);
}
@Operation(summary = "库区-免密添加")

View File

@@ -1,27 +1,50 @@
package org.jeecg.modules.xslmes.controller;
import java.util.Arrays;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.base.controller.JeecgController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation;
import org.jeecg.common.aspect.annotation.AutoLog;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.oConvertUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
import org.jeecg.modules.print.service.IPrintTemplateService;
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
import org.jeecg.modules.print.support.PrintServerPdfJobService;
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
import org.jeecg.modules.xslmes.constant.MesXslPrintConstants;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* @Description: 原材料卡片
@@ -34,10 +57,21 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
@RequestMapping("/xslmes/mesXslRawMaterialCard")
@Slf4j
public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMaterialCard, IMesXslRawMaterialCardService> {
@Autowired
private IMesXslRawMaterialCardService mesXslRawMaterialCardService;
@Autowired
private MesXslStompNotifyService stompNotify;
@Autowired
private PrintServerEnvironmentService printServerEnvironmentService;
@Autowired
private PrintServerPdfJobService printServerPdfJobService;
@Autowired
private IPrintBizTemplateBindService printBizTemplateBindService;
@Autowired
private IPrintTemplateService printTemplateService;
@Autowired
private ObjectMapper objectMapper;
/**
* 分页列表查询
@@ -48,7 +82,36 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
// 库区条件:去掉实体上的值避免 QueryGenerator 再拼一班;用 TRIM 与看板聚合一致(避免首尾空格导致「有汇总无明细」)
String rawWarehouseArea = mesXslRawMaterialCard != null ? mesXslRawMaterialCard.getWarehouseArea() : null;
String areaEq = oConvertUtils.isNotEmpty(rawWarehouseArea) ? rawWarehouseArea.trim() : "";
boolean filterByWarehouseArea = !areaEq.isEmpty();
if (filterByWarehouseArea) {
mesXslRawMaterialCard.setWarehouseArea(null);
}
QueryWrapper<MesXslRawMaterialCard> queryWrapper = QueryGenerator.initQueryWrapper(mesXslRawMaterialCard, req.getParameterMap());
if (filterByWarehouseArea) {
queryWrapper.apply("TRIM(COALESCE(warehouse_area,'')) = {0}", areaEq);
}
// 看板等扩展查询remaining_quantity「有剩余 / 无剩余」(非实体字段,单独拼条件)
String remainQtyFilter = req.getParameter("remainQtyFilter");
if ("has".equals(remainQtyFilter)) {
queryWrapper.gt("remaining_quantity", 0);
} else if ("none".equals(remainQtyFilter)) {
queryWrapper.and(w -> w.isNull("remaining_quantity").or().le("remaining_quantity", 0));
}
String mixKw = req.getParameter("mixKeyword");
if (StringUtils.isNotBlank(mixKw)) {
final String kw = mixKw.trim();
queryWrapper.and(
w -> w.like("barcode", kw)
.or()
.like("batch_no", kw)
.or()
.like("material_name", kw)
.or()
.like("material_id", kw));
}
Page<MesXslRawMaterialCard> page = new Page<>(pageNo, pageSize);
IPage<MesXslRawMaterialCard> pageList = mesXslRawMaterialCardService.page(page, queryWrapper);
return Result.OK(pageList);
@@ -122,6 +185,88 @@ public class MesXslRawMaterialCardController extends JeecgController<MesXslRawMa
return Result.OK("批量删除成功!");
}
/**
* 查询可用打印机(与 /print/template/queryPrinters 返回结构一致)
*/
@Operation(summary = "原材料卡片-查询可用打印机")
@GetMapping(value = "/queryPrinters")
/** 有 list 可进页拉打印机;有 edit 可打印拉打印机,避免单权限缺另一项时 403 */
@RequiresPermissions(
value = {"xslmes:mes_xsl_raw_material_card:list", "xslmes:mes_xsl_raw_material_card:edit"},
logical = Logical.OR)
public Result<Map<String, Object>> queryPrinters() {
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
}
/**
* 根据业务打印绑定生成模板 JSON + 映射后的 printData供前端生成 PDF 后调用 printPdf
*/
@Operation(summary = "原材料卡片-准备原生打印数据")
@GetMapping(value = "/prepareNativePrint")
/** 与 printPdf 一致:准备模板与打印数据属于「打印」动作,使用 edit 权限 */
@RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit")
public Result<Map<String, Object>> prepareNativePrint(@RequestParam(name = "id") String id) {
try {
MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id);
if (card == null) {
return Result.error("未找到原材料卡片");
}
PrintBizTemplateBind bind =
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_CARD_PERM_ID);
if (bind == null) {
return Result.error("请先在「业务打印绑定」中配置原材料卡片与打印模板");
}
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
if (tpl == null) {
return Result.error("绑定的打印模板不存在");
}
ArrayNode mapping =
PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(card);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8);
out.put("cardId", card.getId());
out.put("templateCode", bind.getTemplateCode());
out.put("templateJson", tpl.getTemplateJson());
out.put("paperWidthMm", tpl.getPaperWidthMm());
out.put("paperHeightMm", tpl.getPaperHeightMm());
out.put("paperOrientation", tpl.getPaperOrientation());
out.put("printData", objectMapper.convertValue(printData, Map.class));
return Result.OK(out);
} catch (Exception e) {
log.error("原材料卡片准备打印数据失败 id={}", id, e);
return Result.error("准备打印数据失败: " + e.getMessage());
}
}
/**
* 将前端生成的 PDF Base64 提交到服务器打印机(与 /print/template/directPrintPdf 一致)
*/
@AutoLog(value = "原材料卡片-PDF后端打印")
@Operation(summary = "原材料卡片-PDF后端打印")
@PostMapping(value = "/printPdf")
@RequiresPermissions("xslmes:mes_xsl_raw_material_card:edit")
public Result<String> printPdf(@RequestBody Map<String, Object> body) {
String id = String.valueOf(body.getOrDefault("id", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
if (StringUtils.isBlank(id)) {
return Result.error("id 不能为空");
}
MesXslRawMaterialCard card = mesXslRawMaterialCardService.getById(id);
if (card == null) {
return Result.error("未找到原材料卡片");
}
String prefix =
StringUtils.isNotBlank(card.getBarcode()) ? card.getBarcode() : card.getId();
String fn =
StringUtils.isNotBlank(fileName) ? fileName : ("原材料卡片-" + prefix + ".pdf");
return printServerPdfJobService.submitPdfBase64(
printerName, pdfBase64, fn, "RAW_CARD_" + prefix);
}
/**
* 通过id查询
*/

View File

@@ -1,28 +1,50 @@
package org.jeecg.modules.xslmes.controller;
import java.util.Arrays;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.base.controller.JeecgController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation;
import org.jeecg.common.aspect.annotation.AutoLog;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.print.entity.PrintBizTemplateBind;
import org.jeecg.modules.print.entity.PrintTemplate;
import org.jeecg.modules.print.service.IPrintBizTemplateBindService;
import org.jeecg.modules.print.service.IPrintTemplateService;
import org.jeecg.modules.print.support.PrintServerEnvironmentService;
import org.jeecg.modules.print.support.PrintServerPdfJobService;
import org.jeecg.modules.print.util.PrintBizDataMappingUtil;
import org.jeecg.modules.xslmes.constant.MesXslPrintConstants;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialEntry;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialEntryService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
/**
* @Description: 原料入场记录
@@ -40,6 +62,16 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
private IMesXslRawMaterialEntryService mesXslRawMaterialEntryService;
@Autowired
private MesXslStompNotifyService stompNotify;
@Autowired
private PrintServerEnvironmentService printServerEnvironmentService;
@Autowired
private PrintServerPdfJobService printServerPdfJobService;
@Autowired
private IPrintBizTemplateBindService printBizTemplateBindService;
@Autowired
private IPrintTemplateService printTemplateService;
@Autowired
private ObjectMapper objectMapper;
@Operation(summary = "原料入场记录-分页列表查询")
@GetMapping(value = "/list")
@@ -117,6 +149,79 @@ public class MesXslRawMaterialEntryController extends JeecgController<MesXslRawM
return Result.OK(mesXslRawMaterialEntry);
}
@Operation(summary = "原料入场记录-查询可用打印机")
@GetMapping(value = "/queryPrinters")
@RequiresPermissions(
value = {"xslmes:mes_xsl_raw_material_entry:list", "xslmes:mes_xsl_raw_material_entry:edit"},
logical = Logical.OR)
public Result<Map<String, Object>> queryPrinters() {
return Result.OK(printServerEnvironmentService.buildPrinterQueryResult());
}
/**
* 根据业务打印绑定生成模板 JSON + 映射后的 printData供前端生成 PDF 后调用 printPdf
*/
@Operation(summary = "原料入场记录-准备原生打印数据")
@GetMapping(value = "/prepareNativePrint")
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit")
public Result<Map<String, Object>> prepareNativePrint(@RequestParam(name = "id") String id) {
try {
MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id);
if (entry == null) {
return Result.error("未找到原料入场记录");
}
PrintBizTemplateBind bind =
printBizTemplateBindService.getByBizCode(MesXslPrintConstants.RAW_MATERIAL_ENTRY_PERM_ID);
if (bind == null) {
return Result.error("请先在「业务打印绑定」中配置原料入场记录与打印模板");
}
PrintTemplate tpl = printTemplateService.getById(bind.getTemplateId());
if (tpl == null) {
return Result.error("绑定的打印模板不存在");
}
ArrayNode mapping = PrintBizDataMappingUtil.parseMappingArray(bind.getFieldMappingJson());
JsonNode bizRoot = objectMapper.valueToTree(entry);
ObjectNode printData = PrintBizDataMappingUtil.mapBizToPrintData(bizRoot, mapping);
PrintBizDataMappingUtil.fillMissingDataBindingParamKeys(printData, tpl.getTemplateJson());
Map<String, Object> out = new HashMap<>(8);
out.put("entryId", entry.getId());
out.put("templateCode", bind.getTemplateCode());
out.put("templateJson", tpl.getTemplateJson());
out.put("paperWidthMm", tpl.getPaperWidthMm());
out.put("paperHeightMm", tpl.getPaperHeightMm());
out.put("paperOrientation", tpl.getPaperOrientation());
out.put("printData", objectMapper.convertValue(printData, Map.class));
return Result.OK(out);
} catch (Exception e) {
log.error("原料入场记录准备打印数据失败 id={}", id, e);
return Result.error("准备打印数据失败: " + e.getMessage());
}
}
@AutoLog(value = "原料入场记录-PDF后端打印")
@Operation(summary = "原料入场记录-PDF后端打印")
@PostMapping(value = "/printPdf")
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:edit")
public Result<String> printPdf(@RequestBody Map<String, Object> body) {
String id = String.valueOf(body.getOrDefault("id", "")).trim();
String printerName = String.valueOf(body.getOrDefault("printerName", "")).trim();
String pdfBase64 = String.valueOf(body.getOrDefault("pdfBase64", "")).trim();
String fileName = String.valueOf(body.getOrDefault("fileName", "")).trim();
if (StringUtils.isBlank(id)) {
return Result.error("id 不能为空");
}
MesXslRawMaterialEntry entry = mesXslRawMaterialEntryService.getById(id);
if (entry == null) {
return Result.error("未找到原料入场记录");
}
String prefix =
StringUtils.isNotBlank(entry.getBarcode()) ? entry.getBarcode() : entry.getId();
String fn =
StringUtils.isNotBlank(fileName) ? fileName : ("原料入场记录-" + prefix + ".pdf");
return printServerPdfJobService.submitPdfBase64(
printerName, pdfBase64, fn, "RAW_ENTRY_" + prefix);
}
@RequiresPermissions("xslmes:mes_xsl_raw_material_entry:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslRawMaterialEntry mesXslRawMaterialEntry) {

View File

@@ -0,0 +1,38 @@
package org.jeecg.modules.xslmes.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialWarehouseBoardService;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 原材料库区看板:库区主数据 + 原材料卡片聚合
*/
@Tag(name = "原材料库区看板")
@RestController
@RequestMapping("/xslmes/mesXslRawMaterialWarehouseBoard")
@Slf4j
public class MesXslRawMaterialWarehouseBoardController {
@Autowired
private IMesXslRawMaterialWarehouseBoardService mesXslRawMaterialWarehouseBoardService;
@Operation(summary = "原材料库区看板-汇总数据")
@RequiresPermissions("xslmes:mes_xsl_raw_material_warehouse_board:list")
@GetMapping(value = "/board")
public Result<MesXslRawMaterialWarehouseBoardVO> board(
@RequestParam(name = "warehouseId", required = false) String warehouseId,
@RequestParam(name = "keyword", required = false) String keyword,
@RequestParam(name = "measureType", required = false) String measureType) {
MesXslRawMaterialWarehouseBoardVO data = mesXslRawMaterialWarehouseBoardService.queryBoard(warehouseId, keyword, measureType);
return Result.OK(data);
}
}

View File

@@ -3,6 +3,7 @@ package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.shiro.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
@@ -13,15 +14,22 @@ import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
@@ -36,6 +44,9 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
@Autowired
private IMesXslWarehouseAreaService mesXslWarehouseAreaService;
@Autowired
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private MesXslStompNotifyService stompNotify;
@@ -49,6 +60,7 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, req.getParameterMap());
Page<MesXslWarehouseArea> page = new Page<>(pageNo, pageSize);
IPage<MesXslWarehouseArea> pageList = mesXslWarehouseAreaService.page(page, queryWrapper);
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(pageList.getRecords());
return Result.OK(pageList);
}
@@ -166,13 +178,33 @@ public class MesXslWarehouseAreaController extends JeecgController<MesXslWarehou
if (entity == null) {
return Result.error("未找到对应数据");
}
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(Collections.singletonList(entity));
return Result.OK(entity);
}
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslWarehouseArea mesXslWarehouseArea) {
return super.exportXls(request, mesXslWarehouseArea, MesXslWarehouseArea.class, "MES库区管理");
QueryWrapper<MesXslWarehouseArea> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWarehouseArea, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
queryWrapper.in("id", Arrays.asList(selections.split(",")));
}
List<MesXslWarehouseArea> exportList = mesXslWarehouseAreaService.list(queryWrapper);
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(exportList);
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
mv.addObject(NormalExcelConstants.FILE_NAME, "MES库区管理");
mv.addObject(NormalExcelConstants.CLASS, MesXslWarehouseArea.class);
ExportParams exportParams = new ExportParams("MES库区管理报表", "导出人:" + sysUser.getRealname(), "MES库区管理", ExcelType.XSSF);
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
if (oConvertUtils.isNotEmpty(exportFields)) {
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
}
return mv;
}
@RequiresPermissions("xslmes:mes_xsl_warehouse_area:importExcel")

View File

@@ -0,0 +1,20 @@
package org.jeecg.modules.xslmes.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 按库区(trim 后编码)聚合剩余数量汇总
*/
@Data
public class MesXslAreaRemainQtySumDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 库区编码(已 trim */
private String areaCode;
/** 剩余数量合计 */
private Long qtySum;
}

View File

@@ -0,0 +1,68 @@
package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 业务实体字段配置子表:一类明细表对应一行,字段清单存 JSON。
*/
@Data
@TableName("mes_xsl_biz_entity_field_detail")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "业务实体字段配置-明细表字段清单")
public class MesXslBizEntityFieldDetail implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
@Schema(description = "主表ID")
private String profileId;
@Schema(description = "主实体上明细属性名(与打印绑定 detailProperty 一致,如 lines")
private String detailPropertyName;
@Schema(description = "槽位类型LIST 或 OBJECT")
private String detailSlotKind;
@Schema(description = "明细展示名称")
private String detailName;
@Schema(description = "明细实体 Java 全限定类名")
private String detailEntityClassName;
/** 明细表字段列表 JSON */
@Schema(description = "明细表字段列表JSON 数组)")
private String detailFieldsJson;
@Schema(description = "排序号")
private Integer sortNo;
@Schema(description = "创建人")
private String createBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "更新人")
private String updateBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间")
private Date updateTime;
}

View File

@@ -0,0 +1,72 @@
package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 业务实体字段配置主表:业务名称、主实体类名、主表字段 JSON明细通过 detailList 关联子表。
*/
@Data
@TableName("mes_xsl_biz_entity_field_profile")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "业务实体字段配置主表")
public class MesXslBizEntityFieldProfile implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
@Schema(description = "业务名称")
private String businessName;
@Schema(description = "业务编码(菜单 permission id与业务打印绑定 biz_code、print_biz_perm_entity.perm_id 一致)")
private String businessCode;
@Schema(description = "主实体 Java 全限定类名")
private String entityClassName;
/** 主表实体字段列表 JSON */
@Schema(description = "主表实体字段列表JSON 数组)")
private String mainFieldsJson;
@Schema(description = "备注")
private String remark;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建人")
private String createBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "更新人")
private String updateBy;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新时间")
private Date updateTime;
/** 各明细表对应的字段清单(不落主表) */
@TableField(exist = false)
@Schema(description = "明细表字段配置列表")
private List<MesXslBizEntityFieldDetail> detailList;
}

View File

@@ -0,0 +1,8 @@
package org.jeecg.modules.xslmes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail;
@Mapper
public interface MesXslBizEntityFieldDetailMapper extends BaseMapper<MesXslBizEntityFieldDetail> {}

View File

@@ -0,0 +1,8 @@
package org.jeecg.modules.xslmes.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile;
@Mapper
public interface MesXslBizEntityFieldProfileMapper extends BaseMapper<MesXslBizEntityFieldProfile> {}

View File

@@ -1,7 +1,12 @@
package org.jeecg.modules.xslmes.mapper;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
import java.util.Collection;
import java.util.List;
/**
* @Description: 原材料卡片
@@ -10,4 +15,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
* @Version: V1.0
*/
public interface MesXslRawMaterialCardMapper extends BaseMapper<MesXslRawMaterialCard> {
/**
* 按 trim 后的库区编码汇总原材料卡片剩余数量(当前租户由拦截器补齐)
*/
List<MesXslAreaRemainQtySumDTO> sumRemainingQtyByTrimmedWarehouseArea(@Param("areaCodes") Collection<String> areaCodes);
}

View File

@@ -1,4 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.xslmes.mapper.MesXslRawMaterialCardMapper">
<!-- 库区编码已与卡片 warehouse_area 按 TRIM 对齐(见库区/卡片筛选逻辑) -->
<select id="sumRemainingQtyByTrimmedWarehouseArea"
resultType="org.jeecg.modules.xslmes.dto.MesXslAreaRemainQtySumDTO">
SELECT TRIM(t.warehouse_area) AS areaCode,
COALESCE(SUM(COALESCE(t.remaining_quantity, 0)), 0) AS qtySum
FROM mes_xsl_raw_material_card t
WHERE t.warehouse_area IS NOT NULL
AND TRIM(t.warehouse_area) != ''
AND TRIM(t.warehouse_area) IN
<foreach collection="areaCodes" item="c" open="(" separator="," close=")">
#{c}
</foreach>
GROUP BY TRIM(t.warehouse_area)
</select>
</mapper>

View File

@@ -0,0 +1,226 @@
package org.jeecg.modules.xslmes.print.catalog;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.print.catalog.IPrintBizEntityFieldCatalogProvider;
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile;
import org.jeecg.modules.xslmes.mapper.MesXslBizEntityFieldDetailMapper;
import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService;
import org.springframework.stereotype.Component;
/**
* 将 mes_xsl_biz_entity_field_* 中的缓存提供给打印绑定接口SPI 实现)。
*
* <p>bizCode = {@code print_biz_perm_entity.perm_id}。
*
* <p>「新增绑定」下拉组装时对每条业务若单独查库会产生严重 N+1{@link #beginBulkLookup(Collection)} 在同一请求线程内改为单次 IN 查询。
*/
@Slf4j
@Component
public class PrintBizEntityFieldCatalogProviderImpl implements IPrintBizEntityFieldCatalogProvider {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/** null=非批量模式;非 null=批量模式(可能为空 Map */
private static final ThreadLocal<Map<String, MesXslBizEntityFieldProfile>> BULK_PROFILE_BY_CODE =
new ThreadLocal<>();
@Resource
private IMesXslBizEntityFieldProfileService bizEntityFieldProfileService;
@Resource
private MesXslBizEntityFieldDetailMapper detailMapper;
@Override
public void beginBulkLookup(Collection<String> bizCodes) {
endBulkLookup();
if (bizCodes == null || bizCodes.isEmpty()) {
BULK_PROFILE_BY_CODE.set(Collections.emptyMap());
return;
}
List<String> ids =
bizCodes.stream()
.filter(StringUtils::isNotBlank)
.map(String::trim)
.distinct()
.collect(Collectors.toList());
if (ids.isEmpty()) {
BULK_PROFILE_BY_CODE.set(Collections.emptyMap());
return;
}
List<MesXslBizEntityFieldProfile> list =
bizEntityFieldProfileService
.lambdaQuery()
.in(MesXslBizEntityFieldProfile::getBusinessCode, ids)
.list();
Map<String, MesXslBizEntityFieldProfile> map = new HashMap<>(Math.max(16, list.size() * 2));
for (MesXslBizEntityFieldProfile p : list) {
if (p != null && StringUtils.isNotBlank(p.getBusinessCode())) {
map.put(p.getBusinessCode().trim(), p);
}
}
BULK_PROFILE_BY_CODE.set(map);
}
@Override
public void endBulkLookup() {
BULK_PROFILE_BY_CODE.remove();
}
/** 批量模式下读线程 Map否则单次按编码查询 */
private MesXslBizEntityFieldProfile resolveProfile(String bizCode) {
if (StringUtils.isBlank(bizCode)) {
return null;
}
Map<String, MesXslBizEntityFieldProfile> bulk = BULK_PROFILE_BY_CODE.get();
if (bulk != null) {
return bulk.get(bizCode.trim());
}
return bizEntityFieldProfileService.getByBusinessCode(bizCode);
}
@Override
public String getEntityClassFqn(String bizCode) {
MesXslBizEntityFieldProfile p = resolveProfile(bizCode);
return p != null ? StringUtils.trimToNull(p.getEntityClassName()) : null;
}
@Override
public boolean hasCatalogForBiz(String bizCode) {
return resolveProfile(bizCode) != null;
}
@Override
public List<PrintBizFieldItemVO> listMainFields(String bizCode) {
MesXslBizEntityFieldProfile p = resolveProfile(bizCode);
if (p == null) {
return Collections.emptyList();
}
return parseFieldItems(p.getMainFieldsJson());
}
@Override
public List<PrintBizDetailSlotVO> listDetailSlots(String bizCode) {
MesXslBizEntityFieldProfile p = resolveProfile(bizCode);
if (p == null || StringUtils.isBlank(p.getId())) {
return Collections.emptyList();
}
List<MesXslBizEntityFieldDetail> lines =
detailMapper.selectList(
new LambdaQueryWrapper<MesXslBizEntityFieldDetail>()
.eq(MesXslBizEntityFieldDetail::getProfileId, p.getId())
.orderByAsc(MesXslBizEntityFieldDetail::getSortNo)
.orderByAsc(MesXslBizEntityFieldDetail::getId));
List<PrintBizDetailSlotVO> out = new ArrayList<>();
for (MesXslBizEntityFieldDetail line : lines) {
if (StringUtils.isBlank(line.getDetailPropertyName())) {
continue;
}
out.add(
new PrintBizDetailSlotVO(
line.getDetailPropertyName(),
StringUtils.defaultString(line.getDetailEntityClassName()),
StringUtils.defaultIfBlank(line.getDetailSlotKind(), "LIST"),
StringUtils.defaultIfBlank(line.getDetailName(), line.getDetailPropertyName())));
}
return out;
}
@Override
public List<PrintBizFieldItemVO> listPrefixedDetailFields(String bizCode, String detailProperty, String slotKind) {
if (StringUtils.isAnyBlank(bizCode, detailProperty)) {
return Collections.emptyList();
}
MesXslBizEntityFieldProfile p = resolveProfile(bizCode);
if (p == null || StringUtils.isBlank(p.getId())) {
return Collections.emptyList();
}
String prop = detailProperty.trim();
String reqKind = StringUtils.trimToEmpty(slotKind);
List<MesXslBizEntityFieldDetail> lines =
detailMapper.selectList(
new LambdaQueryWrapper<MesXslBizEntityFieldDetail>()
.eq(MesXslBizEntityFieldDetail::getProfileId, p.getId()));
MesXslBizEntityFieldDetail hit = null;
for (MesXslBizEntityFieldDetail line : lines) {
if (!prop.equals(line.getDetailPropertyName())) {
continue;
}
if (!slotKindMatch(reqKind, line.getDetailSlotKind())) {
continue;
}
hit = line;
break;
}
if (hit == null) {
return Collections.emptyList();
}
List<PrintBizFieldItemVO> raw = parseFieldItems(hit.getDetailFieldsJson());
List<PrintBizFieldItemVO> out = new ArrayList<>(raw.size());
for (PrintBizFieldItemVO x : raw) {
String path = prop + "." + x.getFieldKey();
String label = "明细「" + prop + "」→ " + x.getLabel();
out.add(PrintBizFieldItemVO.copyWithPrefixedPath(x, path, label));
}
return out;
}
private static boolean slotKindMatch(String requested, String stored) {
String r = StringUtils.trimToEmpty(requested);
String s = StringUtils.trimToEmpty(stored);
if (StringUtils.isBlank(s)) {
return true;
}
if (StringUtils.isBlank(r)) {
return true;
}
return r.equalsIgnoreCase(s);
}
/** 解析 JSON 数组:支持 VO 对象元素或纯字符串字段名 */
private List<PrintBizFieldItemVO> parseFieldItems(String json) {
if (StringUtils.isBlank(json)) {
return Collections.emptyList();
}
try {
JsonNode root = OBJECT_MAPPER.readTree(json);
if (!root.isArray()) {
return Collections.emptyList();
}
List<PrintBizFieldItemVO> out = new ArrayList<>();
for (JsonNode n : root) {
if (n.isTextual()) {
String k = n.asText();
if (StringUtils.isNotBlank(k)) {
out.add(PrintBizFieldItemVO.plainStringField(k));
}
continue;
}
if (n.isObject()) {
PrintBizFieldItemVO vo = OBJECT_MAPPER.treeToValue(n, PrintBizFieldItemVO.class);
if (vo != null && StringUtils.isNotBlank(vo.getFieldKey())) {
out.add(vo);
}
}
}
return out;
} catch (Exception e) {
log.warn("解析业务实体字段缓存 JSON 失败: {}", e.getMessage());
return Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,40 @@
package org.jeecg.modules.xslmes.service;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile;
/** 业务实体字段配置(主子表) */
public interface IMesXslBizEntityFieldProfileService extends IService<MesXslBizEntityFieldProfile> {
/** 按业务编码(菜单 permission id查询主表不含 detailList */
MesXslBizEntityFieldProfile getByBusinessCode(String businessCode);
/**
* 按业务编码 upsert用于启动扫描写入明细全量替换。
*
* @param businessCode 与 print_biz_perm_entity.perm_id、biz_code 一致
*/
void upsertScannedProfile(
String businessCode,
String businessName,
String entityFqn,
String mainFieldsJson,
List<MesXslBizEntityFieldDetail> detailRows);
/** 新增主表并保存明细 */
void saveWithDetails(MesXslBizEntityFieldProfile profile);
/** 更新主表并重写明细 */
void updateWithDetails(MesXslBizEntityFieldProfile profile);
/** 按主键删除主表及明细 */
void removeWithDetails(String id);
/** 批量删除主表及明细 */
void removeBatchWithDetails(java.util.Collection<String> ids);
/** 查询主表并填充 detailList */
MesXslBizEntityFieldProfile getByIdWithDetails(String id);
}

View File

@@ -0,0 +1,18 @@
package org.jeecg.modules.xslmes.service;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
/**
* 原材料库区看板(库区 + 原材料卡片聚合)
*/
public interface IMesXslRawMaterialWarehouseBoardService {
/**
* 聚合看板数据
*
* @param warehouseId 所属仓库ID可空表示全部启用库区
* @param keyword 物料名称/条码/批次模糊筛选(可空);非空时仅展示仍有匹配卡片的库区
* @param measureType quantity=占用率按剩余数量与 maxCapacity 对齐weight=按剩余重量
*/
MesXslRawMaterialWarehouseBoardVO queryBoard(String warehouseId, String keyword, String measureType);
}

View File

@@ -3,6 +3,8 @@ package org.jeecg.modules.xslmes.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import java.util.Collection;
/**
* MES 库区管理
*/
@@ -12,4 +14,9 @@ public interface IMesXslWarehouseAreaService extends IService<MesXslWarehouseAre
* 同租户下是否已存在相同库区编码(编辑时传 excludeId 排除自身)
*/
boolean existsSameAreaCode(String areaCode, String excludeId);
/**
* 按配置策略回填列表中的「实际存放量」展示值(不写库)
*/
void enrichDisplayedActualCapacity(Collection<MesXslWarehouseArea> areas);
}

View File

@@ -70,6 +70,11 @@ public class MesXslStompNotifyService {
publish("/topic/sync/mes-warehouse-areas", "MES_WAREHOUSE_AREA_CHANGED", "warehouseAreaId", warehouseAreaId, action);
}
/** 广播打印模板变更事件到 /topic/sync/print-templates */
public void publishPrintTemplateChanged(String action, String templateId) {
publish("/topic/sync/print-templates", "PRINT_TEMPLATE_CHANGED", "templateId", templateId, action);
}
// ─────────────────────────── 私有辅助 ────────────────────────────
private void publish(String topic, String cmd, String idKey, String idValue, String action) {

View File

@@ -0,0 +1,164 @@
package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldDetail;
import org.jeecg.modules.xslmes.entity.MesXslBizEntityFieldProfile;
import org.jeecg.modules.xslmes.mapper.MesXslBizEntityFieldDetailMapper;
import org.jeecg.modules.xslmes.mapper.MesXslBizEntityFieldProfileMapper;
import org.jeecg.modules.xslmes.service.IMesXslBizEntityFieldProfileService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MesXslBizEntityFieldProfileServiceImpl extends ServiceImpl<MesXslBizEntityFieldProfileMapper, MesXslBizEntityFieldProfile>
implements IMesXslBizEntityFieldProfileService {
/** 标记为启动任务根据 print_biz_perm_entity 写入,便于区分手工维护数据 */
private static final String REMARK_PRINT_PERM_SCAN = "print_biz_perm_entity 启动异步扫描";
@Resource
private MesXslBizEntityFieldDetailMapper detailMapper;
@Override
public MesXslBizEntityFieldProfile getByBusinessCode(String businessCode) {
if (StringUtils.isBlank(businessCode)) {
return null;
}
return this.lambdaQuery()
.eq(MesXslBizEntityFieldProfile::getBusinessCode, businessCode.trim())
.last("LIMIT 1")
.one();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void upsertScannedProfile(
String businessCode,
String businessName,
String entityFqn,
String mainFieldsJson,
List<MesXslBizEntityFieldDetail> detailRows) {
if (StringUtils.isBlank(businessCode)) {
return;
}
String code = businessCode.trim();
Date now = new Date();
MesXslBizEntityFieldProfile existing = getByBusinessCode(code);
if (existing == null) {
MesXslBizEntityFieldProfile p = new MesXslBizEntityFieldProfile();
p.setBusinessCode(code);
p.setBusinessName(StringUtils.defaultIfBlank(businessName, code));
p.setEntityClassName(entityFqn);
p.setMainFieldsJson(mainFieldsJson);
p.setRemark(REMARK_PRINT_PERM_SCAN);
p.setCreateTime(now);
p.setUpdateTime(now);
p.setDetailList(detailRows != null ? detailRows : List.of());
saveWithDetails(p);
return;
}
existing.setBusinessName(StringUtils.defaultIfBlank(businessName, existing.getBusinessName()));
existing.setEntityClassName(entityFqn);
existing.setMainFieldsJson(mainFieldsJson);
existing.setRemark(REMARK_PRINT_PERM_SCAN);
existing.setUpdateTime(now);
existing.setDetailList(detailRows != null ? detailRows : List.of());
updateWithDetails(existing);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveWithDetails(MesXslBizEntityFieldProfile profile) {
this.save(profile);
insertDetailRows(profile);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateWithDetails(MesXslBizEntityFieldProfile profile) {
this.updateById(profile);
detailMapper.delete(new LambdaQueryWrapper<MesXslBizEntityFieldDetail>().eq(MesXslBizEntityFieldDetail::getProfileId, profile.getId()));
insertDetailRows(profile);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeWithDetails(String id) {
if (StringUtils.isBlank(id)) {
return;
}
detailMapper.delete(new LambdaQueryWrapper<MesXslBizEntityFieldDetail>().eq(MesXslBizEntityFieldDetail::getProfileId, id));
this.removeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeBatchWithDetails(Collection<String> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
List<String> trimmed = new ArrayList<>();
for (String id : ids) {
if (StringUtils.isNotBlank(id)) {
String t = id.trim();
trimmed.add(t);
detailMapper.delete(new LambdaQueryWrapper<MesXslBizEntityFieldDetail>().eq(MesXslBizEntityFieldDetail::getProfileId, t));
}
}
if (!trimmed.isEmpty()) {
this.removeByIds(trimmed);
}
}
@Override
public MesXslBizEntityFieldProfile getByIdWithDetails(String id) {
MesXslBizEntityFieldProfile profile = this.getById(id);
if (profile == null) {
return null;
}
List<MesXslBizEntityFieldDetail> lines =
detailMapper.selectList(
new LambdaQueryWrapper<MesXslBizEntityFieldDetail>()
.eq(MesXslBizEntityFieldDetail::getProfileId, id)
.orderByAsc(MesXslBizEntityFieldDetail::getSortNo)
.orderByAsc(MesXslBizEntityFieldDetail::getId));
profile.setDetailList(lines);
return profile;
}
/** 写入子表(编辑时已清空旧数据) */
private void insertDetailRows(MesXslBizEntityFieldProfile profile) {
if (profile.getDetailList() == null || profile.getDetailList().isEmpty()) {
return;
}
Date now = new Date();
int seq = 0;
for (MesXslBizEntityFieldDetail row : profile.getDetailList()) {
row.setId(null);
row.setProfileId(profile.getId());
if (row.getSortNo() == null) {
row.setSortNo(seq++);
}
if (row.getCreateTime() == null) {
row.setCreateTime(now);
}
if (row.getUpdateTime() == null) {
row.setUpdateTime(now);
}
if (StringUtils.isBlank(row.getCreateBy())) {
row.setCreateBy(profile.getCreateBy());
}
if (StringUtils.isBlank(row.getUpdateBy())) {
row.setUpdateBy(profile.getUpdateBy());
}
detailMapper.insert(row);
}
}
}

View File

@@ -0,0 +1,248 @@
package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.xslmes.entity.MesXslRawMaterialCard;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialCardService;
import org.jeecg.modules.xslmes.service.IMesXslRawMaterialWarehouseBoardService;
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO.BoardAreaCardVO;
import org.jeecg.modules.xslmes.vo.MesXslRawMaterialWarehouseBoardVO.BoardBandVO;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 原材料库区看板
*/
@Service
@RequiredArgsConstructor
public class MesXslRawMaterialWarehouseBoardServiceImpl implements IMesXslRawMaterialWarehouseBoardService {
private static final String MEASURE_WEIGHT = "weight";
/** 库区编码首段形如 1F、2P、03A 等,仅作看板分组带标题(与仓库分类层级无「楼层」对应关系) */
private static final Pattern BAND_FIRST_SEGMENT = Pattern.compile("^\\d+[A-Za-z]+$");
private final IMesXslWarehouseAreaService mesXslWarehouseAreaService;
private final IMesXslRawMaterialCardService mesXslRawMaterialCardService;
@Override
public MesXslRawMaterialWarehouseBoardVO queryBoard(String warehouseId, String keyword, String measureType) {
String mt = StringUtils.defaultIfBlank(measureType, "quantity").toLowerCase(Locale.ROOT).trim();
if (!MEASURE_WEIGHT.equals(mt)) {
mt = "quantity";
}
MesXslRawMaterialWarehouseBoardVO vo = new MesXslRawMaterialWarehouseBoardVO();
vo.setMeasureType(mt);
QueryWrapper<MesXslWarehouseArea> areaQw = new QueryWrapper<>();
areaQw.eq("status", "0");
if (StringUtils.isNotBlank(warehouseId)) {
areaQw.eq("warehouse_id", warehouseId.trim());
}
areaQw.orderByAsc("area_code");
List<MesXslWarehouseArea> areas = mesXslWarehouseAreaService.list(areaQw);
if (areas.isEmpty()) {
return vo;
}
mesXslWarehouseAreaService.enrichDisplayedActualCapacity(areas);
List<String> areaCodes = areas.stream().map(MesXslWarehouseArea::getAreaCode).filter(StringUtils::isNotBlank).map(String::trim).distinct().collect(Collectors.toList());
QueryWrapper<MesXslRawMaterialCard> cardQw = new QueryWrapper<>();
cardQw.in("warehouse_area", areaCodes);
cardQw.apply("(COALESCE(remaining_quantity,0) > 0 OR COALESCE(remaining_weight,0) > 0)");
String kw = StringUtils.trimToNull(keyword);
if (kw != null) {
cardQw.and(w ->
w.like("material_name", kw).or().like("barcode", kw).or().like("batch_no", kw));
}
List<MesXslRawMaterialCard> cards = mesXslRawMaterialCardService.list(cardQw);
Map<String, List<MesXslRawMaterialCard>> byArea =
cards.stream().filter(c -> StringUtils.isNotBlank(c.getWarehouseArea())).collect(Collectors.groupingBy(c -> c.getWarehouseArea().trim(), Collectors.toList()));
boolean filterByKeyword = kw != null;
Map<String, BoardBandVO> bandMap = new LinkedHashMap<>();
for (MesXslWarehouseArea area : areas) {
String code = StringUtils.trimToEmpty(area.getAreaCode());
if (code.isEmpty()) {
continue;
}
List<MesXslRawMaterialCard> list = byArea.getOrDefault(code, new ArrayList<>());
if (filterByKeyword && list.isEmpty()) {
continue;
}
BoardAreaCardVO cardVo = aggregateArea(area, list, mt);
BandKey bk = resolveBand(area.getAreaCode());
BoardBandVO band = bandMap.computeIfAbsent(bk.key, k -> {
BoardBandVO b = new BoardBandVO();
b.setBandKey(bk.key);
b.setBandLabel(bk.label);
b.setBandSort(bk.sort);
return b;
});
band.getAreas().add(cardVo);
}
List<BoardBandVO> bands =
bandMap.values().stream().sorted(Comparator.comparing(BoardBandVO::getBandSort, Comparator.nullsLast(Integer::compareTo)).thenComparing(BoardBandVO::getBandKey, Comparator.nullsLast(String::compareTo))).collect(Collectors.toList());
for (BoardBandVO band : bands) {
band.getAreas().sort(Comparator.comparing(BoardAreaCardVO::getAreaCode, Comparator.nullsLast(String::compareTo)));
}
vo.setBands(bands);
return vo;
}
private BoardAreaCardVO aggregateArea(MesXslWarehouseArea area, List<MesXslRawMaterialCard> list, String measureType) {
BoardAreaCardVO vo = new BoardAreaCardVO();
vo.setAreaId(area.getId());
vo.setAreaCode(area.getAreaCode());
vo.setAreaName(area.getAreaName());
vo.setWarehouseId(area.getWarehouseId());
vo.setWarehouseName(area.getWarehouseName());
vo.setMaxCapacity(area.getMaxCapacity());
vo.setActualCapacity(area.getActualCapacity());
vo.setCardCount(list.size());
int sumQty = 0;
BigDecimal sumW = BigDecimal.ZERO;
LinkedHashSet<String> materialNames = new LinkedHashSet<>();
for (MesXslRawMaterialCard c : list) {
if (c.getRemainingQuantity() != null) {
sumQty += c.getRemainingQuantity();
}
if (c.getRemainingWeight() != null) {
sumW = sumW.add(c.getRemainingWeight());
}
if (StringUtils.isNotBlank(c.getMaterialName())) {
materialNames.add(c.getMaterialName().trim());
}
}
vo.setCurrentQuantity(sumQty);
vo.setCurrentWeight(sumW.setScale(3, RoundingMode.HALF_UP));
vo.setMaterialKindCount(materialNames.size());
vo.setTopMaterialNames(materialNames.stream().limit(8).collect(Collectors.toList()));
Double usage = null;
Integer maxCap = area.getMaxCapacity();
if (maxCap != null && maxCap > 0) {
if (MEASURE_WEIGHT.equals(measureType)) {
usage = sumW.multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(maxCap), 1, RoundingMode.HALF_UP).doubleValue();
} else {
usage = BigDecimal.valueOf(sumQty).multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(maxCap), 1, RoundingMode.HALF_UP).doubleValue();
}
if (usage > 100) {
usage = 100d;
}
if (usage < 0) {
usage = 0d;
}
}
vo.setUsagePercent(usage);
vo.setAlertLevel(resolveAlert(usage, list.isEmpty()));
return vo;
}
private static String resolveAlert(Double usagePercent, boolean emptyCards) {
if (emptyCards) {
return "empty";
}
if (usagePercent == null) {
return "unknown";
}
if (usagePercent <= 0) {
return "empty";
}
if (usagePercent < 30) {
return "low";
}
if (usagePercent < 90) {
return "normal";
}
if (usagePercent < 100) {
return "high";
}
return "full";
}
private BandKey resolveBand(String areaCode) {
if (StringUtils.isBlank(areaCode)) {
return new BandKey("zzz_other", "其它", 999_000);
}
String trimmed = areaCode.trim();
String head = trimmed.contains("-") ? trimmed.substring(0, trimmed.indexOf('-')) : trimmed;
if (BAND_FIRST_SEGMENT.matcher(head).matches()) {
String normalized = normalizeBandToken(head);
int sort = bandSort(normalized);
return new BandKey(normalized.toLowerCase(Locale.ROOT), normalized, sort);
}
String label = StringUtils.left(head, 16);
if (label.isEmpty()) {
label = trimmed.length() > 16 ? trimmed.substring(0, 16) : trimmed;
}
return new BandKey("z_" + label.toLowerCase(Locale.ROOT), label, 888_000 + Math.abs(label.hashCode() % 1000));
}
private static String normalizeBandToken(String raw) {
if (raw == null) {
return "其它";
}
String s = raw.toUpperCase(Locale.ROOT);
s = s.replace('', 'F').replace('', 'P');
return s;
}
/**
* 简单排序按前缀中的数字升序1F&lt;2F&lt;10F末尾字母次之
*/
private static int bandSort(String label) {
String digits = "";
String tail = "";
for (int i = 0; i < label.length(); i++) {
char ch = label.charAt(i);
if (Character.isDigit(ch)) {
digits += ch;
} else {
tail = label.substring(i);
break;
}
}
int num = 0;
try {
if (!digits.isEmpty()) {
num = Integer.parseInt(digits);
}
} catch (NumberFormatException ignored) {
num = 0;
}
return num * 100 + Math.min(Math.abs(tail.hashCode() % 99), 99);
}
private static final class BandKey {
final String key;
final String label;
final int sort;
BandKey(String key, String label, int sort) {
this.key = key;
this.label = label;
this.sort = sort;
}
}
}

View File

@@ -1,18 +1,25 @@
package org.jeecg.modules.xslmes.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.modules.xslmes.capacity.WarehouseAreaDisplayedActualCapacityMediator;
import org.jeecg.modules.xslmes.entity.MesXslWarehouseArea;
import org.jeecg.modules.xslmes.mapper.MesXslWarehouseAreaMapper;
import org.jeecg.modules.xslmes.service.IMesXslWarehouseAreaService;
import org.springframework.stereotype.Service;
import java.util.Collection;
/**
* MES 库区管理
*/
@Service
@RequiredArgsConstructor
public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseAreaMapper, MesXslWarehouseArea> implements IMesXslWarehouseAreaService {
private final WarehouseAreaDisplayedActualCapacityMediator displayedActualCapacityMediator;
@Override
public boolean existsSameAreaCode(String areaCode, String excludeId) {
if (StringUtils.isBlank(areaCode)) {
@@ -24,4 +31,9 @@ public class MesXslWarehouseAreaServiceImpl extends ServiceImpl<MesXslWarehouseA
.ne(StringUtils.isNotBlank(excludeId), MesXslWarehouseArea::getId, excludeId)
.count() > 0L;
}
@Override
public void enrichDisplayedActualCapacity(Collection<MesXslWarehouseArea> areas) {
displayedActualCapacityMediator.apply(areas);
}
}

View File

@@ -41,11 +41,11 @@ public class MesXslWarehouseServiceImpl extends ServiceImpl<MesXslWarehouseMappe
return;
}
String code = baseMapper.queryCategoryCodeById(e.getWarehouseCategory());
if (!MesXslWarehouseCategory.CUSTOMER_CATEGORY_CODE.equals(code)) {
if (!MesXslWarehouseCategory.isCustomerWarehouseCategoryCode(code)) {
e.setCustomerId(null);
e.setCustomerShortName(null);
}
if (!MesXslWarehouseCategory.SUPPLIER_CATEGORY_CODE.equals(code)) {
if (!MesXslWarehouseCategory.isSupplierWarehouseCategoryCode(code)) {
e.setSupplierId(null);
e.setSupplierShortName(null);
}
@@ -56,11 +56,11 @@ public class MesXslWarehouseServiceImpl extends ServiceImpl<MesXslWarehouseMappe
return;
}
String code = baseMapper.queryCategoryCodeById(e.getWarehouseCategory());
if (MesXslWarehouseCategory.CUSTOMER_CATEGORY_CODE.equals(code)) {
if (MesXslWarehouseCategory.isCustomerWarehouseCategoryCode(code)) {
if (oConvertUtils.isEmpty(e.getCustomerId())) {
throw new JeecgBootException("仓库分类为客户库时,请选择客户");
}
} else if (MesXslWarehouseCategory.SUPPLIER_CATEGORY_CODE.equals(code)) {
} else if (MesXslWarehouseCategory.isSupplierWarehouseCategoryCode(code)) {
if (oConvertUtils.isEmpty(e.getSupplierId())) {
throw new JeecgBootException("仓库分类为供应商库时,请选择供应商");
}

View File

@@ -0,0 +1,91 @@
package org.jeecg.modules.xslmes.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 原材料库区看板:按库区编码前缀等规则形成的「分组带」汇总展示(不按业务楼层拆分)
*/
@Data
@Schema(description = "原材料库区看板")
public class MesXslRawMaterialWarehouseBoardVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "计量口径回显quantity=按剩余数量 weight=按剩余重量")
private String measureType;
@Schema(description = "分组带列表")
private List<BoardBandVO> bands = new ArrayList<>();
@Data
@Schema(description = "看板分组带(如 1F、2F")
public static class BoardBandVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "分组键(排序用)")
private String bandKey;
@Schema(description = "分组标题")
private String bandLabel;
@Schema(description = "排序号,越小越靠前")
private Integer bandSort;
@Schema(description = "该组下的库区卡片")
private List<BoardAreaCardVO> areas = new ArrayList<>();
}
@Data
@Schema(description = "单个库区卡片数据")
public static class BoardAreaCardVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "库区主键")
private String areaId;
@Schema(description = "库区编码")
private String areaCode;
@Schema(description = "库区名称")
private String areaName;
@Schema(description = "所属仓库ID")
private String warehouseId;
@Schema(description = "所属仓库名称")
private String warehouseName;
@Schema(description = "最大存放量(库区主数据)")
private Integer maxCapacity;
@Schema(description = "实际存放量(库区主数据快照)")
private Integer actualCapacity;
@Schema(description = "在库卡片张数(有剩余量)")
private Integer cardCount;
@Schema(description = "物料种类数(去重物料名称)")
private Integer materialKindCount;
@Schema(description = "剩余数量合计")
private Integer currentQuantity;
@Schema(description = "剩余重量合计")
private BigDecimal currentWeight;
@Schema(description = "物料名称摘要(前若干种)")
private List<String> topMaterialNames = new ArrayList<>();
@Schema(description = "占用率 0100无上限或上限为0时为 null")
private Double usagePercent;
@Schema(description = "告警级别empty/low/normal/high/full/unknown")
private String alertLevel;
}
}