package cn.iocoder.yudao.module.finance.controller.admin.voucher; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherEntryRespVO; import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherPageReqVO; import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherRespVO; import cn.iocoder.yudao.module.finance.controller.admin.voucher.vo.FinVoucherSaveReqVO; import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherDO; import cn.iocoder.yudao.module.finance.dal.dataobject.voucher.FinVoucherEntryDO; import cn.iocoder.yudao.module.finance.service.voucher.FinVoucherService; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; @Tag(name = "管理后台 - 记账凭证") @RestController @RequestMapping("/finance/voucher") @Validated public class FinVoucherController { /** * 附件的业务记录类型 */ private static final String ATTACHMENT_RECORD_TYPE = "fin_voucher"; @Resource private FinVoucherService voucherService; @Resource private StorageAttachmentApi storageAttachmentApi; @Resource private AdminUserApi adminUserApi; @PostMapping("/create") @Operation(summary = "创建记账凭证") @PreAuthorize("@ss.hasPermission('finance:voucher:create')") public CommonResult createVoucher(@Valid @RequestBody FinVoucherSaveReqVO createReqVO) { return success(voucherService.createVoucher(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新记账凭证", description = "仅未过账的凭证允许修改") @PreAuthorize("@ss.hasPermission('finance:voucher:update')") public CommonResult updateVoucher(@Valid @RequestBody FinVoucherSaveReqVO updateReqVO) { voucherService.updateVoucher(updateReqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "删除记账凭证") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('finance:voucher:delete')") public CommonResult deleteVoucher(@RequestParam("id") Long id) { voucherService.deleteVoucher(id); return success(true); } @DeleteMapping("/delete-list") @Operation(summary = "批量删除记账凭证") @Parameter(name = "ids", description = "编号列表", required = true) @PreAuthorize("@ss.hasPermission('finance:voucher:delete')") public CommonResult deleteVoucherList(@RequestParam("ids") List ids) { voucherService.deleteVoucherList(ids); return success(true); } @PutMapping("/post") @Operation(summary = "凭证过账", description = "未过账 -> 已过账,过账后凭证不可再修改") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('finance:voucher:update-status')") public CommonResult postVoucher(@RequestParam("id") Long id) { voucherService.postVoucher(id); return success(true); } @PutMapping("/cancel") @Operation(summary = "凭证作废", description = "未过账 -> 已作废") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('finance:voucher:update-status')") public CommonResult cancelVoucher(@RequestParam("id") Long id) { voucherService.cancelVoucher(id); return success(true); } @GetMapping("/get") @Operation(summary = "获得记账凭证", description = "包含凭证分录与附件列表") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('finance:voucher:query')") public CommonResult getVoucher(@RequestParam("id") Long id) { FinVoucherDO voucher = voucherService.getVoucher(id); if (voucher == null) { return success(null); } FinVoucherRespVO respVO = BeanUtils.toBean(voucher, FinVoucherRespVO.class); respVO.setEntries(BeanUtils.toBean(voucherService.getVoucherEntryListByVoucherId(id), FinVoucherEntryRespVO.class)); // 注意:StorageAttachmentApi.listAttachments 在「该记录没有任何附件」时返回 null,而不是空集合。 // 直接 .isEmpty() 会 NPE:新增的凭证还没传附件时,一查详情就 500,必须先兜底成空列表。 List attachments = storageAttachmentApi.listAttachments(ATTACHMENT_RECORD_TYPE, id); if (attachments == null) { attachments = List.of(); } respVO.setAttachmentList(attachments); respVO.setHasAttachment(!attachments.isEmpty()); return success(respVO); } @GetMapping("/page") @Operation(summary = "获得记账凭证分页") @PreAuthorize("@ss.hasPermission('finance:voucher:query')") public CommonResult> getVoucherPage(@Valid FinVoucherPageReqVO pageReqVO) { PageResult pageResult = voucherService.getVoucherPage(pageReqVO); PageResult result = BeanUtils.toBean(pageResult, FinVoucherRespVO.class); fillCreatorName(result.getList()); return success(result); } @GetMapping("/page-with-entry") @Operation(summary = "获得记账凭证分页(含分录)", description = "一次性返回本页所有凭证的分录,避免前端逐条查询") @PreAuthorize("@ss.hasPermission('finance:voucher:query')") public CommonResult> getVoucherPageWithEntry(@Valid FinVoucherPageReqVO pageReqVO) { PageResult pageResult = voucherService.getVoucherPage(pageReqVO); PageResult result = BeanUtils.toBean(pageResult, FinVoucherRespVO.class); if (result.getList().isEmpty()) { return success(result); } fillCreatorName(result.getList()); // 批量查询本页凭证的分录 Map> entryMap = convertMultiMap( voucherService.getVoucherEntryListByVoucherIds( convertList(result.getList(), FinVoucherRespVO::getId)), FinVoucherEntryDO::getVoucherId); result.getList().forEach(voucher -> { voucher.setEntries(BeanUtils.toBean( entryMap.getOrDefault(voucher.getId(), List.of()), FinVoucherEntryRespVO.class)); // 列表页也需要展示附件标记;附件表按 recordId 单条查询,本页最多 20 条,成本可接受 // 同上:没有附件时 listAttachments 返回 null,必须兜底,否则列表里只要有一条没附件的凭证就整页 500 List attachments = storageAttachmentApi.listAttachments(ATTACHMENT_RECORD_TYPE, voucher.getId()); if (attachments == null) { attachments = List.of(); } voucher.setAttachmentList(attachments); voucher.setHasAttachment(!attachments.isEmpty()); }); return success(result); } @GetMapping("/export-excel") @Operation(summary = "导出记账凭证 Excel") @PreAuthorize("@ss.hasPermission('finance:voucher:export')") @ApiAccessLog(operateType = EXPORT) public void exportVoucherExcel(@Valid FinVoucherPageReqVO pageReqVO, HttpServletResponse response) throws IOException { pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = voucherService.getVoucherPage(pageReqVO).getList(); List voList = BeanUtils.toBean(list, FinVoucherRespVO.class); fillCreatorName(voList); // 导出 Excel ExcelUtils.write(response, "记账凭证.xls", "数据", FinVoucherRespVO.class, voList); } /** * 填充制单人昵称。 * BaseDO.creator 存的是用户 ID,列表与导出表里直接展示就是一串数字, * 所以按 ERP 收款单/付款单的既有口径,统一在这里把用户信息批量查一次再回填。 */ private void fillCreatorName(List list) { if (CollUtil.isEmpty(list)) { return; } Map userMap = adminUserApi.getUserMap(convertListByFlatMap(list, voucher -> Stream.of(NumberUtils.parseLong(voucher.getCreator())))); list.forEach(voucher -> MapUtils.findAndThen(userMap, NumberUtils.parseLong(voucher.getCreator()), user -> voucher.setCreatorName(user.getNickname()))); } }