feat(order): 新增管理端完成订单能力(店员端触发)
Some checks failed
Build and Push Backend / docker (push) Failing after 6s

- OrderConstant 新增角色 GROUP_LEADER、触发源 WX_CLERK_MGMT;补充映射
- IPlayOrderInfoService 新增 completeOrderByManagement 方法
- PlayOrderInfoServiceImpl:校验权限(仅运营/组长,组长仅限本组);ACCEPTED 自动切换为 IN_PROGRESS 后完成;抽取 completeOrderInternal;完善 GROUP_LEADER 的 Actor/Source 映射
- WxClerkController 新增 POST /wx/clerk/order/complete 接口,支持备注参数
- 新增请求体 PlayOrderCompleteVo
- 新增单测 PlayOrderInfoServiceImplTest 覆盖核心流程与边界
This commit is contained in:
irving
2025-11-01 15:07:59 -04:00
parent 16ea9b9d48
commit b1fd515fb3
7 changed files with 331 additions and 26 deletions

View File

@@ -214,7 +214,8 @@ public class OrderConstant {
public enum OperatorType {
CUSTOMER("0", "顾客"),
CLERK("1", "店员"),
ADMIN("2", "管理员");
ADMIN("2", "管理员"),
GROUP_LEADER("3", "组长");
private final String code;
private final String description;
@@ -238,6 +239,7 @@ public class OrderConstant {
public enum OrderActor {
CUSTOMER,
CLERK,
GROUP_LEADER,
ADMIN,
SYSTEM;
}
@@ -461,6 +463,10 @@ public class OrderConstant {
* 微信店员端操作触发
*/
WX_CLERK("wx_clerk"),
/**
* 微信店员端管理能力触发(组长/运营)
*/
WX_CLERK_MGMT("wx_clerk_mgmt"),
/**
* 管理后台控制台界面发起
*/

View File

@@ -0,0 +1,20 @@
package com.starry.admin.modules.order.module.vo;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 店员端完成订单请求体。
*/
@Data
@Accessors(chain = true)
public class PlayOrderCompleteVo implements Serializable {
@NotBlank(message = "订单ID不能为空")
private String orderId;
/** 可选备注信息,记录在生命周期日志中。 */
private String remark;
}

View File

@@ -219,6 +219,16 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
**/
void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId);
/**
* 管理端完成订单(运营/组长)
*
* @param operatorByType 操作人类型
* @param operatorBy 操作人标识
* @param orderId 订单ID
* @param remark 操作备注
*/
void completeOrderByManagement(String operatorByType, String operatorBy, String orderId, String remark);
/**
* 修改订单状态为取消订单 管理员、店员、顾客均可操作
*

View File

@@ -792,6 +792,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
return OrderActor.CUSTOMER;
case CLERK:
return OrderActor.CLERK;
case GROUP_LEADER:
return OrderActor.GROUP_LEADER;
case ADMIN:
return OrderActor.ADMIN;
default:

View File

@@ -39,6 +39,7 @@ import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
@@ -726,33 +727,25 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
}
/**
* 修改订单状态为开始订单或者完成订单 只有管理员或者店员本人才能操作
*
* @param operatorByType 操作人类型0:顾客;1:店员;2:管理员)
* @param operatorBy 操作人ID
* @param orderState 订单状态
* @param orderId 订单Id
**/
@Override
public void updateStateTo23(String operatorByType, String operatorBy, String orderState, String orderId) {
// 开始订单只能店员或者管理员操作
if (OrderStatus.IN_PROGRESS.getCode().equals(orderState) && "0".equals(operatorByType)) {
OperatorType operatorType = resolveOperatorTypeOrThrow(operatorByType);
boolean isCustomer = operatorType == OperatorType.CUSTOMER;
boolean isClerk = operatorType == OperatorType.CLERK;
if (OrderStatus.IN_PROGRESS.getCode().equals(orderState) && isCustomer) {
throw new CustomException("禁止操作");
}
// 完成订单只能顾客或者管理员操作
if (OrderStatus.COMPLETED.getCode().equals(orderState) && "1".equals(operatorByType)) {
if (OrderStatus.COMPLETED.getCode().equals(orderState) && isClerk) {
throw new CustomException("禁止操作");
}
PlayOrderInfoEntity entity = this.selectOrderInfoById(orderId);
// 如果该接口是顾客调用,判断是否是本人订单
if ("0".equals(operatorByType) && !entity.getPurchaserBy().equals(operatorBy)) {
if (isCustomer && !entity.getPurchaserBy().equals(operatorBy)) {
throw new CustomException("只能操作本人订单");
}
// 如果该接口是店员调用,判断是否是本人订单
if ("1".equals(operatorByType) && !entity.getAcceptBy().equals(operatorBy)) {
if (isClerk && !entity.getAcceptBy().equals(operatorBy)) {
throw new CustomException("只能操作本人订单");
}
if (OrderStatus.IN_PROGRESS.getCode().equals(orderState)) {
if (!OrderStatus.ACCEPTED.getCode().equals(entity.getOrderStatus())) {
log.error("订单状态异常,不能开始接单,orderId={},orderStace={}", orderId, orderState);
@@ -766,19 +759,47 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
log.error("订单状态异常,不能完成订单,orderId={},orderStace={}", orderId, orderState);
throw new CustomException("订单状态异常,不能开始订单");
}
OrderActor actor = resolveCompletionActor(operatorByType);
orderLifecycleService.completeOrder(
orderId,
OrderCompletionContext.of(
actor,
actor == OrderActor.SYSTEM ? null : operatorBy,
resolveCompletionSource(actor),
"manual"));
if (operatorType == OperatorType.GROUP_LEADER) {
ensureLeaderScope(operatorBy, entity);
}
OrderActor actor = resolveCompletionActor(operatorType.getCode());
completeOrderInternal(
entity,
operatorType,
operatorBy,
"manual",
resolveCompletionSource(actor));
} else {
log.error("修改订单状态异常orderId={},orderStace={}", orderId, orderState);
}
}
public void completeOrderByManagement(String operatorByType, String operatorBy, String orderId, String remark) {
OperatorType operatorType = resolveOperatorTypeOrThrow(operatorByType);
if (operatorType != OperatorType.ADMIN && operatorType != OperatorType.GROUP_LEADER) {
throw new CustomException("当前角色无权完成订单");
}
PlayOrderInfoEntity entity = this.selectOrderInfoById(orderId);
if (operatorType == OperatorType.GROUP_LEADER) {
ensureLeaderScope(operatorBy, entity);
}
if (OrderStatus.ACCEPTED.getCode().equals(entity.getOrderStatus())) {
PlayOrderInfoEntity startUpdate = new PlayOrderInfoEntity(orderId, OrderStatus.IN_PROGRESS.getCode());
startUpdate.setOrderStartTime(LocalDateTime.now());
this.baseMapper.updateById(startUpdate);
entity = this.selectOrderInfoById(orderId);
}
if (!OrderStatus.IN_PROGRESS.getCode().equals(entity.getOrderStatus())) {
throw new CustomException("订单状态异常,无法完成");
}
completeOrderInternal(
entity,
operatorType,
operatorBy,
remark,
OrderTriggerSource.WX_CLERK_MGMT);
}
/**
* 修改订单状态为取消订单 管理员、店员、顾客均可操作
*
@@ -911,6 +932,71 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return orderInfoMapper.deleteById(id);
}
private void completeOrderInternal(
PlayOrderInfoEntity order,
OperatorType operatorType,
String operatorBy,
String remark,
OrderTriggerSource triggerSource) {
OrderActor actor = resolveCompletionActor(operatorType.getCode());
orderLifecycleService.completeOrder(
order.getId(),
OrderCompletionContext.of(
actor,
actor == OrderActor.SYSTEM ? null : operatorBy,
triggerSource,
remark));
}
private void ensureLeaderScope(String operatorBy, PlayOrderInfoEntity order) {
if (StrUtil.isBlank(operatorBy)) {
throw new CustomException("操作人信息缺失");
}
PlayClerkUserInfoEntity leaderInfo = playClerkUserInfoService.getById(operatorBy);
if (leaderInfo == null) {
throw new CustomException("操作人信息不存在");
}
if (StrUtil.isBlank(leaderInfo.getSysUserId())) {
throw new CustomException("账号未绑定系统用户,无法完成订单");
}
PlayPersonnelGroupInfoEntity groupInfo = playClerkGroupInfoService.selectByUserId(leaderInfo.getSysUserId());
if (groupInfo == null) {
throw new CustomException("仅限组长可操作");
}
String orderGroupId = resolveOrderGroupId(order);
if (StrUtil.isBlank(orderGroupId) || !groupInfo.getId().equals(orderGroupId)) {
throw new CustomException("您无权完成该订单");
}
}
private String resolveOrderGroupId(PlayOrderInfoEntity order) {
if (order == null) {
return null;
}
if (StrUtil.isNotBlank(order.getGroupId())) {
return order.getGroupId();
}
if (StrUtil.isNotBlank(order.getAcceptBy())) {
PlayClerkUserInfoEntity acceptClerk = playClerkUserInfoService.getById(order.getAcceptBy());
if (acceptClerk != null) {
return acceptClerk.getGroupId();
}
}
return null;
}
private OperatorType resolveOperatorTypeOrThrow(String operatorByType) {
if (StrUtil.isBlank(operatorByType)) {
throw new CustomException("操作人类型不能为空");
}
try {
return OperatorType.fromCode(operatorByType);
} catch (IllegalArgumentException ex) {
log.warn("Unknown operator type {}", operatorByType, ex);
throw new CustomException("操作人类型异常");
}
}
private OrderActor resolveCompletionActor(String operatorType) {
if (operatorType == null) {
return OrderActor.SYSTEM;
@@ -922,6 +1008,8 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return OrderActor.CUSTOMER;
case CLERK:
return OrderActor.CLERK;
case GROUP_LEADER:
return OrderActor.GROUP_LEADER;
case ADMIN:
return OrderActor.ADMIN;
default:
@@ -942,6 +1030,8 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
return OrderTriggerSource.WX_CUSTOMER;
case CLERK:
return OrderTriggerSource.WX_CLERK;
case GROUP_LEADER:
return OrderTriggerSource.WX_CLERK_MGMT;
case ADMIN:
return OrderTriggerSource.ADMIN_CONSOLE;
default:

View File

@@ -12,12 +12,15 @@ import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
import com.starry.admin.modules.clerk.service.*;
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserInfoServiceImpl;
import com.starry.admin.modules.clerk.service.impl.PlayClerkUserReviewInfoServiceImpl;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.vo.PlayOrderCompleteVo;
import com.starry.admin.modules.order.module.vo.PlayOrderEvaluateQueryVo;
import com.starry.admin.modules.order.module.vo.PlayOrderStateEditVo;
import com.starry.admin.modules.order.service.IPlayOrderEvaluateInfoService;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelAdminInfoService;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
@@ -108,6 +111,9 @@ public class WxClerkController {
@Resource
private IPlayClerkPerformanceService playClerkPerformanceService;
@Resource
private IPlayPersonnelAdminInfoService playPersonnelAdminInfoService;
@Resource
private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;
@Resource
@@ -539,6 +545,33 @@ public class WxClerkController {
return R.ok("成功");
}
@ClerkUserLogin
@PostMapping("/order/complete")
public R completeOrder(@Validated @RequestBody PlayOrderCompleteVo vo) {
PlayClerkUserInfoEntity clerkInfo = ThreadLocalRequestDetail.getClerkUserInfo();
OperatorType operatorType = resolveManagementOperatorType(clerkInfo);
String operatorId = operatorType == OperatorType.GROUP_LEADER ? clerkInfo.getId() : clerkInfo.getSysUserId();
playOrderInfoService.completeOrderByManagement(
operatorType.getCode(),
operatorId,
vo.getOrderId(),
StrUtil.isNotBlank(vo.getRemark()) ? vo.getRemark().trim() : null);
return R.ok("操作成功");
}
private OperatorType resolveManagementOperatorType(PlayClerkUserInfoEntity clerkInfo) {
if (clerkInfo == null || StrUtil.isBlank(clerkInfo.getSysUserId())) {
throw new CustomException("账号未绑定系统用户,无法完成订单");
}
if (playPersonnelAdminInfoService.selectByUserId(clerkInfo.getSysUserId()) != null) {
return OperatorType.ADMIN;
}
if (playPersonnelGroupInfoService.selectByUserId(clerkInfo.getSysUserId()) != null) {
return OperatorType.GROUP_LEADER;
}
throw new CustomException("当前账号无权完成订单");
}
/**
* 店员-取消订单
*