소스 검색

重构教材确认发放业务处理,在发放业务中添加处理:学生确认领取记录生成

大数据与最优化研究所 5 달 전
부모
커밋
4438b08c8e

+ 19 - 0
src/main/java/com/xjrsoft/module/textbook/controller/TextbookStudentClaimController.java

@@ -2,6 +2,7 @@ package com.xjrsoft.module.textbook.controller;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import cn.hutool.core.bean.BeanUtil;
+import com.alibaba.excel.support.ExcelTypeEnum;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -21,9 +22,14 @@ import com.xjrsoft.module.textbook.vo.*;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
 import javax.validation.Valid;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 /**
@@ -219,4 +225,17 @@ public class TextbookStudentClaimController {
     public RT<Boolean> delete(@Valid @RequestBody List<Long> ids) {
         return RT.ok(textbookStudentClaimService.removeBatchByIds(ids));
     }
+
+    @PostMapping("/all-stu-claim-export-query")
+    @ApiOperation(value = "某一学期全校学生领取记录条件导出")
+    @XjrLog(value = "某一学期全校学生领取记录条件导出")
+    public ResponseEntity<byte[]> allStuClaimExportQuery(@Valid @RequestBody AllStuClaimExportQueryDto dto) throws IOException {
+//    @GetMapping("/all-stu-claim-export-query")
+//    @ApiOperation(value = "全校学生领取记录条件导出")
+//    public ResponseEntity<byte[]> allStuClaimExportQuery(@Valid AllStuClaimExportQueryDto dto) throws IOException {
+        ByteArrayOutputStream bot = textbookStudentClaimService.allStuClaimExportQuery(dto);
+        String fileName = "某一学期全校学生领取记录条件导出";
+        fileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
+        return RT.fileStream(bot.toByteArray(), fileName + ExcelTypeEnum.XLSX.getValue());
+    }
 }

+ 17 - 0
src/main/java/com/xjrsoft/module/textbook/dto/AllStuClaimExportQueryDto.java

@@ -0,0 +1,17 @@
+package com.xjrsoft.module.textbook.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class AllStuClaimExportQueryDto {
+
+    @ApiModelProperty("学期主键编号")
+    private String baseSemesterId;
+
+    @ApiModelProperty("班级主键编号")
+    private String classId;
+
+    @ApiModelProperty("学生主键编号")
+    private String userId;
+}

+ 11 - 0
src/main/java/com/xjrsoft/module/textbook/service/ITextbookStudentClaimService.java

@@ -5,7 +5,10 @@ import com.github.yulichang.base.MPJBaseService;
 import com.xjrsoft.module.textbook.dto.*;
 import com.xjrsoft.module.textbook.entity.TextbookStudentClaim;
 import com.xjrsoft.module.textbook.vo.*;
+import org.springframework.web.bind.annotation.RequestBody;
 
+import javax.validation.Valid;
+import java.io.ByteArrayOutputStream;
 import java.util.List;
 
 /**
@@ -94,4 +97,12 @@ public interface ITextbookStudentClaimService extends MPJBaseService<TextbookStu
     Boolean studentConfirm(StudentConfirmDto dto);
 
     List<TeacherConfirmBatchTextbookListVo> teacherConfirmBatchTextbookList(TeacherConfirmBatchTextbookListDto dto);
+
+    /**
+     * 某一学期全校学生领取记录条件导出
+     *
+     * @param dto
+     * @return
+     */
+    ByteArrayOutputStream allStuClaimExportQuery(AllStuClaimExportQueryDto dto) ;
 }

+ 20 - 0
src/main/java/com/xjrsoft/module/textbook/service/impl/TextbookStudentClaimServiceImpl.java

@@ -48,6 +48,7 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.io.ByteArrayOutputStream;
 import java.math.BigDecimal;
 import java.util.*;
 import java.util.stream.Collectors;
@@ -1276,4 +1277,23 @@ public class TextbookStudentClaimServiceImpl extends MPJBaseServiceImpl<Textbook
         ;
         return wfTextbookClaimItemMapper.selectJoinList(TeacherConfirmBatchTextbookListVo.class, wfTextbookClaimItemMPJLambdaWrapper);
     }
