package cn.iocoder.yudao.module.aftersales.service.collaboration;
|
|
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
|
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import cn.iocoder.yudao.module.aftersales.dal.dataobject.collaboration.AfterSaleCollaborationDO;
|
import cn.iocoder.yudao.module.aftersales.dal.dataobject.ticket.AfterSaleTicketDO;
|
import cn.iocoder.yudao.module.aftersales.dal.mysql.collaboration.AfterSaleCollaborationMapper;
|
import cn.iocoder.yudao.module.aftersales.enums.AfterSaleBizTypeEnum;
|
import cn.iocoder.yudao.module.aftersales.enums.permission.AfterSalePermissionLevelEnum;
|
import cn.iocoder.yudao.module.aftersales.service.permission.AfterSalePermissionService;
|
import cn.iocoder.yudao.module.aftersales.service.ticket.AfterSaleTicketService;
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
|
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
|
import cn.iocoder.yudao.module.erp.api.finance.AfterSaleFinanceDraftApi;
|
import cn.iocoder.yudao.module.erp.api.finance.dto.AfterSaleFinanceDraftCreateDTO;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import jakarta.annotation.Resource;
|
import lombok.Data;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.dao.DuplicateKeyException;
|
import java.time.LocalDateTime;
|
import java.math.BigDecimal;
|
import java.util.List;
|
import java.util.Objects;
|
import java.util.Set;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
|
@Service
|
public class AfterSaleCollaborationService {
|
@Resource private AfterSaleCollaborationMapper mapper;
|
@Resource private AfterSaleTicketService ticketService;
|
@Resource private AfterSalePermissionService permissionService;
|
@Resource private PermissionCommonApi permissionApi;
|
@Resource private AdminUserApi userApi;
|
@Resource private AfterSaleFinanceDraftApi financeDraftApi;
|
@Resource private SrmSupplierApi supplierApi;
|
@Resource private CrmCustomerApi customerApi;
|
|
@Data
|
public static class CreateRequest {
|
private Long ticketId;
|
private String category;
|
private String title;
|
private String content;
|
private Long assigneeUserId;
|
private BigDecimal amount;
|
private String direction;
|
private String partyName;
|
private Long partyId;
|
private Long previousId;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long create(CreateRequest input, Long userId) {
|
if (userId == null || input == null || input.getTicketId() == null || input.getAssigneeUserId() == null
|
|| input.getCategory() == null || !Set.of("QUALITY", "FINANCE", "SUPPLIER", "WARRANTY", "RESEARCH", "PROCESS").contains(input.getCategory())
|
|| input.getTitle() == null || input.getTitle().isBlank() || input.getTitle().length() > 200
|
|| input.getContent() == null || input.getContent().isBlank() || input.getContent().length() > 2000)
|
throw invalidParamException("请完整填写协同事项及接收人");
|
boolean financialCategory = Set.of("FINANCE", "SUPPLIER", "WARRANTY").contains(input.getCategory());
|
if (financialCategory && input.getAmount() == null) throw invalidParamException("该协同类型必须填写金额");
|
if (input.getAmount() != null) {
|
if (!Set.of("QUALITY", "FINANCE", "SUPPLIER", "WARRANTY").contains(input.getCategory())
|
|| input.getAmount().signum() <= 0 || input.getAmount().stripTrailingZeros().scale() > 2
|
|| input.getAmount().compareTo(new BigDecimal("9999999999999999.99")) > 0
|
|| input.getDirection() == null || !Set.of("RECEIVABLE", "PAYABLE", "INTERNAL").contains(input.getDirection())
|
|| (input.getPartyName() != null && input.getPartyName().length() > 200)
|
|| (!Set.of("SUPPLIER", "WARRANTY").contains(input.getCategory()) && !"INTERNAL".equals(input.getDirection()) && (input.getPartyName() == null || input.getPartyName().isBlank())))
|
throw invalidParamException("金额、资金方向或往来方不正确");
|
} else if (input.getDirection() != null || input.getPartyId() != null || (input.getPartyName() != null && !input.getPartyName().isBlank())) {
|
throw invalidParamException("填写资金方向或往来方时必须填写金额");
|
}
|
AfterSaleTicketDO ticket = ticketService.getTicket(input.getTicketId());
|
if (ticket == null) throw invalidParamException("售后工单不存在");
|
if (Objects.equals(ticket.getStatus(), 60) || Objects.equals(ticket.getStatus(), 99))
|
throw invalidParamException("已关闭或取消的工单不能发起协同");
|
if (!canWrite(ticket, userId)) throw invalidParamException("无权发起该工单的协同事项");
|
String partyType = null, partyName = null;
|
Long partyId = null;
|
if (input.getAmount() != null) {
|
if ("SUPPLIER".equals(input.getCategory())) {
|
if (input.getPartyId() == null) throw invalidParamException("请选择供应商");
|
var supplier = supplierApi.getSupplier(input.getPartyId()).getCheckedData();
|
if (supplier == null || !Objects.equals(supplier.getStatus(), 0) || supplier.getName() == null || supplier.getName().isBlank())
|
throw invalidParamException("供应商不存在或已停用");
|
partyType = "SUPPLIER"; partyId = supplier.getId(); partyName = supplier.getName();
|
} else if ("WARRANTY".equals(input.getCategory())) {
|
if (ticket.getCustomerId() == null) throw invalidParamException("工单未关联客户,请先完善工单");
|
var customer = customerApi.getCustomer(ticket.getCustomerId());
|
if (customer == null || customer.getName() == null || customer.getName().isBlank())
|
throw invalidParamException("工单关联的客户不存在");
|
partyType = "CUSTOMER"; partyId = customer.getId(); partyName = customer.getName();
|
} else if ("INTERNAL".equals(input.getDirection())) {
|
partyType = "INTERNAL";
|
} else {
|
partyType = "OTHER"; partyName = input.getPartyName().trim();
|
}
|
}
|
userApi.validateUser(input.getAssigneeUserId());
|
if (input.getPreviousId() != null) {
|
AfterSaleCollaborationDO previous = mapper.selectById(input.getPreviousId());
|
if (previous == null || !Objects.equals(previous.getRequestedBy(), userId)
|
|| !Objects.equals(previous.getTicketId(), input.getTicketId()) || !Objects.equals(previous.getCategory(), input.getCategory()))
|
throw invalidParamException("原协同事项不匹配或无权重新发起");
|
boolean allowed = Objects.equals(previous.getStatus(), 4)
|
|| (Objects.equals(previous.getStatus(), 2) && previous.getPreviousId() != null);
|
if (Objects.equals(previous.getStatus(), 3) && previous.getFinanceDraftId() != null) {
|
var review = financeDraftApi.getReviewResults(List.of(previous.getFinanceDraftId())).get(previous.getFinanceDraftId());
|
allowed = review != null && Objects.equals(review.getStatus(), 2);
|
}
|
if (!allowed) throw invalidParamException("只有业务退回、财务退回或已取消的重办事项可以重新发起");
|
}
|
var task = new AfterSaleCollaborationDO();
|
task.setTicketId(input.getTicketId()); task.setCategory(input.getCategory()); task.setTitle(input.getTitle().trim());
|
task.setContent(input.getContent().trim()); task.setAssigneeUserId(input.getAssigneeUserId());
|
task.setRequestedBy(userId); task.setStatus(0); task.setAmount(input.getAmount());
|
task.setDirection(input.getDirection()); task.setPartyName(partyName); task.setPartyType(partyType); task.setPartyId(partyId); task.setPreviousId(input.getPreviousId());
|
try { mapper.insert(task); }
|
catch (DuplicateKeyException e) { throw invalidParamException("该事项已重新发起,请刷新后查看"); }
|
return task.getId();
|
}
|
|
public PageResult<AfterSaleCollaborationDO> myTasks(PageParam page, Long userId) {
|
if (userId == null) throw invalidParamException("用户未登录");
|
boolean canApprove = permissionApi.hasAnyPermissions(userId, "aftersales:collaboration:approve");
|
boolean isAdmin = canApprove && permissionApi.hasAnyRoles(userId, "aftersales_admin");
|
var query = new LambdaQueryWrapperX<AfterSaleCollaborationDO>();
|
query.and(w -> {
|
w.eq(AfterSaleCollaborationDO::getAssigneeUserId, userId).or().eq(AfterSaleCollaborationDO::getRequestedBy, userId)
|
.or().eq(AfterSaleCollaborationDO::getBusinessApproverId, userId);
|
if (canApprove) w.or(approval -> {
|
approval.eq(AfterSaleCollaborationDO::getStatus, 1).isNotNull(AfterSaleCollaborationDO::getAmount);
|
if (!isAdmin) approval.exists("SELECT 1 FROM after_sale_ticket t WHERE t.id = after_sale_collaboration.ticket_id AND t.deleted = 0 "
|
+ "AND (t.owner_user_id = {0} OR EXISTS (SELECT 1 FROM after_sale_permission p WHERE p.biz_type = 1 "
|
+ "AND p.biz_id = t.id AND p.user_id = {1} AND p.level IN (1, 3) AND p.deleted = 0))", userId, userId);
|
});
|
});
|
PageResult<AfterSaleCollaborationDO> result = mapper.selectPage(page, query.orderByDesc(AfterSaleCollaborationDO::getId));
|
attachFinanceReviews(result.getList());
|
return result;
|
}
|
|
public List<AfterSaleCollaborationDO> ticketTasks(Long ticketId, Long userId) {
|
if (userId == null || ticketId == null) throw invalidParamException("参数不正确");
|
AfterSaleTicketDO ticket = ticketService.getTicket(ticketId);
|
if (ticket == null) throw invalidParamException("售后工单不存在");
|
var query = new LambdaQueryWrapperX<AfterSaleCollaborationDO>().eq(AfterSaleCollaborationDO::getTicketId, ticketId);
|
if (!canRead(ticket, userId)) query.and(w -> w.eq(AfterSaleCollaborationDO::getAssigneeUserId, userId).or().eq(AfterSaleCollaborationDO::getRequestedBy, userId));
|
List<AfterSaleCollaborationDO> result = mapper.selectList(query.orderByDesc(AfterSaleCollaborationDO::getId));
|
attachFinanceReviews(result);
|
return result;
|
}
|
|
private void attachFinanceReviews(List<AfterSaleCollaborationDO> tasks) {
|
List<Long> ids = tasks.stream().map(AfterSaleCollaborationDO::getFinanceDraftId).filter(Objects::nonNull).distinct().toList();
|
if (ids.isEmpty()) return;
|
var reviews = financeDraftApi.getReviewResults(ids);
|
tasks.forEach(task -> task.setFinanceReview(reviews.get(task.getFinanceDraftId())));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void complete(Long id, Long userId, String result) {
|
if (id == null || userId == null || result == null || result.isBlank() || result.length() > 2000)
|
throw invalidParamException("请填写不超过 2000 字的处理结果");
|
AfterSaleCollaborationDO task = mapper.selectById(id);
|
if (task == null || !Objects.equals(task.getAssigneeUserId(), userId) || !Objects.equals(task.getStatus(), 0))
|
throw invalidParamException("协同事项不存在、已处理或当前用户不是接收人");
|
if (mapper.update(null, new LambdaUpdateWrapper<AfterSaleCollaborationDO>()
|
.eq(AfterSaleCollaborationDO::getId, id).eq(AfterSaleCollaborationDO::getStatus, 0)
|
.eq(AfterSaleCollaborationDO::getAssigneeUserId, userId).set(AfterSaleCollaborationDO::getStatus, 1)
|
.set(AfterSaleCollaborationDO::getResult, result.trim()).set(AfterSaleCollaborationDO::getCompletedTime, LocalDateTime.now())) != 1)
|
throw invalidParamException("协同事项状态已变化,请刷新后重试");
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void cancel(Long id, Long userId) {
|
if (id == null || userId == null) throw invalidParamException("参数不正确");
|
AfterSaleCollaborationDO task = mapper.selectById(id);
|
if (task == null || !Objects.equals(task.getRequestedBy(), userId) || !Objects.equals(task.getStatus(), 0))
|
throw invalidParamException("只有发起人可以取消待处理事项");
|
if (mapper.update(null, new LambdaUpdateWrapper<AfterSaleCollaborationDO>()
|
.eq(AfterSaleCollaborationDO::getId, id).eq(AfterSaleCollaborationDO::getStatus, 0)
|
.eq(AfterSaleCollaborationDO::getRequestedBy, userId).set(AfterSaleCollaborationDO::getStatus, 2)) != 1)
|
throw invalidParamException("协同事项状态已变化,请刷新后重试");
|
}
|
|
/** 业务审批通过后创建财务审核草稿;草稿创建失败时本次审批整体回滚。 */
|
@Transactional(rollbackFor = Exception.class)
|
public void reviewFinancial(Long id, Long userId, boolean approved, String remark) {
|
if (id == null || userId == null || (remark != null && remark.length() > 1000)
|
|| (!approved && (remark == null || remark.isBlank()))) throw invalidParamException("退回时请填写业务审核意见");
|
AfterSaleCollaborationDO task = mapper.selectById(id);
|
if (task == null || !Objects.equals(task.getStatus(), 1) || task.getAmount() == null)
|
throw invalidParamException("事项不存在或未处于待业务审核状态");
|
AfterSaleTicketDO ticket = ticketService.getTicket(task.getTicketId());
|
if (ticket == null || !canWrite(ticket, userId)) throw invalidParamException("无权审核该工单的协同事项");
|
int changed = mapper.update(null, new LambdaUpdateWrapper<AfterSaleCollaborationDO>()
|
.eq(AfterSaleCollaborationDO::getId, id).eq(AfterSaleCollaborationDO::getStatus, 1)
|
.isNotNull(AfterSaleCollaborationDO::getAmount)
|
.set(AfterSaleCollaborationDO::getStatus, approved ? 3 : 4)
|
.set(AfterSaleCollaborationDO::getBusinessApproverId, userId)
|
.set(AfterSaleCollaborationDO::getBusinessApprovalTime, LocalDateTime.now())
|
.set(AfterSaleCollaborationDO::getBusinessRemark, remark));
|
if (changed != 1) throw invalidParamException("事项状态已变化,请刷新后重试");
|
if (approved) {
|
var input = new AfterSaleFinanceDraftCreateDTO();
|
input.setCollaborationId(id); input.setTicketId(task.getTicketId()); input.setCategory(task.getCategory());
|
input.setDirection(task.getDirection()); input.setAmount(task.getAmount()); input.setPartyName(task.getPartyName());
|
input.setPartyType(task.getPartyType()); input.setPartyId(task.getPartyId());
|
input.setDescription(task.getResult()); input.setBusinessApproverId(userId);
|
Long draftId = financeDraftApi.createDraft(input);
|
if (draftId == null) throw invalidParamException("财务草稿创建失败");
|
var update = new AfterSaleCollaborationDO(); update.setId(id); update.setFinanceDraftId(draftId);
|
if (mapper.updateById(update) != 1) throw invalidParamException("关联财务草稿失败,请重试");
|
}
|
}
|
|
private boolean canWrite(AfterSaleTicketDO ticket, Long userId) {
|
return Objects.equals(ticket.getOwnerUserId(), userId) || permissionApi.hasAnyRoles(userId, "aftersales_admin")
|
|| permissionService.hasPermission(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(), ticket.getId(), userId, AfterSalePermissionLevelEnum.WRITE)
|
|| permissionService.hasPermission(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(), ticket.getId(), userId, AfterSalePermissionLevelEnum.OWNER);
|
}
|
|
private boolean canRead(AfterSaleTicketDO ticket, Long userId) {
|
return canWrite(ticket, userId) || permissionService.hasPermission(AfterSaleBizTypeEnum.AFTER_SALE_TICKET.getType(), ticket.getId(), userId, AfterSalePermissionLevelEnum.READ);
|
}
|
}
|