Prechádzať zdrojové kódy

工作台、标本库管理、标本出/回库管理

hyy 5 mesiacov pred
rodič
commit
83f66f30c0
23 zmenil súbory, kde vykonal 1538 pridanie a 116 odobranie
  1. 5 3
      yudao-module-museums/yudao-module-museums-api/src/main/java/cn/iocoder/yudao/module/museums/enums/ErrorCodeConstants.java
  2. 18 0
      yudao-module-museums/yudao-module-museums-biz/pom.xml
  3. 181 1
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimeninfo/SpecimenInfoController.java
  4. 93 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimeninfo/vo/SpecimenImportExcelVO.java
  5. 23 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimeninfo/vo/SpecimenImportRespVO.java
  6. 161 7
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/SpecimenOutboundController.java
  7. 7 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundApprovalReqVO.java
  8. 5 2
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundPageReqVO.java
  9. 20 2
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundRespVO.java
  10. 7 4
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundSaveReqVO.java
  11. 121 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundWithInfoRespVO.java
  12. 72 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/dataobject/specimeninfo/SpecimenInfoDO.java
  13. 28 8
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/dataobject/specimenoutbound/SpecimenOutboundDO.java
  14. 100 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/mysql/specimeninfo/SpecimenInfoMapper.java
  15. 78 2
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/mysql/specimenoutbound/SpecimenOutboundMapper.java
  16. 10 0
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/photogroup/PhotoGroupService.java
  17. 81 1
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimeninfo/SpecimenInfoService.java
  18. 270 2
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimeninfo/SpecimenInfoServiceImpl.java
  19. 51 18
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimenoutbound/SpecimenOutboundService.java
  20. 156 65
      yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimenoutbound/SpecimenOutboundServiceImpl.java
  21. 47 0
      yudao-module-museums/yudao-module-museums-biz/src/main/resources/mapper/specimeninfo/SpecimenInfoMapper.xml
  22. 1 0
      yudao-module-museums/yudao-module-museums-biz/src/main/resources/mapper/specimenoutbound/SpecimenOutboundMapper.xml
  23. 3 1
      yudao-server/src/main/resources/application.yaml

+ 5 - 3
yudao-module-museums/yudao-module-museums-api/src/main/java/cn/iocoder/yudao/module/museums/enums/ErrorCodeConstants.java

@@ -9,11 +9,13 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
  */
 public class ErrorCodeConstants {
     // ========== 标本管理 1-016-000-000 ==========
-   public static final ErrorCode SPECIMEN_INFO_NOT_EXISTS = new ErrorCode(1-016-000-000, "标本管理不存在");
-
+    public static final ErrorCode SPECIMEN_INFO_NOT_EXISTS = new ErrorCode(1-016-000-000, "标本管理不存在");
+    public static final ErrorCode SPECIMEN_INFO_LIST_IS_EMPTY = new ErrorCode(1-016-000-001, "标本管理不存在");
+    public static final ErrorCode UPLOADED_FOLDER_CANNOT_EMPTY = new ErrorCode(1-016-000-001, "上传的压缩包不能为空");
+    public static final ErrorCode INVALID_IMAGE_FORMAT = new ErrorCode(1-016-000-001, "上传的压缩包不能为空");
     // ========== 标本出库回库信息 1-016-001-000 ==========
     public static final ErrorCode SPECIMEN_OUTBOUND_NOT_EXISTS = new ErrorCode(1-016-001-000, "标本出库回库信息不存在");
-
+    public static final ErrorCode NO_PERMISSION_VIEW_NON_RETURNED_SPECIMENS = new ErrorCode(1-016-001-000, "没有权限查看非已回库的标本出库信息");
     // ========== 博物馆照片组 1-016-002-000 ==========
     public static final ErrorCode PHOTO_GROUP_NOT_EXISTS = new ErrorCode(1-016-002-000, "博物馆照片组不存在");
 

+ 18 - 0
yudao-module-museums/yudao-module-museums-biz/pom.xml

@@ -24,6 +24,18 @@
             <version>${revision}</version>
         </dependency>
 
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-infra-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version> <!-- 请检查最新版本 -->
+        </dependency>
+
         <!-- Web 相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
@@ -55,6 +67,12 @@
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity-engine-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 181 - 1
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimeninfo/SpecimenInfoController.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.museums.controller.admin.specimeninfo;
 
+import io.swagger.v3.oas.annotations.Parameters;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
@@ -8,9 +10,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Operation;
 
-import javax.validation.constraints.*;
 import javax.validation.*;
 import javax.servlet.http.*;
+import java.math.BigDecimal;
+import java.net.URLEncoder;
+import java.time.LocalDateTime;
 import java.util.*;
 import java.io.IOException;
 
@@ -28,6 +32,7 @@ import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
 import cn.iocoder.yudao.module.museums.controller.admin.specimeninfo.vo.*;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
 import cn.iocoder.yudao.module.museums.service.specimeninfo.SpecimenInfoService;
+import org.springframework.web.multipart.MultipartFile;
 
 @Tag(name = "管理后台 - 标本管理")
 @RestController
@@ -92,4 +97,179 @@ public class SpecimenInfoController {
                         BeanUtils.toBean(list, SpecimenInfoRespVO.class));
     }
 
+    @GetMapping("/get-specimen-import-template")
+    @Operation(summary = "获得导入标本信息模板")
+    public void getSpecimenImportTemplate(HttpServletResponse response) throws IOException {
+
+        // 设置响应类型
+        response.setContentType("application/vnd.ms-excel");
+        response.setCharacterEncoding("UTF-8");
+        // URL 编码文件名
+        String fileName = URLEncoder.encode("标本导入模板.xls", "UTF-8");
+        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
+
+        // 手动创建导出 demo
+        List<SpecimenImportExcelVO> list = Arrays.asList(
+                SpecimenImportExcelVO.builder()
+                        .specimenNumber("SM001")
+                        .specimenType(0) // 矿物
+                        .assetNumber("A001")
+                        .storageLocation("博物馆A")
+                        .chineseName("矿石")
+                        .englishName("Mineral")
+                        .composition("硅酸盐")
+                        .origin("中国")
+                        .era("古生代")
+                        .preservedLayer("上新世")
+                        .meteoriteType("无")
+                        .internationalName("Mineral A")
+                        .discoveryTime("2024-01-01") // 使用 LocalDateTime
+                        .fallTime("2024-01-02")
+                        .preservationType(0) // 标本
+                        .size("10cm")
+                        .weight(new BigDecimal("1.5"))
+                        .source(0) // 采购
+                        .provider("供应商A")
+                        .acquisitionTime("2024-01-03")
+                        .purpose("研究")
+                        .description("这是一个矿石标本")
+                        .notes("备注信息")
+                        .imageName("image1.jpg")
+                        .imagePath("/images/image1.jpg")
+                        .build()
+        );
+        ExcelUtils.write(response, "标本导入模板.xls", "标本信息", SpecimenImportExcelVO.class, list);
+    }
+
+
+    @PostMapping("/import-specimen")
+    @Operation(summary = "导入标本")
+    @Parameters({
+            @Parameter(name = "file", description = "Excel 文件", required = true),
+            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
+    })
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:import')")
+    public CommonResult<SpecimenImportRespVO> importSpecimenExcel(@RequestParam("file") MultipartFile file,
+                                                                  @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
+        List<SpecimenImportExcelVO> list = ExcelUtils.read(file, SpecimenImportExcelVO.class);
+        return success(specimenInfoService.importSpecimenList(list, updateSupport));
+    }
+
+    @PostMapping("/import-specimen-images")
+    @Operation(summary = "导入标本图片")
+    @Parameters({
+            @Parameter(name = "file", description = "压缩包文件", required = true)
+    })
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:import-images')")
+    public CommonResult<String> importSpecimenImages(@RequestParam("file") MultipartFile file) throws Exception {
+        // 解压文件并处理图片
+        return success(specimenInfoService.importSpecimenImages(file));
+    }
+
+    @PostMapping("/import-specimen-data")
+    @Operation(summary = "导入标本及其图片")
+    @Parameters({
+            @Parameter(name = "excelFile", description = "Excel 文件", required = true),
+            @Parameter(name = "imageFile", description = "压缩包文件", required = true),
+            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
+    })
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:import')")
+    public CommonResult<String> importSpecimenData(@RequestParam("excelFile") MultipartFile excelFile,
+                                                   @RequestParam("imageFile") MultipartFile imageFile,
+                                                   @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) {
+        try {
+            specimenInfoService.importSpecimenData(excelFile, imageFile, updateSupport);
+            return CommonResult.success("标本及图片导入成功");
+        } catch (Exception e) {
+            return CommonResult.error(500, "导入过程中发生错误: " + e.getMessage());
+        }
+    }
+
+
+    //工作台
+    //根据入库的登记情况统计本年标本入库信息。
+    @GetMapping("/statistics/entry/{year}")
+    @Operation(summary = "统计本年标本入库信息")
+    @Parameter(name = "year", description = "年份", required = true, example = "2024")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:query')")
+    public CommonResult<List<SpecimenInfoDO>> getEntryStatistics(@PathVariable int year) {
+        List<SpecimenInfoDO> entryStatistics = specimenInfoService.getEntryStatistics(year);
+        return success(entryStatistics);
+    }
+
+    //根据标本类别指标统计各类标本库存数,第一个是可查询某个标本类别的所有数据,第二个是可查询各个标本类型的库存数
+    @GetMapping("/statistics/{specimen_type}")
+    @Operation(summary = "按标本类别统计库存数")
+    @Parameter(name = "specimen_type", description = "标本类型", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:query')")
+    public CommonResult<List<SpecimenInfoDO>> getSpecimenTypeStatistics(@PathVariable int specimen_type) {
+        List<SpecimenInfoDO> specimenTypeStatistics = specimenInfoService.getSpecimenTypeStatistics(specimen_type);
+        return success(specimenTypeStatistics);
+    }
+    @GetMapping("/statistics/allType")
+    @Operation(summary = "按标本类别统计库存数")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:query')")
+    public CommonResult<Map<Integer, Integer>> getSpecimenTypeStatistics() {
+        List<Map<String, Object>> specimenTypeStatistics = specimenInfoService.getAllSpecimenTypeStatistics();
+        Map<Integer, Integer> result = new HashMap<>();
+
+        // 将统计结果填充到 Map 中
+        for (Map<String, Object> specimen : specimenTypeStatistics) {
+            Integer type = (Integer) specimen.get("specimen_type");
+            Integer count = ((Long) specimen.get("count")).intValue(); // 处理 Long 转换
+            result.put(type, count);
+        }
+
+        return success(result);
+    }
+
+    //根据出、回、入库的登记信息统计本馆标本历年增减情况。
+    @GetMapping("/statistics/yearly")
+    @Operation(summary = "根据出、回、入库登记统计标本历年增减情况")
+    public CommonResult<Map<Integer, Map<String, Integer>>> getYearlySpecimenStatistics() {
+        Map<Integer, Map<String, Integer>> yearlyStatistics = specimenInfoService.getYearlySpecimenStatistics();
+        return CommonResult.success(yearlyStatistics);
+    }
+
+    //根据入馆凭证中标本来源的登记情况统计
+    @GetMapping("/statistics/source")
+    @Operation(summary = "根据标本来源统计历年标本登记情况")
+    public CommonResult<Map<Integer, Map<String, Integer>>> getYearlySpecimenSourceStatistics() {
+        Map<Integer, Map<String, Integer>> yearlySourceStatistics = specimenInfoService.getYearlySpecimenSourceStatistics();
+        return CommonResult.success(yearlySourceStatistics);
+    }
+
+    //标本来源的登记情况统计
+    //不好用,准备废弃
+    @GetMapping("/statistics/so")
+    @Operation(summary = "历年标本来源增减统计")
+    @Parameter(name = "year", description = "年份", required = true, example = "2024")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:query')")
+    public CommonResult<List<SpecimenInfoDO>> getSpecimenSourceStatistics(@RequestParam int year) {
+        List<SpecimenInfoDO> sourceStatistics = specimenInfoService.getSpecimenSourceStatistics(year);
+        return success(sourceStatistics);
+    }
+
+
+    //标本库管理
+    //实现对标本操作记录进行追溯查看,包括入库记录、编辑记录、出库记录、回库记录等。
+    @GetMapping("/records")
+    @Operation(summary = "获得标本操作记录")
+    @Parameter(name = "id", description = "标本编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-info:query')")
+    public CommonResult<List<SpecimenInfoDO>> select(@RequestParam("id") Long id) {
+        List<SpecimenInfoDO> list = specimenInfoService.getSpecimenRecords(id);
+        return success(list); // 返回整个列表
+    }
+
+
+
+//    @GetMapping("/test")
+//    public Integer test(String status) {
+//        return  specimenInfoService.convertSpecimenType(status);
+//    }
+
+
+
+
 }

