🔀 合并 ticket 01:异常拒绝与撤销接口
This commit is contained in:
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* BladeX Commercial License Agreement
|
||||||
|
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||||
|
* <p>
|
||||||
|
* Use of this software is governed by the Commercial License Agreement
|
||||||
|
* obtained after purchasing a license from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 1. This software is for development use only under a valid license
|
||||||
|
* from BladeX.
|
||||||
|
* <p>
|
||||||
|
* 2. Redistribution of this software's source code to any third party
|
||||||
|
* without a commercial license is strictly prohibited.
|
||||||
|
* <p>
|
||||||
|
* 3. Licensees may copyright their own code but cannot use segments
|
||||||
|
* from this software for such purposes. Copyright of this software
|
||||||
|
* remains with BladeX.
|
||||||
|
* <p>
|
||||||
|
* Using this software signifies agreement to this License, and the software
|
||||||
|
* must not be used for illegal purposes.
|
||||||
|
* <p>
|
||||||
|
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||||
|
* not liable for any claims arising from secondary or illegal development.
|
||||||
|
* <p>
|
||||||
|
* Author: Chill Zhuang (bladejava@qq.com)
|
||||||
|
*/
|
||||||
|
package org.springblade.transport.pojo.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异常处置拒绝请求
|
||||||
|
* <p>
|
||||||
|
* 只有拒绝原因一个业务字段:拒绝复用了结终态,不留独立列,
|
||||||
|
* 原因随跟进记录落进 follow_content。
|
||||||
|
*
|
||||||
|
* @author Chill
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "异常处置拒绝请求")
|
||||||
|
public class ExceptionDisposalRejectRequest implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "异常处置ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "拒绝原因,必填,最多200字")
|
||||||
|
private String rejectReason;
|
||||||
|
|
||||||
|
}
|
||||||
+18
-1
@@ -33,6 +33,7 @@ import org.springblade.core.mp.support.Query;
|
|||||||
import org.springblade.core.secure.annotation.PreAuth;
|
import org.springblade.core.secure.annotation.PreAuth;
|
||||||
import org.springblade.core.tool.api.R;
|
import org.springblade.core.tool.api.R;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||||
|
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||||
@@ -50,7 +51,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
* <p>
|
* <p>
|
||||||
* 对外路径:{@code /api/blade-transport/exception-disposal/**}
|
* 对外路径:{@code /api/blade-transport/exception-disposal/**}
|
||||||
* 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。
|
* 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。
|
||||||
* 列表 / 详情 / 上报 / 跟进 / 完成均仅需登录态(小程序调度端与司机端共用)。
|
* 列表 / 详情 / 上报 / 跟进 / 完成 / 拒绝 / 撤销均仅需登录态(小程序调度端与司机端共用)。
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -114,4 +115,20 @@ public class ExceptionDisposalController extends BladeController {
|
|||||||
return R.success("编辑成功");
|
return R.success("编辑成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/reject")
|
||||||
|
@ApiOperationSupport(order = 8)
|
||||||
|
@Operation(summary = "拒绝异常", description = "调度端拒绝上报:先写一条拒绝原因的跟进记录,再了结;仅需登录态")
|
||||||
|
public R reject(@RequestBody ExceptionDisposalRejectRequest request) {
|
||||||
|
exceptionDisposalService.reject(request);
|
||||||
|
return R.success("已拒绝");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/revoke")
|
||||||
|
@ApiOperationSupport(order = 9)
|
||||||
|
@Operation(summary = "撤销异常上报", description = "上报人撤销自己未了结的上报,不写跟进记录;仅需登录态")
|
||||||
|
public R revoke(@RequestBody ExceptionDisposalFollowRequest request) {
|
||||||
|
exceptionDisposalService.revoke(request.getId());
|
||||||
|
return R.success("已撤销");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -25,6 +25,7 @@ package org.springblade.transport.service;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import org.springblade.core.mp.base.BaseService;
|
import org.springblade.core.mp.base.BaseService;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||||
|
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||||
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||||
@@ -56,4 +57,15 @@ public interface IExceptionDisposalService extends BaseService<ExceptionDisposal
|
|||||||
|
|
||||||
void batchComplete(String ids);
|
void batchComplete(String ids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝异常:先写一条「拒绝:原因」的跟进记录,再了结。
|
||||||
|
* 拒绝了结复用已完成终态,不新增状态值。
|
||||||
|
*/
|
||||||
|
void reject(ExceptionDisposalRejectRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 撤销异常上报:仅限上报人本人、仅限未了结,置为已撤销,不留跟进记录
|
||||||
|
*/
|
||||||
|
void revoke(Long id);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+58
@@ -35,6 +35,7 @@ import org.springblade.system.cache.UserDisplayCache;
|
|||||||
import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper;
|
import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper;
|
||||||
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||||
|
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||||
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
|
||||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||||
import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
||||||
@@ -72,10 +73,14 @@ public class ExceptionDisposalServiceImpl
|
|||||||
private static final String STATUS_PENDING = "pending";
|
private static final String STATUS_PENDING = "pending";
|
||||||
private static final String STATUS_PROCESSING = "processing";
|
private static final String STATUS_PROCESSING = "processing";
|
||||||
private static final String STATUS_COMPLETED = "completed";
|
private static final String STATUS_COMPLETED = "completed";
|
||||||
|
private static final String STATUS_REVOKED = "revoked";
|
||||||
|
|
||||||
/** latest_follow_content 与 follow_content 两列均为 varchar(500) */
|
/** latest_follow_content 与 follow_content 两列均为 varchar(500) */
|
||||||
private static final int FOLLOW_CONTENT_MAX_LENGTH = 500;
|
private static final int FOLLOW_CONTENT_MAX_LENGTH = 500;
|
||||||
|
|
||||||
|
/** 拒绝原因长度上限,与上报说明、异常原因同量级,且远小于跟进记录列宽 */
|
||||||
|
private static final int REJECT_REASON_MAX_LENGTH = 200;
|
||||||
|
|
||||||
private final ExceptionDisposalFollowRecordMapper followRecordMapper;
|
private final ExceptionDisposalFollowRecordMapper followRecordMapper;
|
||||||
private final IWaybillService waybillService;
|
private final IWaybillService waybillService;
|
||||||
private final IDriverWaybillService driverWaybillService;
|
private final IDriverWaybillService driverWaybillService;
|
||||||
@@ -394,6 +399,58 @@ public class ExceptionDisposalServiceImpl
|
|||||||
updateBatchById(disposals);
|
updateBatchById(disposals);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void reject(ExceptionDisposalRejectRequest request) {
|
||||||
|
if (request == null || request.getId() == null) {
|
||||||
|
throw new ServiceException("请选择需要拒绝的异常");
|
||||||
|
}
|
||||||
|
ExceptionDisposal disposal = getExisting(request.getId());
|
||||||
|
// 已完成、已撤销都算已了结:再拒绝一次会覆盖最新处置结论,并往处置记录里多添一条
|
||||||
|
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
|
||||||
|
throw new ServiceException("该异常已处理");
|
||||||
|
}
|
||||||
|
String rejectReason = Func.toStr(request.getRejectReason(), "").trim();
|
||||||
|
if (Func.isBlank(rejectReason)) {
|
||||||
|
throw new ServiceException("请填写拒绝原因");
|
||||||
|
}
|
||||||
|
if (rejectReason.length() > REJECT_REASON_MAX_LENGTH) {
|
||||||
|
throw new ServiceException("拒绝原因不能超过200字");
|
||||||
|
}
|
||||||
|
|
||||||
|
String rejectContent = "拒绝:" + rejectReason;
|
||||||
|
ExceptionDisposalFollowRecord record = new ExceptionDisposalFollowRecord();
|
||||||
|
record.setDisposalId(disposal.getId());
|
||||||
|
record.setFollowContent(rejectContent);
|
||||||
|
// 拒绝弹窗只有原因、没有附件,attachments_json 不写
|
||||||
|
record.setFollowUser(AuthUtil.getUserId());
|
||||||
|
record.setFollowUserName(UserDisplayCache.getUserRealName(AuthUtil.getUserId()));
|
||||||
|
record.setFollowTime(LocalDateTime.now());
|
||||||
|
followRecordMapper.insert(record);
|
||||||
|
|
||||||
|
// 记录先于状态落库,且同在一个事务里:结案了却没有拒绝记录,事后无从知道是谁以什么理由拒绝的
|
||||||
|
disposal.setDisposalStatus(STATUS_COMPLETED);
|
||||||
|
disposal.setLatestFollowContent(rejectContent);
|
||||||
|
updateById(disposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void revoke(Long id) {
|
||||||
|
ExceptionDisposal disposal = getExisting(id);
|
||||||
|
Long currentUserId = AuthUtil.getUserId();
|
||||||
|
// 撤销是上报人收回自己的上报,不是一次处置:别的账号(包括调度员)要推翻上报只能走拒绝
|
||||||
|
if (currentUserId == null || !Objects.equals(disposal.getReporterId(), currentUserId)) {
|
||||||
|
throw new ServiceException("只能撤销本人上报的异常");
|
||||||
|
}
|
||||||
|
if (WaybillProcessSupport.SETTLED_DISPOSAL_STATUSES.contains(disposal.getDisposalStatus())) {
|
||||||
|
throw new ServiceException("该异常已处理");
|
||||||
|
}
|
||||||
|
// 不写跟进记录,也不动 latest_follow_content:处置时间线里不该出现一条由上报人「处置」的记录
|
||||||
|
disposal.setDisposalStatus(STATUS_REVOKED);
|
||||||
|
updateById(disposal);
|
||||||
|
}
|
||||||
|
|
||||||
private LambdaQueryWrapper<ExceptionDisposal> buildQuery(ExceptionDisposalVO query) {
|
private LambdaQueryWrapper<ExceptionDisposal> buildQuery(ExceptionDisposalVO query) {
|
||||||
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||||
.eq(ExceptionDisposal::getIsDeleted, 0)
|
.eq(ExceptionDisposal::getIsDeleted, 0)
|
||||||
@@ -488,6 +545,7 @@ public class ExceptionDisposalServiceImpl
|
|||||||
case STATUS_PENDING -> "待处理";
|
case STATUS_PENDING -> "待处理";
|
||||||
case STATUS_PROCESSING -> "处理中";
|
case STATUS_PROCESSING -> "处理中";
|
||||||
case STATUS_COMPLETED -> "已完成";
|
case STATUS_COMPLETED -> "已完成";
|
||||||
|
case STATUS_REVOKED -> "已撤销";
|
||||||
default -> "";
|
default -> "";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
package org.springblade.transport.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.springblade.core.log.exception.ServiceException;
|
||||||
|
import org.springblade.core.secure.utils.AuthUtil;
|
||||||
|
import org.springblade.system.cache.UserDisplayCache;
|
||||||
|
import org.springblade.transport.mapper.ExceptionDisposalFollowRecordMapper;
|
||||||
|
import org.springblade.transport.mapper.ExceptionDisposalMapper;
|
||||||
|
import org.springblade.transport.pojo.dto.ExceptionDisposalRejectRequest;
|
||||||
|
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||||
|
import org.springblade.transport.pojo.entity.ExceptionDisposalFollowRecord;
|
||||||
|
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
|
||||||
|
import org.springblade.transport.service.IDriverWaybillService;
|
||||||
|
import org.springblade.transport.service.IWaybillService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝与撤销:两个动作都了结异常,但只有拒绝留痕。
|
||||||
|
* <p>
|
||||||
|
* 拒绝复用已完成这个终态(台账仍显示「已处理」),所以它与「完成」的区别只落在跟进记录的第一句;
|
||||||
|
* 撤销是上报人收回,不是处置,因此不留跟进记录。
|
||||||
|
*/
|
||||||
|
class ExceptionDisposalRejectRevokeTest {
|
||||||
|
|
||||||
|
private static final Long DISPOSAL_ID = 6101L;
|
||||||
|
private static final Long REPORTER_ID = 7001L;
|
||||||
|
|
||||||
|
private ExceptionDisposalMapper disposalMapper;
|
||||||
|
private ExceptionDisposalFollowRecordMapper followRecordMapper;
|
||||||
|
private ExceptionDisposalServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initTableInfo() {
|
||||||
|
// 构造 LambdaQueryWrapper 需要 MyBatis-Plus 的实体元数据缓存
|
||||||
|
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||||
|
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||||
|
assistant.setCurrentNamespace("test");
|
||||||
|
TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
disposalMapper = mock(ExceptionDisposalMapper.class);
|
||||||
|
followRecordMapper = mock(ExceptionDisposalFollowRecordMapper.class);
|
||||||
|
service = new ExceptionDisposalServiceImpl(
|
||||||
|
followRecordMapper, mock(IWaybillService.class), mock(IDriverWaybillService.class));
|
||||||
|
inject("baseMapper", disposalMapper);
|
||||||
|
when(disposalMapper.updateById(any(ExceptionDisposal.class))).thenReturn(1);
|
||||||
|
when(followRecordMapper.insert(any(ExceptionDisposalFollowRecord.class))).thenReturn(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ 拒绝
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("待处理异常被拒绝后了结,并留下一条带「拒绝:」前缀的跟进记录")
|
||||||
|
void rejectSettlesPendingDisposalWithFollowRecord() {
|
||||||
|
stubDisposal("pending", REPORTER_ID);
|
||||||
|
|
||||||
|
runAsReporter(() -> service.reject(rejectRequest("地址核实无误,按原计划执行")));
|
||||||
|
|
||||||
|
ExceptionDisposalFollowRecord record = insertedRecord();
|
||||||
|
assertEquals("拒绝:地址核实无误,按原计划执行", record.getFollowContent());
|
||||||
|
assertEquals(DISPOSAL_ID, record.getDisposalId());
|
||||||
|
assertEquals(REPORTER_ID, record.getFollowUser());
|
||||||
|
assertEquals("韦调度", record.getFollowUserName());
|
||||||
|
assertNotNull(record.getFollowTime());
|
||||||
|
assertNull(record.getAttachmentsJson(), "拒绝弹窗没有附件,跟进记录不得凭空写一份");
|
||||||
|
|
||||||
|
ExceptionDisposal settled = updatedDisposal();
|
||||||
|
assertEquals("completed", settled.getDisposalStatus(), "拒绝复用已完成终态,不新增状态值");
|
||||||
|
assertEquals("拒绝:地址核实无误,按原计划执行", settled.getLatestFollowContent(),
|
||||||
|
"列表上的最新跟进说明要与跟进记录同一条文案");
|
||||||
|
verify(followRecordMapper).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ 拒绝门禁与校验
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("处理中的异常同样能被拒绝并了结")
|
||||||
|
void rejectSettlesProcessingDisposal() {
|
||||||
|
stubDisposal("processing", REPORTER_ID);
|
||||||
|
|
||||||
|
runAsReporter(() -> service.reject(rejectRequest("按原计划执行")));
|
||||||
|
|
||||||
|
assertEquals("completed", updatedDisposal().getDisposalStatus());
|
||||||
|
assertEquals("拒绝:按原计划执行", insertedRecord().getFollowContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("已完成的异常再拒绝报「该异常已处理」,且不多写记录、不改状态")
|
||||||
|
void rejectSettledDisposalRejected() {
|
||||||
|
stubDisposal("completed", REPORTER_ID);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.reject(rejectRequest("地址核实无误"))));
|
||||||
|
|
||||||
|
assertEquals("该异常已处理", exception.getMessage());
|
||||||
|
verify(followRecordMapper, never()).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
verify(disposalMapper, never()).updateById(any(ExceptionDisposal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("已撤销的异常同属了结态,再拒绝也是「该异常已处理」")
|
||||||
|
void rejectRevokedDisposalRejected() {
|
||||||
|
stubDisposal("revoked", REPORTER_ID);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.reject(rejectRequest("地址核实无误"))));
|
||||||
|
|
||||||
|
assertEquals("该异常已处理", exception.getMessage());
|
||||||
|
verify(followRecordMapper, never()).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("拒绝原因只有空白时不肯了结,要求填写原因")
|
||||||
|
void blankRejectReasonRejected() {
|
||||||
|
stubDisposal("pending", REPORTER_ID);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.reject(rejectRequest(" "))));
|
||||||
|
|
||||||
|
assertEquals("请填写拒绝原因", exception.getMessage());
|
||||||
|
verify(followRecordMapper, never()).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
verify(disposalMapper, never()).updateById(any(ExceptionDisposal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("拒绝原因超过200字被拒,避免撑爆跟进记录列")
|
||||||
|
void tooLongRejectReasonRejected() {
|
||||||
|
stubDisposal("pending", REPORTER_ID);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.reject(rejectRequest("理".repeat(201)))));
|
||||||
|
|
||||||
|
assertEquals("拒绝原因不能超过200字", exception.getMessage());
|
||||||
|
verify(followRecordMapper, never()).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("拒绝原因去掉首尾空白后落库")
|
||||||
|
void rejectReasonIsTrimmed() {
|
||||||
|
stubDisposal("pending", REPORTER_ID);
|
||||||
|
|
||||||
|
runAsReporter(() -> service.reject(rejectRequest(" 地址核实无误 ")));
|
||||||
|
|
||||||
|
assertEquals("拒绝:地址核实无误", insertedRecord().getFollowContent());
|
||||||
|
assertEquals("拒绝:地址核实无误", updatedDisposal().getLatestFollowContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("已撤销的异常状态名为「已撤销」,不能是空串")
|
||||||
|
void revokedStatusHasDisplayName() {
|
||||||
|
stubDisposal("revoked", REPORTER_ID);
|
||||||
|
when(followRecordMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
ExceptionDisposalVO vo = asReporter(() -> service.detail(DISPOSAL_ID));
|
||||||
|
|
||||||
|
assertEquals("已撤销", vo.getDisposalStatusName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ 撤销
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("上报人撤销自己的待处理异常:置为已撤销,不留跟进记录")
|
||||||
|
void revokeOwnPendingDisposal() {
|
||||||
|
stubDisposal("pending", REPORTER_ID);
|
||||||
|
|
||||||
|
runAsReporter(() -> service.revoke(DISPOSAL_ID));
|
||||||
|
|
||||||
|
assertEquals("revoked", updatedDisposal().getDisposalStatus());
|
||||||
|
verify(followRecordMapper, never()).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("已经有人跟进(处理中)的异常,上报人仍能撤销:撤销取回的是上报本身")
|
||||||
|
void revokeOwnProcessingDisposal() {
|
||||||
|
stubDisposal("processing", REPORTER_ID);
|
||||||
|
|
||||||
|
runAsReporter(() -> service.revoke(DISPOSAL_ID));
|
||||||
|
|
||||||
|
assertEquals("revoked", updatedDisposal().getDisposalStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("撤销别人的上报被拒:撤销只能由上报人发起")
|
||||||
|
void revokeOthersDisposalRejected() {
|
||||||
|
stubDisposal("pending", 7002L);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.revoke(DISPOSAL_ID)));
|
||||||
|
|
||||||
|
assertEquals("只能撤销本人上报的异常", exception.getMessage());
|
||||||
|
verify(disposalMapper, never()).updateById(any(ExceptionDisposal.class));
|
||||||
|
verify(followRecordMapper, never()).insert(any(ExceptionDisposalFollowRecord.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("没有上报人(历史数据)时不得撤销:份属不明按非本人处理")
|
||||||
|
void revokeDisposalWithoutReporterRejected() {
|
||||||
|
stubDisposal("pending", null);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.revoke(DISPOSAL_ID)));
|
||||||
|
|
||||||
|
assertEquals("只能撤销本人上报的异常", exception.getMessage());
|
||||||
|
verify(disposalMapper, never()).updateById(any(ExceptionDisposal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("已完成的异常不能再撤销")
|
||||||
|
void revokeSettledDisposalRejected() {
|
||||||
|
stubDisposal("completed", REPORTER_ID);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.revoke(DISPOSAL_ID)));
|
||||||
|
|
||||||
|
assertEquals("该异常已处理", exception.getMessage());
|
||||||
|
verify(disposalMapper, never()).updateById(any(ExceptionDisposal.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("已撤销的异常再撤销同样是「该异常已处理」")
|
||||||
|
void revokeAlreadyRevokedDisposalRejected() {
|
||||||
|
stubDisposal("revoked", REPORTER_ID);
|
||||||
|
|
||||||
|
ServiceException exception = assertThrows(ServiceException.class,
|
||||||
|
() -> runAsReporter(() -> service.revoke(DISPOSAL_ID)));
|
||||||
|
|
||||||
|
assertEquals("该异常已处理", exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ 夹具
|
||||||
|
|
||||||
|
private void stubDisposal(String status, Long reporterId) {
|
||||||
|
ExceptionDisposal disposal = new ExceptionDisposal();
|
||||||
|
disposal.setId(DISPOSAL_ID);
|
||||||
|
disposal.setDisposalStatus(status);
|
||||||
|
disposal.setReporterId(reporterId);
|
||||||
|
disposal.setIsDeleted(0);
|
||||||
|
when(disposalMapper.selectById(DISPOSAL_ID)).thenReturn(disposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExceptionDisposalRejectRequest rejectRequest(String reason) {
|
||||||
|
ExceptionDisposalRejectRequest request = new ExceptionDisposalRejectRequest();
|
||||||
|
request.setId(DISPOSAL_ID);
|
||||||
|
request.setRejectReason(reason);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExceptionDisposalFollowRecord insertedRecord() {
|
||||||
|
ArgumentCaptor<ExceptionDisposalFollowRecord> captor =
|
||||||
|
ArgumentCaptor.forClass(ExceptionDisposalFollowRecord.class);
|
||||||
|
verify(followRecordMapper).insert(captor.capture());
|
||||||
|
return captor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExceptionDisposal updatedDisposal() {
|
||||||
|
ArgumentCaptor<ExceptionDisposal> captor = ArgumentCaptor.forClass(ExceptionDisposal.class);
|
||||||
|
verify(disposalMapper).updateById(captor.capture());
|
||||||
|
return captor.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 以调度员身份执行,登录态与真实姓名都走缓存 */
|
||||||
|
private void runAsReporter(Runnable action) {
|
||||||
|
asReporter(() -> {
|
||||||
|
action.run();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T asReporter(Supplier<T> action) {
|
||||||
|
try (MockedStatic<AuthUtil> authUtil = mockStatic(AuthUtil.class);
|
||||||
|
MockedStatic<UserDisplayCache> userCache = mockStatic(UserDisplayCache.class)) {
|
||||||
|
authUtil.when(AuthUtil::getUserId).thenReturn(REPORTER_ID);
|
||||||
|
userCache.when(() -> UserDisplayCache.getUserRealName(REPORTER_ID)).thenReturn("韦调度");
|
||||||
|
return action.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void inject(String field, Object value) throws Exception {
|
||||||
|
Class<?> type = service.getClass();
|
||||||
|
while (type != null) {
|
||||||
|
try {
|
||||||
|
Field declared = type.getDeclaredField(field);
|
||||||
|
declared.setAccessible(true);
|
||||||
|
declared.set(service, value);
|
||||||
|
return;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
type = type.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("未找到字段:" + field);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user