+
+    @Override
+    public ByteArrayOutputStream allStuClaimExportQuery(AllStuClaimExportQueryDto dto) {
+        if(StringUtils.isEmpty(dto.getBaseSemesterId())){
+            throw new MyException("请选择学期");
+        }
+
+        BaseSemester baseSemester = baseSemesterMapper.selectById(dto.getBaseSemesterId());
+
+        if(ObjectUtils.isEmpty(baseSemester)){
+            throw new MyException("学期无效,请重新选择");
+        }
+
+        // 获取所有学生信息
+//        AllStuClaimExportQueryVo
+
+
+        return null;
+    }
 }

+ 414 - 0
src/main/java/com/xjrsoft/module/textbook/service/impl/WfTextbookClaimServiceImpl.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.github.yulichang.base.MPJBaseServiceImpl;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import com.xjrsoft.common.enums.ArchivesStatusEnum;
 import com.xjrsoft.common.enums.ClaimTypeEnum;
 import com.xjrsoft.common.enums.DeleteMark;
 import com.xjrsoft.common.enums.IssueModeEnum;
@@ -16,6 +17,7 @@ import com.xjrsoft.common.utils.VoToColumnUtil;
 import com.xjrsoft.module.base.entity.BaseClass;
 import com.xjrsoft.module.base.entity.BaseSemester;
 import com.xjrsoft.module.generator.entity.ImportConfig;
+import com.xjrsoft.module.student.entity.BaseStudentSchoolRoll;
 import com.xjrsoft.module.system.entity.DictionaryDetail;
 import com.xjrsoft.module.teacher.entity.XjrUser;
 import com.xjrsoft.module.teacher.mapper.XjrUserMapper;
@@ -386,6 +388,7 @@ public class WfTextbookClaimServiceImpl extends MPJBaseServiceImpl<WfTextbookCla
         return textbookIssueRecordMapper.selectJoinList(DistributeRecordVo.class, textbookIssueRecordMPJLambdaWrapper);
     }
 
