出库图片模块逻辑修改

新增打印标签功能,向pk数据库插入数据
This commit is contained in:
2025-09-19 11:23:51 +08:00
parent 2e32af8fbe
commit d390060597
11 changed files with 196 additions and 82 deletions

View File

@@ -13,18 +13,17 @@ import java.util.*;
/** /**
* 本地图片保存工具 * 本地图片保存工具
* - 根目录photo.root默认 D:\photo * - 根目录photo.root默认 D:\photo
* - 目录结构:{subDir}/yyyy-MM-dd/ * - 目录结构:{documentType}/yyyy-MM-dd/
* - 文件命名:{billNo}_{sapNo}_{xmMs}_{yyyyMMddHHmmssSSS}_{index}.{ext} * - 文件命名:
* - 返回相对web路径用于拼接 /photo/ 前缀生成可访问URL与文件名 * 入库billNo_gysMc_yyyyMMddHHmmssSSS_index.ext
* 出库billNo_xmMs_yyyyMMddHHmmssSSS_index.ext
*/ */
@Component @Component
public class LocalPhotoUtil { public class LocalPhotoUtil {
/** 本地根目录,可在 yml 配置photo.root: D:\photo */
@Value("${photo.root:D:\\\\photo}") @Value("${photo.root:D:\\\\photo}")
private String rootPath; private String rootPath;
/** 非法文件名字符Windows */
private static final String ILLEGAL_CHARS = "\\/:*?\"<>|"; private static final String ILLEGAL_CHARS = "\\/:*?\"<>|";
/** yyyy-MM-dd目录 */ /** yyyy-MM-dd目录 */
@@ -38,22 +37,28 @@ public class LocalPhotoUtil {
} }
/** /**
* 组装基础文件名billNo_sapNo_xmMs不含后缀、不含时间戳/序号) * 生成基础名:
* - 入库(photoType=0)billNo_gysMc
* - 出库(photoType=1)billNo_xmMs
* - 若为空 → 一律替换成 "无"
*/ */
public String buildBaseName(String billNo, String xmMs) { public String buildBaseName(String photoType, String billNo, String xmMs, String gysMc) {
String b = defaultString(billNo); String safeBillNo = sanitize(defaultString(billNo));
String x = defaultString(xmMs); String part;
String joined = String.join("_", b, x); if ("0".equals(photoType)) {
return sanitize(joined); part = sanitize(defaultString(gysMc, ""));
} else {
part = sanitize(defaultString(xmMs, ""));
}
return sanitize(safeBillNo + "_" + part);
} }
/** /**
* 保存单个文件 * 保存单个文件
* @param file 文件 * @param file 文件
* @param subDir 一级子目录(建议 "photo" 固定,也可按业务传入 * @param subDir 一级子目录(documentType
* @param baseName 基础名(不含扩展名;可用 buildBaseName 生成) * @param baseName 基础名
* @param index 序号从1开始用于同批次区分 * @param index 序号
* @return SaveResult(webPath, fileName)
*/ */
public SaveResult saveOne(MultipartFile file, String subDir, String baseName, int index) throws Exception { public SaveResult saveOne(MultipartFile file, String subDir, String baseName, int index) throws Exception {
String ext = resolveExt(file); String ext = resolveExt(file);
@@ -62,7 +67,6 @@ public class LocalPhotoUtil {
String stamp = nowStamp(); String stamp = nowStamp();
String safeBase = sanitize(baseName); String safeBase = sanitize(baseName);
// 最终文件名base_时间戳_序号.ext
String fileName = safeBase + "_" + stamp + "_" + index + ext; String fileName = safeBase + "_" + stamp + "_" + index + ext;
File dir = new File(rootPath, dirPart); File dir = new File(rootPath, dirPart);
@@ -76,16 +80,13 @@ public class LocalPhotoUtil {
} }
SaveResult r = new SaveResult(); SaveResult r = new SaveResult();
r.setWebPath((dirPart + fileName).replace("\\", "/")); // e.g. photo/2025-09-01/xxx.jpg r.setWebPath((dirPart + fileName).replace("\\", "/"));
r.setFileName(fileName); r.setFileName(fileName);
return r; return r;
} }
/** /**
* 批量保存(顺序与入参 files 一致) * 批量保存
* @param files 文件列表
* @param subDir 一级子目录(建议传 "photo"
* @param baseName 基础名(不含扩展名)
*/ */
public List<SaveResult> saveBatch(List<MultipartFile> files, String subDir, String baseName) throws Exception { public List<SaveResult> saveBatch(List<MultipartFile> files, String subDir, String baseName) throws Exception {
if (files == null || files.isEmpty()) { if (files == null || files.isEmpty()) {
@@ -98,27 +99,14 @@ public class LocalPhotoUtil {
return list; return list;
} }
/** /** 解析扩展名 */
* 便捷方法:直接传业务字段(内部会调用 buildBaseName
* @param files 多文件
* @param subDir 一级子目录(建议 "photo"
* @param billNo 单据号
* @param xmMs 项目描述
*/
public List<SaveResult> saveBatchByBiz(List<MultipartFile> 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) { private String resolveExt(MultipartFile file) {
String origin = file.getOriginalFilename(); String origin = file.getOriginalFilename();
if (origin != null) { if (origin != null) {
int dot = origin.lastIndexOf('.'); int dot = origin.lastIndexOf('.');
if (dot >= 0) { if (dot >= 0) {
String ext = origin.substring(dot); String ext = origin.substring(dot);
if (ext.length() <= 10) { // 简单防御:避免异常超长扩展名 if (ext.length() <= 10) {
return ext; return ext;
} }
} }
@@ -129,7 +117,7 @@ public class LocalPhotoUtil {
return ".bin"; return ".bin";
} }
/** 清洗文件名去空白、替换非法字符、规避Windows保留名、限长 */ /** 清洗文件名 */
public String sanitize(String name) { public String sanitize(String name) {
if (name == null) return "unnamed"; if (name == null) return "unnamed";
String t = name.trim().replaceAll("\\s+", " "); String t = name.trim().replaceAll("\\s+", " ");
@@ -143,14 +131,12 @@ public class LocalPhotoUtil {
if (upper.matches("CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]")) { if (upper.matches("CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]")) {
out = "_" + out; out = "_" + out;
} }
// 限长,给时间戳与序号留空间
if (out.length() > 100) { if (out.length() > 100) {
out = out.substring(0, 100); out = out.substring(0, 100);
} }
return out; return out;
} }
/** null 转空串;带默认值重载 */
private String defaultString(String s) { private String defaultString(String s) {
return s == null ? "" : s; return s == null ? "" : s;
} }
@@ -160,11 +146,8 @@ public class LocalPhotoUtil {
/** 返回值对象 */ /** 返回值对象 */
public static class SaveResult { public static class SaveResult {
/** 相对web路径例如 photo/2025-09-01/xxx.jpg用于拼接 /photo/ 前缀生成URL */
private String webPath; private String webPath;
/** 最终文件名(含扩展名) */
private String fileName; private String fileName;
public String getWebPath() { return webPath; } public String getWebPath() { return webPath; }
public void setWebPath(String webPath) { this.webPath = webPath; } public void setWebPath(String webPath) { this.webPath = webPath; }
public String getFileName() { return fileName; } public String getFileName() { return fileName; }

View File

@@ -33,15 +33,19 @@ public class PhotoController {
* - photoType0(入库相关) / 1(出库相关) * - photoType0(入库相关) / 1(出库相关)
* - billNo单据号 * - billNo单据号
* - xmMs项目描述 * - xmMs项目描述
* - sapNo订单号 * - documentType单据类型建文件夹用不入库
* - gysMc供应商名称入库时命名用
* 返回:每张图片的可访问 URL 列表 * 返回:每张图片的可访问 URL 列表
*/ */
@PostMapping(value = "/upload/batch", consumes = "multipart/form-data") @PostMapping(value = "/upload/batch", consumes = "multipart/form-data")
public AjaxResult uploadBatch(@RequestPart("files") List<MultipartFile> files, public AjaxResult uploadBatch(@RequestPart("files") List<MultipartFile> files,
@RequestParam("photoType") String photoType, @RequestParam("photoType") String photoType,
@RequestParam("billNo") String billNo, @RequestParam("billNo") String billNo,
@RequestParam("xmMs") String xmMs) { @RequestParam(value = "xmMs", required = false, defaultValue = "") String xmMs,
List<String> urls = photoService.uploadAndSave(files, photoType, billNo, xmMs); @RequestParam(value = "documentType", required = false, defaultValue = "default") String documentType,
@RequestParam(value = "gysMc", required = false, defaultValue = "") String gysMc) {
// 有新参数就走新方法;没有则兼容旧逻辑
List<String> urls = photoService.uploadAndSave(files, photoType, billNo, xmMs, documentType, gysMc);
return AjaxResult.success("上传成功", urls); return AjaxResult.success("上传成功", urls);
} }
@@ -50,14 +54,14 @@ public class PhotoController {
* 与多图参数一致,只是文件字段为 file * 与多图参数一致,只是文件字段为 file
* 返回:单张图片的可访问 URL * 返回:单张图片的可访问 URL
*/ */
@PostMapping(value = "/upload", consumes = "multipart/form-data") // @PostMapping(value = "/upload", consumes = "multipart/form-data")
public AjaxResult upload(@RequestPart("file") MultipartFile file, // public AjaxResult upload(@RequestPart("file") MultipartFile file,
@RequestParam("photoType") String photoType, // @RequestParam("photoType") String photoType,
@RequestParam("billNo") String billNo, // @RequestParam("billNo") String billNo,
@RequestParam("xmMs") String xmMs) { // @RequestParam("xmMs") String xmMs) {
List<String> urls = photoService.uploadAndSave(Collections.singletonList(file), photoType, billNo, xmMs); // List<String> urls = photoService.uploadAndSave(Collections.singletonList(file), photoType, billNo, xmMs);
return AjaxResult.success("上传成功", urls.isEmpty() ? null : urls.get(0)); // return AjaxResult.success("上传成功", urls.isEmpty() ? null : urls.get(0));
} // }
/** /**
* 【查询:按 billNo可选photoType取照片URL列表】 * 【查询:按 billNo可选photoType取照片URL列表】

View File

@@ -0,0 +1,27 @@
package com.zg.project.wisdom.controller;
import com.zg.framework.web.domain.AjaxResult;
import com.zg.project.wisdom.domain.dto.PkDatDTO;
import com.zg.project.wisdom.service.IPkDatService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/pk/dat")
public class PkDatController {
@Resource
private IPkDatService pkDatService;
/**
* 批量新增 pk.dat 数据
*/
@PostMapping("/batchAdd")
public AjaxResult batchAdd(@RequestBody List<PkDatDTO> list) {
int rows = pkDatService.batchInsertPkDat(list);
return rows > 0 ? AjaxResult.success("成功插入 " + rows + " 条记录")
: AjaxResult.error("插入失败");
}
}

View File

@@ -0,0 +1,26 @@
package com.zg.project.wisdom.domain.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 前端传参 DTO
*/
@Data
public class PkDatDTO {
/** 物料号 */
private String wlNo;
/** 数量 */
private BigDecimal realQty;
/** 物料凭证SAP单号 */
private String sapNo;
/** 供应商 */
private String gysMc;
/** 备注 */
private String remark;
/** 项目描述 */
private String xmMs;
/** 库位 */
private String pcode;
}

View File

@@ -0,0 +1,11 @@
package com.zg.project.wisdom.mapper;
import com.zg.project.wisdom.domain.dto.PkDatDTO;
import java.util.List;
public interface PkDatMapper {
int batchInsertPkDat(List<PkDatDTO> list);
}

View File

@@ -0,0 +1,14 @@
package com.zg.project.wisdom.service;
import com.zg.project.wisdom.domain.dto.PkDatDTO;
import java.util.List;
public interface IPkDatService {
/**
* 批量插入 pk.dat 数据
* @param list DTO 列表
* @return 插入行数
*/
int batchInsertPkDat(List<PkDatDTO> list);
}

View File

@@ -17,13 +17,17 @@ public interface PhotoService {
* @param files 图片文件列表(必传) * @param files 图片文件列表(必传)
* @param photoType 照片业务类型0=入库相关1=出库相关(必填) * @param photoType 照片业务类型0=入库相关1=出库相关(必填)
* @param billNo 单据号(必填,用于关联) * @param billNo 单据号(必填,用于关联)
* @param xmMs 项目描述(用于文件命名,可为空) * @param xmMs 项目描述(出库时命名,可为空)
* @param documentType 单据类型(用于目录结构,不入库表)
* @param gysMc 供应商名称(入库时命名用)
* @return URLs 列表(每个元素都是可直接 <img src> 的地址) * @return URLs 列表(每个元素都是可直接 <img src> 的地址)
*/ */
List<String> uploadAndSave(List<MultipartFile> files, List<String> uploadAndSave(List<MultipartFile> files,
String photoType, String billNo, String photoType,
String xmMs); String billNo,
String xmMs,
String documentType,
String gysMc);
/** /**
* 按 billNo可选 photoType查询照片 URL 列表 * 按 billNo可选 photoType查询照片 URL 列表

View File

@@ -38,10 +38,13 @@ public class PhotoServiceImpl implements PhotoService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public List<String> uploadAndSave(List<MultipartFile> files, public List<String> uploadAndSave(List<MultipartFile> files,
String photoType, String billNo, String photoType,
String xmMs) { String billNo,
String xmMs,
String documentType,
String gysMc) {
// ========= 1. 入参校验 ========= // 1. 基础校验
if (files == null || files.isEmpty()) { if (files == null || files.isEmpty()) {
throw new IllegalArgumentException("请选择要上传的图片"); throw new IllegalArgumentException("请选择要上传的图片");
} }
@@ -51,48 +54,46 @@ public class PhotoServiceImpl implements PhotoService {
if (StringUtils.isBlank(billNo)) { if (StringUtils.isBlank(billNo)) {
throw new IllegalArgumentException("billNo 不能为空"); throw new IllegalArgumentException("billNo 不能为空");
} }
if (StringUtils.isBlank(documentType)) {
documentType = "default"; // 硬编码兜底
}
// 组装基础文件名billNo_sapNo_xmMs内部会做非法字符清洗 // 2. xmMs / gysMc 兜底处理
final String baseName = localPhotoUtil.buildBaseName( String safeXmMs = StringUtils.isBlank(xmMs) ? "" : xmMs;
StringUtils.defaultString(billNo), String safeGysMc = StringUtils.isBlank(gysMc) ? "" : gysMc;
StringUtils.defaultString(xmMs));
// 3. 基础名(入库用供应商,出库用项目)
final String baseName = localPhotoUtil.buildBaseName(photoType, billNo, safeXmMs, safeGysMc);
try { try {
// ========= 2. 本地落盘 ========= // 4. 保存文件
// 目录D:\photo\photo\yyyy-MM-dd\ List<LocalPhotoUtil.SaveResult> saved = localPhotoUtil.saveBatch(files, documentType, baseName);
List<SaveResult> saved = localPhotoUtil.saveBatch(files, "", baseName);
// ========= 3. 生成URL + 组装入库对象 ========= // 5. 拼 URL + 入库对象
List<String> urls = new ArrayList<String>(saved.size()); List<String> urls = new ArrayList<>(saved.size());
List<StockPhoto> rows = new ArrayList<StockPhoto>(saved.size()); List<StockPhoto> rows = new ArrayList<>(saved.size());
for (SaveResult s : saved) { for (LocalPhotoUtil.SaveResult s : saved) {
// 根据当前上下文拼 URL例如http://host:port/photo/photo/2025-09-01/xxx.jpg
String url = ServletUriComponentsBuilder.fromCurrentContextPath() String url = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/photo/") // 与 StaticResourceConfig.addResourceHandlers 保持一致 .path("/photo/")
.path(s.getWebPath()) // 形如photo/2025-09-01/xxx.jpg .path(s.getWebPath())
.toUriString(); .toUriString();
urls.add(url); urls.add(url);
// 只存一个URL到表中其余信息按你最简诉求不存
StockPhoto p = new StockPhoto(); StockPhoto p = new StockPhoto();
p.setBillNo(billNo); p.setBillNo(billNo);
p.setPhotoType(photoType); p.setPhotoType(photoType);
p.setFileName(s.getFileName()); p.setFileName(s.getFileName());
p.setUrl(url); p.setUrl(url);
// 如需记录创建人p.setCreateBy(SecurityUtils.getUserId().toString());
rows.add(p); rows.add(p);
} }
// ========= 4. 批量入库 =========
if (!rows.isEmpty()) { if (!rows.isEmpty()) {
stockPhotoMapper.batchInsert(rows); stockPhotoMapper.batchInsert(rows);
} }
return urls; return urls;
} catch (Exception e) { } catch (Exception e) {
// Service 层抛出 RuntimeException触发 @Transactional 回滚数据库写入
// (注意:文件已落盘无法自动回滚,必要时可加失败清理逻辑)
throw new RuntimeException("上传/保存失败:" + e.getMessage(), e); throw new RuntimeException("上传/保存失败:" + e.getMessage(), e);
} }
} }

View File

@@ -0,0 +1,24 @@
package com.zg.project.wisdom.service.impl;
import com.zg.project.wisdom.domain.dto.PkDatDTO;
import com.zg.project.wisdom.mapper.PkDatMapper;
import com.zg.project.wisdom.service.IPkDatService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class PkDatServiceImpl implements IPkDatService {
@Resource
private PkDatMapper pkDatMapper;
@Override
public int batchInsertPkDat(List<PkDatDTO> list) {
if (list == null || list.isEmpty()) {
return 0;
}
return pkDatMapper.batchInsertPkDat(list);
}
}

View File

@@ -0,0 +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="com.zg.project.wisdom.mapper.PkDatMapper">
<insert id="batchInsertPkDat" parameterType="java.util.List">
INSERT INTO pk.dat
(wlh, num, ste, lnum, prf, gys, mrk, des_pro, inf, printer)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.wlNo}, #{item.realQty}, 1, 1, #{item.sapNo},
#{item.gysMc}, #{item.remark}, #{item.xmMs}, #{item.pcode}, 1)
</foreach>
</insert>
</mapper>

View File

@@ -340,6 +340,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
a.xm_ms, a.xm_ms,
a.xm_no_ck, a.xm_no_ck,
a.xm_ms_ck, a.xm_ms_ck,
a.gys_mc,
a.wl_no, a.wl_no,
a.wl_ms, a.wl_ms,
a.gys_no, a.gys_no,
@@ -365,6 +366,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
MIN(t.xm_ms) AS xm_ms, MIN(t.xm_ms) AS xm_ms,
MIN(t.xm_no_ck) AS xm_no_ck, MIN(t.xm_no_ck) AS xm_no_ck,
MIN(t.xm_ms_ck) AS xm_ms_ck, MIN(t.xm_ms_ck) AS xm_ms_ck,
MIN(t.gys_mc) AS gys_mc,
MIN(t.wl_no) AS wl_no, MIN(t.wl_no) AS wl_no,
MIN(t.wl_ms) AS wl_ms, MIN(t.wl_ms) AS wl_ms,
MIN(t.gys_no) AS gys_no, MIN(t.gys_no) AS gys_no,