+ 93 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimeninfo/vo/SpecimenImportExcelVO.java

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.museums.controller.admin.specimeninfo.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.math.BigDecimal;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
+public class SpecimenImportExcelVO {
+
+    @ExcelProperty("标本类型(矿物0、岩石矿石1、化石3、陨石4)")
+    private Integer specimenType;
+
+    @ExcelProperty("标本编号")
+    private String specimenNumber;
+
+    @ExcelProperty("资产号")
+    private String assetNumber;
+
+    @ExcelProperty("存放位置")
+    private String storageLocation;
+
+    @ExcelProperty("中文名称")
+    private String chineseName;
+
+    @ExcelProperty("英文名称")
+    private String englishName;
+
+    @ExcelProperty("成分")
+    private String composition;
+
+    @ExcelProperty("产地")
+    private String origin;
+
+    @ExcelProperty("时代")
+    private String era;
+
+    @ExcelProperty("保存地层")
+    private String preservedLayer;
+
+    @ExcelProperty("陨石类型")
+    private String meteoriteType;
+
+    @ExcelProperty("国际命名")
+    private String internationalName;
+
+    @ExcelProperty("发现时间")
+    private String discoveryTime; // 或者使用 LocalDate
+
+    @ExcelProperty("降落时间")
+    private String fallTime; // 或者使用 LocalDate
+
+    @ExcelProperty("保存类型(标本0、光片1、薄片模型3、其他4)")
+    private Integer preservationType;
+
+    @ExcelProperty("尺寸")
+    private String size;
+
+    @ExcelProperty("重量")
+    private BigDecimal weight;
+
+    @ExcelProperty("来源(采购0、捐赠1、采集3、其他4)")
+    private Integer source;
+
+    @ExcelProperty("标本提供者")
+    private String provider;
+
+    @ExcelProperty("入藏时间")
+    private String acquisitionTime; // 或者使用 LocalDate
+
+    @ExcelProperty("用途")
+    private String purpose;
+
+    @ExcelProperty("描述")
+    private String description;
+
+    @ExcelProperty("备注")
+    private String notes;
+
+    @ExcelProperty("图片名称")
+    private String imageName;
+
+    @ExcelProperty("图片路径")
+    private String imagePath;
+}

