From 8c6cc08cacc912742c089891cee7344f8e8b8b47 Mon Sep 17 00:00:00 2001 From: wenshijun Date: Thu, 4 Sep 2025 16:55:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9B=BE=E7=89=87=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/zg/common/utils/LocalPhotoUtil.java | 173 +++++++++++++ .../zg/framework/config/ResourcesConfig.java | 5 + .../zg/framework/config/SecurityConfig.java | 2 + .../wisdom/controller/PhotoController.java | 86 +++++++ .../wisdom/controller/RkInfoController.java | 14 ++ .../controller/RkStatisticsController.java | 11 + .../zg/project/wisdom/domain/StockPhoto.java | 95 +++++++ .../wisdom/domain/dto/PhotoDeleteDTO.java | 15 ++ .../project/wisdom/domain/vo/BillGroupVO.java | 39 +++ .../wisdom/domain/vo/PcdeDetailSimpleVO.java | 16 ++ .../wisdom/domain/vo/SceneAvailableVO.java | 19 ++ .../project/wisdom/mapper/RkInfoMapper.java | 9 + .../wisdom/mapper/RkStatisticsMapper.java | 12 + .../wisdom/mapper/StockPhotoMapper.java | 36 +++ .../wisdom/service/IRkInfoService.java | 10 +- .../project/wisdom/service/PhotoService.java | 40 +++ .../wisdom/service/RkStatisticsService.java | 7 + .../wisdom/service/impl/PhotoServiceImpl.java | 131 ++++++++++ .../service/impl/RkInfoServiceImpl.java | 8 + .../service/impl/RkStatisticsServiceImpl.java | 26 ++ .../resources/mybatis/wisdom/GysJhMapper.xml | 6 +- .../resources/mybatis/wisdom/RkInfoMapper.xml | 238 ++++++++++++++++++ .../mybatis/wisdom/RkStatisticsMapper.xml | 55 ++++ .../mybatis/wisdom/StockPhotoMapper.xml | 72 ++++++ 24 files changed, 1119 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/zg/common/utils/LocalPhotoUtil.java create mode 100644 src/main/java/com/zg/project/wisdom/controller/PhotoController.java create mode 100644 src/main/java/com/zg/project/wisdom/domain/StockPhoto.java create mode 100644 src/main/java/com/zg/project/wisdom/domain/dto/PhotoDeleteDTO.java create mode 100644 src/main/java/com/zg/project/wisdom/domain/vo/BillGroupVO.java create mode 100644 src/main/java/com/zg/project/wisdom/domain/vo/PcdeDetailSimpleVO.java create mode 100644 src/main/java/com/zg/project/wisdom/domain/vo/SceneAvailableVO.java create mode 100644 src/main/java/com/zg/project/wisdom/mapper/StockPhotoMapper.java create mode 100644 src/main/java/com/zg/project/wisdom/service/PhotoService.java create mode 100644 src/main/java/com/zg/project/wisdom/service/impl/PhotoServiceImpl.java create mode 100644 src/main/resources/mybatis/wisdom/StockPhotoMapper.xml diff --git a/src/main/java/com/zg/common/utils/LocalPhotoUtil.java b/src/main/java/com/zg/common/utils/LocalPhotoUtil.java new file mode 100644 index 0000000..7747295 --- /dev/null +++ b/src/main/java/com/zg/common/utils/LocalPhotoUtil.java @@ -0,0 +1,173 @@ +package com.zg.common.utils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 本地图片保存工具 + * - 根目录:photo.root(默认 D:\photo) + * - 目录结构:{subDir}/yyyy-MM-dd/ + * - 文件命名:{billNo}_{sapNo}_{xmMs}_{yyyyMMddHHmmssSSS}_{index}.{ext} + * - 返回:相对web路径(用于拼接 /photo/ 前缀生成可访问URL)与文件名 + */ +@Component +public class LocalPhotoUtil { + + /** 本地根目录,可在 yml 配置:photo.root: D:\photo */ + @Value("${photo.root:D:\\\\photo}") + private String rootPath; + + /** 非法文件名字符(Windows) */ + private static final String ILLEGAL_CHARS = "\\/:*?\"<>|"; + + /** yyyy-MM-dd(目录) */ + private String today() { + return new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + } + + /** yyyyMMddHHmmssSSS(文件戳) */ + private String nowStamp() { + return new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); + } + + /** + * 组装基础文件名:billNo_sapNo_xmMs(不含后缀、不含时间戳/序号) + */ + public String buildBaseName(String billNo, String xmMs) { + String b = defaultString(billNo); + String x = defaultString(xmMs); + String joined = String.join("_", b, x); + return sanitize(joined); + } + + /** + * 保存单个文件 + * @param file 文件 + * @param subDir 一级子目录(建议 "photo" 固定,也可按业务传入) + * @param baseName 基础名(不含扩展名;可用 buildBaseName 生成) + * @param index 序号(从1开始),用于同批次区分 + * @return SaveResult(webPath, fileName) + */ + public SaveResult saveOne(MultipartFile file, String subDir, String baseName, int index) throws Exception { + String ext = resolveExt(file); + String safeSub = sanitize(defaultString(subDir, "")); + String dirPart = safeSub + File.separator + today() + File.separator; + + String stamp = nowStamp(); + String safeBase = sanitize(baseName); + // 最终文件名:base_时间戳_序号.ext + String fileName = safeBase + "_" + stamp + "_" + index + ext; + + File dir = new File(rootPath, dirPart); + if (!dir.exists() && !dir.mkdirs()) { + throw new RuntimeException("创建目录失败:" + dir.getAbsolutePath()); + } + + File dest = new File(dir, fileName); + try (InputStream in = file.getInputStream()) { + Files.copy(in, dest.toPath()); + } + + SaveResult r = new SaveResult(); + r.setWebPath((dirPart + fileName).replace("\\", "/")); // e.g. photo/2025-09-01/xxx.jpg + r.setFileName(fileName); + return r; + } + + /** + * 批量保存(顺序与入参 files 一致) + * @param files 文件列表 + * @param subDir 一级子目录(建议传 "photo") + * @param baseName 基础名(不含扩展名) + */ + public List saveBatch(List files, String subDir, String baseName) throws Exception { + if (files == null || files.isEmpty()) { + return Collections.emptyList(); + } + List list = new ArrayList<>(files.size()); + for (int i = 0; i < files.size(); i++) { + list.add(saveOne(files.get(i), subDir, baseName, i + 1)); + } + return list; + } + + /** + * 便捷方法:直接传业务字段(内部会调用 buildBaseName) + * @param files 多文件 + * @param subDir 一级子目录(建议 "photo") + * @param billNo 单据号 + * @param xmMs 项目描述 + */ + public List saveBatchByBiz(List files, String subDir, + String billNo, String xmMs) throws Exception { + String base = buildBaseName(billNo, xmMs); + return saveBatch(files, subDir, base); + } + + /** 解析扩展名:优先取原文件名,其次按 contentType 兜底 */ + private String resolveExt(MultipartFile file) { + String origin = file.getOriginalFilename(); + if (origin != null) { + int dot = origin.lastIndexOf('.'); + if (dot >= 0) { + String ext = origin.substring(dot); + if (ext.length() <= 10) { // 简单防御:避免异常超长扩展名 + return ext; + } + } + } + String ct = file.getContentType(); + if ("image/png".equalsIgnoreCase(ct)) return ".png"; + if ("image/jpeg".equalsIgnoreCase(ct) || "image/jpg".equalsIgnoreCase(ct)) return ".jpg"; + return ".bin"; + } + + /** 清洗文件名:去空白、替换非法字符、规避Windows保留名、限长 */ + public String sanitize(String name) { + if (name == null) return "unnamed"; + String t = name.trim().replaceAll("\\s+", " "); + StringBuilder sb = new StringBuilder(t.length()); + for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + sb.append(ILLEGAL_CHARS.indexOf(c) >= 0 ? '_' : c); + } + String out = sb.toString(); + String upper = out.toUpperCase(Locale.ROOT); + if (upper.matches("CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]")) { + out = "_" + out; + } + // 限长,给时间戳与序号留空间 + if (out.length() > 100) { + out = out.substring(0, 100); + } + return out; + } + + /** null 转空串;带默认值重载 */ + private String defaultString(String s) { + return s == null ? "" : s; + } + private String defaultString(String s, String def) { + return (s == null || s.isEmpty()) ? def : s; + } + + /** 返回值对象 */ + public static class SaveResult { + /** 相对web路径,例如 photo/2025-09-01/xxx.jpg(用于拼接 /photo/ 前缀生成URL) */ + private String webPath; + /** 最终文件名(含扩展名) */ + private String fileName; + + public String getWebPath() { return webPath; } + public void setWebPath(String webPath) { this.webPath = webPath; } + public String getFileName() { return fileName; } + public void setFileName(String fileName) { this.fileName = fileName; } + } +} diff --git a/src/main/java/com/zg/framework/config/ResourcesConfig.java b/src/main/java/com/zg/framework/config/ResourcesConfig.java index 12a35c6..fe7dd4f 100644 --- a/src/main/java/com/zg/framework/config/ResourcesConfig.java +++ b/src/main/java/com/zg/framework/config/ResourcesConfig.java @@ -32,6 +32,11 @@ public class ResourcesConfig implements WebMvcConfigurer registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); + + /**把 /photo/** 映射到 D:/photo/ */ + registry.addResourceHandler("/photo/**") + .addResourceLocations("file:D:/photo/"); // 关键:file: 前缀 + 结尾 / + /** swagger配置 */ registry.addResourceHandler("/swagger-ui/**") .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") diff --git a/src/main/java/com/zg/framework/config/SecurityConfig.java b/src/main/java/com/zg/framework/config/SecurityConfig.java index 25c2d35..4131fdd 100644 --- a/src/main/java/com/zg/framework/config/SecurityConfig.java +++ b/src/main/java/com/zg/framework/config/SecurityConfig.java @@ -121,6 +121,8 @@ public class SecurityConfig "/system/config/**", "/AutoInventory/**", "/ws/**", + "/photo/**", + "/wisdom/stock/**", "/wisdom/**", "/mock/**", "/information/device/**", diff --git a/src/main/java/com/zg/project/wisdom/controller/PhotoController.java b/src/main/java/com/zg/project/wisdom/controller/PhotoController.java new file mode 100644 index 0000000..16fea77 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/controller/PhotoController.java @@ -0,0 +1,86 @@ +package com.zg.project.wisdom.controller; + +import com.zg.framework.web.domain.AjaxResult; +import com.zg.project.wisdom.domain.dto.PhotoDeleteDTO; +import com.zg.project.wisdom.service.PhotoService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 【照片上传接口】 + * - 负责接收前端图片(单图/多图),并调用 Service 完成:落盘 + URL入库 + * - 依赖静态资源映射:/photo/** -> D:\photo\ + */ +@RestController +@RequestMapping("/photo") +public class PhotoController { + + @Resource + private PhotoService photoService; + + /** + * 【多图上传】(推荐) + * 提交方式:multipart/form-data + * 参数: + * - files:多文件(同名字段,可传多张) + * - photoType:0(入库相关) / 1(出库相关) + * - billNo:单据号 + * - xmMs:项目描述 + * - sapNo:订单号 + * 返回:每张图片的可访问 URL 列表 + */ + @PostMapping(value = "/upload/batch", consumes = "multipart/form-data") + public AjaxResult uploadBatch(@RequestPart("files") List files, + @RequestParam("photoType") String photoType, + @RequestParam("billNo") String billNo, + @RequestParam("xmMs") String xmMs) { + List urls = photoService.uploadAndSave(files, photoType, billNo, xmMs); + return AjaxResult.success("上传成功", urls); + } + + /** + * 【单图上传】(可选) + * 与多图参数一致,只是文件字段为 file + * 返回:单张图片的可访问 URL + */ + @PostMapping(value = "/upload", consumes = "multipart/form-data") + public AjaxResult upload(@RequestPart("file") MultipartFile file, + @RequestParam("photoType") String photoType, + @RequestParam("billNo") String billNo, + @RequestParam("xmMs") String xmMs) { + List urls = photoService.uploadAndSave(Collections.singletonList(file), photoType, billNo, xmMs); + return AjaxResult.success("上传成功", urls.isEmpty() ? null : urls.get(0)); + } + + /** + * 【查询:按 billNo(可选photoType)取照片URL列表】 + * GET /photo/list?billNo=...&photoType=0|1 + * - billNo 必填 + * - photoType 可不传:不传则返回该单据下所有类型的照片 + * 返回:纯 URL 列表 + */ + @GetMapping("/list") + public AjaxResult list(@RequestParam("billNo") String billNo, + @RequestParam(value = "photoType", required = false) String photoType) { + return AjaxResult.success(photoService.listUrlsByBill(billNo, photoType)); + } + + /** + * 【删除:按 URL 列表】 + * @param dto + * @return + */ + @PostMapping(value = "/delete", consumes = "application/json") +// @PreAuthorize("@ss.hasPermi('wisdom:photo:remove')") + public AjaxResult delete(@Validated @RequestBody PhotoDeleteDTO dto) { + Map result = photoService.deleteByUrls(dto.getUrls()); + return AjaxResult.success("删除完成", result); + } +} diff --git a/src/main/java/com/zg/project/wisdom/controller/RkInfoController.java b/src/main/java/com/zg/project/wisdom/controller/RkInfoController.java index 261e9ba..71aab25 100644 --- a/src/main/java/com/zg/project/wisdom/controller/RkInfoController.java +++ b/src/main/java/com/zg/project/wisdom/controller/RkInfoController.java @@ -1,5 +1,6 @@ package com.zg.project.wisdom.controller; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -7,6 +8,8 @@ import javax.servlet.http.HttpServletResponse; import com.github.pagehelper.PageHelper; import com.zg.project.wisdom.domain.dto.*; +import com.zg.project.wisdom.domain.vo.BillGroupVO; +import io.swagger.annotations.ApiOperation; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @@ -44,6 +47,17 @@ public class RkInfoController extends BaseController return getDataTable(list); } + @ApiOperation("按单据分组(bill_no)列表:若存在出库则同时返回 bill_no_ck") + @PreAuthorize("@ss.hasPermi('wisdom:stock:list')") + @PostMapping("/bill/groups") + public TableDataInfo billGroups(@RequestBody RkInfoQueryDTO query) { + // 分页 + PageHelper.startPage(query.getPageNum(), query.getPageSize()); + // 查询 + List rows = rkInfoService.selectGroupedByBill(query); + + return getDataTable(rows); + } /** * 导出库存单据主列表 diff --git a/src/main/java/com/zg/project/wisdom/controller/RkStatisticsController.java b/src/main/java/com/zg/project/wisdom/controller/RkStatisticsController.java index c83b07d..494a65a 100644 --- a/src/main/java/com/zg/project/wisdom/controller/RkStatisticsController.java +++ b/src/main/java/com/zg/project/wisdom/controller/RkStatisticsController.java @@ -43,6 +43,17 @@ public class RkStatisticsController { return AjaxResult.success(resp); } + /** + * 按仓库查询:场景列表 + 每个场景的可用库位明细 + * 示例:GET /warehouse/positions/available?warehouseCode=CK001 + */ + @ApiOperation("按仓库查询各场景的可用库位(排除未出库占用)") + @GetMapping("/warehouse/available") + public AjaxResult listAvailableByWarehouse(@RequestParam String warehouseCode) { + List data = rkStatisticsService.listAvailableByWarehouse(warehouseCode); + return AjaxResult.success(data); + } + /** * 当前库存根据入库类型统计项目数,每个项目的条目数,实际入库总数,金额总和(仅未出库) */ diff --git a/src/main/java/com/zg/project/wisdom/domain/StockPhoto.java b/src/main/java/com/zg/project/wisdom/domain/StockPhoto.java new file mode 100644 index 0000000..1ddd75a --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/domain/StockPhoto.java @@ -0,0 +1,95 @@ +package com.zg.project.wisdom.domain; + +import com.zg.framework.web.domain.BaseEntity; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.io.Serializable; + +/** + * 单据照片对象 stock_photo + * 按 bill_no 聚合查询;仅存一个可访问的 URL;photo_type:0=入库、1=出库 + */ +public class StockPhoto extends BaseEntity implements Serializable { + private static final long serialVersionUID = 1L; + + /** 主键ID */ + private Long id; + + /** 单据号(对应 rk_info.bill_no,用于聚合查询) */ + private String billNo; + + /** 照片业务类型:0=入库相关,1=出库相关 */ + private String photoType; + + /** 实际文件名(含扩展名),示例:I202508110001_101360951_项目名_时间戳_序号.jpg */ + private String fileName; + + /** 可直接访问的图片URL(形如 /photo/2025-09-01/xxx.jpg 或完整http地址) */ + private String url; + + /** 删除标记:0=正常,1=已删除 */ + private String isDelete; + + /* ========== Getter / Setter ========== */ + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + + public String getBillNo() { + return billNo; + } + public void setBillNo(String billNo) { + this.billNo = billNo; + } + + public String getPhotoType() { + return photoType; + } + public void setPhotoType(String photoType) { + this.photoType = photoType; + } + + public String getFileName() { + return fileName; + } + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + + public String getIsDelete() { + return isDelete; + } + public void setIsDelete(String isDelete) { + this.isDelete = isDelete; + } + + /* ========== toString(RuoYi 风格) ========== */ + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("billNo", getBillNo()) + .append("photoType", getPhotoType()) + .append("fileName", getFileName()) + .append("url", getUrl()) + .append("isDelete", getIsDelete()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/src/main/java/com/zg/project/wisdom/domain/dto/PhotoDeleteDTO.java b/src/main/java/com/zg/project/wisdom/domain/dto/PhotoDeleteDTO.java new file mode 100644 index 0000000..a8ce443 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/domain/dto/PhotoDeleteDTO.java @@ -0,0 +1,15 @@ +package com.zg.project.wisdom.domain.dto; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +/** 删除图片请求体:传 URL 列表 */ +public class PhotoDeleteDTO { + + /** 要删除的图片地址(与 stock_photo.url 一致) */ + @NotEmpty + private List urls; + + public List getUrls() { return urls; } + public void setUrls(List urls) { this.urls = urls; } +} \ No newline at end of file diff --git a/src/main/java/com/zg/project/wisdom/domain/vo/BillGroupVO.java b/src/main/java/com/zg/project/wisdom/domain/vo/BillGroupVO.java new file mode 100644 index 0000000..e1807d3 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/domain/vo/BillGroupVO.java @@ -0,0 +1,39 @@ +package com.zg.project.wisdom.domain.vo;// package com.zg.project.wisdom.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +@Data +public class BillGroupVO { + /** 分组主键:统一用入库单号 bill_no(若为空会用 bill_no_ck 兜底) */ + private String billNo; + + /** 出库单号(出库单据时有值,前端可展示/操作) */ + private String billNoCk; + + /** 单据类型:IN / OUT */ + private String billType; + + /** 单据时间:IN 取最早 rk_time;OUT 取最早 ly_time */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date billTime; + + // ——主数据常用抬头字段(从首条提取即可)—— + private String xj; + private String xmNo; + private String xmMs; + private String rkType; private String rkTypeName; + private String ckType; private String ckTypeName; + private String teamCode; private String teamName; + private String cangkuName; + private String lihuoYName; + private String remark; + + // ——聚合指标—— + private Integer itemCount; // 明细条数 + private BigDecimal sumQty; // 数量合计(sum real_qty) + private BigDecimal sumAmount; // 金额合计(sum ht_dj * real_qty) +} diff --git a/src/main/java/com/zg/project/wisdom/domain/vo/PcdeDetailSimpleVO.java b/src/main/java/com/zg/project/wisdom/domain/vo/PcdeDetailSimpleVO.java new file mode 100644 index 0000000..ea319ae --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/domain/vo/PcdeDetailSimpleVO.java @@ -0,0 +1,16 @@ +package com.zg.project.wisdom.domain.vo; + +import lombok.Data; + +/** + * 可用库位简单视图 + */ +@Data +public class PcdeDetailSimpleVO { + /** 库位编码 */ + private String pcode; + /** 标签(如有) */ + private String tag; + /** 编码ID(如有) */ + private String encodedId; +} diff --git a/src/main/java/com/zg/project/wisdom/domain/vo/SceneAvailableVO.java b/src/main/java/com/zg/project/wisdom/domain/vo/SceneAvailableVO.java new file mode 100644 index 0000000..f0e34f2 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/domain/vo/SceneAvailableVO.java @@ -0,0 +1,19 @@ +package com.zg.project.wisdom.domain.vo; + +import lombok.Data; +import java.util.List; + +/** + * 场景下的可用库位 + */ +@Data +public class SceneAvailableVO { + /** 场景编码(pcde_detail.scene,对应 scene_mapping.scene_code) */ + private String sceneCode; + /** 场景名称(scene_mapping.scene_name) */ + private String sceneName; + /** 可用库位数量 */ + private Integer availableCount; + /** 可用库位明细 */ + private List positions; +} diff --git a/src/main/java/com/zg/project/wisdom/mapper/RkInfoMapper.java b/src/main/java/com/zg/project/wisdom/mapper/RkInfoMapper.java index 84ef252..0d4966f 100644 --- a/src/main/java/com/zg/project/wisdom/mapper/RkInfoMapper.java +++ b/src/main/java/com/zg/project/wisdom/mapper/RkInfoMapper.java @@ -7,6 +7,8 @@ import java.util.Map; import com.zg.project.Inventory.domain.vo.PcdeCntVO; import com.zg.project.Inventory.domain.vo.RkInfoMatchVO; import com.zg.project.wisdom.domain.RkInfo; +import com.zg.project.wisdom.domain.StockPhoto; +import com.zg.project.wisdom.domain.dto.RkInfoQueryDTO; import org.apache.ibatis.annotations.Param; /** @@ -33,6 +35,13 @@ public interface RkInfoMapper */ public List selectRkInfoList(RkInfo rkInfo); + /** + * 使用 selectRkInfoVo 作为子查询,外层按 bill_no 分组聚合 + * 不新增 resultMap / VO,直接用 RkInfoResult 映射需要的字段 + */ + List selectGroupedByBill(@Param("q") RkInfoQueryDTO query, + @Param("needAudit") Integer needAudit); + /** * 修改库存单据主 diff --git a/src/main/java/com/zg/project/wisdom/mapper/RkStatisticsMapper.java b/src/main/java/com/zg/project/wisdom/mapper/RkStatisticsMapper.java index 8260dde..22329ec 100644 --- a/src/main/java/com/zg/project/wisdom/mapper/RkStatisticsMapper.java +++ b/src/main/java/com/zg/project/wisdom/mapper/RkStatisticsMapper.java @@ -99,4 +99,16 @@ public interface RkStatisticsMapper { /** 出库:在 [startTs, endTs) 时间范围内、is_chuku=1 的原始行(仅最小过滤) */ List selectOutRange(@Param("startTs") String startTs, @Param("endTs") String endTs); + + /** + * 外层:按场景汇总可用数量,并级联查询可用库位明细(返回 SceneAvailableVO 列表) + */ + List selectAvailableByWarehouse(@Param("warehouseCode") String warehouseCode); + + /** + * 内层:指定仓库 + 场景,查询该场景下的可用库位明细 + */ + List selectAvailablePositionsByWarehouseAndScene( + @Param("warehouseCode") String warehouseCode, + @Param("sceneCode") String sceneCode); } diff --git a/src/main/java/com/zg/project/wisdom/mapper/StockPhotoMapper.java b/src/main/java/com/zg/project/wisdom/mapper/StockPhotoMapper.java new file mode 100644 index 0000000..3c5378d --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/mapper/StockPhotoMapper.java @@ -0,0 +1,36 @@ +package com.zg.project.wisdom.mapper; + +import com.zg.project.wisdom.domain.StockPhoto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 【单据照片Mapper】 + * - 负责 URL 的持久化(只存一条URL) + */ +public interface StockPhotoMapper { + + /** + * 新增(单条) + */ + int insert(StockPhoto photo); + + /** + * 批量新增(推荐用于多图一次上传) + */ + int batchInsert(@Param("list") List list); + + /** + * 按 bill_no(可选 photoType)查询图片列表 + */ + List selectByBillNo(@Param("billNo") String billNo, + @Param("photoType") String photoType); + + /** + * 批量删除 + * @param urls + * @return + */ + int softDeleteByUrls(@Param("urls") List urls); +} diff --git a/src/main/java/com/zg/project/wisdom/service/IRkInfoService.java b/src/main/java/com/zg/project/wisdom/service/IRkInfoService.java index 8f74a0e..33f9f5b 100644 --- a/src/main/java/com/zg/project/wisdom/service/IRkInfoService.java +++ b/src/main/java/com/zg/project/wisdom/service/IRkInfoService.java @@ -1,13 +1,12 @@ package com.zg.project.wisdom.service; import java.util.List; -import java.util.Map; import com.zg.project.Inventory.domain.dto.QueryDTO; import com.zg.project.Inventory.domain.vo.ChartDataVO; import com.zg.project.wisdom.domain.RkInfo; import com.zg.project.wisdom.domain.dto.*; -import com.zg.project.wisdom.domain.vo.RkSubmitResultVO; +import com.zg.project.wisdom.domain.vo.BillGroupVO; /** * 库存单据主Service接口 @@ -27,12 +26,17 @@ public interface IRkInfoService /** * 查询库存单据主列表 - * + * * @param rkInfo 库存单据主 * @return 库存单据主集合 */ public List selectRkInfoList(RkInfo rkInfo); + /** + * 按 bill_no 分组返回单据列表(复用 selectRkInfoVo,SQL 外层分组聚合) + * @param query 与 /list 相同的查询条件 + */ + List selectGroupedByBill(RkInfoQueryDTO query); /** * 修改库存单据主 diff --git a/src/main/java/com/zg/project/wisdom/service/PhotoService.java b/src/main/java/com/zg/project/wisdom/service/PhotoService.java new file mode 100644 index 0000000..62d744d --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/service/PhotoService.java @@ -0,0 +1,40 @@ +package com.zg.project.wisdom.service; + +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Map; + +/** + * 【照片上传服务接口】 + * - 定义:多图落盘 + URL入库 的能力 + */ +public interface PhotoService { + + /** + * 多图上传并落库,返回每张图片的可访问URL + * + * @param files 图片文件列表(必传) + * @param photoType 照片业务类型:0=入库相关,1=出库相关(必填) + * @param billNo 单据号(必填,用于关联) + * @param xmMs 项目描述(用于文件命名,可为空) + * @return URLs 列表(每个元素都是可直接 的地址) + */ + List uploadAndSave(List files, + String photoType, String billNo, + String xmMs); + + + /** + * 按 billNo(可选 photoType)查询照片 URL 列表 + */ + List listUrlsByBill(String billNo, String photoType); + + /** + * 按 URL 批量删除(仅数据库软删,不删除磁盘文件) + * @param urls 待删URL列表 + * @return 结果统计:dbUpdated(数据库更新行数), total(传入数) + */ + Map deleteByUrls(List urls); + +} diff --git a/src/main/java/com/zg/project/wisdom/service/RkStatisticsService.java b/src/main/java/com/zg/project/wisdom/service/RkStatisticsService.java index a17a11d..117be72 100644 --- a/src/main/java/com/zg/project/wisdom/service/RkStatisticsService.java +++ b/src/main/java/com/zg/project/wisdom/service/RkStatisticsService.java @@ -80,4 +80,11 @@ public interface RkStatisticsService { * 返回每个时间桶的一行数据(固定 7 行或 6 行) */ List getIOBuckets(Integer range); + + + /** + * 根据仓库编码,返回各场景的可用库位(含数量与明细) + * 规则:排除 rk_info 中未出库(is_chuku=0 或 NULL)的占用库位 + */ + List listAvailableByWarehouse(String warehouseCode); } diff --git a/src/main/java/com/zg/project/wisdom/service/impl/PhotoServiceImpl.java b/src/main/java/com/zg/project/wisdom/service/impl/PhotoServiceImpl.java new file mode 100644 index 0000000..78341a0 --- /dev/null +++ b/src/main/java/com/zg/project/wisdom/service/impl/PhotoServiceImpl.java @@ -0,0 +1,131 @@ +package com.zg.project.wisdom.service.impl; + +import com.zg.common.utils.LocalPhotoUtil; +import com.zg.common.utils.LocalPhotoUtil.SaveResult; +import com.zg.project.wisdom.domain.StockPhoto; +import com.zg.project.wisdom.mapper.StockPhotoMapper; +import com.zg.project.wisdom.service.PhotoService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 【照片上传服务实现】 + * 核心流程: + * 1) 使用 LocalPhotoUtil 将文件保存到本地(按:photo/yyyy-MM-dd/) + * 2) 拼接可访问URL(/photo/** 静态映射) + * 3) 批量写入 stock_photo(只存一个 url) + * 4) 返回 URL 列表 + */ +@Service +public class PhotoServiceImpl implements PhotoService { + + @Resource + private LocalPhotoUtil localPhotoUtil; + + @Resource + private StockPhotoMapper stockPhotoMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public List uploadAndSave(List files, + String photoType, String billNo, + String xmMs) { + + // ========= 1. 入参校验 ========= + if (files == null || files.isEmpty()) { + throw new IllegalArgumentException("请选择要上传的图片"); + } + if (!("0".equals(photoType) || "1".equals(photoType))) { + throw new IllegalArgumentException("photoType 只能为 0(入库) 或 1(出库)"); + } + if (StringUtils.isBlank(billNo)) { + throw new IllegalArgumentException("billNo 不能为空"); + } + + // 组装基础文件名:billNo_sapNo_xmMs(内部会做非法字符清洗) + final String baseName = localPhotoUtil.buildBaseName( + StringUtils.defaultString(billNo), + StringUtils.defaultString(xmMs)); + + try { + // ========= 2. 本地落盘 ========= + // 目录:D:\photo\photo\yyyy-MM-dd\ + List saved = localPhotoUtil.saveBatch(files, "", baseName); + + // ========= 3. 生成URL + 组装入库对象 ========= + List urls = new ArrayList(saved.size()); + List rows = new ArrayList(saved.size()); + + for (SaveResult s : saved) { + // 根据当前上下文拼 URL,例如:http://host:port/photo/photo/2025-09-01/xxx.jpg + String url = ServletUriComponentsBuilder.fromCurrentContextPath() + .path("/photo/") // 与 StaticResourceConfig.addResourceHandlers 保持一致 + .path(s.getWebPath()) // 形如:photo/2025-09-01/xxx.jpg + .toUriString(); + urls.add(url); + + // 只存一个URL到表中(其余信息按你最简诉求不存) + StockPhoto p = new StockPhoto(); + p.setBillNo(billNo); + p.setPhotoType(photoType); + p.setFileName(s.getFileName()); + p.setUrl(url); + // 如需记录创建人:p.setCreateBy(SecurityUtils.getUserId().toString()); + rows.add(p); + } + + // ========= 4. 批量入库 ========= + if (!rows.isEmpty()) { + stockPhotoMapper.batchInsert(rows); + } + + return urls; + } catch (Exception e) { + // Service 层抛出 RuntimeException,触发 @Transactional 回滚数据库写入 + // (注意:文件已落盘无法自动回滚,必要时可加失败清理逻辑) + throw new RuntimeException("上传/保存失败:" + e.getMessage(), e); + } + } + + @Override + public List listUrlsByBill(String billNo, String photoType) { + if (StringUtils.isBlank(billNo)) { + throw new IllegalArgumentException("billNo 不能为空"); + } + return stockPhotoMapper.selectByBillNo(billNo, + StringUtils.isBlank(photoType) ? null : photoType) + .stream() + .map(StockPhoto::getUrl) + .collect(Collectors.toList()); + } + + /** + * 批量删除 + * @param urls 待删URL列表 + * @return + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Map deleteByUrls(List urls) { + if (urls == null || urls.isEmpty()) { + throw new IllegalArgumentException("urls 不能为空"); + } + // 仅数据库软删 + int dbUpdated = stockPhotoMapper.softDeleteByUrls(urls); + + Map ret = new HashMap(2); + ret.put("total", urls.size()); + ret.put("dbUpdated", dbUpdated); + return ret; + } +} diff --git a/src/main/java/com/zg/project/wisdom/service/impl/RkInfoServiceImpl.java b/src/main/java/com/zg/project/wisdom/service/impl/RkInfoServiceImpl.java index d214588..c7aca90 100644 --- a/src/main/java/com/zg/project/wisdom/service/impl/RkInfoServiceImpl.java +++ b/src/main/java/com/zg/project/wisdom/service/impl/RkInfoServiceImpl.java @@ -125,6 +125,14 @@ public class RkInfoServiceImpl implements IRkInfoService return list; } + @Override + public List selectGroupedByBill(RkInfoQueryDTO query) { + // 读取审核开关(1=开启;其它=关闭) + boolean needAudit = "1".equals(configService.selectConfigByKey("rk.audit.enabled")); + // 直接传给 Mapper;不改 DTO 结构,走多参数方式 + return rkInfoMapper.selectGroupedByBill(query, needAudit ? 1 : 0); + } + /** * 修改库存单据主 * diff --git a/src/main/java/com/zg/project/wisdom/service/impl/RkStatisticsServiceImpl.java b/src/main/java/com/zg/project/wisdom/service/impl/RkStatisticsServiceImpl.java index a15a6bc..f7a0cd2 100644 --- a/src/main/java/com/zg/project/wisdom/service/impl/RkStatisticsServiceImpl.java +++ b/src/main/java/com/zg/project/wisdom/service/impl/RkStatisticsServiceImpl.java @@ -388,4 +388,30 @@ public class RkStatisticsServiceImpl implements RkStatisticsService { Set inProjects = new HashSet<>(); Set outProjects = new HashSet<>(); } + + + @Override + public List listAvailableByWarehouse(String warehouseCode) { + List list = rkStatisticsMapper.selectAvailableByWarehouse(warehouseCode); + + if (list == null || list.isEmpty()) { + // 没有查到任何数据时,返回一个默认对象 + SceneAvailableVO vo = new SceneAvailableVO(); +// vo.setSceneCode("N/A"); // 默认场景编码 +// vo.setSceneName("无可用场景"); // 默认场景名称 + vo.setAvailableCount(0); // 可用数量为0 + vo.setPositions(new ArrayList<>()); // 空数组 + + list = new ArrayList<>(); + list.add(vo); + } else { + // 保证 positions 不为 null + for (SceneAvailableVO vo : list) { + if (vo.getPositions() == null) { + vo.setPositions(new ArrayList<>()); + } + } + } + return list; + } } diff --git a/src/main/resources/mybatis/wisdom/GysJhMapper.xml b/src/main/resources/mybatis/wisdom/GysJhMapper.xml index 0f35c8e..e8b23c8 100644 --- a/src/main/resources/mybatis/wisdom/GysJhMapper.xml +++ b/src/main/resources/mybatis/wisdom/GysJhMapper.xml @@ -56,9 +56,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and sap_no like concat('%', #{sapNo}, '%') - - and status = #{status} - + + and (status is null or trim(status) != '1') + and is_delete = #{isDelete} diff --git a/src/main/resources/mybatis/wisdom/RkInfoMapper.xml b/src/main/resources/mybatis/wisdom/RkInfoMapper.xml index 849101a..4289cf4 100644 --- a/src/main/resources/mybatis/wisdom/RkInfoMapper.xml +++ b/src/main/resources/mybatis/wisdom/RkInfoMapper.xml @@ -317,6 +317,244 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ORDER BY ri.rk_time DESC + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mybatis/wisdom/StockPhotoMapper.xml b/src/main/resources/mybatis/wisdom/StockPhotoMapper.xml new file mode 100644 index 0000000..fb16d15 --- /dev/null +++ b/src/main/resources/mybatis/wisdom/StockPhotoMapper.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + INSERT INTO stock_photo ( + bill_no, photo_type, file_name, url, + is_delete, create_by, create_time + ) VALUES ( + #{billNo}, #{photoType}, #{fileName}, #{url}, + '0', #{createBy}, NOW() + ) + + + + + INSERT INTO stock_photo ( + bill_no, photo_type, file_name, url, + is_delete, create_by, create_time + ) VALUES + + (#{it.billNo}, #{it.photoType}, #{it.fileName}, #{it.url}, + '0', #{it.createBy}, NOW()) + + + + + + + + UPDATE stock_photo + SET is_delete = '1', + update_time = NOW() + WHERE url IN + + #{u} + + + + +