+/*
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean confirmDistribute(ConfirmDistributeDto dto) {
@@ -401,6 +404,21 @@ public class WfTextbookClaimServiceImpl extends MPJBaseServiceImpl<WfTextbookCla
             issueTotalNum += wfTextbookClaimItem.getIssueNumber();
         }
 
+        // 如果是班级申领,获取班级所有学生
+        List<XjrUser> stuList = new ArrayList<>();
+        if (ClaimTypeEnum.ClaimStudent.getCode().equals(wfTextbookClaim.getClaimType())
+                && ObjectUtils.isNotEmpty(wfTextbookClaim.getClassId())
+        ) {
+            MPJLambdaWrapper<XjrUser> xjrUserMPJLambdaWrapper = new MPJLambdaWrapper<>();
+            xjrUserMPJLambdaWrapper
+                    .select(XjrUser::getId)
+                    .innerJoin(BaseStudentSchoolRoll.class, BaseStudentSchoolRoll::getUserId, XjrUser::getId)
+                    .eq(BaseStudentSchoolRoll::getClassId, wfTextbookClaim.getClassId())
+                    .eq(BaseStudentSchoolRoll::getArchivesStatus, ArchivesStatusEnum.FB2901.getCode())
+            ;
+            stuList = xjrUserMapper.selectJoinList(XjrUser.class, xjrUserMPJLambdaWrapper);
+        }
+
         // 获取所有入库记录
         List<Long> textbookWarehouseRecordIds = new ArrayList<>();
         for (ConfirmDistributeDto.TextbookClaimItem textbookClaimItem : dto.getTextbookClaimItemList()) {
@@ -602,6 +620,27 @@ public class WfTextbookClaimServiceImpl extends MPJBaseServiceImpl<WfTextbookCla
                         insertList.add(insertTextbookStudentClaim);
                     }
                 }
+
+                // 如果申领项是班级申领,发放的时候应该直接生成领取记录
+                if (ClaimTypeEnum.ClaimClass.getCode().equals(wfTextbookClaim.getClaimType())) {
+                    for (XjrUser stu : stuList) {
+                        oldTextbookStudentClaim = byUserIdAndTextbookId.get("" + stu.getId() + wfTextbookClaimItem.getTextbookId());
+                        if (ObjectUtils.isEmpty(oldTextbookStudentClaim)) {
+                            insertTextbookStudentClaim = new TextbookStudentClaim();
+                            insertTextbookStudentClaim.setBaseSemesterId(wfTextbookClaim.getBaseSemesterId());
+                            insertTextbookStudentClaim.setClassId(wfTextbookClaim.getClassId());
+                            insertTextbookStudentClaim.setStudentUserId(stu.getId());
+                            insertTextbookStudentClaim.setTextbookId(wfTextbookClaimItem.getTextbookId());
+                            insertTextbookStudentClaim.setIsClaim(0);
+                            insertTextbookStudentClaim.setCreateDate(nowDate);
+                            insertTextbookStudentClaim.setCreateUserId(loginUserId);
+                            insertTextbookStudentClaim.setTextbookWarehouseRecordId(textbookWarehouseRecord.getId());
+                            insertTextbookStudentClaim.setPrice(textbookWarehouseRecord.getSubtotal());
+                            insertTextbookStudentClaim.setClaimSource(2);
+                            insertList.add(insertTextbookStudentClaim);
+                        }
+                    }
+                }
             }
 
             // 修改征订项发放数量
@@ -650,6 +689,381 @@ public class WfTextbookClaimServiceImpl extends MPJBaseServiceImpl<WfTextbookCla
         wfTextbookClaimWfTextbookClaimMapper.updateById(updateWfTextbookClaim);
         return true;
     }
+*/
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean confirmDistribute(ConfirmDistributeDto dto) {
+        // 1. 参数校验和初始化
+        validateInput(dto);
+
+        // 2. 获取基础数据
+        WfTextbookClaim claim = getClaimData(dto);
+        List<XjrUser> students = getRelatedStudents(claim);
+        Map<Long, TextbookWarehouseRecord> warehouseRecords = getWarehouseRecords(dto);
+
+        // 3. 处理发放逻辑
+        processDistribution(claim, dto, warehouseRecords, students);
+
+        // 4. 更新申领状态
+        updateClaimStatus(claim);
+
+        return true;
+    }
+
+    // 1. 参数校验和初始化
+    private void validateInput(ConfirmDistributeDto dto) {
+        if (ObjectUtils.isEmpty(dto.getTextbookClaimId())) {
+            throw new MyException("申领ID不能为空");
+        }
+        if (CollectionUtils.isEmpty(dto.getTextbookClaimItemList())) {
+            throw new MyException("申领项列表不能为空");
+        }
+    }
+
+    // 2. 获取申领数据
+    private WfTextbookClaim getClaimData(ConfirmDistributeDto dto) {
+        WfTextbookClaim claim = this.getByIdDeep(dto.getTextbookClaimId());
+        if (ObjectUtils.isEmpty(claim) || CollectionUtils.isEmpty(claim.getWfTextbookClaimItemList())) {
+            throw new MyException("未找到申领数据或申领项为空");
+        }
+        return claim;
+    }
+
+    // 2. 获取关联学生数据
+    private List<XjrUser> getRelatedStudents(WfTextbookClaim claim) {
+        if (!ClaimTypeEnum.ClaimClass.getCode().equals(claim.getClaimType()) ||
+                ObjectUtils.isEmpty(claim.getClassId())) {
+            return Collections.emptyList();
+        }
+
+        return xjrUserMapper.selectJoinList(XjrUser.class,
+                new MPJLambdaWrapper<XjrUser>()
+                        .select(XjrUser::getId)
+                        .innerJoin(BaseStudentSchoolRoll.class,
+                                BaseStudentSchoolRoll::getUserId, XjrUser::getId)
+                        .eq(BaseStudentSchoolRoll::getClassId, claim.getClassId())
+                        .eq(BaseStudentSchoolRoll::getArchivesStatus,
+                                ArchivesStatusEnum.FB2901.getCode()));
+    }
+
+    // 2. 获取关联的入库单号数据
+    private Map<Long, TextbookWarehouseRecord> getWarehouseRecords(ConfirmDistributeDto dto) {
+        List<Long> recordIds = dto.getTextbookClaimItemList().stream()
+                .flatMap(item -> item.getTextbookWarehouseRecords().stream()
+                        .map(ConfirmDistributeDto.TextbookWarehouseRecords::getTextbookWarehouseRecordId))
+                .collect(Collectors.toList());
+
+        if (CollectionUtils.isEmpty(recordIds)) {
+            throw new MyException("入库单号无效");
+        }
+
+        return textbookWarehouseRecordMapper.selectList(
+                        Wrappers.lambdaQuery(TextbookWarehouseRecord.class)
+                                .in(TextbookWarehouseRecord::getId, recordIds)
+                                .eq(TextbookWarehouseRecord::getDeleteMark, DeleteMark.NODELETE.getCode()))
+                .stream()
+                .collect(Collectors.toMap(TextbookWarehouseRecord::getId, Function.identity()));
+    }
+
+    // 3. 处理发放逻辑
+    private void processDistribution(WfTextbookClaim claim, ConfirmDistributeDto dto,
+                                     Map<Long, TextbookWarehouseRecord> warehouseRecords,
+                                     List<XjrUser> students) {
+        Date now = new Date();
+        Long userId = StpUtil.getLoginIdAsLong();
+        int issueTimes = claim.getIssueTimes() + 1;
+
+        // 获取现有领取记录
+        Map<String, TextbookStudentClaim> existingClaims = getExistingStudentClaims(claim);
+
+        // 处理每个申领项
+        for (ConfirmDistributeDto.TextbookClaimItem item : dto.getTextbookClaimItemList()) {
+            processClaimItem(dto, claim, item, warehouseRecords, students, existingClaims,
+                    now, userId, issueTimes);
+        }
+    }
+
+    // 获取现有领取记录
+    private Map<String, TextbookStudentClaim> getExistingStudentClaims(WfTextbookClaim claim) {
+        return textbookStudentClaimMapper.selectList(
+                        Wrappers.lambdaQuery(TextbookStudentClaim.class)
+                                .eq(TextbookStudentClaim::getBaseSemesterId, claim.getBaseSemesterId())
+                                .eq(TextbookStudentClaim::getClassId, claim.getClassId()))
+                .stream()
+                .collect(Collectors.toMap(
+                        t -> t.getStudentUserId() + "_" + t.getTextbookId(),
+                        Function.identity()));
+    }
+
+    private void processClaimItem(ConfirmDistributeDto dto, WfTextbookClaim claim,
+                                  ConfirmDistributeDto.TextbookClaimItem item,
+                                  Map<Long, TextbookWarehouseRecord> warehouseRecords,
+                                  List<XjrUser> students,
+                                  Map<String, TextbookStudentClaim> existingClaims,
+                                  Date now, Long userId, int issueTimes) {
+        WfTextbookClaimItem claimItem = claim.getWfTextbookClaimItemList().stream()
+                .filter(i -> i.getId().equals(item.getTextbookClaimItemId()))
+                .findFirst()
+                .orElseThrow(() -> new MyException("申领详细数据发生更改"));
+
+        validateIssueQuantity(claimItem, item);
+
+        // 处理每个入库记录
+        for (ConfirmDistributeDto.TextbookWarehouseRecords record : item.getTextbookWarehouseRecords()) {
+            TextbookWarehouseRecord warehouseRecord = warehouseRecords.get(record.getTextbookWarehouseRecordId());
+            if (warehouseRecord == null) {
+                throw new MyException("未找到对应入库详细数据");
+            }
+
+            // 验证发放数量是否合法
+            validateRemainingQuantity(warehouseRecord, record, item);
+
+            // 更新库存
+            updateWarehouseRecord(warehouseRecord, record, now, userId);
+
+            // 创建出库记录
+            TextbookIssueRecord issueRecord = createIssueRecord(claim, claimItem, warehouseRecord, dto,
+                    record, now, userId, issueTimes);
+            textbookIssueRecordMapper.insert(issueRecord);
+
+            // 处理学生领取记录
+            processStudentClaims(claim, claimItem, warehouseRecord, students,
+                    existingClaims, now, userId);
+        }
+
+        // 更新申领项
+        updateClaimItem(claimItem, item.getConfirmTotalNumber());
+    }
+
+    // 验证发放数量是否合法
+    private void validateIssueQuantity(WfTextbookClaimItem claimItem,
+                                       ConfirmDistributeDto.TextbookClaimItem item) {
+        int issued = ObjectUtils.defaultIfNull(claimItem.getIssueNumber(), 0);
+        int applied = ObjectUtils.defaultIfNull(claimItem.getApplicantNumber(), 0);
+        int confirming = ObjectUtils.defaultIfNull(item.getConfirmTotalNumber(), 0);
+
+        if (issued + confirming > applied) {
+            throw new MyException(String.format(
+                    "教材[%s]发放总数量(%d)超出申领数量(%d)",
+                    item.getTextbookIdCn(),
+                    issued + confirming,
+                    applied
+            ));
+        }
+    }
+
+    // 验证库存是否充足
+    private void validateRemainingQuantity(TextbookWarehouseRecord warehouseRecord,
+                                           ConfirmDistributeDto.TextbookWarehouseRecords record,
+                                           ConfirmDistributeDto.TextbookClaimItem item
+    ) {
+        int remaining = ObjectUtils.defaultIfNull(warehouseRecord.getRemainNumber(), 0);
+        int confirming = ObjectUtils.defaultIfNull(record.getConfirmNumber(), 0);
+
+        if (remaining < confirming) {
+            throw new MyException(String.format(
+                    "教材[%s]发放数量(%d)大于库存剩余数量(%d)",
+                    item.getTextbookIdCn(),
+                    confirming,
+                    remaining
+            ));
+        }
+    }
+
+    // 验证库存是否充足
+    private void updateWarehouseRecord(TextbookWarehouseRecord record,
+                                       ConfirmDistributeDto.TextbookWarehouseRecords confirmRecord,
+                                       Date now, Long userId) {
+        TextbookWarehouseRecord update = new TextbookWarehouseRecord();
+        update.setId(record.getId());
+        update.setModifyUserId(userId);
+        update.setModifyDate(now);
+        update.setIssuedNumber(ObjectUtils.defaultIfNull(record.getIssuedNumber(), 0)
+                + confirmRecord.getConfirmNumber());
+        update.setRemainNumber(ObjectUtils.defaultIfNull(record.getRemainNumber(), 0)
+                - confirmRecord.getConfirmNumber());
+
+        textbookWarehouseRecordMapper.updateById(update);
+    }
+
+    // 创建出库记录
+    private TextbookIssueRecord createIssueRecord(WfTextbookClaim claim,
+                                                  WfTextbookClaimItem claimItem,
+                                                  TextbookWarehouseRecord warehouseRecord,
+                                                  ConfirmDistributeDto dto,
+                                                  ConfirmDistributeDto.TextbookWarehouseRecords confirmRecord,
+                                                  Date now, Long userId, int issueTimes) {
+        // 生成出库单号
+        String orderNumber = generateOrderNumber(claimItem.getId(), warehouseRecord.getId());
+
+        TextbookIssueRecord record = new TextbookIssueRecord();
+        record.setBaseSemesterId(claim.getBaseSemesterId());
+        record.setOrderNumber(orderNumber);
+        record.setIssueMode(getIssueMode(claim.getClaimType()));
+        record.setDataId(claim.getId());
+        record.setDataItemId(claimItem.getId());
+        record.setTextbookWarehouseRecordId(warehouseRecord.getId());
+        record.setTextbookId(claimItem.getTextbookId());
+        record.setIssueNumber(confirmRecord.getConfirmNumber());
+        record.setRecedeNumber(0);
+        record.setActualIssueNumber(confirmRecord.getConfirmNumber());
+        record.setActualTotalPrice(
+                warehouseRecord.getSubtotal().multiply(BigDecimal.valueOf(confirmRecord.getConfirmNumber()))
+        );
+
+        if (ObjectUtils.isNotEmpty(dto.getReceiveUserId())) {
+            record.setReceiveUserId(dto.getReceiveUserId());
+        } else {
+            record.setReceiveUserId(claim.getApplicantUserId());
+        }
+        record.setIssueUserId(userId);
+        record.setCreateDate(now);
+        record.setRemark(dto.getRemark());
+        record.setIssueTimes(issueTimes);
+
+        return record;
+    }
+
+    // 构造出库单号
+    private String generateOrderNumber(Long claimItemId, Long warehouseRecordId) {
+        LambdaQueryWrapper<TextbookIssueRecord> query = Wrappers.lambdaQuery(TextbookIssueRecord.class)
+                .eq(TextbookIssueRecord::getDataItemId, claimItemId)
+                .eq(TextbookIssueRecord::getTextbookWarehouseRecordId, warehouseRecordId)
+                .eq(TextbookIssueRecord::getDeleteMark, DeleteMark.NODELETE.getCode())
+                .orderByDesc(TextbookIssueRecord::getOrderNumber)
+                .last("LIMIT 1");
+
+        TextbookIssueRecord lastRecord = textbookIssueRecordMapper.selectOne(query);
+
+        if (lastRecord != null) {
+            // 已有记录则递增序号
+            String[] parts = lastRecord.getOrderNumber().split("-");
+            int seq = Integer.parseInt(parts[parts.length - 1]) + 1;
+            return parts[0] + "-" + String.format("%03d", seq);
+        } else {
+            // 新记录生成初始单号
+            return "CK" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+                    + "-001";
+        }
+    }
+
+    // 判定申领类型
+    private String getIssueMode(String claimType) {
+        if (ClaimTypeEnum.ClaimStudent.getCode().equals(claimType)) {
+            return IssueModeEnum.ImStudent.getCode();
+        } else if (ClaimTypeEnum.ClaimClass.getCode().equals(claimType)) {
+            return IssueModeEnum.ImClass.getCode();
+        } else if (ClaimTypeEnum.ClaimTeacher.getCode().equals(claimType)) {
+            return IssueModeEnum.ImTeacher.getCode();
+        }
+        throw new MyException("未知的申领类型: " + claimType);
+    }
+
+    // 处理学生领取记录
+    private void processStudentClaims(WfTextbookClaim claim,
+                                      WfTextbookClaimItem claimItem,
+                                      TextbookWarehouseRecord warehouseRecord,
+                                      List<XjrUser> students,
+                                      Map<String, TextbookStudentClaim> existingClaims,
+                                      Date now, Long userId) {
+        List<TextbookStudentClaim> toInsert = new ArrayList<>();
+        List<TextbookStudentClaim> toUpdate = new ArrayList<>();
+
+        if (ClaimTypeEnum.ClaimStudent.getCode().equals(claim.getClaimType())) {
+            // 个人申领直接确认
+            handleStudentClaim(claim.getApplicantUserId(), claim, claimItem,
+                    warehouseRecord, existingClaims, now, userId, toInsert, toUpdate);
+        } else if (ClaimTypeEnum.ClaimClass.getCode().equals(claim.getClaimType())) {
+            // 班级申领为每个学生生成记录
+            students.forEach(student ->
+                    handleStudentClaim(student.getId(), claim, claimItem,
+                            warehouseRecord, existingClaims, now, userId, toInsert, toUpdate));
+        }
+
+        // 批量操作
+        if (!toInsert.isEmpty()) {
+            for (TextbookStudentClaim insert : toInsert){
+                textbookStudentClaimMapper.insert(insert);
+            }
+
+        }
+        if (!toUpdate.isEmpty()) {
+            for (TextbookStudentClaim update : toUpdate){
+                textbookStudentClaimMapper.updateById(update);
+            }
+        }
+    }
+
+    // 新增或修改领取记录
+    private void handleStudentClaim(Long studentId,
+                                    WfTextbookClaim claim,
+                                    WfTextbookClaimItem claimItem,
+                                    TextbookWarehouseRecord warehouseRecord,
+                                    Map<String, TextbookStudentClaim> existingClaims,
+                                    Date now, Long userId,
+                                    List<TextbookStudentClaim> toInsert,
+                                    List<TextbookStudentClaim> toUpdate) {
+        String key = studentId + "_" + claimItem.getTextbookId();
+        TextbookStudentClaim existing = existingClaims.get(key);
+
+        if (existing != null) {
+            if ((ClaimTypeEnum.ClaimStudent.getCode().equals(claim.getClaimType()))
+                    && !Integer.valueOf(1).equals(existing.getIsClaim())
+            ) {
+                TextbookStudentClaim update = new TextbookStudentClaim();
+                update.setId(existing.getId());
+                update.setIsClaim(1);
+                update.setModifyDate(now);
+                update.setModifyUserId(userId);
+                update.setTextbookWarehouseRecordId(warehouseRecord.getId());
+                update.setPrice(warehouseRecord.getSubtotal());
+                update.setClaimSource(2); // 管理员发放
+                toUpdate.add(update);
+            }
+        } else {
+            TextbookStudentClaim insert = new TextbookStudentClaim();
+            insert.setBaseSemesterId(claim.getBaseSemesterId());
+            insert.setClassId(claim.getClassId());
+            insert.setStudentUserId(studentId);
+            insert.setTextbookId(claimItem.getTextbookId());
+            insert.setIsClaim(ClaimTypeEnum.ClaimStudent.getCode().equals(claim.getClaimType()) ? 1 : 0);
+            insert.setCreateDate(now);
+            insert.setCreateUserId(userId);
+            insert.setTextbookWarehouseRecordId(warehouseRecord.getId());
+            insert.setPrice(warehouseRecord.getSubtotal());
+            insert.setClaimSource(2); // 管理员发放
+            toInsert.add(insert);
+        }
+    }
+
+    // 更新申领项
+    private void updateClaimItem(WfTextbookClaimItem claimItem, int confirmNumber) {
+        WfTextbookClaimItem update = new WfTextbookClaimItem();
+        update.setId(claimItem.getId());
+        update.setIssueNumber(ObjectUtils.defaultIfNull(claimItem.getIssueNumber(), 0) + confirmNumber);
+        wfTextbookClaimWfTextbookClaimItemMapper.updateById(update);
+    }
+
+    // 4. 更新申领状态
+    private void updateClaimStatus(WfTextbookClaim claim) {
+        long incompleteItems = wfTextbookClaimWfTextbookClaimItemMapper.selectCount(
+                new MPJLambdaWrapper<WfTextbookClaimItem>()
+                        .select(WfTextbookClaimItem::getId)
+                        .innerJoin(WfTextbookClaim.class,
+                                WfTextbookClaim::getId, WfTextbookClaimItem::getWfTextbookClaimId)
+                        .lt(WfTextbookClaimItem::getIssueNumber, WfTextbookClaimItem::getApplicantNumber)
+                        .eq(WfTextbookClaimItem::getWfTextbookClaimId, claim.getId()));
+
+        WfTextbookClaim update = new WfTextbookClaim();
+        update.setId(claim.getId());
+        update.setStatus(incompleteItems > 0 ? 2 : 3);
+        update.setIssueTimes(claim.getIssueTimes() + 1);
+        update.setModifyUserId(StpUtil.getLoginIdAsLong());
+        update.setModifyDate(new Date());
+
+        wfTextbookClaimWfTextbookClaimMapper.updateById(update);
+    }
 
     @Override
     @Transactional(rollbackFor = Exception.class)