+ 23 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimeninfo/vo/SpecimenImportRespVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.museums.controller.admin.specimeninfo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+@Builder
+@Schema(description = "标本导入 Response VO")
+public class SpecimenImportRespVO {
+
+    @Schema(description = "创建成功的标本编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> createSpecimenNumbers;
+
+    @Schema(description = "更新成功的标本编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    private List<String> updateSpecimenNumbers;
+
+    @Schema(description = "导入失败的标本集合,key 为标本编号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Map<String, String> failureSpecimenNumbers;
+}

+ 161 - 7
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/SpecimenOutboundController.java

@@ -14,11 +14,15 @@ import javax.servlet.http.*;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.io.IOException;
+import java.util.stream.Collectors;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -26,6 +30,8 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
 import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
+import static cn.iocoder.yudao.module.museums.enums.ErrorCodeConstants.NO_PERMISSION_VIEW_NON_RETURNED_SPECIMENS;
+import static cn.iocoder.yudao.module.museums.enums.ErrorCodeConstants.SPECIMEN_OUTBOUND_NOT_EXISTS;
 
 import cn.iocoder.yudao.module.museums.controller.admin.specimenoutbound.vo.*;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound.SpecimenOutboundDO;
@@ -60,19 +66,22 @@ public class SpecimenOutboundController {
     @PostMapping("/approve")
     @Operation(summary = "审批通过")
     @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:approve')")
-    public CommonResult<Void> approveSpecimenOutbound(@RequestParam("id") Long id) {
-        // 更新状态为审批通过,并插入当前时间作为审批时间
-        specimenOutboundService.updateStatus(id, 1, LocalDateTime.now(), null);
+    public CommonResult<Void> approveSpecimenOutbound(@RequestBody SpecimenOutboundApprovalReqVO req) {
+        req.setApproveUsers(getLoginUserId()); // 获取操作员ID
+        req.setApprovalTime(LocalDateTime.now()); // 设置当前时间
+        req.setStatus(1); // 设置审批状态为通过
+        specimenOutboundService.updateStatus(req);
         return success(null);
     }
 
     @PostMapping("/reject")
     @Operation(summary = "审批驳回")
     @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:reject')")
-    public CommonResult<Void> rejectSpecimenOutbound(@RequestParam("id") Long id,
-                                                     @RequestParam("processInstanceId") String processInstanceId) {
-        // 更新状态为审批驳回,并插入当前时间作为审批时间
-        specimenOutboundService.updateStatus(id, 2, LocalDateTime.now(), processInstanceId);
+    public CommonResult<Void> rejectSpecimenOutbound(@Valid @RequestBody SpecimenOutboundApprovalReqVO req) {
+        req.setApproveUsers(getLoginUserId()); // 获取操作员ID
+        req.setApprovalTime(LocalDateTime.now()); // 设置当前时间
+        req.setStatus(2); // 设置审批状态为驳回
+        specimenOutboundService.updateStatus(req);
         return success(null);
     }
 
@@ -112,6 +121,26 @@ public class SpecimenOutboundController {
         return success(BeanUtils.toBean(specimenOutbound, SpecimenOutboundRespVO.class));
     }
 
+    @GetMapping("/getReturn")
+    @Operation(summary = "获得标本回库信息")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+    public CommonResult<SpecimenOutboundRespVO> getSpecimenReturnInformation(@RequestParam("id") Long id) {
+        SpecimenOutboundDO specimenOutbound = specimenOutboundService.getSpecimenReturnInformation(id);
+        if (specimenOutbound != null && specimenOutbound.getStatus() == 4) {
+            SpecimenOutboundRespVO respVO = new SpecimenOutboundRespVO();
+            respVO.setReturner(specimenOutbound.getReturner());
+            respVO.setReceiver(specimenOutbound.getReceiver());
+            respVO.setReturnDate(specimenOutbound.getReturnDate());
+            respVO.setRemarks(specimenOutbound.getRemarks());
+            respVO.setSpecimenCondition(specimenOutbound.getSpecimenCondition());
+            return success(respVO);
+        } else {
+            throw exception(NO_PERMISSION_VIEW_NON_RETURNED_SPECIMENS);
+        }
+    }
+
+
     @GetMapping("/page")
     @Operation(summary = "获得标本出库回库信息分页")
     @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
@@ -133,4 +162,129 @@ public class SpecimenOutboundController {
                         BeanUtils.toBean(list, SpecimenOutboundRespVO.class));
     }
 
+    //工作台
+    //根据出库的登记情况统计本年标本出库信息。
+    @GetMapping("/statistics/outgoing/{year}")
+    @Operation(summary = "根据出库登记情况统计本年标本出库信息")
+    @Parameter(name = "year", description = "年份", required = true, example = "2024")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+    public CommonResult<List<SpecimenOutboundDO>> getOutboundStatistics(@PathVariable int year) {
+        List<SpecimenOutboundDO> outboundStatistics = specimenOutboundService.getOutboundStatistics(year);
+        return success(outboundStatistics);
+    }
+
+    @GetMapping("/statistics/return/{year}")
+    @Operation(summary = "统计本年标本回库信息")
+    @Parameter(name = "year", description = "年份", required = true, example = "2024")
+    public CommonResult<List<SpecimenOutboundDO>> getReturnStatistics(@PathVariable int year) {
+        List<SpecimenOutboundDO> result = specimenOutboundService.getReturnStatistics(year);
+        return success(result);
+    }
+
+//    @GetMapping("/outboundOrder")
+//    @Operation(summary = "获得标本出库单")
+//    @Parameter(name = "infoIds", description = "标本ID列表", required = true, example = "1,2,3,4")
+//    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+//    public CommonResult<List<SpecimenOutboundInfoVO>> getSpecimenOutbound(@RequestParam("infoIds") String infoIds) {
+//        List<Long> ids = Arrays.stream(infoIds.split(","))
+//                .map(Long::valueOf)
+//                .collect(Collectors.toList());
+//        List<SpecimenOutboundDO> specimenOutbounds = specimenOutboundService.getSpecimenOutboundsByIds(ids);
+//
+//        // 手动转换 SpecimenOutboundDO 到 SpecimenOutboundRespVO
+//        List<SpecimenOutboundInfoVO> response = specimenOutbounds.stream()
+//                .map(outbound -> {
+//                    SpecimenOutboundInfoVO vo = new SpecimenOutboundInfoVO();
+//                    vo.setId(outbound.getId());
+//                    vo.setChineseName(outbound.getChineseName());
+//                    vo.setSpecimenNumber(outbound.getSpecimenNumber());
+//                    vo.setApplicantName(outbound.getApplicantName());
+//                    vo.setApplicationDate(outbound.getApplicationDate());
+//                    vo.setImagePath(outbound.getImagePath());
+//                    vo.setStorageLocation(outbound.getStorageLocation());
+//                    // 这里可以继续设置其他字段
+//                    return vo;
+//                })
+//                .collect(Collectors.toList());
+//
+    //能用
+//        return success(response);
+//    }
+
+//    @GetMapping("/specimenInfo")
+//    @Operation(summary = "获得标本信息")
+//    @Parameter(name = "outboundOrderId", description = "标本出库回库表的ID", required = true, example = "1")
+//    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+//    public CommonResult<List<SpecimenOutboundInfoVO>> getSpecimenInfo(@RequestParam("outboundOrderId") Long outboundOrderId) {
+//        // 根据出库单ID获取信息
+//        List<SpecimenOutboundDO> specimenOutbounds = specimenOutboundService.getSpecimenOutboundsByOutboundOrderId(outboundOrderId);
+//
+//        // 手动转换 SpecimenOutboundDO 到 SpecimenOutboundInfoVO
+//        List<SpecimenOutboundInfoVO> response = specimenOutbounds.stream()
+//                .map(outbound -> {
+//                    SpecimenOutboundInfoVO vo = new SpecimenOutboundInfoVO();
+//                    vo.setInfoId(outbound.getInfoId()); // 直接获取infoId
+//                    vo.setImagePath(outbound.getImagePath());
+//                    vo.setStorageLocation(outbound.getStorageLocation());
+//                    vo.setSpecimenNumber(outbound.getSpecimenNumber());
+//                    return vo;
+//                })
+//                .collect(Collectors.toList());
+//
+//        return success(response);
+//    }
+
+//    @GetMapping("/specimenInfo")
+//    @Operation(summary = "获得标本信息")
+//    @Parameter(name = "outboundOrderId", description = "标本出库回库表的ID", required = true, example = "1")
+//    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+//    public CommonResult<List<SpecimenOutboundInfoVO>> getSpecimenInfo(@RequestParam("outboundOrderId") Long outboundOrderId) {
+//        // 根据出库单ID获取信息
+//        List<SpecimenOutboundDO> specimenOutbounds = specimenOutboundService.getSpecimenOutboundsByOutboundOrderId(outboundOrderId);
+//
+//        // 手动转换 SpecimenOutboundDO 到 SpecimenOutboundInfoVO
+//        List<SpecimenOutboundInfoVO> response = specimenOutbounds.stream()
+//                .map(outbound -> {
+//                    SpecimenOutboundInfoVO vo = new SpecimenOutboundInfoVO();
+//                    vo.setInfoId(outbound.getInfoId()); // 直接获取infoId
+//                    vo.setImagePath(outbound.getImagePath());
+//                    vo.setStorageLocation(outbound.getStorageLocation());
+//                    vo.setSpecimenNumber(outbound.getSpecimenNumber());
+//                    return vo;
+//                })
+//                .collect(Collectors.toList());
+//
+//        return success(response);
+//    }
+
+//    @GetMapping("/specimenInfo")
+//    @Operation(summary = "获得标本出库回库信息")
+//    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+//    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+//    public CommonResult<SpecimenOutboundWithInfoRespVO> getSpecimen(@RequestParam("id") Long id) {
+//        SpecimenOutboundWithInfoRespVO specimenOutbound = specimenOutboundService.getSpecimenOutboundWithInfo(id);
+//
+//        if (specimenOutbound == null) {
+//            throw exception(NO_PERMISSION_VIEW_NON_RETURNED_SPECIMENS);
+//        }
+//
+//        return success(specimenOutbound);
+//    }
+
+    @GetMapping("/specimenInfo")
+    @Operation(summary = "获得标本出库回库信息")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('museums:specimen-outbound:query')")
+    public CommonResult<SpecimenOutboundWithInfoRespVO> getSpecimen(@RequestParam("id") Long id) {
+        SpecimenOutboundWithInfoRespVO specimenOutbound = specimenOutboundService.getSpecimenOutboundWithInfo(id);
+
+        if (specimenOutbound == null) {
+            throw exception(NO_PERMISSION_VIEW_NON_RETURNED_SPECIMENS);
+        }
+
+        return success(specimenOutbound);
+    }
+
+
+
 }

+ 7 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundApprovalReqVO.java

@@ -18,5 +18,12 @@ public class SpecimenOutboundApprovalReqVO {
     private Integer status;
 
     @Schema(description = "驳回原因", example = "反对")
+    @NotEmpty(message = "驳回原因是必填项")
     private String processInstanceId;
+
+    @Schema(description = "审批员", example = "反对")
+    private Long approveUsers;
+
+    @Schema(description = "审批时间")
+    private LocalDateTime approvalTime;
 }

+ 5 - 2
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundPageReqVO.java

@@ -19,10 +19,10 @@ public class SpecimenOutboundPageReqVO extends PageParam {
     private String infoId;
 
     @Schema(description = "中文名称", example = "张三")
-    private String chineseName;
+    private String chinese;
 
     @Schema(description = "申请出库的标本编号")
-    private String specimenNumber;
+    private String number;
 
     @Schema(description = "申请人或申请单位", example = "张三")
     private String applicantName;
@@ -77,4 +77,7 @@ public class SpecimenOutboundPageReqVO extends PageParam {
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] approvalTime;
 
+    @Schema(description = "审批员")
+    private Long approveUsers;
+
 }

+ 20 - 2
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundRespVO.java

@@ -24,11 +24,11 @@ public class SpecimenOutboundRespVO {
 
     @Schema(description = "中文名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
     @ExcelProperty("中文名称")
-    private String chineseName;
+    private String chinese;
 
     @Schema(description = "申请出库的标本编号", requiredMode = Schema.RequiredMode.REQUIRED)
     @ExcelProperty("申请出库的标本编号")
-    private String specimenNumber;
+    private String number;
 
     @Schema(description = "申请人或申请单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
     @ExcelProperty("申请人或申请单位")
@@ -96,4 +96,22 @@ public class SpecimenOutboundRespVO {
     @ExcelProperty("审批时间")
     private LocalDateTime approvalTime;
 
+    @Schema(description = "审批员")
+    @ExcelProperty("审批员")
+    private Long approveUsers;
+
+    @Schema(description = "图片路径")
+    @ExcelProperty("图片路径")
+    private String imagePath;
+
+    @Schema(description = "存放位置")
+    @ExcelProperty("存放位置")
+    private String storageLocation;
+
+    @Schema(description = "标本编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("标本编号")
+    private String specimenNumber;
+
+
+
 }

+ 7 - 4
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundSaveReqVO.java

@@ -19,12 +19,12 @@ public class SpecimenOutboundSaveReqVO {
     private String infoId;
 
     @Schema(description = "中文名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
-    @NotEmpty(message = "中文名称不能为空")
-    private String chineseName;
+//    @NotEmpty(message = "中文名称不能为空")
+    private String chinese;
 
     @Schema(description = "申请出库的标本编号", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotEmpty(message = "申请出库的标本编号不能为空")
-    private String specimenNumber;
+//    @NotEmpty(message = "申请出库的标本编号不能为空")
+    private String number;
 
     @Schema(description = "申请人或申请单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
     @NotEmpty(message = "申请人或申请单位不能为空")
@@ -73,4 +73,7 @@ public class SpecimenOutboundSaveReqVO {
     @Schema(description = "审批时间")
     private LocalDateTime approvalTime;
 
+    @Schema(description = "审批员")
+    private Long approveUsers;
+
 }

+ 121 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/controller/admin/specimenoutbound/vo/SpecimenOutboundWithInfoRespVO.java

@@ -0,0 +1,121 @@
+package cn.iocoder.yudao.module.museums.controller.admin.specimenoutbound.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Schema(description = "管理后台 - 标本出库回库信息 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class SpecimenOutboundWithInfoRespVO extends SpecimenOutboundRespVO{
+    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    @ExcelProperty("主键")
+    private Long id;
+
+    @Schema(description = "关联到总表中的标本ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty("关联到总表中的标本ID")
+    private String infoId;
+
+    @Schema(description = "中文名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @ExcelProperty("中文名称")
+    private String chinese;
+
+    @Schema(description = "申请出库的标本编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("申请出库的标本编号")
+    private String number;
+
+    @Schema(description = "申请人或申请单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @ExcelProperty("申请人或申请单位")
+    private String applicantName;
+
+    @Schema(description = "申请日期")
+    @ExcelProperty("申请日期")
+    private LocalDateTime applicationDate;
+
+    @Schema(description = "申请出库的用途", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("申请出库的用途")
+    private String applicationUsage;
+
+    @Schema(description = "附件上传")
+    @ExcelProperty("附件上传")
+    private String attachments;
+
+    @Schema(description = "审批状态", example = "2")
+    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
+    @DictFormat("museums_specimen_info") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer status;
+
+    @Schema(description = "备注信息")
+    @ExcelProperty("备注信息")
+    private String remarks;
+
+    @Schema(description = "出库时间")
+    @ExcelProperty("出库时间")
+    private LocalDateTime outgoingTime;
+
+    @Schema(description = "标本情况")
+    @ExcelProperty("标本情况")
+    private String specimenCondition;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "标本状态(已出库、已回库、出库审批中)", example = "2")
+    @ExcelProperty(value = "标本状态(已出库、已回库、出库审批中)", converter = DictConvert.class)
+    @DictFormat("museums_specimen_info") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sampleStatus;
+
+    @Schema(description = "驳回原因", example = "27504")
+    @ExcelProperty("驳回原因")
+    private String processInstanceId;
+
+    @Schema(description = "出库员")
+    @ExcelProperty("出库员")
+    private Long operator;
+
+    @Schema(description = "退还人")
+    @ExcelProperty("退还人")
+    private String returner;
+
+    @Schema(description = "点收人")
+    @ExcelProperty("点收人")
+    private String receiver;
+
+    @Schema(description = "退还日期")
+    @ExcelProperty("退还日期")
+    private LocalDateTime returnDate;
+
+    @Schema(description = "审批时间")
+    @ExcelProperty("审批时间")
+    private LocalDateTime approvalTime;
+
+    @Schema(description = "审批员")
+    @ExcelProperty("审批员")
+    private Long approveUsers;
+
+//    @Schema(description = "图片路径")
+//    @ExcelProperty("图片路径")
+//    private String imagePath;
+//
+//    @Schema(description = "存放位置")
+//    @ExcelProperty("存放位置")
+//    private String storageLocation;
+//
+//    @Schema(description = "标本编号", requiredMode = Schema.RequiredMode.REQUIRED)
+//    @ExcelProperty("标本编号")
+//    private String specimenNumber;
+
+
+    @Schema(description = "关联标本信息列表")
+    @ExcelProperty("关联标本信息")
+    private List<SpecimenInfoDO> specimenInfoList;
+
+}

+ 72 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/dataobject/specimeninfo/SpecimenInfoDO.java

@@ -11,6 +11,8 @@ import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Controller;
 
 /**
  * 标本管理 DO
@@ -25,6 +27,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
+@Component
 public class SpecimenInfoDO extends BaseDO {
 
     /**
@@ -157,4 +160,73 @@ public class SpecimenInfoDO extends BaseDO {
      */
     private String deletedReason;
 
+    /**
+     * 新加的
+     */
+    /**
+     * 申请人或申请单位
+     */
+    @TableField(exist = false)
+    private String applicantName;
+    /**
+     * 申请日期
+     */
+    @TableField(exist = false)
+    private LocalDateTime applicationDate;
+    /**
+     * 申请出库的用途
+     */
+    @TableField(exist = false)
+    private String applicationUsage;
+    /**
+     * 出库时间
+     */
+    @TableField(exist = false)
+    private LocalDateTime outgoingTime;
+    /**
+     * 驳回原因
+     */
+    @TableField(exist = false)
+    private String processInstanceId;
+    /**
+     * 出库员
+     */
+    @TableField(exist = false)
+    private Long returnOperator;
+    /**
+     * 退还人
+     */
+    @TableField(exist = false)
+    private String returner;
+    /**
+     * 点收人
+     */
+    @TableField(exist = false)
+    private String receiver;
+    /**
+     * 退还日期
+     */
+    @TableField(exist = false)
+    private LocalDateTime returnDate;
+    /**
+     * 审批时间
+     */
+    @TableField(exist = false)
+    private LocalDateTime approvalTime;
+    /**
+     * 出库申请表创建者
+     */
+    @TableField(exist = false)
+    private String outboundCreator;
+    /**
+     * 审批者
+     */
+    @TableField(exist = false)
+    private Long approveUsers;
+    /**
+     * 审批时间
+     */
+    @TableField(exist = false)
+    private LocalDateTime applicantCreateTime;
+
 }

+ 28 - 8
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/dataobject/specimenoutbound/SpecimenOutboundDO.java

@@ -1,13 +1,9 @@
 package cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound;
 
 import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
+
 import java.time.LocalDateTime;
+
 import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
@@ -38,11 +34,11 @@ public class SpecimenOutboundDO extends BaseDO {
     /**
      * 中文名称
      */
-    private String chineseName;
+    private String chinese;
     /**
      * 申请出库的标本编号
      */
-    private String specimenNumber;
+    private String number;
     /**
      * 申请人或申请单位
      */
@@ -107,5 +103,29 @@ public class SpecimenOutboundDO extends BaseDO {
      * 审批时间
      */
     private LocalDateTime approvalTime;
+    /**
+     * 审批者
+     */
+    private Long approveUsers;
+    /**
+     * 存放位置
+     */
+    private String storageLocation;
+    /**
+     * 中文名称
+     */
+    private String chineseName;
+    /**
+     * 图片路径
+     */
+    private String imagePath;
+    /**
+     * 标本类型
+     */
+    private Integer specimenType;
+    /**
+     * 标本编号
+     */
+    private String specimenNumber;
 
 }

+ 100 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/mysql/specimeninfo/SpecimenInfoMapper.java

@@ -6,8 +6,13 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
+import cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound.SpecimenOutboundDO;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.ibatis.annotations.Mapper;
 import cn.iocoder.yudao.module.museums.controller.admin.specimeninfo.vo.*;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 
 /**
  * 标本管理 Mapper
@@ -52,4 +57,99 @@ public interface SpecimenInfoMapper extends BaseMapperX<SpecimenInfoDO> {
                 .orderByDesc(SpecimenInfoDO::getId));
     }
 
+    /**
+     * 更新标本信息中的馆藏状态
+     *
+     * @param id 标本ID
+     * @param collectionStatus 馆藏状态
+     */
+    @Update("UPDATE museums_specimen_info SET collection_status = #{collection_status} WHERE id = #{id} AND deleted = 0")
+    void updateCollectionStatus(@Param("id") Long id, @Param("collection_status") Integer collectionStatus);
+
+    /**
+     * 根据年份统计已出库和已回库的标本信息
+     *
+     * @param year 年份
+     * @return 标本回库信息列表
+     */
+    @Select("SELECT * FROM museums_specimen_info " + "WHERE YEAR(entry_date) = #{year} " + "AND deleted = 0")
+    List<SpecimenInfoDO> selectEntryStatisticsByYear(int year);
+
+    /**
+     * 根据标本类别统计库存数
+     *
+     * @param specimen_type 标本类型
+     * @return 标本类别信息列表
+     */
+    @Select("SELECT * FROM museums_specimen_info " +
+            "WHERE specimen_type = #{specimen_type} " +
+            "AND deleted = 0")
+    List<SpecimenInfoDO> selectSpecimenTypeStatistics(int specimen_type);
+    /**
+     * 根据标本类别统计各类标本库存数量
+     *
+     * @return 各类标本的库存数量统计
+     */
+@Select("SELECT specimen_type, COUNT(*) AS count FROM museums_specimen_info " +
+        "WHERE deleted = 0 " +
+        "GROUP BY specimen_type")
+List<Map<String, Object>> selectAllSpecimenTypeStatistics();
+
+    /**
+     * 根据年份统计标本来源增减情况
+     *
+     * @param year 年份
+     * @return 标本来源统计信息列表
+     */
+    @Select("SELECT source, COUNT(*) as count " +
+            "FROM museums_specimen_info " +
+            "WHERE YEAR(acquisition_time) = #{year} AND deleted = 0 " +
+            "GROUP BY source")
+    List<SpecimenInfoDO> selectSpecimenSourceStatistics(int year);
+
+    //根据出、回、入库登记统计标本历年增减情况
+    @Select("SELECT * FROM museums_specimen_info WHERE deleted = 0")
+    List<SpecimenInfoDO> getInStockRecords();
+
+    //标本库管理
+    //实现对标本操作记录进行追溯查看,包括入库记录、编辑记录、出库记录、回库记录等。
+    default List<SpecimenInfoDO> getSpecimenRecords(Long id) {
+        MPJLambdaWrapper<SpecimenInfoDO> wrapper = new MPJLambdaWrapper<>();
+        wrapper.selectAll(SpecimenInfoDO.class)
+                //出库申请记录
+                .selectAs(SpecimenOutboundDO::getCreator, SpecimenInfoDO::getOutboundCreator)//创建者
+                .selectAs(SpecimenOutboundDO::getCreateTime, SpecimenInfoDO::getApplicantCreateTime)//创建时间
+                .selectAs(SpecimenOutboundDO::getApplicantName, SpecimenInfoDO::getApplicantName)//申请人或申请单位
+                .selectAs(SpecimenOutboundDO::getApplicationUsage, SpecimenInfoDO::getApplicationUsage)//申请出库的用途
+                //出库审批记录
+                .selectAs(SpecimenOutboundDO::getApproveUsers, SpecimenInfoDO::getApproveUsers)//审批员
+                .selectAs(SpecimenOutboundDO::getApprovalTime, SpecimenInfoDO::getApprovalTime)//审批时间
+                .selectAs(SpecimenOutboundDO::getProcessInstanceId, SpecimenInfoDO::getProcessInstanceId)//驳回原因
+                //出库记录
+                .selectAs(SpecimenOutboundDO::getOperator, SpecimenInfoDO::getOperator)//出库员
+                .selectAs(SpecimenOutboundDO::getOutgoingTime, SpecimenInfoDO::getOutgoingTime)//出库时间
+                //回库记录
+                .selectAs(SpecimenOutboundDO::getReturner, SpecimenInfoDO::getReturner)//退还人
+                .selectAs(SpecimenOutboundDO::getReceiver, SpecimenInfoDO::getReceiver)//点收人
+                .selectAs(SpecimenOutboundDO::getReturnDate, SpecimenInfoDO::getReturnDate)//退还日期
+
+                .leftJoin(SpecimenOutboundDO.class, SpecimenOutboundDO::getInfoId, SpecimenInfoDO::getId)
+                .in(SpecimenOutboundDO::getInfoId, id); // 使用in条件
+
+        return selectJoinList(SpecimenInfoDO.class, wrapper);
+    }
+
+    default SpecimenInfoDO selectBySpecimenNumber(String specimenNumber){
+        return selectOne(SpecimenInfoDO::getSpecimenNumber,specimenNumber);
+    }
+
+    @Select("SELECT * FROM museums_specimen_info WHERE image_name = #{imageName}")
+    SpecimenInfoDO selectByImageName(@Param("imageName") String imageName);
+
+
+
+    SpecimenInfoDO selectById(Long id);
+
+    // 根据多个 ID 查询标本信息
+    List<SpecimenInfoDO> selectByIds(List<Long> ids);
 }

+ 78 - 2
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/dal/mysql/specimenoutbound/SpecimenOutboundMapper.java

@@ -8,6 +8,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound.SpecimenOutboundDO;
 import org.apache.ibatis.annotations.Mapper;
 import cn.iocoder.yudao.module.museums.controller.admin.specimenoutbound.vo.*;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * 标本出库回库信息 Mapper
@@ -20,8 +22,8 @@ public interface SpecimenOutboundMapper extends BaseMapperX<SpecimenOutboundDO>
     default PageResult<SpecimenOutboundDO> selectPage(SpecimenOutboundPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<SpecimenOutboundDO>()
                 .eqIfPresent(SpecimenOutboundDO::getInfoId, reqVO.getInfoId())
-                .likeIfPresent(SpecimenOutboundDO::getChineseName, reqVO.getChineseName())
-                .eqIfPresent(SpecimenOutboundDO::getSpecimenNumber, reqVO.getSpecimenNumber())
+                .likeIfPresent(SpecimenOutboundDO::getChinese, reqVO.getChinese())
+                .eqIfPresent(SpecimenOutboundDO::getNumber, reqVO.getNumber())
                 .likeIfPresent(SpecimenOutboundDO::getApplicantName, reqVO.getApplicantName())
                 .betweenIfPresent(SpecimenOutboundDO::getApplicationDate, reqVO.getApplicationDate())
                 .eqIfPresent(SpecimenOutboundDO::getApplicationUsage, reqVO.getApplicationUsage())
@@ -38,7 +40,81 @@ public interface SpecimenOutboundMapper extends BaseMapperX<SpecimenOutboundDO>
                 .eqIfPresent(SpecimenOutboundDO::getReceiver, reqVO.getReceiver())
                 .betweenIfPresent(SpecimenOutboundDO::getReturnDate, reqVO.getReturnDate())
                 .betweenIfPresent(SpecimenOutboundDO::getApprovalTime, reqVO.getApprovalTime())
+                .eqIfPresent(SpecimenOutboundDO::getApproveUsers, reqVO.getApproveUsers())
                 .orderByDesc(SpecimenOutboundDO::getId));
+
     }
 
+
+    /**
+     * 根据年份统计已出库和已回库的标本信息
+     *
+     * @param year 年份
+     * @return 标本出库信息列表
+     */
+    @Select("SELECT * FROM museums_specimen_outbound " +
+            "WHERE (status = 3 OR status = 4) " +
+            "AND YEAR(application_date) = #{year} " +
+            "AND deleted = 0")
+    List<SpecimenOutboundDO> selectOutgoingStatisticsByYear(int year);
+
+    /**
+     * 根据年份统计已出库和已回库的标本信息
+     *
+     * @param year 年份
+     * @return 标本回库信息列表
+     */
+    @Select("SELECT * FROM museums_specimen_outbound " +
+            "WHERE (status = 4) " +
+            "AND YEAR(application_date) = #{year} " +
+            "AND deleted = 0")
+    List<SpecimenOutboundDO> selectReturnStatisticsByYear(int year);
+
+    //根据出、回、入库登记统计标本历年增减情况
+    @Select("SELECT * FROM museums_specimen_outbound WHERE deleted = 0")
+    List<SpecimenOutboundDO> getOutboundRecords();
+
+//    @Select({
+//            "<script>",
+//            "SELECT so.*, si.specimen_number, si.image_path, si.storage_location ",
+//            "FROM museums_specimen_outbound so ",
+//            "JOIN museums_specimen_info si ON so.info_id = si.id ",
+//            "WHERE so.info_id IN ",
+//            "<foreach item='id' collection='infoIds' open='(' separator=',' close=')'>",
+//            "#{id}",
+//            "</foreach>",
+//            "</script>"
+//    })
+//    List<SpecimenOutboundDO> selectByIds(@Param("infoIds") List<Long> infoIds);
+
+    @Select({
+            "<script>",
+            "SELECT so.*, si.specimen_number, si.image_path, si.storage_location ",
+            "FROM museums_specimen_outbound so ",
+            "JOIN museums_specimen_info si ON so.info_id = si.id ",
+            "WHERE so.info_id IN ",
+            "<foreach item='id' collection='infoIds' open='(' separator=',' close=')'>",
+            "#{id}",
+            "</foreach>",
+            "</script>"
+    })
+    List<SpecimenOutboundDO> selectByIds(@Param("infoIds") List<Long> infoIds);
+
+
+    @Select("SELECT * FROM museums_specimen_outbound WHERE id = #{outboundId}")
+    SpecimenOutboundDO selectById(@Param("outboundId") Long outboundId);
+
+
+    @Select({
+            "<script>",
+            "SELECT si.specimen_number, si.image_path, si.storage_location ",
+            "FROM museums_specimen_outbound so ",
+            "JOIN museums_specimen_info si ON so.info_id = si.id ",
+            "WHERE so.outbound_order_id = #{outboundOrderId}",
+            "</script>"
+    })
+    List<SpecimenOutboundDO> selectByOutboundOrderId(@Param("outboundOrderId") Long outboundOrderId);
+
+
+
 }

+ 10 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/photogroup/PhotoGroupService.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.museums.controller.admin.photogroup.vo.*;
 import cn.iocoder.yudao.module.museums.dal.dataobject.photogroup.PhotoGroupDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 博物馆照片组 Service 接口
@@ -22,6 +23,15 @@ public interface PhotoGroupService {
      */
     Integer createPhotoGroup(@Valid PhotoGroupSaveReqVO createReqVO);
 
+    /**
+     * 保存照片
+     *
+     * @param file 照片压缩包
+     * @param groupId 照片组ID
+     */
+    void savePhotos(MultipartFile file, Integer groupId);
+
+
     /**
      * 更新博物馆照片组
      *

+ 81 - 1
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimeninfo/SpecimenInfoService.java

@@ -5,7 +5,8 @@ import javax.validation.*;
 import cn.iocoder.yudao.module.museums.controller.admin.specimeninfo.vo.*;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound.SpecimenOutboundDO;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 标本管理 Service 接口
@@ -52,4 +53,83 @@ public interface SpecimenInfoService {
      */
     PageResult<SpecimenInfoDO> getSpecimenInfoPage(SpecimenInfoPageReqVO pageReqVO);
 
+    /**
+     * 批量导入标本
+     *
+     * @param importSpecimens     导入标本列表
+     * @param isUpdateSupport     是否支持更新
+     * @return 导入结果
+     */
+    SpecimenImportRespVO importSpecimenList(List<SpecimenImportExcelVO> importSpecimens, boolean isUpdateSupport);
+
+    /**
+     * 批量导入标本图片
+     *
+     * @param file 压缩包文件
+     * @return 导入结果
+     */
+    String importSpecimenImages(MultipartFile file) throws Exception;
+
+    void importSpecimenData(MultipartFile excelFile, MultipartFile imageFile, boolean updateSupport) throws Exception;
+
+    //工作台
+    /**
+     * 统计本年标本入库信息
+     *
+     * @param year 年份
+     * @return 标本入库信息列表
+     */
+    List<SpecimenInfoDO> getEntryStatistics(int year);
+    /**
+     * 按标本类别统计库存数
+     *
+     * @param specimen_type 标本类别
+     * @return 标本类别信息列表
+     */
+    List<SpecimenInfoDO> getSpecimenTypeStatistics(int specimen_type);
+    /**
+     * 按标本类别统计所有库存数量
+     *
+     * @return 各类标本的库存数量统计
+     */
+     List<Map<String, Object>> getAllSpecimenTypeStatistics();
+
+    /**
+     * 根据出、回、入库登记统计标本历年增减情况
+     *
+     * @return 年份与标本统计信息的映射
+     */
+    Map<Integer, Map<String, Integer>> getYearlySpecimenStatistics();
+    /**
+     * 根据标本来源统计历年标本登记情况
+     *
+     * @return 年份与标本来源统计信息的映射
+     */
+    Map<Integer, Map<String, Integer>> getYearlySpecimenSourceStatistics();
+
+
+    //不好用,准备废弃
+    /**
+     * 历年标本来源增减统计
+     *
+     * @param year 年份
+     * @return 标本来源统计信息列表
+     */
+    List<SpecimenInfoDO> getSpecimenSourceStatistics(int year);
+
+
+    //标本库管理
+    //实现对标本操作记录进行追溯查看,包括入库记录、编辑记录、出库记录、回库记录等。
+    /**
+     * 历年标本来源增减统计
+     *
+     * @param id 标本总表id
+     * @return 标本操作记录
+     */
+    List<SpecimenInfoDO> getSpecimenRecords(Long id);
+
+
+
+
+
 }

+ 270 - 2
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimeninfo/SpecimenInfoServiceImpl.java

@@ -1,21 +1,35 @@
 package cn.iocoder.yudao.module.museums.service.specimeninfo;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.infra.api.file.FileApi;
+import cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound.SpecimenOutboundDO;
+import cn.iocoder.yudao.module.museums.dal.mysql.specimenoutbound.SpecimenOutboundMapper;
+import org.apache.tomcat.util.http.fileupload.FileUtils;
 import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
+
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
 
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
 import cn.iocoder.yudao.module.museums.controller.admin.specimeninfo.vo.*;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 
 import cn.iocoder.yudao.module.museums.dal.mysql.specimeninfo.SpecimenInfoMapper;
+import org.springframework.web.multipart.MultipartFile;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.museums.enums.ErrorCodeConstants.*;
+import cn.hutool.core.io.IoUtil;
 
 /**
  * 标本管理 Service 实现类
@@ -28,6 +42,18 @@ public class SpecimenInfoServiceImpl implements SpecimenInfoService {
 
     @Resource
     private SpecimenInfoMapper specimenInfoMapper;
+    @Resource
+    private SpecimenOutboundMapper specimenOutboundMapper;
+    @Resource
+    private FileApi fileApi;
+
+    public SpecimenInfoServiceImpl(SpecimenInfoMapper specimenInfoMapper, FileApi fileApi) {
+        this.specimenInfoMapper = specimenInfoMapper;
+        this.fileApi = fileApi;
+    }
+
+    @Resource
+    private SpecimenInfoDO specimenInfoDO;
 
     @Override
     public Integer createSpecimenInfo(SpecimenInfoSaveReqVO createReqVO) {
@@ -71,4 +97,246 @@ public class SpecimenInfoServiceImpl implements SpecimenInfoService {
         return specimenInfoMapper.selectPage(pageReqVO);
     }
 
+    //标本Excel的批量导入
+    @Override
+    @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
+    public SpecimenImportRespVO importSpecimenList(List<SpecimenImportExcelVO> importSpecimens, boolean isUpdateSupport) {
+// 1.1 参数校验
+        if (CollUtil.isEmpty(importSpecimens)) {
+            throw exception(SPECIMEN_INFO_LIST_IS_EMPTY);
+        }
+
+        // 2. 遍历,逐个创建或更新
+        SpecimenImportRespVO respVO = SpecimenImportRespVO.builder()
+                .createSpecimenNumbers(new ArrayList<>())
+                .updateSpecimenNumbers(new ArrayList<>())
+                .failureSpecimenNumbers(new LinkedHashMap<>()).build();
+
+        importSpecimens.forEach(importSpecimen -> {
+            // 校验逻辑可以根据需要添加
+
+            // 2.1. 判断是否存在
+            SpecimenInfoDO existSpecimen = specimenInfoMapper.selectBySpecimenNumber(importSpecimen.getSpecimenNumber());
+
+            // 1. 校验图片名格式
+            if (!isValidImageName(importSpecimen.getImageName())) {
+                respVO.getFailureSpecimenNumbers().put(importSpecimen.getSpecimenNumber(), "图片名称格式不正确");
+                return;
+            }
+
+            if (existSpecimen == null) {
+                // 2.2.1 不存在则插入
+                SpecimenInfoDO newSpecimen = BeanUtils.toBean(importSpecimen, SpecimenInfoDO.class);
+                specimenInfoMapper.insert(newSpecimen);
+                respVO.getCreateSpecimenNumbers().add(importSpecimen.getSpecimenNumber());
+                return;
+            }
+
+            // 2.2.2 如果存在,判断是否允许更新
+//            if (!isUpdateSupport) {
+//                respVO.getFailureSpecimenNumbers().put(importSpecimen.getSpecimenNumber(), "标本编号已存在,且不支持更新");
+//                return;
+//            }
+
+            // 更新逻辑
+            SpecimenInfoDO updateSpecimen = BeanUtils.toBean(importSpecimen, SpecimenInfoDO.class);
+            updateSpecimen.setId(existSpecimen.getId()); // 设置 ID 进行更新
+            specimenInfoMapper.updateById(updateSpecimen);
+            respVO.getUpdateSpecimenNumbers().add(importSpecimen.getSpecimenNumber());
+        });
+
+        return respVO;
+    }
+    private boolean isValidImageName(String imageName) {
+        return imageName != null && imageName.matches(".*\\.(jpg|jpeg|png|gif)$"); // 检查格式
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
+    public String importSpecimenImages(MultipartFile file) throws Exception {
+        // 校验文件类型
+        if (!file.getOriginalFilename().endsWith(".zip")) {
+            throw exception(UPLOADED_FOLDER_CANNOT_EMPTY);
+        }
+        // 创建临时目录存放解压后的文件
+        File tempDir = Files.createTempDirectory("specimen_images").toFile();
+        try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream())) {
+            ZipEntry entry;
+            while ((entry = zipInputStream.getNextEntry()) != null) {
+                if (!entry.isDirectory()) {
+                    File newFile = new File(tempDir, entry.getName());
+                    // 进行解压
+                    try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(newFile))) {
+                        byte[] buffer = new byte[1024];
+                        int len;
+                        while ((len = zipInputStream.read(buffer)) > 0) {
+                            bos.write(buffer, 0, len);
+                        }
+                    }
+                }
+            }
+        }
+        // 处理每个图片文件
+        File[] imageFiles = tempDir.listFiles();
+        if (imageFiles != null) {
+            for (File imageFile : imageFiles) {
+                String imageName = imageFile.getName();
+                if (!isValidImageName(imageName)) {
+                    // 如果不符合格式,抛出异常或记录日志
+                    System.err.println("无效的图片格式: " + imageName);
+                    continue; // 或者 throw new Exception("无效的图片格式: " + imageName);
+                }
+                // 根据图片名称查找对应的标本
+                SpecimenInfoDO specimenInfo = specimenInfoMapper.selectByImageName(imageName);
+                if (specimenInfo != null) {
+                    // 上传图片并获取URL
+                    String imagePath = fileApi.createFile(Files.readAllBytes(imageFile.toPath()));
+                    // 更新标本信息中的图片路径
+                    specimenInfo.setImagePath(imagePath);
+                    specimenInfoMapper.updateById(specimenInfo);
+                }
+            }
+        }
+
+        // 清理临时文件
+        FileUtils.deleteDirectory(tempDir);
+        return "标本图片导入成功";
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class) // 事务管理
+    public void importSpecimenData(MultipartFile excelFile, MultipartFile imageFile, boolean updateSupport) throws Exception {
+        // 1. 导入标本
+        List<SpecimenImportExcelVO> list = ExcelUtils.read(excelFile, SpecimenImportExcelVO.class);
+        SpecimenImportRespVO importRespVO = importSpecimenList(list, updateSupport);
+
+        // 2. 导入图片
+        String imageImportResult = importSpecimenImages(imageFile);
+
+        // 可以根据需要记录导入结果
+        System.out.println("标本导入结果: " + importRespVO);
+        System.out.println("标本图片导入结果: " + imageImportResult);
+    }
+
+
+    //工作台
+    //根据入库的登记情况统计本年标本入库信息
+    @Override
+    public List<SpecimenInfoDO> getEntryStatistics(int year) {
+        return specimenInfoMapper.selectEntryStatisticsByYear(year);
+    }
+
+    //按标本类别统计库存数
+    @Override
+    public List<SpecimenInfoDO> getSpecimenTypeStatistics(int specimen_type) {
+        return specimenInfoMapper.selectSpecimenTypeStatistics(specimen_type);
+    }
+    @Override
+    public List<Map<String, Object>> getAllSpecimenTypeStatistics() {
+        return specimenInfoMapper.selectAllSpecimenTypeStatistics();
+    }
+
+
+    //根据出、回、入库登记统计标本历年增减情况
+    @Override
+    public Map<Integer, Map<String, Integer>> getYearlySpecimenStatistics() {
+        // 年份与标本统计信息的映射
+        Map<Integer, Map<String, Integer>> yearlyDataMap = new HashMap<>();
+
+        // 获取入库记录
+        List<SpecimenInfoDO> inStockRecords = specimenInfoMapper.getInStockRecords();
+        if (inStockRecords == null) {
+            inStockRecords = new ArrayList<>(); // 初始化为空列表
+        }
+        for (SpecimenInfoDO record : inStockRecords) {
+            if (record.getCreateTime() != null) { // 检查时间字段
+                int year = record.getCreateTime().getYear(); // 获取年份
+                yearlyDataMap.putIfAbsent(year, new HashMap<>());
+                yearlyDataMap.get(year).put("inStockCount", yearlyDataMap.get(year).getOrDefault("inStockCount", 0) + 1);
+            }
+        }
+
+        // 获取出库记录
+        List<SpecimenOutboundDO> outboundRecords = specimenOutboundMapper.getOutboundRecords();
+        if (outboundRecords == null) {
+            outboundRecords = new ArrayList<>(); // 初始化为空列表
+        }
+        for (SpecimenOutboundDO record : outboundRecords) {
+            if (record.getOutgoingTime() != null) { // 检查时间字段
+                int year = record.getOutgoingTime().getYear(); // 获取年份
+                yearlyDataMap.putIfAbsent(year, new HashMap<>());
+                yearlyDataMap.get(year).put("outStockCount", yearlyDataMap.get(year).getOrDefault("outStockCount", 0) + 1);
+            }
+        }
+
+        // 获取回库记录
+        List<SpecimenOutboundDO> inboundRecords = specimenOutboundMapper.getOutboundRecords();
+        if (inboundRecords == null) {
+            inboundRecords = new ArrayList<>(); // 初始化为空列表
+        }
+        for (SpecimenOutboundDO record : inboundRecords) {
+            if (record.getReturnDate() != null) { // 检查时间字段
+                int year = record.getReturnDate().getYear(); // 获取年份
+                yearlyDataMap.putIfAbsent(year, new HashMap<>());
+                yearlyDataMap.get(year).put("returnCount", yearlyDataMap.get(year).getOrDefault("returnCount", 0) + 1);
+            }
+        }
+
+        return yearlyDataMap;
+    }
+
+    @Override
+    public Map<Integer, Map<String, Integer>> getYearlySpecimenSourceStatistics() {
+        // 年份与标本来源统计信息的映射
+        Map<Integer, Map<String, Integer>> yearlySourceDataMap = new HashMap<>();
+
+        // 获取入库记录
+        List<SpecimenInfoDO> inStockRecords = specimenInfoMapper.getInStockRecords();
+        if (inStockRecords == null) {
+            inStockRecords = new ArrayList<>(); // 初始化为空列表
+        }
+
+        // 统计入库记录
+        for (SpecimenInfoDO record : inStockRecords) {
+            if (record != null && record.getCreateTime() != null) { // 检查 record 和 createTime
+                int year = record.getCreateTime().getYear(); // 获取年份
+                yearlySourceDataMap.putIfAbsent(year, new HashMap<>());
+
+                // 根据标本来源进行统计
+                String sourceKey = getSourceKey(record.getSource());
+                yearlySourceDataMap.get(year).put(sourceKey, yearlySourceDataMap.get(year).getOrDefault(sourceKey, 0) + 1);
+            }
+        }
+
+        return yearlySourceDataMap;
+    }
+
+    // Helper 方法将整数源转换为字符串
+    private String getSourceKey(Integer source) { // 使用 Integer 允许 null
+        if (source == null) return "未知"; // 处理 null 的情况
+        switch (source) {
+            case 0: return "采购";
+            case 1: return "捐赠";
+            case 2: return "采集";
+            default: return "其他";
+        }
+    }
+
+
+    //根据入馆凭证中标本来源的登记情况统计
+    //不好用,准备废弃
+    @Override
+    public List<SpecimenInfoDO> getSpecimenSourceStatistics(int year) {
+        return specimenInfoMapper.selectSpecimenSourceStatistics(year);
+    }
+
+    //标本库管理
+    //实现对标本操作记录进行追溯查看,包括入库记录、编辑记录、出库记录、回库记录等。
+    @Override
+    public List<SpecimenInfoDO> getSpecimenRecords(Long id) {
+        return specimenInfoMapper.getSpecimenRecords(id);
+    }
+
+
+
 }

+ 51 - 18
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimenoutbound/SpecimenOutboundService.java

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.museums.service.specimenoutbound;
 
 import javax.validation.*;
 import cn.iocoder.yudao.module.museums.controller.admin.specimenoutbound.vo.*;
+import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
 import cn.iocoder.yudao.module.museums.dal.dataobject.specimenoutbound.SpecimenOutboundDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 标本出库回库信息 Service 接口
@@ -22,13 +24,6 @@ public interface SpecimenOutboundService {
      */
     Long createSpecimenOutbound(@Valid SpecimenOutboundSaveReqVO createReqVO);
 
-//    /**
-//     * 创建标本出库回库信息
-//     *
-//     * @param returnReqVO 创建信息
-//     */
-//    void approvalSpecimenOutbound(@Valid SpecimenOutboundConfirmReqVO returnReqVO);
-
     /**
      * 标本回库
      *
@@ -36,11 +31,6 @@ public interface SpecimenOutboundService {
      */
     void updateSpecimenOutbound(@Valid SpecimenOutboundReturnReqVO updateReqVO);
 
-//    //确认出库
-//    void confirmOutbound(Long id, String operator);
-
-//    Object confirmOutbound(SpecimenOutboundSaveReqVO confirmReqVO);
-
     /**
      * 确认标本出库
      *
@@ -49,6 +39,14 @@ public interface SpecimenOutboundService {
      */
     void confirmOutbound(Long id, Long operator);
 
+    /**
+     * 更新标本信息中的馆藏状态
+     *
+     * @param id 标本ID
+     */
+    void updateSpecimenInfoLendStatus(Long id);
+    void updateSpecimenInfoExistStatus(Long id);
+
     /**
      * 删除标本出库回库信息
      *
@@ -63,6 +61,14 @@ public interface SpecimenOutboundService {
      * @return 标本出库回库信息
      */
     SpecimenOutboundDO getSpecimenOutbound(Long id);
+    /**
+     * 获得标本出库回库信息
+     *
+     * @param id 编号
+     * @return 标本出库回库信息
+     */
+    SpecimenOutboundDO getSpecimenReturnInformation(Long id);
+
 
     /**
      * 获得标本出库回库信息分页
@@ -72,15 +78,42 @@ public interface SpecimenOutboundService {
      */
     PageResult<SpecimenOutboundDO> getSpecimenOutboundPage(SpecimenOutboundPageReqVO pageReqVO);
 
+//    /**
+//     * 更新标本审批状态
+//     *
+//     * @param id              编号
+//     * @param status          状态(2:审批通过,3:审批驳回)
+//     * @param approvalTime    审批时间
+//     * @param processInstanceId 驳回原因
+//     */
+    void updateStatus(SpecimenOutboundApprovalReqVO req);
+
+    //工作台
     /**
-     * 更新标本审批状态
+     * 根据年份获取标本出库统计信息
      *
-     * @param id              编号
-     * @param status          状态(2:审批通过,3:审批驳回)
-     * @param approvalTime    审批时间
-     * @param processInstanceId 驳回原因
+     * @param year 年份
+     * @return 标本出库信息列表
+     */
+    List<SpecimenOutboundDO> getOutboundStatistics(int year);
+
+    /**
+     * 根据回库的登记情况统计本年标本回库信息
+     * @param year 年份
+     * @return 标本回库信息列表
      */
-    void updateStatus(Long id, Integer status, LocalDateTime approvalTime, String processInstanceId);
+    List<SpecimenOutboundDO> getReturnStatistics(int year);
+
+
+    /**
+     * 根据出库信息ID获得标本详细信息
+     * @param id 出库信息编号
+     * @return 标本出库回库信息及相关标本信息
+     */
+    SpecimenOutboundWithInfoRespVO getSpecimenOutboundWithInfo(Long id);
+
+
+
 
 
 }

+ 156 - 65
yudao-module-museums/yudao-module-museums-biz/src/main/java/cn/iocoder/yudao/module/museums/service/specimenoutbound/SpecimenOutboundServiceImpl.java

@@ -1,5 +1,9 @@
 package cn.iocoder.yudao.module.museums.service.specimenoutbound;
 
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.museums.dal.dataobject.specimeninfo.SpecimenInfoDO;
+import cn.iocoder.yudao.module.museums.dal.mysql.specimeninfo.SpecimenInfoMapper;
+import org.apache.commons.beanutils.BeanUtilsBean;
 import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 
@@ -13,10 +17,15 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.museums.dal.mysql.specimenoutbound.SpecimenOutboundMapper;
 
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.module.museums.enums.ErrorCodeConstants.*;
 
+
 /**
  * 标本出库回库信息 Service 实现类
  *
@@ -29,6 +38,9 @@ public class SpecimenOutboundServiceImpl implements SpecimenOutboundService {
     @Resource
     private SpecimenOutboundMapper specimenOutboundMapper;
 
+    @Resource
+    private SpecimenInfoMapper specimenInfoMapper; // 引入标本信息的Mapper
+
     //创建出库申请
     @Override
     public Long createSpecimenOutbound(SpecimenOutboundSaveReqVO createReqVO) {
@@ -39,15 +51,6 @@ public class SpecimenOutboundServiceImpl implements SpecimenOutboundService {
         return specimenOutbound.getId();
     }
 
-//    @Override
-//    public Integer approvalSpecimenOutbound(SpecimenOutboundConfirmReqVO approvalReqVO) {
-//        // 插入
-//        SpecimenOutboundDO specimenOutbound = BeanUtils.toBean(approvalReqVO, SpecimenOutboundDO.class);
-//        specimenOutboundMapper.insert(specimenOutbound);
-//        // 返回
-//        return specimenOutbound.getId();
-//    }
-
     //用来填写回库表单,为方便添加数据,现在先用上面这段
 //    @Override
 //    public void updateSpecimenOutbound(SpecimenOutboundSaveReqVO updateReqVO) {
@@ -57,7 +60,7 @@ public class SpecimenOutboundServiceImpl implements SpecimenOutboundService {
 //        SpecimenOutboundDO updateObj = BeanUtils.toBean(updateReqVO, SpecimenOutboundDO.class);
 //        specimenOutboundMapper.updateById(updateObj);
 //    }
-    //测试成功
+    //填写回库表单测试成功
     @Override
     public void updateSpecimenOutbound(SpecimenOutboundReturnReqVO updateReqVO) {
         // 校验存在
@@ -85,75 +88,45 @@ public class SpecimenOutboundServiceImpl implements SpecimenOutboundService {
 
         // 更新数据库
         specimenOutboundMapper.updateById(existingSpecimen);
-    }
-
-    //待定
-//    @Override
-//    public void approvalSpecimenOutbound(SpecimenOutboundConfirmReqVO approvalReqVO) {
-//        // 校验存在
-//        validateSpecimenOutboundExists(approvalReqVO.getId());
-//        // 更新
-//        SpecimenOutboundDO approvalObj = BeanUtils.toBean(approvalReqVO, SpecimenOutboundDO.class);
-//        specimenOutboundMapper.updateById(approvalObj);
-//    }
 
+        // 更新标本信息的馆藏状态为0(在馆)
+        updateSpecimenInfoExistStatus(Long.valueOf(existingSpecimen.getInfoId()));
+    }
+    @Override
+    public void updateSpecimenInfoExistStatus(Long id) {
+        // 更新馆藏状态为0(在馆)
+        specimenInfoMapper.updateCollectionStatus(id, 0);
+    }
 
     //出库审批
     @Override
-    public void updateStatus(Long id, Integer status, LocalDateTime approvalTime, String processInstanceId) {
-        SpecimenOutboundDO specimenOutbound = specimenOutboundMapper.selectById(id);
-//        if (specimenOutbound == null) {
-//            throw new EntityNotFoundException("标本出库记录未找到");
-//        }
-        // 获取当前标本的状态
-        SpecimenOutboundDO existingSpecimen = specimenOutboundMapper.selectById(id);
+    public void updateStatus(SpecimenOutboundApprovalReqVO req) {
+        // 从数据库获取标本出库记录
+        SpecimenOutboundDO specimenOutbound = specimenOutboundMapper.selectById(req.getId());
 
-        // 判断状态是否为审批中状态(1
-        if (existingSpecimen == null || existingSpecimen.getStatus() != 0) {
+        // 判断状态是否为审批中状态(0)
+        if (specimenOutbound == null || specimenOutbound.getStatus() != 0) {
             throw new IllegalStateException("只有状态为审批中的标本才能进行审批");
         }
 
         // 更新状态
-        specimenOutbound.setStatus(status);
-
-        if (status == 1) { // 审批通过
-            specimenOutbound.setApprovalTime(approvalTime); // 设置当前审批时间
-        } else if (status == 2) { // 审批驳回
-            specimenOutbound.setProcessInstanceId(processInstanceId);
-            specimenOutbound.setApprovalTime(approvalTime); // 设置当前驳回时间
+        specimenOutbound.setStatus(req.getStatus());
+        if (req.getStatus() == 1) { // 审批通过
+            specimenOutbound.setApprovalTime(req.getApprovalTime()); // 设置当前审批时间
+        } else if (req.getStatus() == 2) { // 审批驳回
+            specimenOutbound.setProcessInstanceId(req.getProcessInstanceId());
+            specimenOutbound.setApprovalTime(req.getApprovalTime()); // 设置当前驳回时间
         }
 
+        // 获取当前用户 ID
+        SecurityFrameworkUtils UserContext = null;
+        Long userId = UserContext.getLoginUserId(); // 通过方法获取当前用户 ID
+        specimenOutbound.setApproveUsers(userId); // 设置操作员 ID
+
+        // 更新数据库
         specimenOutboundMapper.updateById(specimenOutbound);
     }
 
-    //确认出库
-//    @Override
-//    public void confirmOutbound(Long id, String operator) {
-//        // 校验存在
-//        validateSpecimenOutboundExists((long) Math.toIntExact(id));
-//
-//        // 获取当前标本的状态
-//        SpecimenOutboundDO existingSpecimen = specimenOutboundMapper.selectById(id);
-//
-//        // 判断状态是否为审批通过(3)
-//        if (existingSpecimen == null || existingSpecimen.getStatus() != 3) {
-//            throw new IllegalStateException("只能确认状态为审批通过的标本出库单");
-//        }
-//
-//        // 更新状态为已出库(5)
-//        existingSpecimen.setStatus(5);  // 设置为已出库
-//        existingSpecimen.setOperator(Long.valueOf(operator));  // 保存出库员ID
-//        existingSpecimen.setOutgoingTime(LocalDateTime.now());  // 设置出库时间为当前时间
-//
-//        // 更新数据库
-//        specimenOutboundMapper.updateById(existingSpecimen);
-//    }
-
-//    @Override
-//    public Object confirmOutbound(SpecimenOutboundSaveReqVO confirmReqVO) {
-//        return null;
-//    }
-
     //确认出库
     @Override
     public void confirmOutbound(Long id, Long operator) {
@@ -177,6 +150,14 @@ public class SpecimenOutboundServiceImpl implements SpecimenOutboundService {
 
         // 更新数据库
         specimenOutboundMapper.updateById(existingSpecimen);
+
+        // 更新标本信息的馆藏状态为1(借出)
+        updateSpecimenInfoLendStatus(Long.valueOf(existingSpecimen.getInfoId()));
+    }
+    @Override
+    public void updateSpecimenInfoLendStatus(Long id) {
+        // 更新馆藏状态为1(借出)
+        specimenInfoMapper.updateCollectionStatus(id, 1);
     }
 
     @Override
@@ -198,9 +179,119 @@ public class SpecimenOutboundServiceImpl implements SpecimenOutboundService {
         return specimenOutboundMapper.selectById(id);
     }
 
+    @Override
+    public SpecimenOutboundDO getSpecimenReturnInformation(Long id) {
+        // 获取标本出库回库信息
+        SpecimenOutboundDO specimenOutbound = specimenOutboundMapper.selectById(id);
+
+        // 检查审批状态是否为4(已回库)
+        if (specimenOutbound != null && specimenOutbound.getStatus() == 4) {
+            return specimenOutbound;
+        } else {
+            // 如果审批状态不是4,抛出异常
+            throw exception(NO_PERMISSION_VIEW_NON_RETURNED_SPECIMENS);
+        }
+    }
+
     @Override
     public PageResult<SpecimenOutboundDO> getSpecimenOutboundPage(SpecimenOutboundPageReqVO pageReqVO) {
         return specimenOutboundMapper.selectPage(pageReqVO);
     }
 
+    //工作台
+    //根据出库的登记情况统计本年标本出库信息
+    @Override
+    public List<SpecimenOutboundDO> getOutboundStatistics(int year) {
+        return specimenOutboundMapper.selectOutgoingStatisticsByYear(year);
+    }
+
+    //根据回库的登记情况统计本年标本回库信息
+    @Override
+    public List<SpecimenOutboundDO> getReturnStatistics(int year) {
+        return specimenOutboundMapper.selectReturnStatisticsByYear(year);
+    }
+
+
+//    @Override
+//    public List<SpecimenOutboundDO> getSpecimenOutboundsByOutboundOrderId(Long outboundOrderId) {
+//        return specimenOutboundMapper.selectByOutboundOrderId(outboundOrderId);
+//    }
+
+
+//    @Override
+//    public SpecimenOutboundWithInfoRespVO getSpecimenOutboundWithInfo(Long id) {
+//        SpecimenOutboundDO specimenOutbound = specimenOutboundMapper.selectById(id);
+//
+//        if (specimenOutbound == null) {
+//            return null; // 或抛出自定义异常
+//        }
+//
+//
+//        String infoId = specimenOutbound.getInfoId();
+//        SpecimenInfoDO specimenInfo = specimenInfoMapper.selectById(infoId);
+//
+//        SpecimenOutboundWithInfoRespVO response = new SpecimenOutboundWithInfoRespVO();
+//
+//        try {
+//            BeanUtilsBean.getInstance().copyProperties(response, specimenOutbound);
+//        } catch (Exception e) {
+//            // 处理异常,例如记录日志
+//        }
+//
+//        if (specimenInfo != null) {
+//            response.setSpecimenNumber(specimenInfo.getSpecimenNumber());
+//            response.setImagePath(specimenInfo.getImagePath());
+//            response.setStorageLocation(specimenInfo.getStorageLocation());
+//        }
+//
+//        return response;
+//    }
+
+
+    @Override
+    public SpecimenOutboundWithInfoRespVO getSpecimenOutboundWithInfo(Long id) {
+        // 查询出库信息
+        SpecimenOutboundDO specimenOutbound = specimenOutboundMapper.selectById(id);
+
+        // 如果没有找到相关出库信息,返回 null 或抛出异常
+        if (specimenOutbound == null) {
+            return null; // 或抛出自定义异常
+        }
+
+        // 查询关联的标本信息
+        String infoIds = specimenOutbound.getInfoId(); // 获取 info_id,假设是字符串
+        List<Long> idList = Arrays.stream(infoIds.split(","))
+                .map(Long::parseLong)
+                .collect(Collectors.toList());
+
+        // 查询所有关联的标本信息
+        List<SpecimenInfoDO> specimenInfoList = specimenInfoMapper.selectByIds(idList); // 假设你有这个 Mapper
+
+        // 创建返回对象
+        SpecimenOutboundWithInfoRespVO response = new SpecimenOutboundWithInfoRespVO();
+
+        // 手动复制出库信息属性
+        response.setId(specimenOutbound.getId());
+        response.setInfoId(specimenOutbound.getInfoId());
+        response.setChinese(specimenOutbound.getChinese());
+        response.setApplicantName(specimenOutbound.getApplicantName());
+        response.setApplicationDate(specimenOutbound.getApplicationDate());
+        response.setApplicationUsage(specimenOutbound.getApplicationUsage());
+        response.setImagePath(specimenOutbound.getImagePath());
+        response.setStorageLocation(specimenOutbound.getStorageLocation());
+        response.setSpecimenNumber(specimenOutbound.getSpecimenNumber());
+
+        // 设置所有关联的标本信息
+        response.setSpecimenInfoList(specimenInfoList);
+
+        return response;
+    }
+
+
+
+
+
+
+
+
 }

+ 47 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/resources/mapper/specimeninfo/SpecimenInfoMapper.xml

@@ -9,4 +9,51 @@
         文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
      -->
 
+<!--    &lt;!&ndash; 查询入库记录 &ndash;&gt;-->
+<!--    <select id="selectInboundRecords" parameterType="java.lang.Integer" resultType="InboundRecord">-->
+<!--        SELECT * FROM museums_inbound_records WHERE specimen_id = #{id}-->
+<!--    </select>-->
+
+<!--    &lt;!&ndash; 查询编辑记录 &ndash;&gt;-->
+<!--    <select id="selectEditRecords" parameterType="java.lang.Integer" resultType="EditRecord">-->
+<!--        SELECT updater, update_time-->
+<!--        FROM museums_specimen_info-->
+<!--        WHERE id = #{id}-->
+<!--    </select>-->
+
+<!--    &lt;!&ndash; 查询出库申请记录 &ndash;&gt;-->
+<!--    <select id="selectOutboundApplicationRecords" parameterType="java.lang.Integer" resultType="OutboundApplicationRecord">-->
+<!--        SELECT creator, create_time, applicant_name, usage AS application_usage-->
+<!--        FROM museums_specimen_outbound-->
+<!--        WHERE info_id = #{id}-->
+<!--    </select>-->
+
+<!--    &lt;!&ndash; 查询审批记录 &ndash;&gt;-->
+<!--    <select id="selectApprovalRecords" parameterType="java.lang.Integer" resultType="ApprovalRecord">-->
+<!--        SELECT approver, approval_time, process_instance_id-->
+<!--        FROM museums_specimen_outbound-->
+<!--        WHERE info_id = #{id}-->
+<!--    </select>-->
+
+<!--    &lt;!&ndash; 查询出库记录 &ndash;&gt;-->
+<!--    <select id="selectOutboundRecords" parameterType="java.lang.Integer" resultType="OutboundRecord">-->
+<!--        SELECT operator, outgoing_time-->
+<!--        FROM museums_specimen_outbound-->
+<!--        WHERE info_id = #{id}-->
+<!--    </select>-->
+
+<!--    &lt;!&ndash; 查询回库记录 &ndash;&gt;-->
+<!--    <select id="selectReturnRecords" parameterType="java.lang.Integer" resultType="ReturnRecord">-->
+<!--        SELECT returner, receiver, return_date, specimen_condition-->
+<!--        FROM museums_specimen_outbound-->
+<!--        WHERE info_id = #{id}-->
+<!--    </select>-->
+
+    <select id="selectByIds" >
+        SELECT * FROM museums_specimen_info WHERE id IN
+        <foreach item="id" collection="list" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
 </mapper>

+ 1 - 0
yudao-module-museums/yudao-module-museums-biz/src/main/resources/mapper/specimenoutbound/SpecimenOutboundMapper.xml

@@ -9,4 +9,5 @@
         文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
      -->
 
+
 </mapper>

+ 3 - 1
yudao-server/src/main/resources/application.yaml

@@ -8,6 +8,7 @@ spring:
   main:
     allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
 
+
   # Servlet 配置
   servlet:
     # 文件上传相关配置项
@@ -29,6 +30,8 @@ spring:
     redis:
       time-to-live: 1h # 设置过期时间为 1 小时
 
+
+
 server:
   servlet:
     encoding:
@@ -313,5 +316,4 @@ yudao:
       kd100:
         key: pLXUGAwK5305
         customer: E77DF18BE109F454A5CD319E44BF5177
-
 debug: false