xiaoyi
5 天以前 fae95228037f0c1304c1917ae5fff452f2160c5a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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);
    }
}