+ 100 - 0
src/main/java/com/xjrsoft/module/textbook/vo/AllStuClaimExportQueryVo.java

@@ -0,0 +1,100 @@
+package com.xjrsoft.module.textbook.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ContentStyle;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class AllStuClaimExportQueryVo {
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("学期")
+    @ApiModelProperty("学期ID(base_semester)")
+    private String baseSemesterIdCn;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelIgnore
+    @ApiModelProperty("申领类型(xjr_dictionary_item[claim_type])")
+    private String claimType;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("申领类型")
+    @ApiModelProperty("申领类型(xjr_dictionary_item[claim_type])")
+    private String claimTypeCn;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("领取人")
+    @ApiModelProperty("领取人")
+    private String receiver;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelIgnore
+    @ApiModelProperty("申请人")
+    private String applicantUserIdCn;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("班级名称")
+    @ApiModelProperty("班级名称")
+    private String classIdCn;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("申请数量")
+    @ApiModelProperty("申请总数量")
+    private Integer applicantTatolNumber;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelIgnore
+    @ApiModelProperty("状态()")
+    private Integer status;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("发放状态")
+    @ApiModelProperty("状态()")
+    private String statusStr;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("发放数量")
+    @ApiModelProperty("已经发放总数量")
+    private Integer issueTatolNumber;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("教材名称")
+    @ApiModelProperty("教材名称")
+    private String textbookIdCN;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("国际标准刊号")
+    @ApiModelProperty("国际标准刊号")
+    private String issn;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("申请数量")
+    @ApiModelProperty("申请数量")
+    private Integer applicantNumber;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("已发放数量")
+    @ApiModelProperty("已发放数量")
+    private Integer issueNumber;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("出库时间")
+    @ApiModelProperty("出库时间")
+    private String issueDate;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("出库人员")
+    @ApiModelProperty("出库人员")
+    private String issueUser;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("出库单号")
+    @ApiModelProperty("出库单号(标识+当前时间(YYYYMMDDHHmmss)+三位序号+当前申领项出库次数(-n))")
+    private String orderNumber;
+
+    @ContentStyle(dataFormat = 49)
+    @ExcelProperty("单次出库数量")
+    @ApiModelProperty("单次出库数量")
+    private Integer onceIssueNumber;
+}