diff --git a/klp-ui/src/api/wms/seal.js b/klp-ui/src/api/wms/seal.js new file mode 100644 index 00000000..f2aef6a9 --- /dev/null +++ b/klp-ui/src/api/wms/seal.js @@ -0,0 +1,97 @@ +import request from '@/utils/request' + +// 用印申请 +export function listSealReq(query) { + return request({ + url: '/wms/seal/list', + method: 'get', + params: query + }) +} + +export function getSealReq(bizId) { + return request({ + url: `/wms/seal/${bizId}`, + method: 'get' + }) +} + +export function addSealReq(data) { + return request({ + url: '/wms/seal', + method: 'post', + data + }) +} + +export function editSealReq(data) { + return request({ + url: '/wms/seal', + method: 'put', + data + }) +} + +export function delSealReq(bizIds) { + return request({ + url: `/wms/seal/${bizIds}`, + method: 'delete' + }) +} + +export function approveSealReq(bizId, approvalOpinion) { + return request({ + url: `/wms/seal/${bizId}/approve`, + method: 'post', + params: { approvalOpinion } + }) +} + +export function rejectSealReq(bizId, approvalOpinion) { + return request({ + url: `/wms/seal/${bizId}/reject`, + method: 'post', + params: { approvalOpinion } + }) +} + +export function cancelSealReq(bizId) { + return request({ + url: `/wms/seal/${bizId}/cancel`, + method: 'post' + }) +} + +export function stampSealJava(bizId, data) { + const payload = { + targetFileUrl: String(data.targetFileUrl || ''), + stampImageUrl: String(data.stampImageUrl || ''), + pageNo: Number(data.pageNo) || 1, + xPx: Number(data.xPx) || 0, + yPx: Number(data.yPx) || 0, + viewportWidth: data.viewportWidth !== undefined && data.viewportWidth !== null ? Number(data.viewportWidth) : undefined, + viewportHeight: data.viewportHeight !== undefined && data.viewportHeight !== null ? Number(data.viewportHeight) : undefined + } + if (data.widthPx !== undefined && data.widthPx !== null) { + payload.widthPx = Number(data.widthPx) + } + if (data.heightPx !== undefined && data.heightPx !== null) { + payload.heightPx = Number(data.heightPx) + } + if (payload.viewportWidth === undefined) delete payload.viewportWidth + if (payload.viewportHeight === undefined) delete payload.viewportHeight + + return request({ + url: `/wms/seal/${bizId}/stamp/java`, + method: 'post', + data: payload + }) +} + +export function stampSealPython(bizId, data) { + return request({ + url: `/wms/seal/${bizId}/stamp/python`, + method: 'post', + data + }) +} diff --git a/klp-ui/src/views/wms/seal/seal.vue b/klp-ui/src/views/wms/seal/seal.vue new file mode 100644 index 00000000..a64856ff --- /dev/null +++ b/klp-ui/src/views/wms/seal/seal.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/klp-ui/src/views/wms/seal/sealDetail.vue b/klp-ui/src/views/wms/seal/sealDetail.vue new file mode 100644 index 00000000..3ea12caf --- /dev/null +++ b/klp-ui/src/views/wms/seal/sealDetail.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/klp-wms/src/main/java/com/klp/config/StampProperties.java b/klp-wms/src/main/java/com/klp/config/StampProperties.java new file mode 100644 index 00000000..74fc79c2 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/config/StampProperties.java @@ -0,0 +1,40 @@ +package com.klp.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * Stamp service configuration. + */ +@Data +@Component +@ConfigurationProperties(prefix = "stamp") +public class StampProperties { + + private PythonService pythonService = new PythonService(); + private JavaService javaService = new JavaService(); + + @Data + public static class PythonService { + /** + * Whether to call external python stamp service. + */ + private boolean enabled = false; + private String baseUrl; + private String apiKey; + private Integer timeoutMs = 5000; + } + + @Data + public static class JavaService { + /** + * Enable in-Java stamping. + */ + private boolean enabled = true; + /** + * Default DPI for px → user-space conversion if needed. + */ + private Integer dpi = 72; + } +} diff --git a/klp-wms/src/main/java/com/klp/controller/WmsSealReqController.java b/klp-wms/src/main/java/com/klp/controller/WmsSealReqController.java new file mode 100644 index 00000000..34768930 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/controller/WmsSealReqController.java @@ -0,0 +1,98 @@ +package com.klp.controller; + +import com.klp.common.annotation.Log; +import com.klp.common.core.controller.BaseController; +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.domain.R; +import com.klp.common.core.page.TableDataInfo; +import com.klp.common.enums.BusinessType; +import com.klp.domain.bo.WmsSealReqBo; +import com.klp.domain.bo.WmsSealStampBo; +import com.klp.domain.vo.WmsSealReqVo; +import com.klp.service.IWmsSealReqService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 用印申请 + */ +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/wms/seal") +public class WmsSealReqController extends BaseController { + + private final IWmsSealReqService service; + + @GetMapping("/list") + public TableDataInfo list(WmsSealReqBo bo, PageQuery pageQuery) { + return service.queryPageList(bo, pageQuery); + } + + @GetMapping("/{bizId}") + public R getInfo(@PathVariable @NotNull Long bizId) { + return R.ok(service.queryById(bizId)); + } + + @Log(title = "用印申请", businessType = BusinessType.INSERT) + @PostMapping + public R add(@Validated @RequestBody WmsSealReqBo bo) { + return toAjax(service.insertByBo(bo)); + } + + @Log(title = "用印申请", businessType = BusinessType.UPDATE) + @PutMapping + public R edit(@Validated @RequestBody WmsSealReqBo bo) { + return toAjax(service.updateByBo(bo)); + } + + @Log(title = "用印申请", businessType = BusinessType.DELETE) + @DeleteMapping("/{bizIds}") + public R remove(@PathVariable @NotEmpty Long[] bizIds) { + return toAjax(service.deleteWithValidByIds(java.util.Arrays.asList(bizIds), true)); + } + + @Log(title = "用印申请", businessType = BusinessType.UPDATE) + @PostMapping("/{bizId}/approve") + public R approve(@PathVariable @NotNull Long bizId, + @RequestParam(required = false) String approvalOpinion) { + return toAjax(service.approveByDeptLeader(bizId, approvalOpinion)); + } + + @Log(title = "用印申请", businessType = BusinessType.UPDATE) + @PostMapping("/{bizId}/reject") + public R reject(@PathVariable @NotNull Long bizId, + @RequestParam(required = false) String approvalOpinion) { + return toAjax(service.rejectByDeptLeader(bizId, approvalOpinion)); + } + + @Log(title = "用印申请", businessType = BusinessType.UPDATE) + @PostMapping("/{bizId}/cancel") + public R cancel(@PathVariable @NotNull Long bizId) { + return toAjax(service.updateStatus(bizId, "canceled")); + } + + @Log(title = "用印盖章(Java)", businessType = BusinessType.UPDATE) + @PostMapping("/{bizId}/stamp/java") + public R stampJava(@PathVariable @NotNull Long bizId, @Validated @RequestBody WmsSealStampBo bo) { + log.info("收到盖章请求 - bizId: {}, yPx: {}, xPx: {}, pageNo: {}, yPx类型: {}", + bizId, bo.getYPx(), bo.getXPx(), bo.getPageNo(), + bo.getYPx() != null ? bo.getYPx().getClass().getName() : "null"); + if (bo.getYPx() == null) { + log.error("yPx 为 null!接收到的 bo 对象: {}", bo); + } + return R.ok(service.stampWithJava(bizId, bo)); + } + + @Log(title = "用印盖章(Python)", businessType = BusinessType.UPDATE) + @PostMapping("/{bizId}/stamp/python") + public R stampPython(@PathVariable @NotNull Long bizId, @Validated @RequestBody WmsSealStampBo bo) { + return R.ok(service.stampWithPython(bizId, bo)); + } +} diff --git a/klp-wms/src/main/java/com/klp/domain/WmsSealReq.java b/klp-wms/src/main/java/com/klp/domain/WmsSealReq.java new file mode 100644 index 00000000..bad1da26 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/WmsSealReq.java @@ -0,0 +1,55 @@ +package com.klp.domain; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import com.klp.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用印申请 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("hrm_seal_req") +public class WmsSealReq extends BaseEntity { + + /** 业务ID */ + @TableId + private Long bizId; + + /** 申请人ID */ + private Long empId; + + /** 申请部门ID */ + private Long deptId; + + /** 用印类型(公章/合同章/财务章等) */ + private String sealType; + + /** 用途说明 */ + private String purpose; + + /** 申请材料附件ID列表(CSV,对应sys_oss) */ + private String applyFileIds; + + /** 是否需要回执 1是0否 */ + private Integer receiptRequired; + + /** 回执状态 none/pending/done */ + private String receiptStatus; + + /** 回执附件ID列表(CSV,对应sys_oss或直接URL) */ + private String receiptFileIds; + + /** 状态 draft/running/approved/rejected/canceled */ + private String status; + + /** 备注 */ + private String remark; + + /** 删除标识 0正常 2删除 */ + @TableLogic + private Integer delFlag; +} diff --git a/klp-wms/src/main/java/com/klp/domain/bo/WmsSealReqBo.java b/klp-wms/src/main/java/com/klp/domain/bo/WmsSealReqBo.java new file mode 100644 index 00000000..383f83d4 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/bo/WmsSealReqBo.java @@ -0,0 +1,46 @@ +package com.klp.domain.bo; + +import com.klp.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 用印申请 Bo + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WmsSealReqBo extends BaseEntity { + + /** 业务ID(编辑/审批时必填) */ + private Long bizId; + + /** 申请人ID */ + @NotNull(message = "申请人不能为空") + private Long empId; + + /** 用印类型 */ + @NotBlank(message = "用印类型不能为空") + private String sealType; + + /** 用途说明 */ + private String purpose; + + /** 申请材料附件ID列表(CSV,对应sys_oss) */ + private String applyFileIds; + + /** 是否需要回执 1是0否 */ + private Integer receiptRequired; + + /** 备注 */ + private String remark; + + /** 申请部门ID */ + @NotNull(message = "申请部门不能为空") + private Long deptId; + + /** 状态 draft/running/approved/rejected/canceled */ + private String status; +} diff --git a/klp-wms/src/main/java/com/klp/domain/bo/WmsSealStampBo.java b/klp-wms/src/main/java/com/klp/domain/bo/WmsSealStampBo.java new file mode 100644 index 00000000..38c82f88 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/bo/WmsSealStampBo.java @@ -0,0 +1,98 @@ +package com.klp.domain.bo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 盖章命令(Java/Python 坐标统一使用 px) + */ +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.ALWAYS) +public class WmsSealStampBo { + + /** 待盖章 PDF 的 OSS 完整 URL */ + @NotBlank(message = "待盖章文件地址不能为空") + @JsonProperty("targetFileUrl") + private String targetFileUrl; + + /** 章图片 OSS 完整 URL(透明 PNG/JPG) */ + @NotBlank(message = "章图片地址不能为空") + @JsonProperty("stampImageUrl") + private String stampImageUrl; + + /** 页码(从1开始) */ + @NotNull + @Min(1) + @JsonProperty("pageNo") + private Integer pageNo; + + /** 左下角 X 坐标(px) */ + @NotNull + @Min(0) + @JsonProperty("xPx") + @JsonInclude(JsonInclude.Include.ALWAYS) + @Setter(lombok.AccessLevel.NONE) + private Integer xPx; + + /** 左下角 Y 坐标(px) */ + @NotNull + @Min(0) + @JsonProperty("yPx") + @JsonInclude(JsonInclude.Include.ALWAYS) + @Setter(lombok.AccessLevel.NONE) + private Integer yPx; + + /** 盖章宽度(px,可选) */ + @Min(1) + @JsonProperty("widthPx") + private Integer widthPx; + + /** 盖章高度(px,可选) */ + @Min(1) + @JsonProperty("heightPx") + private Integer heightPx; + + /** + * 前端渲染的 viewport 宽度(像素):用于把前端点击坐标换算成 PDFBox 坐标(pt)。 + * 注意:这不是 PDF 页面原始宽度,而是 pdf.js 按 scale 渲染到 canvas 的宽度。 + */ + @Min(1) + @JsonProperty("viewportWidth") + private Integer viewportWidth; + + /** 前端渲染的 viewport 高度(像素):用于坐标换算 */ + @Min(1) + @JsonProperty("viewportHeight") + private Integer viewportHeight; + + /** + * 手动添加 setter 方法,确保 Jackson 能够正确映射 yPx 字段 + * Lombok 生成的 setYPx() 可能与 Jackson 的字段名映射不匹配 + */ + @JsonSetter("yPx") + public void setYPx(Integer yPx) { + this.yPx = yPx; + } + + /** + * 手动添加 setter 方法,确保 Jackson 能够正确映射 xPx 字段 + */ + @JsonSetter("xPx") + public void setXPx(Integer xPx) { + this.xPx = xPx; + } +} diff --git a/klp-wms/src/main/java/com/klp/domain/vo/WmsSealReqVo.java b/klp-wms/src/main/java/com/klp/domain/vo/WmsSealReqVo.java new file mode 100644 index 00000000..d2decefe --- /dev/null +++ b/klp-wms/src/main/java/com/klp/domain/vo/WmsSealReqVo.java @@ -0,0 +1,50 @@ +package com.klp.domain.vo; + +import com.klp.common.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 用印申请 VO + */ +@Data +public class WmsSealReqVo implements Serializable { + private static final long serialVersionUID = 1L; + + @Excel(name = "业务ID") + private Long bizId; + + @Excel(name = "申请人ID") + private Long empId; + + @Excel(name = "用印类型") + private String sealType; + + @Excel(name = "用途说明") + private String purpose; + + @Excel(name = "申请材料附件ID列表") + private String applyFileIds; + + @Excel(name = "是否需要回执") + private Integer receiptRequired; + + @Excel(name = "回执状态") + private String receiptStatus; + + @Excel(name = "回执附件ID列表") + private String receiptFileIds; + + @Excel(name = "状态") + private String status; + + @Excel(name = "备注") + private String remark; + + private String createBy; + private Date createTime; + private String updateBy; + private Date updateTime; +} diff --git a/klp-wms/src/main/java/com/klp/mapper/WmsSealReqMapper.java b/klp-wms/src/main/java/com/klp/mapper/WmsSealReqMapper.java new file mode 100644 index 00000000..bbd74b61 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/mapper/WmsSealReqMapper.java @@ -0,0 +1,11 @@ +package com.klp.mapper; + +import com.klp.common.core.mapper.BaseMapperPlus; +import com.klp.domain.WmsSealReq; +import com.klp.domain.vo.WmsSealReqVo; + +/** + * 用印申请 Mapper + */ +public interface WmsSealReqMapper extends BaseMapperPlus { +} diff --git a/klp-wms/src/main/java/com/klp/service/IWmsSealReqService.java b/klp-wms/src/main/java/com/klp/service/IWmsSealReqService.java new file mode 100644 index 00000000..0e006690 --- /dev/null +++ b/klp-wms/src/main/java/com/klp/service/IWmsSealReqService.java @@ -0,0 +1,50 @@ +package com.klp.service; + +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.page.TableDataInfo; +import com.klp.domain.bo.WmsSealReqBo; +import com.klp.domain.bo.WmsSealStampBo; +import com.klp.domain.vo.WmsSealReqVo; + +import java.util.Collection; +import java.util.List; + +public interface IWmsSealReqService { + + WmsSealReqVo queryById(Long bizId); + + TableDataInfo queryPageList(WmsSealReqBo bo, PageQuery pageQuery); + + List queryList(WmsSealReqBo bo); + + Boolean insertByBo(WmsSealReqBo bo); + + Boolean updateByBo(WmsSealReqBo bo); + + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 部门负责人审批通过 + */ + Boolean approveByDeptLeader(Long bizId, String approvalOpinion); + + /** + * 部门负责人审批驳回 + */ + Boolean rejectByDeptLeader(Long bizId, String approvalOpinion); + + /** + * 简单状态更新(draft/running/approved/rejected/canceled) + */ + Boolean updateStatus(Long bizId, String status); + + /** + * Java 盖章,返回盖章后文件 URL + */ + String stampWithJava(Long bizId, WmsSealStampBo cmd); + + /** + * Python 盖章占位(调用外部服务) + */ + String stampWithPython(Long bizId, WmsSealStampBo cmd); +} diff --git a/klp-wms/src/main/java/com/klp/service/impl/WmsSealReqServiceImpl.java b/klp-wms/src/main/java/com/klp/service/impl/WmsSealReqServiceImpl.java new file mode 100644 index 00000000..20eded6f --- /dev/null +++ b/klp-wms/src/main/java/com/klp/service/impl/WmsSealReqServiceImpl.java @@ -0,0 +1,363 @@ +package com.klp.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.io.IoUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.domain.entity.SysUser; +import com.klp.common.core.page.TableDataInfo; +import com.klp.common.exception.ServiceException; +import com.klp.config.StampProperties; +import com.klp.domain.WmsApproval; +import com.klp.domain.WmsApprovalTask; +import com.klp.domain.WmsDept; +import com.klp.domain.WmsSealReq; +import com.klp.domain.bo.WmsSealReqBo; +import com.klp.domain.bo.WmsSealStampBo; +import com.klp.domain.vo.WmsSealReqVo; +import com.klp.mapper.WmsApprovalMapper; +import com.klp.mapper.WmsApprovalTaskMapper; +import com.klp.mapper.WmsDeptMapper; +import com.klp.mapper.WmsSealReqMapper; +import com.klp.oss.core.OssClient; +import com.klp.oss.entity.UploadResult; +import com.klp.oss.factory.OssFactory; +import com.klp.service.IWmsSealReqService; +import com.klp.system.mapper.SysUserMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * 用印申请 服务实现 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class WmsSealReqServiceImpl implements IWmsSealReqService { + + private final WmsSealReqMapper baseMapper; + private final WmsDeptMapper wmsDeptMapper; + private final SysUserMapper sysUserMapper; + private final WmsApprovalMapper approvalMapper; + private final WmsApprovalTaskMapper approvalTaskMapper; + private final StampProperties stampProperties; + + @Override + public WmsSealReqVo queryById(Long bizId) { + return baseMapper.selectVoById(bizId); + } + + @Override + public TableDataInfo queryPageList(WmsSealReqBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + @Override + public List queryList(WmsSealReqBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean insertByBo(WmsSealReqBo bo) { + WmsSealReq add = BeanUtil.toBean(bo, WmsSealReq.class); + add.setStatus(defaultStatus(add.getStatus())); + validEntityBeforeSave(add); + boolean ok = baseMapper.insert(add) > 0; + if (ok) { + createDeptLeaderApproval(add, bo.getDeptId()); + } + return ok; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateByBo(WmsSealReqBo bo) { + if (bo.getBizId() == null) { + throw new ServiceException("bizId不能为空"); + } + WmsSealReq update = BeanUtil.toBean(bo, WmsSealReq.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 可添加业务校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean approveByDeptLeader(Long bizId, String approvalOpinion) { + WmsSealReq req = baseMapper.selectById(bizId); + if (req == null) { + throw new ServiceException("用印申请不存在"); + } + WmsApproval approval = getApprovalByApplyId(bizId); + if (approval == null) { + throw new ServiceException("审批记录不存在"); + } + WmsApprovalTask task = getPendingTaskForCurrentLeader(approval.getApprovalId(), req.getDeptId()); + if (task == null) { + throw new ServiceException("当前用户不是该部门负责人或审批任务不存在"); + } + + task.setTaskStatus("approved"); + task.setApprovalOpinion(approvalOpinion); + task.setApprovalTime(new Date()); + approvalTaskMapper.updateById(task); + + approval.setApprovalStatus("已同意"); + approval.setFinalStatus("all_approved"); + approval.setApprovalOpinion(approvalOpinion); + approval.setApprovalTime(new Date()); + approvalMapper.updateById(approval); + + updateStatus(bizId, "approved"); + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean rejectByDeptLeader(Long bizId, String approvalOpinion) { + WmsSealReq req = baseMapper.selectById(bizId); + if (req == null) { + throw new ServiceException("用印申请不存在"); + } + WmsApproval approval = getApprovalByApplyId(bizId); + if (approval == null) { + throw new ServiceException("审批记录不存在"); + } + WmsApprovalTask task = getPendingTaskForCurrentLeader(approval.getApprovalId(), req.getDeptId()); + if (task == null) { + throw new ServiceException("当前用户不是该部门负责人或审批任务不存在"); + } + + task.setTaskStatus("rejected"); + task.setApprovalOpinion(approvalOpinion); + task.setApprovalTime(new Date()); + approvalTaskMapper.updateById(task); + + approval.setApprovalStatus("已驳回"); + approval.setFinalStatus("rejected"); + approval.setApprovalOpinion(approvalOpinion); + approval.setApprovalTime(new Date()); + approvalMapper.updateById(approval); + + updateStatus(bizId, "rejected"); + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean updateStatus(Long bizId, String status) { + WmsSealReq req = new WmsSealReq(); + req.setBizId(bizId); + req.setStatus(status); + return baseMapper.updateById(req) > 0; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String stampWithJava(Long bizId, WmsSealStampBo cmd) { + if (!Boolean.TRUE.equals(stampProperties.getJavaService().isEnabled())) { + throw new ServiceException("Java盖章未启用"); + } + String resultUrl = doPdfStamp(cmd); + WmsSealReq update = new WmsSealReq(); + update.setBizId(bizId); + update.setReceiptStatus("done"); + update.setReceiptFileIds(resultUrl); + baseMapper.updateById(update); + return resultUrl; + } + + @Override + public String stampWithPython(Long bizId, WmsSealStampBo cmd) { + if (!Boolean.TRUE.equals(stampProperties.getPythonService().isEnabled())) { + throw new ServiceException("Python盖章未启用"); + } + throw new ServiceException("Python盖章接口未实现"); + } + + private LambdaQueryWrapper buildQueryWrapper(WmsSealReqBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getBizId() != null, WmsSealReq::getBizId, bo.getBizId()); + lqw.eq(bo.getEmpId() != null, WmsSealReq::getEmpId, bo.getEmpId()); + lqw.eq(bo.getSealType() != null, WmsSealReq::getSealType, bo.getSealType()); + lqw.eq(bo.getStatus() != null, WmsSealReq::getStatus, bo.getStatus()); + lqw.orderByDesc(WmsSealReq::getCreateTime); + return lqw; + } + + private void validEntityBeforeSave(WmsSealReq entity) { + // 预留数据校验,如唯一约束 + } + + private String defaultStatus(String status) { + return status == null ? "draft" : status; + } + + private void createDeptLeaderApproval(WmsSealReq req, Long deptId) { + if (deptId == null) { + return; + } + WmsDept dept = wmsDeptMapper.selectById(deptId); + if (dept == null || dept.getLeader() == null) { + return; + } + SysUser leader = sysUserMapper.selectById(dept.getLeader()); + if (leader == null) { + return; + } + + WmsApproval approval = new WmsApproval(); + approval.setApplyType("seal"); + approval.setApplyId(req.getBizId()); + approval.setApproverName(leader.getNickName()); + approval.setApprovalStatus("待审批"); + approval.setApprovalType("single"); + approval.setRequiredApprovers(1); + approval.setCurrentApprovers(0); + approval.setFinalStatus("pending"); + approval.setCreateBy(req.getCreateBy()); + approval.setCreateTime(req.getCreateTime()); + approvalMapper.insert(approval); + + WmsApprovalTask task = new WmsApprovalTask(); + task.setApprovalId(approval.getApprovalId()); + task.setApproverId(leader.getUserId()); + task.setApproverName(leader.getNickName()); + task.setTaskStatus("pending"); + task.setCreateBy(req.getCreateBy()); + task.setCreateTime(req.getCreateTime()); + approvalTaskMapper.insert(task); + + updateStatus(req.getBizId(), "running"); + } + + private WmsApproval getApprovalByApplyId(Long applyId) { + return approvalMapper.selectOne(Wrappers.lambdaQuery() + .eq(WmsApproval::getApplyType, "seal") + .eq(WmsApproval::getApplyId, applyId) + .eq(WmsApproval::getDelFlag, 0)); + } + + private WmsApprovalTask getPendingTaskForCurrentLeader(Long approvalId, Long deptId) { + if (approvalId == null || deptId == null) { + return null; + } + WmsDept dept = wmsDeptMapper.selectById(deptId); + if (dept == null || dept.getLeader() == null) { + return null; + } + Long leaderId = dept.getLeader(); + return approvalTaskMapper.selectOne(Wrappers.lambdaQuery() + .eq(WmsApprovalTask::getApprovalId, approvalId) + .eq(WmsApprovalTask::getApproverId, leaderId) + .eq(WmsApprovalTask::getTaskStatus, "pending") + .eq(WmsApprovalTask::getDelFlag, 0)); + } + + private String doPdfStamp(WmsSealStampBo cmd) { + try (InputStream pdfIn = getObject(cmd.getTargetFileUrl()); + InputStream imgIn = getObject(cmd.getStampImageUrl()); + PDDocument document = PDDocument.load(pdfIn); + ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + + int pageIndex = cmd.getPageNo() - 1; + if (pageIndex < 0 || pageIndex >= document.getNumberOfPages()) { + throw new ServiceException("页码超出范围"); + } + PDPage page = document.getPage(pageIndex); + PDRectangle mediaBox = page.getMediaBox(); + + byte[] imgBytes = IoUtil.readBytes(imgIn); + PDImageXObject image = PDImageXObject.createFromByteArray(document, imgBytes, "stamp"); + + float stampW = image.getWidth(); + float stampH = image.getHeight(); + + float pdfW = mediaBox.getWidth(); + float pdfH = mediaBox.getHeight(); + + float x; + float y; + if (cmd.getViewportWidth() != null && cmd.getViewportHeight() != null + && cmd.getViewportWidth() > 0 && cmd.getViewportHeight() > 0) { + float ratioX = pdfW / cmd.getViewportWidth(); + float ratioY = pdfH / cmd.getViewportHeight(); + x = cmd.getXPx() * ratioX; + y = cmd.getYPx() * ratioY; + } else { + x = cmd.getXPx(); + y = cmd.getYPx(); + } + + final float maxCm = 4.0f; + final float maxPt = maxCm * 72.0f / 2.54f; + float scale = Math.min(maxPt / stampW, maxPt / stampH); + scale = Math.min(scale, 1.0f); + + float width = stampW * scale; + float height = stampH * scale; + + log.info("[stamp] pdfW={},pdfH={}, viewportW={},viewportH={}, ratioX={},ratioY={}, xPx={},yPx={}, x={},y={}, stampW={},stampH={}, drawW={},drawH={}", + pdfW, pdfH, + cmd.getViewportWidth(), cmd.getViewportHeight(), + (cmd.getViewportWidth() != null && cmd.getViewportWidth() > 0) ? (pdfW / cmd.getViewportWidth()) : null, + (cmd.getViewportHeight() != null && cmd.getViewportHeight() > 0) ? (pdfH / cmd.getViewportHeight()) : null, + cmd.getXPx(), cmd.getYPx(), + x, y, + stampW, stampH, + width, height); + + if (x + width > mediaBox.getWidth()) { + x = Math.max(0, mediaBox.getWidth() - width); + } + if (y + height > mediaBox.getHeight()) { + y = Math.max(0, mediaBox.getHeight() - height); + } + + try (PDPageContentStream contentStream = new PDPageContentStream(document, page, + PDPageContentStream.AppendMode.APPEND, true, true)) { + contentStream.drawImage(image, x, y, width, height); + } + + document.save(bos); + + OssClient storage = OssFactory.instance(); + UploadResult uploadResult = storage.uploadSuffix(bos.toByteArray(), ".pdf", "application/pdf"); + return uploadResult.getUrl(); + } catch (Exception e) { + log.error("PDF盖章失败", e); + throw new ServiceException("PDF盖章失败: " + e.getMessage()); + } + } + + private InputStream getObject(String url) { + OssClient storage = OssFactory.instance(); + return storage.getObjectContent(url); + } +} diff --git a/klp-wms/src/main/resources/mapper/klp/WmsSealReqMapper.xml b/klp-wms/src/main/resources/mapper/klp/WmsSealReqMapper.xml new file mode 100644 index 00000000..1bb363db --- /dev/null +++ b/klp-wms/src/main/resources/mapper/klp/WmsSealReqMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + +