接入运营商提现开票待办闭环

This commit is contained in:
jsowell
2026-07-17 17:48:14 +08:00
parent bc20330f1b
commit 3e299f40f9
14 changed files with 920 additions and 16 deletions

View File

@@ -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';

View File

@@ -124,6 +124,9 @@ public class OrderService {
@Autowired @Autowired
private ClearingWithdrawInfoService clearingWithdrawInfoService; private ClearingWithdrawInfoService clearingWithdrawInfoService;
@Autowired
private MerchantWithdrawInvoiceService merchantWithdrawInvoiceService;
@Autowired @Autowired
private MemberAdapayRecordService memberAdapayRecordService; private MemberAdapayRecordService memberAdapayRecordService;
@@ -1464,22 +1467,7 @@ public class OrderService {
log.info("取现成功 data:{}", JSON.toJSONString(data)); log.info("取现成功 data:{}", JSON.toJSONString(data));
JSONObject jsonObject = JSON.parseObject(data); JSONObject jsonObject = JSON.parseObject(data);
String withdrawCode = jsonObject.getString("id"); String withdrawCode = jsonObject.getString("id");
// 通过取现id查询取现数据 merchantWithdrawInvoiceService.handleWithdrawSucceeded(withdrawCode);
ClearingWithdrawInfo clearingWithdrawInfo = clearingWithdrawInfoService.selectByWithdrawCode(withdrawCode);
if (clearingWithdrawInfo != null) {
clearingWithdrawInfo.setWithdrawStatus(Constants.ONE);
clearingWithdrawInfo.setUpdateTime(DateUtils.getNowDate());
clearingWithdrawInfoService.updateByPrimaryKeySelective(clearingWithdrawInfo);
}
// 清分账单数据更新
List<ClearingBillInfo> clearingBillInfos = clearingBillInfoService.selectByWithdrawCode(withdrawCode);
if (CollectionUtils.isNotEmpty(clearingBillInfos)) {
for (ClearingBillInfo clearingBillInfo : clearingBillInfos) {
clearingBillInfo.setBillStatus("4");
}
clearingBillInfoService.updateBatchSelective(clearingBillInfos);
}
} }
/** /**

View File

@@ -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()));
}
}

View File

@@ -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<TodoTaskCreateCommand> 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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<ClearingBillInfo> 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<Long> 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<String, Object> query = new HashMap<>(2);
query.put("merchantId", withdrawInfo.getMerchantId());
query.put("withdrawCode", withdrawInfo.getWithdrawCode());
Map<String, Object> 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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jsowell.pile.mapper.ClearingWithdrawInvoiceMapper">
<resultMap id="BaseResultMap" type="com.jsowell.pile.domain.ClearingWithdrawInvoice">
<id column="id" property="id" />
<result column="withdraw_code" property="withdrawCode" />
<result column="merchant_id" property="merchantId" />
<result column="invoice_status" property="invoiceStatus" />
<result column="invoice_number" property="invoiceNumber" />
<result column="voucher_url" property="voucherUrl" />
<result column="submitted_by" property="submittedBy" />
<result column="submitted_name" property="submittedName" />
<result column="submitted_time" property="submittedTime" />
<result column="closed_by" property="closedBy" />
<result column="closed_name" property="closedName" />
<result column="closed_time" property="closedTime" />
<result column="close_reason" property="closeReason" />
<result column="create_by" property="createBy" />
<result column="create_time" property="createTime" />
<result column="update_by" property="updateBy" />
<result column="update_time" property="updateTime" />
<result column="remark" property="remark" />
<result column="del_flag" property="delFlag" />
</resultMap>
<sql id="BaseColumnList">
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
</sql>
<select id="selectByWithdrawCode" resultMap="BaseResultMap">
select <include refid="BaseColumnList" />
from clearing_withdraw_invoice
where withdraw_code = #{withdrawCode}
and del_flag = '0'
limit 1
</select>
<insert id="insertSelective" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
insert into clearing_withdraw_invoice (
withdraw_code, merchant_id, invoice_status, create_by, create_time, del_flag
) values (
#{withdrawCode}, #{merchantId}, #{invoiceStatus}, #{createBy}, sysdate(), #{delFlag}
)
</insert>
<update id="markSubmitted">
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>
<update id="markClosed">
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'
</update>
</mapper>