feat(ai-report): 新增委托单位管理和报告审批功能

- 新增委托单位管理模块,支持增删改查、导入导出功能
- 实现委托单位 Excel 模板下载和数据导入功能
- 添加报告审批抽象处理器,支持审批流程日志记录
- 统一文件存储路径配置,将各模块独立路径改为统一根目录
- 新增 PQDIF 文件名存储字段,完善文件管理功能
- 修复代码中的乱码问题和错误提示信息
- 优化系统常量定义和字典配置管理
This commit is contained in:
2026-07-01 13:15:57 +08:00
parent 0ea686d3cb
commit c1ad7feec2
99 changed files with 6511 additions and 24 deletions

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>report-approval</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>report-model</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>test-report</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,41 @@
package com.njcn.gather.aireport.reportapproval.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class ReportApprovalUserNameResolver {
private final ReportApprovalMapper reportApprovalMapper;
public Map<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ReportModelUserNameVO> users = reportApprovalMapper.selectUserNamesByIds(validIds);
if (users == null || users.isEmpty()) {
return Collections.emptyMap();
}
return users.stream()
.filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName()))
.collect(Collectors.toMap(ReportModelUserNameVO::getId, ReportModelUserNameVO::getName, (first, second) -> first));
}
}

View File

@@ -0,0 +1,71 @@
package com.njcn.gather.aireport.reportapproval.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import com.njcn.gather.aireport.reportapproval.service.ReportApprovalService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
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.RestController;
@Api(tags = "报告审批管理")
@RestController
@RequestMapping("/api/report-approval")
@RequiredArgsConstructor
public class ReportApprovalController extends BaseController {
private final ReportApprovalService reportApprovalService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询待审报告分页")
@GetMapping("/pending-page")
public HttpResult<Page<ReportApprovalPendingVO>> pendingPage(@ModelAttribute ReportApprovalParam.PendingQueryParam param) {
String methodDescribe = getMethodDescribe("pendingPage");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportApprovalService.pendingPage(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("审批通过")
@PostMapping("/pass")
public HttpResult<Boolean> pass(@RequestBody @Validated ReportApprovalParam.AuditParam param) {
String methodDescribe = getMethodDescribe("pass");
boolean result = reportApprovalService.pass(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("审批退回")
@PostMapping("/reject")
public HttpResult<Boolean> reject(@RequestBody @Validated ReportApprovalParam.AuditParam param) {
String methodDescribe = getMethodDescribe("reject");
boolean result = reportApprovalService.reject(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询审批日志分页")
@GetMapping("/log-page")
public HttpResult<Page<ReportApprovalLogVO>> logPage(@ModelAttribute ReportApprovalParam.LogQueryParam param) {
String methodDescribe = getMethodDescribe("logPage");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportApprovalService.logPage(param), methodDescribe);
}
}

View File

@@ -0,0 +1,85 @@
package com.njcn.gather.aireport.reportapproval.handler;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.po.ReportApprovalLogPO;
import com.njcn.web.utils.RequestUtil;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
public abstract class AbstractReportApprovalHandler implements ReportApprovalHandler {
private final ReportApprovalMapper reportApprovalMapper;
private final ReportApprovalUserNameResolver reportApprovalUserNameResolver;
protected AbstractReportApprovalHandler(ReportApprovalMapper reportApprovalMapper,
ReportApprovalUserNameResolver reportApprovalUserNameResolver) {
this.reportApprovalMapper = reportApprovalMapper;
this.reportApprovalUserNameResolver = reportApprovalUserNameResolver;
}
@Override
public boolean audit(ReportApprovalParam.AuditParam param, boolean passed) {
if (param == null || StrUtil.isBlank(param.getReportId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "审批参数不能为空");
}
String reason = trimToNull(param.getApprovalReason());
if (reason == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "审批原因不能为空");
}
return doAudit(param.getReportId(), reason, passed);
}
protected abstract boolean doAudit(String reportId, String reason, boolean passed);
protected void saveLog(String reportId, String reportName, String beforeState, String afterState,
String reason, String approverId, LocalDateTime approvalTime) {
ReportApprovalLogPO log = new ReportApprovalLogPO();
log.setId(UUID.randomUUID().toString());
log.setReportType(getReportType());
log.setReportId(reportId);
log.setReportName(reportName);
log.setApprovalResult(afterState);
log.setApprovalReason(reason);
log.setBeforeState(beforeState);
log.setAfterState(afterState);
log.setApproverId(approverId);
log.setApproverName(resolveUserName(approverId));
log.setApprovalTime(approvalTime);
log.setStatus(ReportApprovalConst.STATUS_NORMAL);
log.setCreateBy(approverId);
log.setCreateTime(approvalTime);
log.setUpdateBy(approverId);
log.setUpdateTime(approvalTime);
reportApprovalMapper.insert(log);
}
protected String resolveCurrentUserId() {
String userId = RequestUtil.getUserId();
return StrUtil.isBlank(userId) ? null : userId;
}
private String resolveUserName(String userId) {
if (StrUtil.isBlank(userId)) {
return null;
}
Map<String, String> userNameMap = reportApprovalUserNameResolver.resolveUserNames(Arrays.asList(userId));
return userNameMap.containsKey(userId) ? userNameMap.get(userId) : userId;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.aireport.reportapproval.handler;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
public interface ReportApprovalHandler {
String getReportType();
boolean audit(ReportApprovalParam.AuditParam param, boolean passed);
}

View File

@@ -0,0 +1,35 @@
package com.njcn.gather.aireport.reportapproval.handler;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class ReportApprovalHandlerRegistry {
private final Map<String, ReportApprovalHandler> handlerMap = new LinkedHashMap<String, ReportApprovalHandler>();
public ReportApprovalHandlerRegistry(List<ReportApprovalHandler> handlers) {
if (handlers == null || handlers.isEmpty()) {
return;
}
for (ReportApprovalHandler handler : handlers) {
if (handler != null && StrUtil.isNotBlank(handler.getReportType())) {
handlerMap.put(handler.getReportType(), handler);
}
}
}
public ReportApprovalHandler requireHandler(String reportType) {
ReportApprovalHandler handler = handlerMap.get(reportType);
if (handler == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "不支持的报告类型");
}
return handler;
}
}

View File

@@ -0,0 +1,58 @@
package com.njcn.gather.aireport.reportapproval.handler;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class ReportModelApprovalHandler extends AbstractReportApprovalHandler {
private final ReportModelService reportModelService;
public ReportModelApprovalHandler(ReportApprovalMapper reportApprovalMapper,
ReportApprovalUserNameResolver reportApprovalUserNameResolver,
ReportModelService reportModelService) {
super(reportApprovalMapper, reportApprovalUserNameResolver);
this.reportModelService = reportModelService;
}
@Override
public String getReportType() {
return ReportApprovalConst.REPORT_TYPE_REPORT_MODEL;
}
@Override
protected boolean doAudit(String reportId, String reason, boolean passed) {
ReportModelPO reportModel = reportModelService.requireNormal(reportId);
if (!ReportModelConst.STATE_AUDITING.equals(reportModel.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅待审状态的报告模板允许审批");
}
String userId = resolveCurrentUserId();
LocalDateTime now = LocalDateTime.now();
String targetState = passed ? ReportModelConst.STATE_APPROVED : ReportModelConst.STATE_REJECTED;
boolean updated = reportModelService.lambdaUpdate()
.set(ReportModelPO::getState, targetState)
.set(ReportModelPO::getCheckId, userId)
.set(ReportModelPO::getCheckTime, now)
.set(ReportModelPO::getCheckResult, reason)
.set(ReportModelPO::getUpdateBy, userId)
.set(ReportModelPO::getUpdateTime, now)
.eq(ReportModelPO::getId, reportId)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.eq(ReportModelPO::getState, ReportModelConst.STATE_AUDITING)
.update();
if (!updated) {
throw new BusinessException(CommonResponseEnum.FAIL, "报告模板状态已变更,请刷新后重试");
}
saveLog(reportModel.getId(), reportModel.getName(), ReportModelConst.STATE_AUDITING, targetState, reason, userId, now);
return true;
}
}

View File

@@ -0,0 +1,59 @@
package com.njcn.gather.aireport.reportapproval.handler;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.service.TestReportService;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Component
public class TestReportApprovalHandler extends AbstractReportApprovalHandler {
private final TestReportService testReportService;
public TestReportApprovalHandler(ReportApprovalMapper reportApprovalMapper,
ReportApprovalUserNameResolver reportApprovalUserNameResolver,
TestReportService testReportService) {
super(reportApprovalMapper, reportApprovalUserNameResolver);
this.testReportService = testReportService;
}
@Override
public String getReportType() {
return ReportApprovalConst.REPORT_TYPE_TEST_REPORT;
}
@Override
protected boolean doAudit(String reportId, String reason, boolean passed) {
TestReportPO testReport = testReportService.requireNormal(reportId);
if (!TestReportConst.STATE_AUDITING.equals(testReport.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅待审状态的普测报告允许审批");
}
String userId = resolveCurrentUserId();
LocalDateTime now = LocalDateTime.now();
String targetState = passed ? TestReportConst.STATE_APPROVED : TestReportConst.STATE_REJECTED;
boolean updated = testReportService.lambdaUpdate()
.set(TestReportPO::getState, targetState)
.set(TestReportPO::getCheckId, userId)
.set(TestReportPO::getCheckTime, LocalDate.now())
.set(TestReportPO::getCheckResult, reason)
.set(TestReportPO::getUpdateBy, userId)
.set(TestReportPO::getUpdateTime, now)
.eq(TestReportPO::getId, reportId)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.eq(TestReportPO::getState, TestReportConst.STATE_AUDITING)
.update();
if (!updated) {
throw new BusinessException(CommonResponseEnum.FAIL, "普测报告状态已变更,请刷新后重试");
}
saveLog(testReport.getId(), testReport.getNo(), TestReportConst.STATE_AUDITING, targetState, reason, userId, now);
return true;
}
}

View File

@@ -0,0 +1,95 @@
package com.njcn.gather.aireport.reportapproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.po.ReportApprovalLogPO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
@Mapper
public interface ReportApprovalMapper extends BaseMapper<ReportApprovalLogPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ReportModelUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
@Select({
"<script>",
"select count(1) from (",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"REPORT_MODEL\"'>",
"select model.id",
"from ai_reportmodel model",
"where model.status = 1 and model.state = '02'",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\"'>",
"union all",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"TEST_REPORT\"'>",
"select report.id",
"from ai_testreport report",
"where report.status = 1 and report.state = '02'",
"</if>",
") pending",
"</script>"
})
long countPendingReports(@Param("param") ReportApprovalParam.PendingQueryParam param);
@Select({
"<script>",
"select * from (",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"REPORT_MODEL\"'>",
"select 'REPORT_MODEL' as reportType, model.id as reportId, model.name as reportName, model.state,",
"model.update_by as submitterId, user.name as submitterName, model.update_time as submitTime",
"from ai_reportmodel model",
"left join sys_user user on user.id = model.update_by",
"where model.status = 1 and model.state = '02'",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\"'>",
"union all",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"TEST_REPORT\"'>",
"select 'TEST_REPORT' as reportType, report.id as reportId, report.no as reportName, report.state,",
"report.update_by as submitterId, user.name as submitterName, report.update_time as submitTime",
"from ai_testreport report",
"left join sys_user user on user.id = report.update_by",
"where report.status = 1 and report.state = '02'",
"</if>",
") pending",
"order by pending.submitTime desc",
"limit #{offset}, #{size}",
"</script>"
})
List<ReportApprovalPendingVO> selectPendingPage(@Param("offset") long offset,
@Param("size") long size,
@Param("param") ReportApprovalParam.PendingQueryParam param);
@Select({
"<script>",
"select id, report_type as reportType, report_id as reportId, report_name as reportName,",
"approval_result as approvalResult, approval_reason as approvalReason, before_state as beforeState,",
"after_state as afterState, approver_name as approverName, approval_time as approvalTime",
"from ai_report_approval_log",
"where status = 1",
"<if test='param.reportType != null and param.reportType != \"\"'>",
"and report_type = #{param.reportType}",
"</if>",
"order by approval_time desc, create_time desc",
"</script>"
})
Page<ReportApprovalLogVO> selectLogPage(Page<ReportApprovalLogVO> page,
@Param("param") ReportApprovalParam.LogQueryParam param);
}

View File

@@ -0,0 +1,12 @@
package com.njcn.gather.aireport.reportapproval.pojo.constant;
public final class ReportApprovalConst {
public static final String REPORT_TYPE_REPORT_MODEL = "REPORT_MODEL";
public static final String REPORT_TYPE_TEST_REPORT = "TEST_REPORT";
public static final Integer STATUS_NORMAL = 1;
private ReportApprovalConst() {
}
}

View File

@@ -0,0 +1,42 @@
package com.njcn.gather.aireport.reportapproval.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class ReportApprovalParam {
@Data
public static class AuditParam {
@ApiModelProperty("报告类型")
@NotBlank(message = "报告类型不能为空")
private String reportType;
@ApiModelProperty("报告ID")
@NotBlank(message = "报告ID不能为空")
private String reportId;
@ApiModelProperty("审批原因")
@NotBlank(message = "审批原因不能为空")
@Size(max = 128, message = "审批原因不能超过128个字符")
private String approvalReason;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class PendingQueryParam extends BaseParam {
@ApiModelProperty("报告类型")
private String reportType;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class LogQueryParam extends BaseParam {
@ApiModelProperty("报告类型")
private String reportType;
}
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.aireport.reportapproval.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("ai_report_approval_log")
public class ReportApprovalLogPO implements Serializable {
private static final long serialVersionUID = 1900859716151337132L;
@TableId("id")
private String id;
@TableField("report_type")
private String reportType;
@TableField("report_id")
private String reportId;
@TableField("report_name")
private String reportName;
@TableField("approval_result")
private String approvalResult;
@TableField("approval_reason")
private String approvalReason;
@TableField("before_state")
private String beforeState;
@TableField("after_state")
private String afterState;
@TableField("approver_id")
private String approverId;
@TableField("approver_name")
private String approverName;
@TableField("approval_time")
private LocalDateTime approvalTime;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,38 @@
package com.njcn.gather.aireport.reportapproval.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ReportApprovalLogVO {
@ApiModelProperty("日志ID")
private String id;
@ApiModelProperty("报告类型")
private String reportType;
@ApiModelProperty("报告ID")
private String reportId;
@ApiModelProperty("报告名称")
private String reportName;
@ApiModelProperty("审批结果")
private String approvalResult;
@ApiModelProperty("审批原因")
private String approvalReason;
@ApiModelProperty("审批前状态")
private String beforeState;
@ApiModelProperty("审批后状态")
private String afterState;
@ApiModelProperty("审批人")
private String approverName;
@ApiModelProperty("审批时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime approvalTime;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.aireport.reportapproval.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ReportApprovalPendingVO {
@ApiModelProperty("报告类型")
private String reportType;
@ApiModelProperty("报告ID")
private String reportId;
@ApiModelProperty("报告名称")
private String reportName;
@ApiModelProperty("报告状态")
private String state;
@ApiModelProperty("提交人ID")
private String submitterId;
@ApiModelProperty("提交人")
private String submitterName;
@ApiModelProperty("提交时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime submitTime;
}

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.aireport.reportapproval.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
public interface ReportApprovalService {
boolean pass(ReportApprovalParam.AuditParam param);
boolean reject(ReportApprovalParam.AuditParam param);
Page<ReportApprovalPendingVO> pendingPage(ReportApprovalParam.PendingQueryParam param);
Page<ReportApprovalLogVO> logPage(ReportApprovalParam.LogQueryParam param);
}

View File

@@ -0,0 +1,77 @@
package com.njcn.gather.aireport.reportapproval.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandler;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandlerRegistry;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import com.njcn.gather.aireport.reportapproval.service.ReportApprovalService;
import com.njcn.web.factory.PageFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
@Service
public class ReportApprovalServiceImpl implements ReportApprovalService {
private final ReportApprovalMapper reportApprovalMapper;
private final ReportApprovalHandlerRegistry reportApprovalHandlerRegistry;
public ReportApprovalServiceImpl(ReportApprovalMapper reportApprovalMapper,
ReportApprovalHandlerRegistry reportApprovalHandlerRegistry) {
this.reportApprovalMapper = reportApprovalMapper;
this.reportApprovalHandlerRegistry = reportApprovalHandlerRegistry;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean pass(ReportApprovalParam.AuditParam param) {
return audit(param, true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean reject(ReportApprovalParam.AuditParam param) {
return audit(param, false);
}
@Override
public Page<ReportApprovalPendingVO> pendingPage(ReportApprovalParam.PendingQueryParam param) {
ReportApprovalParam.PendingQueryParam query = param == null ? new ReportApprovalParam.PendingQueryParam() : param;
if (StrUtil.isNotBlank(query.getReportType())) {
reportApprovalHandlerRegistry.requireHandler(query.getReportType());
}
long current = PageFactory.getPageNum(query);
long size = PageFactory.getPageSize(query);
long offset = Math.max(0L, (current - 1L) * size);
long total = reportApprovalMapper.countPendingReports(query);
List<ReportApprovalPendingVO> records = total <= 0
? Collections.<ReportApprovalPendingVO>emptyList()
: reportApprovalMapper.selectPendingPage(offset, size, query);
Page<ReportApprovalPendingVO> page = new Page<ReportApprovalPendingVO>(current, size, total);
page.setRecords(records);
return page;
}
@Override
public Page<ReportApprovalLogVO> logPage(ReportApprovalParam.LogQueryParam param) {
ReportApprovalParam.LogQueryParam query = param == null ? new ReportApprovalParam.LogQueryParam() : param;
return reportApprovalMapper.selectLogPage(
new Page<ReportApprovalLogVO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query);
}
private boolean audit(ReportApprovalParam.AuditParam param, boolean passed) {
if (param == null || StrUtil.isBlank(param.getReportType()) || StrUtil.isBlank(param.getReportId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "审批参数不能为空");
}
ReportApprovalHandler handler = reportApprovalHandlerRegistry.requireHandler(param.getReportType());
return handler.audit(param, passed);
}
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.aireport.reportapproval.pojo.vo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDateTime;
public class ReportApprovalVOTimeFormatTest {
@Test
public void pendingAndLogTimeJsonUseStandardTwentyFourHourFormat() throws Exception {
ReportApprovalPendingVO pendingVO = new ReportApprovalPendingVO();
pendingVO.setSubmitTime(LocalDateTime.of(2026, 6, 29, 10, 0, 0));
ReportApprovalLogVO logVO = new ReportApprovalLogVO();
logVO.setApprovalTime(LocalDateTime.of(2026, 6, 29, 18, 30, 45));
ObjectMapper objectMapper = new ObjectMapper();
String pendingJson = objectMapper.writeValueAsString(pendingVO);
String logJson = objectMapper.writeValueAsString(logVO);
Assert.assertTrue(pendingJson.contains("\"submitTime\":\"2026-06-29 10:00:00\""));
Assert.assertFalse(pendingJson.contains("2026-06-29T10"));
Assert.assertTrue(logJson.contains("\"approvalTime\":\"2026-06-29 18:30:45\""));
Assert.assertFalse(logJson.contains("2026-06-29T18"));
}
}

View File

@@ -0,0 +1,75 @@
package com.njcn.gather.aireport.reportapproval.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandler;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandlerRegistry;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ReportApprovalServiceImplTest {
@Test
public void pendingPageShouldUseDatabasePagingResult() {
ReportApprovalMapper mapper = mock(ReportApprovalMapper.class);
ReportApprovalHandlerRegistry registry = new ReportApprovalHandlerRegistry(Collections.<ReportApprovalHandler>emptyList());
ReportApprovalServiceImpl service = new ReportApprovalServiceImpl(mapper, registry);
ReportApprovalParam.PendingQueryParam param = new ReportApprovalParam.PendingQueryParam();
param.setPageNum(2);
param.setPageSize(5);
ReportApprovalPendingVO record = new ReportApprovalPendingVO();
record.setReportType(ReportApprovalConst.REPORT_TYPE_REPORT_MODEL);
record.setReportId("model-001");
record.setReportName("模板A");
record.setSubmitterId("user-001");
record.setSubmitterName("张三");
record.setSubmitTime(LocalDateTime.of(2026, 6, 29, 10, 0, 0));
List<ReportApprovalPendingVO> records = Arrays.asList(record);
when(mapper.countPendingReports(param)).thenReturn(11L);
when(mapper.selectPendingPage(5L, 5L, param)).thenReturn(records);
Page<ReportApprovalPendingVO> page = service.pendingPage(param);
Assert.assertEquals(2L, page.getCurrent());
Assert.assertEquals(5L, page.getSize());
Assert.assertEquals(11L, page.getTotal());
Assert.assertEquals(1, page.getRecords().size());
Assert.assertEquals("user-001", page.getRecords().get(0).getSubmitterId());
Assert.assertEquals("张三", page.getRecords().get(0).getSubmitterName());
verify(mapper).countPendingReports(param);
verify(mapper).selectPendingPage(5L, 5L, param);
}
@Test
public void passShouldDelegateToMatchedHandler() {
ReportApprovalMapper mapper = mock(ReportApprovalMapper.class);
ReportApprovalHandler handler = mock(ReportApprovalHandler.class);
when(handler.getReportType()).thenReturn(ReportApprovalConst.REPORT_TYPE_TEST_REPORT);
when(handler.audit(org.mockito.ArgumentMatchers.any(ReportApprovalParam.AuditParam.class), eq(true))).thenReturn(true);
ReportApprovalHandlerRegistry registry = new ReportApprovalHandlerRegistry(Arrays.asList(handler));
ReportApprovalServiceImpl service = new ReportApprovalServiceImpl(mapper, registry);
ReportApprovalParam.AuditParam param = new ReportApprovalParam.AuditParam();
param.setReportType(ReportApprovalConst.REPORT_TYPE_TEST_REPORT);
param.setReportId("report-001");
param.setApprovalReason("通过");
boolean result = service.pass(param);
Assert.assertTrue(result);
verify(handler).audit(param, true);
}
}