fix: allow manager cancellation of random orders

This commit is contained in:
irving
2025-12-31 23:02:43 -05:00
parent 911a974e51
commit ec5c1782c6
2 changed files with 268 additions and 2 deletions

View File

@@ -44,7 +44,9 @@ import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService; import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator; import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity; import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService; import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.personnel.service.IPlayPersonnelAdminInfoService;
import com.starry.admin.modules.shop.module.constant.CouponUseState; import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService; import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.weichat.entity.order.*; import com.starry.admin.modules.weichat.entity.order.*;
@@ -98,6 +100,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
@Resource @Resource
private IPlayPersonnelGroupInfoService playClerkGroupInfoService; private IPlayPersonnelGroupInfoService playClerkGroupInfoService;
@Resource
private IPlayPersonnelAdminInfoService playPersonnelAdminInfoService;
@Resource @Resource
private IPlayCouponDetailsService playCouponDetailsService; private IPlayCouponDetailsService playCouponDetailsService;
@@ -1057,7 +1062,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
throw new CustomException("只能操作本人订单"); throw new CustomException("只能操作本人订单");
} }
if ("1".equals(operatorByType) && !operatorBy.equals(orderInfo.getAcceptBy())) { if ("1".equals(operatorByType) && !operatorBy.equals(orderInfo.getAcceptBy())) {
throw new CustomException("只能操作本人订单"); if (!isClerkManagementOperator(operatorBy)) {
throw new CustomException("只能操作本人订单");
}
} }
// 取消订单(必须订单未接单或者为开始状态) // 取消订单(必须订单未接单或者为开始状态)
if (!orderInfo.getOrderStatus().equals(OrderStatus.PENDING.getCode()) if (!orderInfo.getOrderStatus().equals(OrderStatus.PENDING.getCode())
@@ -1079,6 +1086,22 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
notificationSender.sendOrderCancelMessageAsync(orderInfo, refundReason); notificationSender.sendOrderCancelMessageAsync(orderInfo, refundReason);
} }
private boolean isClerkManagementOperator(String clerkId) {
if (StringUtils.isBlank(clerkId)) {
return false;
}
PlayClerkUserInfoEntity clerkInfo = playClerkUserInfoService.selectById(clerkId);
if (clerkInfo == null || StringUtils.isBlank(clerkInfo.getSysUserId())) {
return false;
}
PlayPersonnelAdminInfoEntity adminInfo = playPersonnelAdminInfoService.selectByUserId(clerkInfo.getSysUserId());
if (adminInfo != null) {
return true;
}
PlayPersonnelGroupInfoEntity groupInfo = playClerkGroupInfoService.selectByUserId(clerkInfo.getSysUserId());
return groupInfo != null;
}
/** /**
* 已接单/服务中的订单强制取消,仅允许店员本人或管理员操作 * 已接单/服务中的订单强制取消,仅允许店员本人或管理员操作
*/ */
@@ -1087,7 +1110,9 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
public void forceCancelOngoingOrder(String operatorByType, String operatorBy, String orderId, BigDecimal refundAmount, public void forceCancelOngoingOrder(String operatorByType, String operatorBy, String orderId, BigDecimal refundAmount,
String refundReason, List<String> images) { String refundReason, List<String> images) {
if (!"2".equals(operatorByType)) { if (!"2".equals(operatorByType)) {
throw new CustomException("禁止操作"); if (!("1".equals(operatorByType) && isClerkManagementOperator(operatorBy))) {
throw new CustomException("禁止操作");
}
} }
PlayOrderInfoEntity orderInfo = this.selectOrderInfoById(orderId); PlayOrderInfoEntity orderInfo = this.selectOrderInfoById(orderId);
if (!OrderStatus.ACCEPTED.getCode().equals(orderInfo.getOrderStatus()) if (!OrderStatus.ACCEPTED.getCode().equals(orderInfo.getOrderStatus())

View File

@@ -15,8 +15,11 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder; import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.task.OverdueOrderHandlerTask; import com.starry.admin.common.task.OverdueOrderHandlerTask;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant; import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity; import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelAdminInfoService;
import com.starry.admin.modules.shop.module.constant.CouponUseState; import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity; import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo; import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
@@ -39,6 +42,19 @@ import org.springframework.test.web.servlet.MvcResult;
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport { class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
private static final String DEFAULT_WECHAT_CODE = "apitest-customer-wx";
private static final String CANCEL_REASON = "API random cancel by role";
private static final int CODE_OK = 200;
private static final int CODE_FAIL = 500;
private static final int SINGLE_QUANTITY = 1;
private static final String CLERK_STATE_ACTIVE = "1";
private static final String CLERK_ONLINE_ENABLED = "1";
private static final String CLERK_LISTING_ENABLED = "1";
private static final String CLERK_DISPLAY_ENABLED = "1";
private static final String CLERK_RANDOM_ORDER_ENABLED = "1";
private static final String CLERK_FIXING_LEVEL = "1";
private static final String CLERK_ONBOARDING_STATE = "1";
@MockBean @MockBean
private NotificationSender notificationSender; private NotificationSender notificationSender;
@@ -51,6 +67,9 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService; private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
@org.springframework.beans.factory.annotation.Autowired
private IPlayPersonnelAdminInfoService playPersonnelAdminInfoService;
@Test @Test
void randomOrderFailsWhenBalanceInsufficient() throws Exception { void randomOrderFailsWhenBalanceInsufficient() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID); SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
@@ -243,6 +262,228 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
} }
} }
@Test
void randomOrderCancellationPermissions_pendingOrder() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String nonManagerClerkId = "clerk-apitest-nonmgr-" + IdUtils.getUuid().substring(0, 6);
String adminClerkId = "clerk-apitest-admin-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserId = "apitest-admin-user-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserCode = "apitest-admin-code-" + IdUtils.getUuid().substring(0, 6);
String customerToken = null;
String leaderToken = null;
String nonManagerToken = null;
String adminToken = null;
try {
resetCustomerBalance();
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
leaderToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, leaderToken);
nonManagerToken = createClerk(nonManagerClerkId, null, null);
adminToken = createClerk(adminClerkId, adminSysUserId, null);
createAdminInfo(adminSysUserId, adminSysUserCode);
String pendingOrderId = createRandomOrder("API random pending " + IdUtils.getUuid(), customerToken);
assertCancelAsClerk(pendingOrderId, nonManagerToken, CODE_FAIL);
String leaderOrderId = createRandomOrder("API random pending leader " + IdUtils.getUuid(), customerToken);
assertCancelAsClerk(leaderOrderId, leaderToken, CODE_OK);
assertOrderStatus(leaderOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
String adminOrderId = createRandomOrder("API random pending admin " + IdUtils.getUuid(), customerToken);
assertCancelAsClerk(adminOrderId, adminToken, CODE_OK);
assertOrderStatus(adminOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
} finally {
if (nonManagerClerkId != null) {
removeClerk(nonManagerClerkId);
}
if (adminClerkId != null) {
removeClerk(adminClerkId);
}
removeAdminInfo(adminSysUserId);
CustomSecurityContextHolder.remove();
}
}
@Test
void randomOrderCancellationPermissions_acceptedOrder() throws Exception {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
String nonManagerClerkId = "clerk-apitest-nonmgr-" + IdUtils.getUuid().substring(0, 6);
String otherClerkId = "clerk-apitest-other-" + IdUtils.getUuid().substring(0, 6);
String adminClerkId = "clerk-apitest-admin-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserId = "apitest-admin-user-" + IdUtils.getUuid().substring(0, 6);
String adminSysUserCode = "apitest-admin-code-" + IdUtils.getUuid().substring(0, 6);
String customerToken = null;
String leaderToken = null;
String nonManagerToken = null;
String otherToken = null;
String adminToken = null;
try {
resetCustomerBalance();
customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
leaderToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, leaderToken);
nonManagerToken = createClerk(nonManagerClerkId, null, null);
otherToken = createClerk(otherClerkId, null, null);
adminToken = createClerk(adminClerkId, adminSysUserId, null);
createAdminInfo(adminSysUserId, adminSysUserCode);
String acceptedOrderId = createRandomOrder("API random accepted " + IdUtils.getUuid(), customerToken);
acceptOrder(acceptedOrderId, nonManagerToken);
assertCancelAsClerk(acceptedOrderId, otherToken, CODE_FAIL);
assertCancelAsClerk(acceptedOrderId, nonManagerToken, CODE_OK);
assertOrderStatus(acceptedOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
String leaderOrderId = createRandomOrder("API random accepted leader " + IdUtils.getUuid(), customerToken);
acceptOrder(leaderOrderId, nonManagerToken);
assertCancelAsClerk(leaderOrderId, leaderToken, CODE_OK);
assertOrderStatus(leaderOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
String adminOrderId = createRandomOrder("API random accepted admin " + IdUtils.getUuid(), customerToken);
acceptOrder(adminOrderId, nonManagerToken);
assertCancelAsClerk(adminOrderId, adminToken, CODE_OK);
assertOrderStatus(adminOrderId, OrderConstant.OrderStatus.CANCELLED.getCode());
} finally {
removeClerk(nonManagerClerkId);
removeClerk(otherClerkId);
removeClerk(adminClerkId);
removeAdminInfo(adminSysUserId);
CustomSecurityContextHolder.remove();
}
}
private String createRandomOrder(String remark, String customerToken) throws Exception {
String payload = "{" +
"\"sex\":\"" + OrderConstant.Gender.FEMALE.getCode() + "\"," +
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
"\"commodityQuantity\":" + SINGLE_QUANTITY + "," +
"\"weiChatCode\":\"" + DEFAULT_WECHAT_CODE + "\"," +
"\"excludeHistory\":\"" + OrderConstant.EXCLUDE_HISTORY_NO + "\"," +
"\"couponIds\":[]," +
"\"remark\":\"" + remark + "\"" +
"}";
mockMvc.perform(post("/wx/custom/order/random")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(CODE_OK));
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
.eq(PlayOrderInfoEntity::getRemark, remark)
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
.last("limit 1")
.one();
Assertions.assertThat(order).isNotNull();
return order.getId();
}
private void acceptOrder(String orderId, String clerkTokenValue) throws Exception {
mockMvc.perform(get("/wx/clerk/order/accept")
.param("id", orderId)
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkTokenValue))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(CODE_OK));
}
private void assertCancelAsClerk(String orderId, String clerkTokenValue, int expectedCode) throws Exception {
String payload = "{" +
"\"orderId\":\"" + orderId + "\"," +
"\"refundReason\":\"" + CANCEL_REASON + "\"," +
"\"images\":[]" +
"}";
mockMvc.perform(post("/wx/clerk/order/cancellation")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkTokenValue)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(expectedCode));
}
private void assertOrderStatus(String orderId, String expectedStatus) {
ensureTenantContext();
PlayOrderInfoEntity order = playOrderInfoService.selectOrderInfoById(orderId);
Assertions.assertThat(order).isNotNull();
Assertions.assertThat(order.getOrderStatus()).isEqualTo(expectedStatus);
}
private String createClerk(String clerkId, String sysUserId, String groupId) {
ensureTenantContext();
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
entity.setId(clerkId);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setSysUserId(sysUserId);
entity.setOpenid("openid-" + clerkId);
entity.setNickname("测试店员-" + clerkId);
entity.setGroupId(groupId);
entity.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
entity.setFixingLevel(CLERK_FIXING_LEVEL);
entity.setSex(OrderConstant.Gender.FEMALE.getCode());
entity.setPhone("1390000" + clerkId.substring(0, 4));
entity.setWeiChatCode("apitest-clerk-" + clerkId);
entity.setAvatar("https://example.com/avatar.png");
entity.setAccountBalance(BigDecimal.ZERO);
entity.setOnboardingState(CLERK_ONBOARDING_STATE);
entity.setListingState(CLERK_LISTING_ENABLED);
entity.setDisplayState(CLERK_DISPLAY_ENABLED);
entity.setOnlineState(CLERK_ONLINE_ENABLED);
entity.setRandomOrderState(CLERK_RANDOM_ORDER_ENABLED);
entity.setClerkState(CLERK_STATE_ACTIVE);
entity.setEntryTime(LocalDateTime.now());
clerkUserInfoService.save(entity);
String token = wxTokenService.createWxUserToken(clerkId);
clerkUserInfoService.updateTokenById(clerkId, token);
return token;
}
private void removeClerk(String clerkId) {
if (clerkId == null) {
return;
}
ensureTenantContext();
clerkUserInfoService.removeById(clerkId);
}
private void createAdminInfo(String sysUserId, String sysUserCode) {
if (sysUserId == null) {
return;
}
ensureTenantContext();
PlayPersonnelAdminInfoEntity entity = new PlayPersonnelAdminInfoEntity();
entity.setId("admin-info-" + sysUserId);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setSysUserId(sysUserId);
entity.setSysUserCode(sysUserCode);
entity.setAdminName("API测试管理员");
entity.setLeaderName("API测试负责人");
entity.setAddTime(LocalDateTime.now());
playPersonnelAdminInfoService.save(entity);
}
private void removeAdminInfo(String sysUserId) {
if (sysUserId == null) {
return;
}
ensureTenantContext();
playPersonnelAdminInfoService.lambdaUpdate()
.eq(PlayPersonnelAdminInfoEntity::getSysUserId, sysUserId)
.remove();
}
@Test @Test
// 测试用例:订单已接单后由管理员强制取消,也应释放所使用的优惠券 // 测试用例:订单已接单后由管理员强制取消,也应释放所使用的优惠券
void randomOrderForceCancelReleasesCoupon() throws Exception { void randomOrderForceCancelReleasesCoupon() throws Exception {