diff --git a/docs/sql/clearing_withdraw_invoice.sql b/docs/sql/clearing_withdraw_invoice.sql new file mode 100644 index 000000000..702ae173b --- /dev/null +++ b/docs/sql/clearing_withdraw_invoice.sql @@ -0,0 +1,33 @@ +-- 运营商提现向平台开票信息表 +-- 执行前请确认 sys_todo_task、待办菜单权限 SQL 已部署。 + +CREATE TABLE IF NOT EXISTS `clearing_withdraw_invoice` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `withdraw_code` varchar(64) NOT NULL COMMENT '提现编号', + `merchant_id` varchar(64) NOT NULL COMMENT '运营商ID', + `invoice_status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待提交、SUBMITTED已提交、CLOSED已关闭', + `invoice_number` varchar(100) DEFAULT NULL COMMENT '发票号码', + `voucher_url` varchar(1000) DEFAULT NULL COMMENT '发票凭证地址', + `submitted_by` bigint DEFAULT NULL COMMENT '提交人用户ID', + `submitted_name` varchar(64) DEFAULT NULL COMMENT '提交人账号', + `submitted_time` datetime DEFAULT NULL COMMENT '提交时间', + `closed_by` bigint DEFAULT NULL COMMENT '关闭人用户ID', + `closed_name` varchar(64) DEFAULT NULL COMMENT '关闭人账号', + `closed_time` datetime DEFAULT NULL COMMENT '关闭时间', + `close_reason` varchar(500) DEFAULT NULL COMMENT '关闭原因', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0正常 1删除)', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_withdraw_invoice_code` (`withdraw_code`), + KEY `idx_withdraw_invoice_merchant_status` (`merchant_id`, `invoice_status`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运营商提现向平台开票信息表'; + +-- 完成权限分配给运营商管理员角色,取消权限只分配给平台任务关闭角色。 +-- INSERT INTO sys_role_menu (role_id, menu_id) +-- SELECT <运营商管理员角色ID>, menu_id FROM sys_menu WHERE perms = 'system:todo:complete'; +-- INSERT INTO sys_role_menu (role_id, menu_id) +-- SELECT <平台任务关闭角色ID>, menu_id FROM sys_menu WHERE perms = 'system:todo:cancel'; diff --git a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java index 15cdb3810..eb8786135 100644 --- a/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java +++ b/jsowell-admin/src/main/java/com/jsowell/service/OrderService.java @@ -124,6 +124,9 @@ public class OrderService { @Autowired private ClearingWithdrawInfoService clearingWithdrawInfoService; + @Autowired + private MerchantWithdrawInvoiceService merchantWithdrawInvoiceService; + @Autowired private MemberAdapayRecordService memberAdapayRecordService; @@ -1464,22 +1467,7 @@ public class OrderService { log.info("取现成功 data:{}", JSON.toJSONString(data)); JSONObject jsonObject = JSON.parseObject(data); String withdrawCode = jsonObject.getString("id"); - // 通过取现id查询取现数据 - ClearingWithdrawInfo clearingWithdrawInfo = clearingWithdrawInfoService.selectByWithdrawCode(withdrawCode); - if (clearingWithdrawInfo != null) { - clearingWithdrawInfo.setWithdrawStatus(Constants.ONE); - clearingWithdrawInfo.setUpdateTime(DateUtils.getNowDate()); - clearingWithdrawInfoService.updateByPrimaryKeySelective(clearingWithdrawInfo); - } - - // 清分账单数据更新 - List clearingBillInfos = clearingBillInfoService.selectByWithdrawCode(withdrawCode); - if (CollectionUtils.isNotEmpty(clearingBillInfos)) { - for (ClearingBillInfo clearingBillInfo : clearingBillInfos) { - clearingBillInfo.setBillStatus("4"); - } - clearingBillInfoService.updateBatchSelective(clearingBillInfos); - } + merchantWithdrawInvoiceService.handleWithdrawSucceeded(withdrawCode); } /** diff --git a/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MerchantWithdrawInvoiceController.java b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MerchantWithdrawInvoiceController.java new file mode 100644 index 000000000..2f02bd82b --- /dev/null +++ b/jsowell-admin/src/main/java/com/jsowell/web/controller/pile/MerchantWithdrawInvoiceController.java @@ -0,0 +1,54 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.annotation.Log; +import com.jsowell.common.core.controller.BaseController; +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.common.enums.BusinessType; +import com.jsowell.pile.dto.WithdrawInvoiceCloseRequest; +import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest; +import com.jsowell.pile.service.MerchantWithdrawInvoiceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 运营商提现向平台开票接口。 + */ +@RestController +@RequestMapping("/pile/withdrawInvoice") +public class MerchantWithdrawInvoiceController extends BaseController { + @Autowired + private MerchantWithdrawInvoiceService merchantWithdrawInvoiceService; + + @PreAuthorize("@ss.hasPermi('system:todo:query')") + @GetMapping("/{withdrawCode}") + public AjaxResult getInfo(@PathVariable String withdrawCode, @RequestParam Long todoId) { + return AjaxResult.success(merchantWithdrawInvoiceService.getForAssignee( + withdrawCode, todoId, getUserId())); + } + + @PreAuthorize("@ss.hasPermi('system:todo:complete')") + @Log(title = "运营商提现开票", businessType = BusinessType.UPDATE) + @PutMapping("/{withdrawCode}/submit") + public AjaxResult submit(@PathVariable String withdrawCode, + @Validated @RequestBody WithdrawInvoiceSubmitRequest request) { + return toAjax(merchantWithdrawInvoiceService.submit(withdrawCode, request, + getUserId(), getUsername())); + } + + @PreAuthorize("@ss.hasPermi('system:todo:cancel')") + @Log(title = "运营商提现开票", businessType = BusinessType.UPDATE) + @PutMapping("/{withdrawCode}/close") + public AjaxResult close(@PathVariable String withdrawCode, + @Validated @RequestBody WithdrawInvoiceCloseRequest request) { + return toAjax(merchantWithdrawInvoiceService.close(withdrawCode, request.getReason(), + getUserId(), getUsername())); + } +} diff --git a/jsowell-admin/src/test/java/com/jsowell/pile/service/MerchantWithdrawInvoiceServiceImplTest.java b/jsowell-admin/src/test/java/com/jsowell/pile/service/MerchantWithdrawInvoiceServiceImplTest.java new file mode 100644 index 000000000..e393e1570 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/pile/service/MerchantWithdrawInvoiceServiceImplTest.java @@ -0,0 +1,174 @@ +package com.jsowell.pile.service; + +import com.jsowell.common.constant.Constants; +import com.jsowell.pile.constant.MerchantWithdrawInvoiceConstants; +import com.jsowell.pile.domain.ClearingBillInfo; +import com.jsowell.pile.domain.ClearingWithdrawInfo; +import com.jsowell.pile.domain.ClearingWithdrawInvoice; +import com.jsowell.pile.domain.PileMerchantInfo; +import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest; +import com.jsowell.pile.mapper.ClearingWithdrawInfoMapper; +import com.jsowell.pile.mapper.ClearingWithdrawInvoiceMapper; +import com.jsowell.pile.service.impl.MerchantWithdrawInvoiceServiceImpl; +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskCreateCommand; +import com.jsowell.system.service.TodoTaskAssigneeService; +import com.jsowell.system.service.TodoTaskService; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class MerchantWithdrawInvoiceServiceImplTest { + @Test + void handleSucceeded_shouldUpdateWithdrawAndCreateAdminTodos() { + Dependencies dependencies = new Dependencies(); + MerchantWithdrawInvoiceServiceImpl service = dependencies.createService(); + ClearingWithdrawInfo withdrawInfo = withdrawInfo(Constants.ZERO); + when(dependencies.withdrawInfoMapper.selectByWithdrawCode("W100")).thenReturn(withdrawInfo); + when(dependencies.invoiceMapper.selectByWithdrawCode("W100")).thenReturn(null); + when(dependencies.invoiceMapper.insertSelective(any(ClearingWithdrawInvoice.class))).thenReturn(1); + ClearingBillInfo billInfo = new ClearingBillInfo(); + billInfo.setId(5); + when(dependencies.clearingBillInfoService.selectByWithdrawCode("W100")) + .thenReturn(Collections.singletonList(billInfo)); + PileMerchantInfo merchantInfo = new PileMerchantInfo(); + merchantInfo.setDeptId("200"); + when(dependencies.pileMerchantInfoService.selectPileMerchantInfoById(100L)).thenReturn(merchantInfo); + when(dependencies.assigneeService.findActiveOperatorAdminUserIdsByDeptTree(200L)) + .thenReturn(Arrays.asList(8L, 9L)); + + service.handleWithdrawSucceeded("W100"); + + verify(dependencies.withdrawInfoMapper).updateByPrimaryKeySelective(withdrawInfo); + assertEquals(Constants.ONE, withdrawInfo.getWithdrawStatus()); + assertEquals("4", billInfo.getBillStatus()); + verify(dependencies.clearingBillInfoService).updateBatchSelective(Collections.singletonList(billInfo)); + ArgumentCaptor captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class); + verify(dependencies.todoTaskService, times(2)).createTask(captor.capture()); + assertEquals(Arrays.asList(8L, 9L), Arrays.asList( + captor.getAllValues().get(0).getAssigneeUserId(), + captor.getAllValues().get(1).getAssigneeUserId())); + assertEquals(MerchantWithdrawInvoiceConstants.TASK_TYPE, captor.getValue().getTaskType()); + assertEquals(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, captor.getValue().getBusinessType()); + assertEquals("financeDetail", captor.getValue().getRouteName()); + } + + @Test + void handleSucceeded_shouldNotRecreateTodoAfterInvoiceSubmitted() { + Dependencies dependencies = new Dependencies(); + MerchantWithdrawInvoiceServiceImpl service = dependencies.createService(); + when(dependencies.withdrawInfoMapper.selectByWithdrawCode("W100")) + .thenReturn(withdrawInfo(Constants.ONE)); + ClearingWithdrawInvoice invoice = invoice(MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED); + when(dependencies.invoiceMapper.selectByWithdrawCode("W100")).thenReturn(invoice); + when(dependencies.clearingBillInfoService.selectByWithdrawCode("W100")) + .thenReturn(Collections.emptyList()); + + service.handleWithdrawSucceeded("W100"); + + verify(dependencies.todoTaskService, never()).createTask(any(TodoTaskCreateCommand.class)); + } + + @Test + void submit_shouldCompleteAllBusinessTodos() { + Dependencies dependencies = new Dependencies(); + MerchantWithdrawInvoiceServiceImpl service = dependencies.createService(); + when(dependencies.todoTaskService.getForAssignee(20L, 8L)).thenReturn(todoTask()); + when(dependencies.invoiceMapper.selectByWithdrawCode("W100")) + .thenReturn(invoice(MerchantWithdrawInvoiceConstants.STATUS_PENDING)); + when(dependencies.invoiceMapper.markSubmitted(any(ClearingWithdrawInvoice.class))).thenReturn(1); + WithdrawInvoiceSubmitRequest request = new WithdrawInvoiceSubmitRequest(); + request.setTodoId(20L); + request.setInvoiceNumber("INV-100"); + request.setVoucherUrl("/profile/upload/invoice.pdf"); + + assertTrue(service.submit("W100", request, 8L, "operator")); + + verify(dependencies.todoTaskService).completeByBusiness( + MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, "W100", null, 8L, "operator"); + } + + @Test + void close_shouldCancelAllBusinessTodos() { + Dependencies dependencies = new Dependencies(); + MerchantWithdrawInvoiceServiceImpl service = dependencies.createService(); + when(dependencies.invoiceMapper.selectByWithdrawCode("W100")) + .thenReturn(invoice(MerchantWithdrawInvoiceConstants.STATUS_PENDING)); + when(dependencies.invoiceMapper.markClosed(any(ClearingWithdrawInvoice.class))).thenReturn(1); + + assertTrue(service.close("W100", "无需开票", 1L, "admin")); + + verify(dependencies.todoTaskService).cancelByBusiness( + MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, "W100", null, "admin", "无需开票"); + } + + private static ClearingWithdrawInfo withdrawInfo(String status) { + ClearingWithdrawInfo withdrawInfo = new ClearingWithdrawInfo(); + withdrawInfo.setId(1); + withdrawInfo.setMerchantId("100"); + withdrawInfo.setWithdrawCode("W100"); + withdrawInfo.setWithdrawStatus(status); + return withdrawInfo; + } + + private static ClearingWithdrawInvoice invoice(String status) { + ClearingWithdrawInvoice invoice = new ClearingWithdrawInvoice(); + invoice.setId(10L); + invoice.setMerchantId("100"); + invoice.setWithdrawCode("W100"); + invoice.setInvoiceStatus(status); + return invoice; + } + + private static SysTodoTask todoTask() { + SysTodoTask task = new SysTodoTask(); + task.setTodoId(20L); + task.setTaskType(MerchantWithdrawInvoiceConstants.TASK_TYPE); + task.setBusinessType(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE); + task.setBusinessId("W100"); + task.setTaskStatus("PENDING"); + return task; + } + + private static void setField(Object target, String fieldName, Object value) { + try { + Field field = MerchantWithdrawInvoiceServiceImpl.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + private static class Dependencies { + private final ClearingWithdrawInfoMapper withdrawInfoMapper = mock(ClearingWithdrawInfoMapper.class); + private final ClearingWithdrawInvoiceMapper invoiceMapper = mock(ClearingWithdrawInvoiceMapper.class); + private final ClearingBillInfoService clearingBillInfoService = mock(ClearingBillInfoService.class); + private final PileMerchantInfoService pileMerchantInfoService = mock(PileMerchantInfoService.class); + private final TodoTaskAssigneeService assigneeService = mock(TodoTaskAssigneeService.class); + private final TodoTaskService todoTaskService = mock(TodoTaskService.class); + + private MerchantWithdrawInvoiceServiceImpl createService() { + MerchantWithdrawInvoiceServiceImpl service = new MerchantWithdrawInvoiceServiceImpl(); + setField(service, "clearingWithdrawInfoMapper", withdrawInfoMapper); + setField(service, "clearingWithdrawInvoiceMapper", invoiceMapper); + setField(service, "clearingBillInfoService", clearingBillInfoService); + setField(service, "pileMerchantInfoService", pileMerchantInfoService); + setField(service, "todoTaskAssigneeService", assigneeService); + setField(service, "todoTaskService", todoTaskService); + return service; + } + } +} diff --git a/jsowell-admin/src/test/java/com/jsowell/web/controller/pile/MerchantWithdrawInvoiceControllerTest.java b/jsowell-admin/src/test/java/com/jsowell/web/controller/pile/MerchantWithdrawInvoiceControllerTest.java new file mode 100644 index 000000000..afd1460f7 --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/web/controller/pile/MerchantWithdrawInvoiceControllerTest.java @@ -0,0 +1,78 @@ +package com.jsowell.web.controller.pile; + +import com.jsowell.common.core.domain.AjaxResult; +import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest; +import com.jsowell.pile.service.MerchantWithdrawInvoiceService; +import com.jsowell.pile.vo.web.MerchantWithdrawInvoiceVO; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class MerchantWithdrawInvoiceControllerTest { + @Test + void getInfo_shouldUseCurrentLoginUser() { + MerchantWithdrawInvoiceService service = mock(MerchantWithdrawInvoiceService.class); + TestController controller = newController(service, 8L, "operator"); + MerchantWithdrawInvoiceVO view = new MerchantWithdrawInvoiceVO(); + when(service.getForAssignee("W100", 20L, 8L)).thenReturn(view); + + AjaxResult result = controller.getInfo("W100", 20L); + + assertSame(view, result.get(AjaxResult.DATA_TAG)); + verify(service).getForAssignee("W100", 20L, 8L); + } + + @Test + void submit_shouldPassCurrentUserAndOperator() { + MerchantWithdrawInvoiceService service = mock(MerchantWithdrawInvoiceService.class); + TestController controller = newController(service, 8L, "operator"); + WithdrawInvoiceSubmitRequest request = new WithdrawInvoiceSubmitRequest(); + request.setTodoId(20L); + request.setInvoiceNumber("INV-100"); + request.setVoucherUrl("/invoice.pdf"); + when(service.submit("W100", request, 8L, "operator")).thenReturn(true); + + controller.submit("W100", request); + + verify(service).submit("W100", request, 8L, "operator"); + } + + private static TestController newController(MerchantWithdrawInvoiceService service, + Long userId, String username) { + TestController controller = new TestController(userId, username); + try { + Field field = MerchantWithdrawInvoiceController.class + .getDeclaredField("merchantWithdrawInvoiceService"); + field.setAccessible(true); + field.set(controller, service); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + return controller; + } + + private static class TestController extends MerchantWithdrawInvoiceController { + private final Long userId; + private final String username; + + private TestController(Long userId, String username) { + this.userId = userId; + this.username = username; + } + + @Override + public Long getUserId() { + return userId; + } + + @Override + public String getUsername() { + return username; + } + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/constant/MerchantWithdrawInvoiceConstants.java b/jsowell-pile/src/main/java/com/jsowell/pile/constant/MerchantWithdrawInvoiceConstants.java new file mode 100644 index 000000000..c49b5ce73 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/constant/MerchantWithdrawInvoiceConstants.java @@ -0,0 +1,17 @@ +package com.jsowell.pile.constant; + +/** + * 运营商提现开票常量。 + */ +public final class MerchantWithdrawInvoiceConstants { + public static final String TASK_TYPE = "MERCHANT_WITHDRAW_INVOICE"; + public static final String BUSINESS_TYPE = "CLEARING_WITHDRAW_INVOICE"; + public static final String ROUTE_NAME = "financeDetail"; + + public static final String STATUS_PENDING = "PENDING"; + public static final String STATUS_SUBMITTED = "SUBMITTED"; + public static final String STATUS_CLOSED = "CLOSED"; + + private MerchantWithdrawInvoiceConstants() { + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/domain/ClearingWithdrawInvoice.java b/jsowell-pile/src/main/java/com/jsowell/pile/domain/ClearingWithdrawInvoice.java new file mode 100644 index 000000000..fdbcb782d --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/domain/ClearingWithdrawInvoice.java @@ -0,0 +1,36 @@ +package com.jsowell.pile.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.jsowell.common.core.domain.BaseEntity; +import lombok.Data; + +import java.util.Date; + +/** + * 运营商提现开票信息 clearing_withdraw_invoice。 + */ +@Data +public class ClearingWithdrawInvoice extends BaseEntity { + private static final long serialVersionUID = 1L; + + private Long id; + private String withdrawCode; + private String merchantId; + private String invoiceStatus; + private String invoiceNumber; + private String voucherUrl; + private Long submittedBy; + private String submittedName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submittedTime; + + private Long closedBy; + private String closedName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date closedTime; + + private String closeReason; + private String delFlag; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/WithdrawInvoiceCloseRequest.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WithdrawInvoiceCloseRequest.java new file mode 100644 index 000000000..9f82d0725 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WithdrawInvoiceCloseRequest.java @@ -0,0 +1,16 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 平台关闭提现开票任务请求。 + */ +@Data +public class WithdrawInvoiceCloseRequest { + @NotBlank(message = "关闭原因不能为空") + @Size(max = 500, message = "关闭原因不能超过500个字符") + private String reason; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/WithdrawInvoiceSubmitRequest.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WithdrawInvoiceSubmitRequest.java new file mode 100644 index 000000000..930e6d7a1 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/WithdrawInvoiceSubmitRequest.java @@ -0,0 +1,24 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 提交提现发票信息请求。 + */ +@Data +public class WithdrawInvoiceSubmitRequest { + @NotNull(message = "待办任务 ID 不能为空") + private Long todoId; + + @NotBlank(message = "发票号码不能为空") + @Size(max = 100, message = "发票号码不能超过100个字符") + private String invoiceNumber; + + @NotBlank(message = "发票凭证不能为空") + @Size(max = 1000, message = "发票凭证地址不能超过1000个字符") + private String voucherUrl; +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/mapper/ClearingWithdrawInvoiceMapper.java b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/ClearingWithdrawInvoiceMapper.java new file mode 100644 index 000000000..42bec08a9 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/mapper/ClearingWithdrawInvoiceMapper.java @@ -0,0 +1,17 @@ +package com.jsowell.pile.mapper; + +import com.jsowell.pile.domain.ClearingWithdrawInvoice; +import org.apache.ibatis.annotations.Param; + +/** + * 运营商提现开票信息 Mapper。 + */ +public interface ClearingWithdrawInvoiceMapper { + ClearingWithdrawInvoice selectByWithdrawCode(@Param("withdrawCode") String withdrawCode); + + int insertSelective(ClearingWithdrawInvoice invoice); + + int markSubmitted(ClearingWithdrawInvoice invoice); + + int markClosed(ClearingWithdrawInvoice invoice); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/MerchantWithdrawInvoiceService.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/MerchantWithdrawInvoiceService.java new file mode 100644 index 000000000..b779cfae9 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/MerchantWithdrawInvoiceService.java @@ -0,0 +1,18 @@ +package com.jsowell.pile.service; + +import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest; +import com.jsowell.pile.vo.web.MerchantWithdrawInvoiceVO; + +/** + * 运营商提现向平台开票服务。 + */ +public interface MerchantWithdrawInvoiceService { + void handleWithdrawSucceeded(String withdrawCode); + + MerchantWithdrawInvoiceVO getForAssignee(String withdrawCode, Long todoId, Long userId); + + boolean submit(String withdrawCode, WithdrawInvoiceSubmitRequest request, + Long userId, String operatorName); + + boolean close(String withdrawCode, String reason, Long operatorId, String operatorName); +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MerchantWithdrawInvoiceServiceImpl.java b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MerchantWithdrawInvoiceServiceImpl.java new file mode 100644 index 000000000..64809816f --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/service/impl/MerchantWithdrawInvoiceServiceImpl.java @@ -0,0 +1,332 @@ +package com.jsowell.pile.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.jsowell.common.constant.Constants; +import com.jsowell.common.exception.ServiceException; +import com.jsowell.common.util.DateUtils; +import com.jsowell.common.util.StringUtils; +import com.jsowell.pile.constant.MerchantWithdrawInvoiceConstants; +import com.jsowell.pile.domain.ClearingBillInfo; +import com.jsowell.pile.domain.ClearingWithdrawInfo; +import com.jsowell.pile.domain.ClearingWithdrawInvoice; +import com.jsowell.pile.domain.PileMerchantInfo; +import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest; +import com.jsowell.pile.mapper.ClearingWithdrawInfoMapper; +import com.jsowell.pile.mapper.ClearingWithdrawInvoiceMapper; +import com.jsowell.pile.service.ClearingBillInfoService; +import com.jsowell.pile.service.MerchantWithdrawInvoiceService; +import com.jsowell.pile.service.PileMerchantInfoService; +import com.jsowell.pile.vo.web.MerchantWithdrawInvoiceVO; +import com.jsowell.system.constant.TodoTaskConstants; +import com.jsowell.system.domain.SysTodoTask; +import com.jsowell.system.domain.dto.TodoTaskCreateCommand; +import com.jsowell.system.service.TodoTaskAssigneeService; +import com.jsowell.system.service.TodoTaskService; +import org.apache.commons.collections4.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 运营商提现向平台开票服务实现。 + */ +@Service +public class MerchantWithdrawInvoiceServiceImpl implements MerchantWithdrawInvoiceService { + private static final Logger log = LoggerFactory.getLogger(MerchantWithdrawInvoiceServiceImpl.class); + + @Resource + private ClearingWithdrawInfoMapper clearingWithdrawInfoMapper; + + @Resource + private ClearingWithdrawInvoiceMapper clearingWithdrawInvoiceMapper; + + @Autowired + private ClearingBillInfoService clearingBillInfoService; + + @Autowired + private PileMerchantInfoService pileMerchantInfoService; + + @Autowired + private TodoTaskAssigneeService todoTaskAssigneeService; + + @Autowired + private TodoTaskService todoTaskService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void handleWithdrawSucceeded(String withdrawCode) { + String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64); + ClearingWithdrawInfo withdrawInfo = clearingWithdrawInfoMapper.selectByWithdrawCode(safeWithdrawCode); + if (withdrawInfo == null) { + throw new ServiceException("提现记录不存在,无法创建开票待办"); + } + + Date now = DateUtils.getNowDate(); + if (!Constants.ONE.equals(withdrawInfo.getWithdrawStatus()) || withdrawInfo.getArrivalTime() == null) { + withdrawInfo.setWithdrawStatus(Constants.ONE); + withdrawInfo.setArrivalTime(now); + withdrawInfo.setUpdateTime(now); + clearingWithdrawInfoMapper.updateByPrimaryKeySelective(withdrawInfo); + } + completeClearingBills(safeWithdrawCode); + + ClearingWithdrawInvoice invoice = getOrCreateInvoice(withdrawInfo); + if (!MerchantWithdrawInvoiceConstants.STATUS_PENDING.equals(invoice.getInvoiceStatus())) { + return; + } + createTodos(withdrawInfo); + } + + @Override + public MerchantWithdrawInvoiceVO getForAssignee(String withdrawCode, Long todoId, Long userId) { + String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64); + validateTodo(safeWithdrawCode, todoId, userId); + ClearingWithdrawInvoice invoice = requireInvoice(safeWithdrawCode); + ClearingWithdrawInfo withdrawInfo = clearingWithdrawInfoMapper.selectByWithdrawCode(safeWithdrawCode); + return buildView(invoice, withdrawInfo); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submit(String withdrawCode, WithdrawInvoiceSubmitRequest request, + Long userId, String operatorName) { + if (request == null) { + throw new ServiceException("发票提交参数不能为空"); + } + String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64); + SysTodoTask todoTask = validateTodo(safeWithdrawCode, request.getTodoId(), userId); + + ClearingWithdrawInvoice invoice = requireInvoice(safeWithdrawCode); + if (MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED.equals(invoice.getInvoiceStatus())) { + return true; + } + if (!TodoTaskConstants.isActiveStatus(todoTask.getTaskStatus())) { + throw new ServiceException("当前待办状态不允许提交发票"); + } + if (!MerchantWithdrawInvoiceConstants.STATUS_PENDING.equals(invoice.getInvoiceStatus())) { + throw new ServiceException("当前提现开票状态不允许提交"); + } + + Date now = DateUtils.getNowDate(); + invoice.setInvoiceStatus(MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED); + invoice.setInvoiceNumber(normalizeRequired(request.getInvoiceNumber(), "发票号码", 100)); + invoice.setVoucherUrl(normalizeRequired(request.getVoucherUrl(), "发票凭证", 1000)); + invoice.setSubmittedBy(userId); + invoice.setSubmittedName(normalizeOperator(operatorName, userId)); + invoice.setSubmittedTime(now); + invoice.setUpdateBy(invoice.getSubmittedName()); + invoice.setUpdateTime(now); + if (clearingWithdrawInvoiceMapper.markSubmitted(invoice) == 0) { + ClearingWithdrawInvoice current = requireInvoice(safeWithdrawCode); + if (MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED.equals(current.getInvoiceStatus())) { + return true; + } + throw new ServiceException("当前提现开票状态不允许提交"); + } + + todoTaskService.completeByBusiness(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, + safeWithdrawCode, null, userId, invoice.getSubmittedName()); + return true; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean close(String withdrawCode, String reason, Long operatorId, String operatorName) { + String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64); + if (operatorId == null || operatorId <= 0) { + throw new ServiceException("关闭操作人不能为空"); + } + ClearingWithdrawInvoice invoice = requireInvoice(safeWithdrawCode); + if (MerchantWithdrawInvoiceConstants.STATUS_CLOSED.equals(invoice.getInvoiceStatus())) { + return true; + } + if (!MerchantWithdrawInvoiceConstants.STATUS_PENDING.equals(invoice.getInvoiceStatus())) { + throw new ServiceException("当前提现开票状态不允许关闭"); + } + + Date now = DateUtils.getNowDate(); + String safeOperator = normalizeOperator(operatorName, operatorId); + String safeReason = normalizeRequired(reason, "关闭原因", 500); + invoice.setInvoiceStatus(MerchantWithdrawInvoiceConstants.STATUS_CLOSED); + invoice.setClosedBy(operatorId); + invoice.setClosedName(safeOperator); + invoice.setClosedTime(now); + invoice.setCloseReason(safeReason); + invoice.setUpdateBy(safeOperator); + invoice.setUpdateTime(now); + if (clearingWithdrawInvoiceMapper.markClosed(invoice) == 0) { + ClearingWithdrawInvoice current = requireInvoice(safeWithdrawCode); + if (MerchantWithdrawInvoiceConstants.STATUS_CLOSED.equals(current.getInvoiceStatus())) { + return true; + } + throw new ServiceException("当前提现开票状态不允许关闭"); + } + + todoTaskService.cancelByBusiness(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, + safeWithdrawCode, null, safeOperator, safeReason); + return true; + } + + private void completeClearingBills(String withdrawCode) { + List billInfos = clearingBillInfoService.selectByWithdrawCode(withdrawCode); + if (CollectionUtils.isEmpty(billInfos)) { + return; + } + for (ClearingBillInfo billInfo : billInfos) { + billInfo.setBillStatus("4"); + } + clearingBillInfoService.updateBatchSelective(billInfos); + } + + private ClearingWithdrawInvoice getOrCreateInvoice(ClearingWithdrawInfo withdrawInfo) { + ClearingWithdrawInvoice invoice = clearingWithdrawInvoiceMapper.selectByWithdrawCode(withdrawInfo.getWithdrawCode()); + if (invoice != null) { + return invoice; + } + + invoice = new ClearingWithdrawInvoice(); + invoice.setWithdrawCode(withdrawInfo.getWithdrawCode()); + invoice.setMerchantId(withdrawInfo.getMerchantId()); + invoice.setInvoiceStatus(MerchantWithdrawInvoiceConstants.STATUS_PENDING); + invoice.setCreateBy("adapay-callback"); + invoice.setDelFlag("0"); + try { + clearingWithdrawInvoiceMapper.insertSelective(invoice); + return invoice; + } catch (DuplicateKeyException exception) { + invoice = clearingWithdrawInvoiceMapper.selectByWithdrawCode(withdrawInfo.getWithdrawCode()); + if (invoice != null) { + return invoice; + } + throw exception; + } + } + + private void createTodos(ClearingWithdrawInfo withdrawInfo) { + if (StringUtils.isBlank(withdrawInfo.getMerchantId())) { + log.warn("提现记录缺少运营商,无法创建开票待办,withdrawCode={}", withdrawInfo.getWithdrawCode()); + return; + } + + PileMerchantInfo merchantInfo; + try { + merchantInfo = pileMerchantInfoService.selectPileMerchantInfoById(Long.parseLong(withdrawInfo.getMerchantId())); + } catch (NumberFormatException exception) { + log.warn("提现记录运营商 ID 非法,无法创建开票待办,withdrawCode={}, merchantId={}", + withdrawInfo.getWithdrawCode(), withdrawInfo.getMerchantId()); + return; + } + if (merchantInfo == null || StringUtils.isBlank(merchantInfo.getDeptId())) { + log.warn("提现记录运营商未配置部门,无法创建开票待办,withdrawCode={}, merchantId={}", + withdrawInfo.getWithdrawCode(), withdrawInfo.getMerchantId()); + return; + } + + Long deptId; + try { + deptId = Long.parseLong(merchantInfo.getDeptId()); + } catch (NumberFormatException exception) { + log.warn("提现记录运营商部门 ID 非法,无法创建开票待办,withdrawCode={}, deptId={}", + withdrawInfo.getWithdrawCode(), merchantInfo.getDeptId()); + return; + } + List assigneeUserIds = todoTaskAssigneeService.findActiveOperatorAdminUserIdsByDeptTree(deptId); + if (CollectionUtils.isEmpty(assigneeUserIds)) { + log.warn("提现记录运营商部门下没有正常管理员,无法创建开票待办,withdrawCode={}, deptId={}", + withdrawInfo.getWithdrawCode(), deptId); + return; + } + + String routeParams = buildRouteParams(withdrawInfo); + for (Long assigneeUserId : assigneeUserIds) { + String idempotentKey = MerchantWithdrawInvoiceConstants.TASK_TYPE + ":" + + MerchantWithdrawInvoiceConstants.BUSINESS_TYPE + ":" + + withdrawInfo.getWithdrawCode() + ":" + assigneeUserId; + todoTaskService.createTask(TodoTaskCreateCommand.builder() + .taskType(MerchantWithdrawInvoiceConstants.TASK_TYPE) + .title("运营商提现向平台开票") + .summary("提现已成功,请向平台提交发票,提现编号:" + withdrawInfo.getWithdrawCode()) + .businessType(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE) + .businessId(withdrawInfo.getWithdrawCode()) + .routeName(MerchantWithdrawInvoiceConstants.ROUTE_NAME) + .routeParams(routeParams) + .assigneeUserId(assigneeUserId) + .assigneeMerchantId(withdrawInfo.getMerchantId()) + .priority(TodoTaskConstants.PRIORITY_IMPORTANT) + .idempotentKey(idempotentKey) + .createBy("adapay-callback") + .build()); + } + } + + private String buildRouteParams(ClearingWithdrawInfo withdrawInfo) { + Map query = new HashMap<>(2); + query.put("merchantId", withdrawInfo.getMerchantId()); + query.put("withdrawCode", withdrawInfo.getWithdrawCode()); + Map routeParams = new HashMap<>(1); + routeParams.put("query", query); + return JSON.toJSONString(routeParams); + } + + private SysTodoTask validateTodo(String withdrawCode, Long todoId, Long userId) { + SysTodoTask todoTask = todoTaskService.getForAssignee(todoId, userId); + if (!MerchantWithdrawInvoiceConstants.TASK_TYPE.equals(todoTask.getTaskType()) + || !MerchantWithdrawInvoiceConstants.BUSINESS_TYPE.equals(todoTask.getBusinessType()) + || !withdrawCode.equals(todoTask.getBusinessId())) { + throw new ServiceException("待办任务与提现开票信息不匹配"); + } + return todoTask; + } + + private ClearingWithdrawInvoice requireInvoice(String withdrawCode) { + ClearingWithdrawInvoice invoice = clearingWithdrawInvoiceMapper.selectByWithdrawCode(withdrawCode); + if (invoice == null) { + throw new ServiceException("提现开票信息不存在"); + } + return invoice; + } + + private MerchantWithdrawInvoiceVO buildView(ClearingWithdrawInvoice invoice, + ClearingWithdrawInfo withdrawInfo) { + MerchantWithdrawInvoiceVO view = new MerchantWithdrawInvoiceVO(); + BeanUtils.copyProperties(invoice, view); + if (withdrawInfo != null) { + view.setWithdrawAmt(withdrawInfo.getWithdrawAmt()); + view.setFeeAmt(withdrawInfo.getFeeAmt()); + view.setCreditedAmt(withdrawInfo.getCreditedAmt()); + view.setApplicationTime(withdrawInfo.getApplicationTime()); + view.setArrivalTime(withdrawInfo.getArrivalTime()); + } + return view; + } + + private String normalizeRequired(String value, String fieldName, int maxLength) { + String safeValue = StringUtils.trim(value); + if (StringUtils.isEmpty(safeValue)) { + throw new ServiceException(fieldName + "不能为空"); + } + if (safeValue.length() > maxLength) { + throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符"); + } + return safeValue; + } + + private String normalizeOperator(String operatorName, Long operatorId) { + String safeOperator = StringUtils.isNotEmpty(operatorName) + ? operatorName.trim() : String.valueOf(operatorId); + if (safeOperator.length() > 64) { + throw new ServiceException("操作人名称不能超过64个字符"); + } + return safeOperator; + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MerchantWithdrawInvoiceVO.java b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MerchantWithdrawInvoiceVO.java new file mode 100644 index 000000000..c82629bc5 --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/vo/web/MerchantWithdrawInvoiceVO.java @@ -0,0 +1,41 @@ +package com.jsowell.pile.vo.web; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 运营商提现开票详情。 + */ +@Data +public class MerchantWithdrawInvoiceVO { + private Long id; + private String withdrawCode; + private String merchantId; + private String invoiceStatus; + private String invoiceNumber; + private String voucherUrl; + private Long submittedBy; + private String submittedName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date submittedTime; + + private String closedName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date closedTime; + + private String closeReason; + private BigDecimal withdrawAmt; + private BigDecimal feeAmt; + private BigDecimal creditedAmt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date applicationTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date arrivalTime; +} diff --git a/jsowell-pile/src/main/resources/mapper/pile/ClearingWithdrawInvoiceMapper.xml b/jsowell-pile/src/main/resources/mapper/pile/ClearingWithdrawInvoiceMapper.xml new file mode 100644 index 000000000..6e70f4d29 --- /dev/null +++ b/jsowell-pile/src/main/resources/mapper/pile/ClearingWithdrawInvoiceMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + id, withdraw_code, merchant_id, invoice_status, invoice_number, voucher_url, + submitted_by, submitted_name, submitted_time, closed_by, closed_name, closed_time, + close_reason, create_by, create_time, update_by, update_time, remark, del_flag + + + + + + insert into clearing_withdraw_invoice ( + withdraw_code, merchant_id, invoice_status, create_by, create_time, del_flag + ) values ( + #{withdrawCode}, #{merchantId}, #{invoiceStatus}, #{createBy}, sysdate(), #{delFlag} + ) + + + + update clearing_withdraw_invoice + set invoice_status = #{invoiceStatus}, + invoice_number = #{invoiceNumber}, + voucher_url = #{voucherUrl}, + submitted_by = #{submittedBy}, + submitted_name = #{submittedName}, + submitted_time = #{submittedTime}, + update_by = #{updateBy}, + update_time = #{updateTime} + where id = #{id} + and invoice_status = 'PENDING' + and del_flag = '0' + + + + update clearing_withdraw_invoice + set invoice_status = #{invoiceStatus}, + closed_by = #{closedBy}, + closed_name = #{closedName}, + closed_time = #{closedTime}, + close_reason = #{closeReason}, + update_by = #{updateBy}, + update_time = #{updateTime} + where id = #{id} + and invoice_status = 'PENDING' + and del_flag = '0' + +