| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329 |
- package org.dromara.crm.service.impl;
- import cn.hutool.core.date.DateUtil;
- import cn.hutool.core.util.IdUtil;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.baomidou.mybatisplus.core.toolkit.Wrappers;
- import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.dromara.common.core.exception.ServiceException;
- import org.dromara.common.core.utils.MapstructUtils;
- import org.dromara.common.core.utils.StringUtils;
- import org.dromara.common.mybatis.core.page.PageQuery;
- import org.dromara.common.mybatis.core.page.TableDataInfo;
- import org.dromara.common.satoken.utils.LoginHelper;
- import org.dromara.crm.domain.CrmQuote;
- import org.dromara.crm.domain.CrmContract;
- import org.dromara.crm.domain.bo.CrmContractBo;
- import org.dromara.crm.domain.bo.CrmContractProductBo;
- import org.dromara.crm.domain.bo.CrmQuoteBo;
- import org.dromara.crm.domain.bo.CrmQuoteProductBo;
- import org.dromara.crm.domain.vo.CrmQuoteProductVo;
- import org.dromara.crm.domain.vo.CrmQuoteVo;
- import org.dromara.crm.mapper.CrmContractMapper;
- import org.dromara.crm.mapper.CrmQuoteMapper;
- import org.dromara.crm.mapper.CrmCustomerMapper;
- import org.dromara.crm.domain.CrmCustomer;
- import org.dromara.crm.service.*;
- import org.dromara.crm.service.ICrmQuoteApprovalService;
- import org.dromara.crm.service.ICrmQuotePdfService;
- import org.dromara.crm.service.ICrmQuoteProductService;
- import org.dromara.crm.service.ICrmQuoteService;
- import org.dromara.system.service.ISysUserService;
- import org.dromara.system.domain.vo.SysUserVo;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.math.BigDecimal;
- import java.time.LocalDate;
- import java.time.LocalDateTime;
- import java.time.format.DateTimeFormatter;
- import java.util.Collection;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.stream.Collectors;
- /**
- * 报价单Service实现
- *
- * @author 系统管理员
- * @date 2025-01-03
- */
- @Slf4j
- @RequiredArgsConstructor
- @Service
- public class CrmQuoteServiceImpl implements ICrmQuoteService {
- private final CrmQuoteMapper baseMapper;
- private final ICrmQuoteProductService quoteProductService;
- private final ICrmQuoteApprovalService quoteApprovalService;
- private final ICrmQuotePdfService quotePdfService;
- private final ICrmQuoteShareLogService quoteShareLogService;
- private final ICrmContractService contractService;
- private final CrmContractMapper contractMapper;
- private final ICrmOpportunityService opportunityService;
- private final ICrmContactService contactService;
- private final ISysUserService userService;
- private final CrmCustomerMapper customerMapper;
- private final ICrmSupplierInfoService supplierInfoService;
- private final ICrmProductService productService;
- /**
- * 构建查询条件
- */
- private LambdaQueryWrapper<CrmQuote> buildQueryWrapper(CrmQuoteBo bo) {
- LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
- lqw.like(StringUtils.isNotBlank(bo.getQuoteNo()), CrmQuote::getQuoteNo, bo.getQuoteNo());
- // 快速检索关键字(报价主题/客户名称 OR 模糊)
- if (StringUtils.isNotBlank(bo.getKeyword())) {
- String keyword = bo.getKeyword();
- lqw.and(wrapper -> wrapper
- .like(CrmQuote::getQuoteTitle, keyword)
- .or()
- .like(CrmQuote::getCustomerName, keyword));
- } else {
- // 高级筛选:报价主题和客户名称
- // 若两个字段都传入且值相同,按 OR 处理;否则各自模糊匹配(AND)
- if (StringUtils.isNotBlank(bo.getQuoteTitle()) && StringUtils.isNotBlank(bo.getCustomerName())
- && bo.getQuoteTitle().equals(bo.getCustomerName())) {
- String keyword = bo.getQuoteTitle();
- lqw.and(wrapper -> wrapper
- .like(CrmQuote::getQuoteTitle, keyword)
- .or()
- .like(CrmQuote::getCustomerName, keyword));
- } else {
- lqw.like(StringUtils.isNotBlank(bo.getQuoteTitle()), CrmQuote::getQuoteTitle, bo.getQuoteTitle());
- lqw.like(StringUtils.isNotBlank(bo.getCustomerName()), CrmQuote::getCustomerName, bo.getCustomerName());
- }
- }
- lqw.eq(bo.getCustomerId() != null, CrmQuote::getCustomerId, bo.getCustomerId());
- lqw.eq(bo.getOpportunityId() != null, CrmQuote::getOpportunityId, bo.getOpportunityId());
- lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CrmQuote::getStatus, bo.getStatus());
- // 排除指定状态(如默认排除已作废的记录)
- lqw.ne(StringUtils.isNotBlank(bo.getExcludeStatus()), CrmQuote::getStatus, bo.getExcludeStatus());
- lqw.eq(StringUtils.isNotBlank(bo.getQuoteType()), CrmQuote::getQuoteType, bo.getQuoteType());
- lqw.eq(bo.getOwnerUserId() != null, CrmQuote::getOwnerUserId, bo.getOwnerUserId());
- lqw.eq(bo.getIsLatest() != null, CrmQuote::getIsLatest, bo.getIsLatest());
- lqw.ge(bo.getParams().get("minAmount") != null, CrmQuote::getFinalAmount, bo.getParams().get("minAmount"));
- lqw.le(bo.getParams().get("maxAmount") != null, CrmQuote::getFinalAmount, bo.getParams().get("maxAmount"));
- // 报价日期范围查询
- if (bo.getQuoteDateBegin() != null && !bo.getQuoteDateBegin().isEmpty()) {
- lqw.ge(CrmQuote::getQuoteDate, LocalDate.parse(bo.getQuoteDateBegin()));
- }
- if (bo.getQuoteDateEnd() != null && !bo.getQuoteDateEnd().isEmpty()) {
- lqw.le(CrmQuote::getQuoteDate, LocalDate.parse(bo.getQuoteDateEnd()));
- }
- lqw.between(bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null,
- CrmQuote::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime"));
- // 查询某个用户已审批过的报价单
- if (bo.getApprovedByUserId() != null) {
- lqw.inSql(CrmQuote::getId,
- "SELECT DISTINCT quote_id FROM crm_quote_approval " +
- "WHERE approver_id = " + bo.getApprovedByUserId() +
- " AND approval_result IN ('approved', 'rejected') " +
- " AND del_flag = 0");
- }
- lqw.orderByDesc(CrmQuote::getCreateTime);
- return lqw;
- }
- @Override
- public TableDataInfo<CrmQuoteVo> queryPageList(CrmQuoteBo bo, PageQuery pageQuery) {
- LambdaQueryWrapper<CrmQuote> lqw = buildQueryWrapper(bo);
- Page<CrmQuoteVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
- // 填充canApprove字段(当前用户是否可以审批)
- Long currentUserId = LoginHelper.getUserId();
- for (CrmQuoteVo vo : result.getRecords()) {
- // 只有待审批状态的报价单才需要检查
- if ("submitted".equals(vo.getStatus()) && currentUserId != null) {
- vo.setCanApprove(quoteApprovalService.canUserApprove(vo.getId(), currentUserId));
- } else {
- vo.setCanApprove(false);
- }
- }
- fillContractLinkedCount(result.getRecords());
- return TableDataInfo.build(result);
- }
- @Override
- public List<CrmQuoteVo> queryList(CrmQuoteBo bo) {
- LambdaQueryWrapper<CrmQuote> lqw = buildQueryWrapper(bo);
- List<CrmQuoteVo> list = baseMapper.selectVoList(lqw);
- // 填充canApprove字段(当前用户是否可以审批)
- Long currentUserId = LoginHelper.getUserId();
- for (CrmQuoteVo vo : list) {
- // 只有待审批状态的报价单才需要检查
- if ("submitted".equals(vo.getStatus()) && currentUserId != null) {
- vo.setCanApprove(quoteApprovalService.canUserApprove(vo.getId(), currentUserId));
- } else {
- vo.setCanApprove(false);
- }
- }
- fillContractLinkedCount(list);
- return list;
- }
- @Override
- public CrmQuoteVo queryById(Long id) {
- CrmQuoteVo vo = baseMapper.selectVoById(id);
- if (vo != null) {
- // 查询产品列表
- vo.setProductList(quoteProductService.queryListByQuoteId(id));
- // 填充商机名称
- if (vo.getOpportunityId() != null) {
- try {
- var opportunityVo = opportunityService.queryById(vo.getOpportunityId());
- if (opportunityVo != null) {
- vo.setOpportunityName(opportunityVo.getOpportunityName());
- }
- } catch (Exception e) {
- log.warn("查询商机名称失败, opportunityId: {}", vo.getOpportunityId(), e);
- }
- }
- // 自动补全联系人手机和邮箱(如果为空但有联系人ID)
- if (vo.getContactId() != null &&
- (StringUtils.isBlank(vo.getContactMobile()) || StringUtils.isBlank(vo.getContactEmail()))) {
- try {
- var contact = contactService.queryById(vo.getContactId());
- if (contact != null) {
- if (StringUtils.isBlank(vo.getContactMobile())) {
- vo.setContactMobile(contact.getMobile());
- }
- if (StringUtils.isBlank(vo.getContactEmail())) {
- vo.setContactEmail(contact.getEmail());
- }
- }
- } catch (Exception e) {
- log.warn("查询联系人信息失败, contactId: {}", vo.getContactId(), e);
- }
- }
- // 填充负责人手机号
- if (vo.getOwnerUserId() != null && StringUtils.isBlank(vo.getOwnerUserMobile())) {
- try {
- SysUserVo user = userService.selectUserById(vo.getOwnerUserId());
- if (user != null && StringUtils.isNotBlank(user.getPhonenumber())) {
- vo.setOwnerUserMobile(user.getPhonenumber());
- }
- } catch (Exception e) {
- log.warn("查询负责人手机号失败, ownerUserId: {}", vo.getOwnerUserId(), e);
- }
- }
- // 填充报价主体名称
- if (vo.getSupplierId() != null && StringUtils.isBlank(vo.getSupplierName())) {
- try {
- var supplierInfo = supplierInfoService.queryById(vo.getSupplierId());
- if (supplierInfo != null) {
- vo.setSupplierName(supplierInfo.getSupplierName());
- }
- } catch (Exception e) {
- log.warn("查询报价主体信息失败, supplierId: {}", vo.getSupplierId(), e);
- }
- }
- }
- return vo;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Boolean insertByBo(CrmQuoteBo bo) {
- CrmQuote entity = MapstructUtils.convert(bo, CrmQuote.class);
- // 查询客户信息
- CrmCustomer customer = null;
- if (entity.getCustomerId() != null) {
- customer = customerMapper.selectById(entity.getCustomerId());
- }
- // 查询供方信息
- String supplierShortName = "山峡";
- if (entity.getSupplierId() != null) {
- try {
- var supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
- if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) {
- supplierShortName = supplierInfo.getShortName();
- } else if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierName())) {
- supplierShortName = supplierInfo.getSupplierName();
- }
- } catch (Exception e) {
- log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e);
- }
- }
- // 设置默认值(需要在生成编号之前设置报价日期)
- if (entity.getVersion() == null) {
- entity.setVersion("V1.0");
- }
- if (entity.getParentId() == null) {
- entity.setParentId(0L);
- }
- if (entity.getIsLatest() == null) {
- entity.setIsLatest(1);
- }
- if (entity.getStatus() == null) {
- entity.setStatus("draft");
- }
- if (StringUtils.isBlank(entity.getQuoteType())) {
- entity.setQuoteType("sales");
- }
- if (entity.getValidityDays() == null) {
- entity.setValidityDays(30);
- }
- if (entity.getQuoteDate() == null) {
- entity.setQuoteDate(LocalDate.now());
- }
- // 生成报价单编号(根据供方编码和报价日期)
- // 如果报价单号为空或包含预览标记(***),则重新生成
- if (StringUtils.isBlank(entity.getQuoteNo()) ||
- (StringUtils.isNotBlank(entity.getQuoteNo()) && entity.getQuoteNo().contains("***"))) {
- entity.setQuoteNo(generateQuoteNo(entity.getSupplierId(), entity.getQuoteDate()));
- }
- // 自动生成报价主题
- String quoteNo = entity.getQuoteNo();
- String customerShortName = customer != null && StringUtils.isNotBlank(customer.getShortName())
- ? customer.getShortName()
- : "";
- if (StringUtils.isBlank(entity.getQuoteTitle())) {
- // 用户未填写报价主题,根据报价类型自动生成
- // 销售:供方简称-客户简称(报价单编号)
- // 采购:客户简称-供方简称(报价单编号)
- String quoteTitle;
- if ("purchase".equals(entity.getQuoteType())) {
- // 采购:客户简称-供方简称(报价单编号)
- quoteTitle = customerShortName + "-" + supplierShortName + "(" + quoteNo + ")";
- } else {
- // 销售:供方简称-客户简称(报价单编号)
- quoteTitle = supplierShortName + "-" + customerShortName + "(" + quoteNo + ")";
- }
- entity.setQuoteTitle(quoteTitle);
- }
- if (entity.getOwnerUserId() == null) {
- entity.setOwnerUserId(LoginHelper.getUserId());
- entity.setOwnerUserName(LoginHelper.getNickname());
- }
- // 填充联系人手机和邮箱
- if (entity.getContactId() != null) {
- try {
- var contact = contactService.queryById(entity.getContactId());
- if (contact != null) {
- entity.setContactMobile(contact.getMobile());
- entity.setContactEmail(contact.getEmail());
- }
- } catch (Exception e) {
- log.warn("查询联系人信息失败, contactId: {}", entity.getContactId(), e);
- }
- }
- // 填充报价主体名称
- if (entity.getSupplierId() != null && StringUtils.isBlank(entity.getSupplierName())) {
- try {
- var supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
- if (supplierInfo != null) {
- entity.setSupplierName(supplierInfo.getSupplierName());
- }
- } catch (Exception e) {
- log.warn("查询报价主体信息失败, supplierId: {}", entity.getSupplierId(), e);
- }
- }
- // 计算到期日期
- if (entity.getValidityDays() != null && entity.getQuoteDate() != null) {
- entity.setExpireDate(entity.getQuoteDate().plusDays(entity.getValidityDays()));
- }
- // 计算金额
- resolveQuoteProductIds(bo.getProducts());
- calculateAmount(entity, bo.getProducts());
- // 保存报价单
- boolean flag = baseMapper.insert(entity) > 0;
- if (flag) {
- bo.setId(entity.getId());
- // 保存产品明细
- if (bo.getProducts() != null && !bo.getProducts().isEmpty()) {
- quoteProductService.batchInsert(entity.getId(), bo.getProducts());
- }
- }
- return flag;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Boolean updateByBo(CrmQuoteBo bo) {
- CrmQuote entity = MapstructUtils.convert(bo, CrmQuote.class);
- // 只有草稿状态才能修改
- CrmQuote oldEntity = baseMapper.selectById(bo.getId());
- if (oldEntity == null) {
- throw new ServiceException("报价单不存在");
- }
- if (!"draft".equals(oldEntity.getStatus())) {
- throw new ServiceException("只有草稿状态的报价单才能修改");
- }
- if (StringUtils.isBlank(entity.getQuoteType())) {
- entity.setQuoteType(StringUtils.isNotBlank(oldEntity.getQuoteType()) ? oldEntity.getQuoteType() : "sales");
- }
- // 填充联系人手机和邮箱
- if (entity.getContactId() != null) {
- try {
- var contact = contactService.queryById(entity.getContactId());
- if (contact != null) {
- entity.setContactMobile(contact.getMobile());
- entity.setContactEmail(contact.getEmail());
- }
- } catch (Exception e) {
- log.warn("查询联系人信息失败, contactId: {}", entity.getContactId(), e);
- }
- }
- // 检查客户、供方或报价日期是否发生变化
- boolean customerChanged = !java.util.Objects.equals(oldEntity.getCustomerId(), entity.getCustomerId());
- boolean supplierChanged = !java.util.Objects.equals(oldEntity.getSupplierId(), entity.getSupplierId());
- boolean quoteDateChanged = !java.util.Objects.equals(oldEntity.getQuoteDate(), entity.getQuoteDate());
- // 查询客户信息
- CrmCustomer customer = null;
- if (entity.getCustomerId() != null) {
- customer = customerMapper.selectById(entity.getCustomerId());
- }
- // 查询供方信息
- String supplierShortName = "山峡";
- if (entity.getSupplierId() != null) {
- try {
- var supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
- if (supplierInfo != null) {
- entity.setSupplierName(supplierInfo.getSupplierName());
- if (StringUtils.isNotBlank(supplierInfo.getShortName())) {
- supplierShortName = supplierInfo.getShortName();
- } else if (StringUtils.isNotBlank(supplierInfo.getSupplierName())) {
- supplierShortName = supplierInfo.getSupplierName();
- }
- }
- } catch (Exception e) {
- log.warn("查询报价主体信息失败, supplierId: {}", entity.getSupplierId(), e);
- }
- }
- // 如果报价日期发生变化,需要重新生成编号(根据新的报价日期生成)
- if (quoteDateChanged && entity.getQuoteDate() != null) {
- entity.setQuoteNo(generateQuoteNo(entity.getSupplierId(), entity.getQuoteDate()));
- // 报价单号变化了,需要更新报价主题
- String customerShortName = "";
- if (customer != null && StringUtils.isNotBlank(customer.getShortName())) {
- customerShortName = customer.getShortName();
- } else if (entity.getCustomerName() != null) {
- customerShortName = entity.getCustomerName();
- } else if (oldEntity.getCustomerName() != null) {
- customerShortName = oldEntity.getCustomerName();
- }
- String currentQuoteNo = entity.getQuoteNo();
- // 根据报价类型生成报价主题
- // 销售:供方简称-客户简称(报价单编号)
- // 采购:客户简称-供方简称(报价单编号)
- String quoteTitle;
- if ("purchase".equals(entity.getQuoteType())) {
- // 采购:客户简称-供方简称(报价单编号)
- quoteTitle = customerShortName + "-" + supplierShortName + "(" + currentQuoteNo + ")";
- } else {
- // 销售:供方简称-客户简称(报价单编号)
- quoteTitle = supplierShortName + "-" + customerShortName + "(" + currentQuoteNo + ")";
- }
- entity.setQuoteTitle(quoteTitle);
- }
- // 如果客户或供方发生变化,更新报价主题和报价单号(只更新编码和简称,不改变日期和序列号)
- else if (customerChanged || supplierChanged) {
- String quoteNo = oldEntity.getQuoteNo(); // 保持原报价单号不变
- // 如果供方发生变化,只更新报价单号中的供方编码部分,保持日期和序列号不变
- if (supplierChanged && StringUtils.isNotBlank(quoteNo)) {
- // 报价单号格式:{供方编码}DD-YYYYMMDD001
- // 提取日期和序列号部分(DD-YYYYMMDD001)
- String dateAndSeq = null;
- if (quoteNo.contains("DD-") && quoteNo.length() > 3) {
- int ddIndex = quoteNo.indexOf("DD-");
- if (ddIndex >= 0 && ddIndex + 3 < quoteNo.length()) {
- dateAndSeq = quoteNo.substring(ddIndex);
- }
- }
- // 获取新的供方编码
- String newSupplierCode = "SX";
- if (entity.getSupplierId() != null) {
- try {
- var supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
- if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) {
- newSupplierCode = supplierInfo.getSupplierCode();
- }
- } catch (Exception e) {
- log.warn("查询供方信息失败,使用默认编码,供方ID: {}", entity.getSupplierId(), e);
- }
- }
- // 如果成功提取了日期和序列号,则用新编码替换
- if (dateAndSeq != null) {
- quoteNo = newSupplierCode + dateAndSeq;
- entity.setQuoteNo(quoteNo);
- }
- // 如果提取失败,保持原报价单号不变
- }
- // 更新报价主题(只更新简称部分,报价单号使用当前数据的报价单号,确保与entity.getQuoteNo()保持一致)
- String customerShortName = customer != null && StringUtils.isNotBlank(customer.getShortName())
- ? customer.getShortName()
- : "";
- // 使用entity.getQuoteNo()确保报价主题中的报价单号与当前数据的报价单号保持一致
- // 如果供方变化且成功更新了报价单号,entity.getQuoteNo()就是更新后的值
- // 如果供方没变化或提取失败,entity.getQuoteNo()就是原值
- String currentQuoteNo = entity.getQuoteNo() != null ? entity.getQuoteNo() : oldEntity.getQuoteNo();
- // 根据报价类型生成报价主题
- // 销售:供方简称-客户简称(报价单编号)
- // 采购:客户简称-供方简称(报价单编号)
- String quoteTitle;
- if ("purchase".equals(entity.getQuoteType())) {
- // 采购:客户简称-供方简称(报价单编号)
- quoteTitle = customerShortName + "-" + supplierShortName + "(" + currentQuoteNo + ")";
- } else {
- // 销售:供方简称-客户简称(报价单编号)
- quoteTitle = supplierShortName + "-" + customerShortName + "(" + currentQuoteNo + ")";
- }
- entity.setQuoteTitle(quoteTitle);
- }
- // 计算到期日期
- if (entity.getValidityDays() != null && entity.getQuoteDate() != null) {
- entity.setExpireDate(entity.getQuoteDate().plusDays(entity.getValidityDays()));
- }
- // 计算金额
- resolveQuoteProductIds(bo.getProducts());
- calculateAmount(entity, bo.getProducts());
- boolean flag = baseMapper.updateById(entity) > 0;
- if (flag && bo.getProducts() != null) {
- // 删除旧产品明细
- quoteProductService.deleteByQuoteId(bo.getId());
- // 插入新产品明细
- quoteProductService.batchInsert(bo.getId(), bo.getProducts());
- }
- return flag;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
- if (isValid) {
- // 校验是否可以删除
- for (Long id : ids) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- continue;
- }
- if (!"draft".equals(entity.getStatus())) {
- throw new ServiceException("只能删除草稿状态的报价单");
- }
- if (entity.getContractId() != null) {
- throw new ServiceException("已转合同的报价单不能删除");
- }
- }
- }
- return baseMapper.deleteByIds(ids) > 0;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Boolean submitQuote(Long id, List<Long> approverIds, String remark) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- if (!"draft".equals(entity.getStatus())) {
- throw new ServiceException("只有草稿状态的报价单才能提交");
- }
- // 更新状态
- entity.setStatus("submitted");
- baseMapper.updateById(entity);
- // 创建审批记录
- if (approverIds != null && !approverIds.isEmpty()) {
- quoteApprovalService.createApprovalRecords(id, approverIds);
- }
- // 自动生成PDF
- try {
- String pdfUrl = quotePdfService.generateQuotePdf(id);
- entity.setPdfUrl(pdfUrl);
- entity.setPdfGenerated(1);
- baseMapper.updateById(entity);
- } catch (Exception e) {
- log.warn("报价单提交时自动生成PDF失败: quoteId={}", id, e);
- }
- return true;
- }
- @Override
- public Boolean saveTemplate(Long id, String templateContent) {
- // 检查报价单是否存在
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- log.info("========== 开始保存报价单模板内容 ==========");
- log.info("quoteId={}", id);
- log.info("contentLength={}", templateContent != null ? templateContent.length() : 0);
- log.info("content前200字符={}", templateContent != null && templateContent.length() > 200
- ? templateContent.substring(0, 200)
- : templateContent);
- log.info("包含HTML标签={}", templateContent != null && templateContent.contains("<"));
- // 使用 LambdaUpdateWrapper 只更新 template_content 字段,避免乐观锁问题
- int result = baseMapper.update(null,
- Wrappers.<CrmQuote>lambdaUpdate()
- .eq(CrmQuote::getId, id)
- .set(CrmQuote::getTemplateContent, templateContent));
- log.info("保存报价单模板内容完成: quoteId={}, success={}, affectedRows={}",
- id, result > 0, result);
- // 验证保存结果
- CrmQuote savedEntity = baseMapper.selectById(id);
- if (savedEntity != null && savedEntity.getTemplateContent() != null) {
- log.info("验证保存结果: 数据库中的内容长度={}", savedEntity.getTemplateContent().length());
- log.info("验证保存结果: 数据库中的内容前200字符={}",
- savedEntity.getTemplateContent().length() > 200
- ? savedEntity.getTemplateContent().substring(0, 200)
- : savedEntity.getTemplateContent());
- } else {
- log.warn("验证保存结果: 数据库中未找到模板内容!");
- }
- return result > 0;
- }
- @Override
- public String getTemplate(Long id) {
- log.info("========== 开始获取报价单模板内容 ==========");
- log.info("quoteId={}", id);
- // 直接查询,确保获取最新数据
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- String templateContent = entity.getTemplateContent();
- log.info("获取报价单模板内容完成:");
- log.info("- hasTemplate={}", templateContent != null);
- log.info("- contentLength={}", templateContent != null ? templateContent.length() : 0);
- log.info("- content前200字符={}", templateContent != null && templateContent.length() > 200
- ? templateContent.substring(0, 200)
- : templateContent);
- log.info("- 包含HTML标签={}", templateContent != null && templateContent.contains("<"));
- return templateContent;
- }
- @Override
- public String generatePdf(Long id) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- // 调用PDF生成服务
- String pdfUrl = quotePdfService.generateQuotePdf(id);
- // 更新PDF信息
- entity.setPdfUrl(pdfUrl);
- entity.setPdfGenerated(1);
- baseMapper.updateById(entity);
- return pdfUrl;
- }
- @Override
- public String generateShare(Long id, Integer validityDays, String password) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- // 生成分享码(8位随机码)
- String shareCode = IdUtil.fastSimpleUUID().substring(0, 8);
- // 计算分享链接过期时间
- LocalDateTime shareExpireTime = null;
- if (validityDays != null && validityDays > 0) {
- shareExpireTime = LocalDateTime.now().plusDays(validityDays);
- }
- // 生成分享链接
- // TODO: 这里应该从配置文件读取域名
- String shareLink = "http://localhost:8080/share/" + shareCode;
- // 更新分享信息
- entity.setShareCode(shareCode);
- entity.setShareLink(shareLink);
- entity.setSharePassword(password); // 设置密码(可为空)
- entity.setShareExpireTime(shareExpireTime);
- baseMapper.updateById(entity);
- log.info("生成分享链接成功: quoteId={}, shareCode={}, hasPassword={}, expireTime={}",
- id, shareCode, (password != null && !password.isEmpty()), shareExpireTime);
- return shareLink;
- }
- @Override
- public CrmQuoteVo viewByShareCode(String shareCode, String password, String ipAddress, String userAgent) {
- LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
- lqw.eq(CrmQuote::getShareCode, shareCode);
- CrmQuote entity = baseMapper.selectOne(lqw);
- if (entity == null) {
- throw new ServiceException("分享链接无效");
- }
- // 检查分享链接是否过期
- if (entity.getShareExpireTime() != null && LocalDateTime.now().isAfter(entity.getShareExpireTime())) {
- throw new ServiceException("分享链接已过期");
- }
- // 验证密码(如果设置了密码)
- if (StringUtils.isNotBlank(entity.getSharePassword())) {
- if (StringUtils.isBlank(password) || !entity.getSharePassword().equals(password)) {
- throw new ServiceException("分享密码错误");
- }
- }
- // 检查报价单是否已过期
- if (entity.getExpireDate() != null && LocalDate.now().isAfter(entity.getExpireDate())) {
- // 更新报价单状态为已过期
- if (!"expired".equals(entity.getStatus())) {
- entity.setStatus("expired");
- baseMapper.updateById(entity);
- }
- }
- // 增加查看次数
- entity.setViewCount((entity.getViewCount() == null ? 0 : entity.getViewCount()) + 1);
- entity.setLastViewTime(LocalDateTime.now());
- // 更新状态:如果是已提交或已审批,改为已查看
- if ("submitted".equals(entity.getStatus()) || "approved".equals(entity.getStatus())) {
- entity.setStatus("viewed");
- }
- baseMapper.updateById(entity);
- // 记录访问日志
- try {
- quoteShareLogService.recordVisit(entity.getId(), shareCode, ipAddress, userAgent);
- } catch (Exception e) {
- log.warn("记录访问日志失败: quoteId={}, shareCode={}", entity.getId(), shareCode, e);
- }
- log.info("通过分享码查看报价单: shareCode={}, quoteId={}, viewCount={}, ip={}",
- shareCode, entity.getId(), entity.getViewCount(), ipAddress);
- CrmQuoteVo vo = MapstructUtils.convert(entity, CrmQuoteVo.class);
- vo.setProductList(quoteProductService.queryListByQuoteId(entity.getId()));
- return vo;
- }
- @Override
- public Boolean acceptQuote(Long id, String remark) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- entity.setStatus("accepted");
- return baseMapper.updateById(entity) > 0;
- }
- @Override
- public Boolean rejectQuote(Long id, String rejectReason, String remark) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- entity.setStatus("rejected");
- entity.setRejectReason(rejectReason);
- return baseMapper.updateById(entity) > 0;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Long toContract(Long id, String contractNo, String contractTitle, LocalDate signDate, LocalDate startDate,
- LocalDate endDate) {
- // 1. 查询报价单信息
- CrmQuote quote = baseMapper.selectById(id);
- if (quote == null) {
- throw new ServiceException("报价单不存在");
- }
- if (!"accepted".equals(quote.getStatus())) {
- throw new ServiceException("只有已接受的报价单才能转合同");
- }
- if (quote.getContractId() != null) {
- throw new ServiceException("该报价单已转为合同,不能重复转换");
- }
- // 2. 查询报价单产品明细
- List<CrmQuoteProductVo> quoteProducts = quoteProductService.queryListByQuoteId(id);
- if (quoteProducts == null || quoteProducts.isEmpty()) {
- throw new ServiceException("报价单没有产品明细,无法转合同");
- }
- // 3. 构建合同Bo对象
- CrmContractBo contractBo = new CrmContractBo();
- contractBo.setContractNo(contractNo);
- contractBo.setContractName(contractTitle);
- contractBo.setCustomerId(quote.getCustomerId());
- contractBo.setOpportunityId(quote.getOpportunityId());
- contractBo.setQuoteId(quote.getId());
- contractBo.setContactId(quote.getContactId());
- contractBo.setContractType("sale"); // 默认销售合同
- contractBo.setAmount(quote.getFinalAmount());
- contractBo.setSignDate(signDate != null ? signDate : LocalDate.now());
- contractBo.setStartDate(startDate != null ? startDate : LocalDate.now());
- contractBo.setEndDate(endDate); // 可为空
- contractBo.setOwnerUserId(quote.getOwnerUserId());
- contractBo.setPaymentTerms(quote.getPaymentTerms());
- contractBo.setDeliveryTerms(quote.getDeliveryTerms());
- contractBo.setRemark("由报价单" + quote.getQuoteNo() + "转换生成");
- // 4. 复制产品明细(只设置Bo中存在的字段)
- List<CrmContractProductBo> contractProducts = new java.util.ArrayList<>();
- for (CrmQuoteProductVo quoteProduct : quoteProducts) {
- CrmContractProductBo contractProduct = new CrmContractProductBo();
- contractProduct.setProductId(quoteProduct.getProductId());
- contractProduct.setQuantity(quoteProduct.getQuantity());
- contractProduct.setPrice(quoteProduct.getPrice());
- contractProduct.setDiscount(quoteProduct.getDiscount());
- contractProduct.setDescription(quoteProduct.getDescription());
- contractProduct.setSort(quoteProduct.getSort());
- contractProduct.setDeliveryStatus("pending");
- contractProduct.setDeliveredQuantity(0);
- contractProducts.add(contractProduct);
- }
- contractBo.setProducts(contractProducts);
- // 5. 调用合同服务创建合同
- Boolean success = contractService.insertByBo(contractBo);
- if (!success) {
- throw new ServiceException("创建合同失败");
- }
- Long contractId = contractBo.getId();
- // 6. 更新报价单状态
- quote.setStatus("contract");
- quote.setContractId(contractId);
- baseMapper.updateById(quote);
- log.info("报价单转合同成功: quoteId={}, quoteNo={}, contractId={}, contractNo={}",
- id, quote.getQuoteNo(), contractId, contractNo);
- return contractId;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Long createNewVersion(Long id, String version, String remark) {
- CrmQuote oldEntity = baseMapper.selectById(id);
- if (oldEntity == null) {
- throw new ServiceException("报价单不存在");
- }
- // 创建新版本
- CrmQuote newEntity = new CrmQuote();
- newEntity.setQuoteNo(oldEntity.getQuoteNo());
- newEntity.setQuoteTitle(oldEntity.getQuoteTitle());
- newEntity.setCustomerId(oldEntity.getCustomerId());
- newEntity.setCustomerName(oldEntity.getCustomerName());
- newEntity.setOpportunityId(oldEntity.getOpportunityId());
- newEntity.setContactId(oldEntity.getContactId());
- newEntity.setContactName(oldEntity.getContactName());
- newEntity.setContactMobile(oldEntity.getContactMobile());
- newEntity.setContactEmail(oldEntity.getContactEmail());
- newEntity.setVersion(version);
- newEntity.setParentId(id);
- newEntity.setIsLatest(1);
- newEntity.setStatus("draft");
- newEntity.setValidityDays(oldEntity.getValidityDays());
- newEntity.setQuoteDate(LocalDate.now());
- newEntity.setExpireDate(LocalDate.now().plusDays(oldEntity.getValidityDays()));
- newEntity.setOwnerUserId(oldEntity.getOwnerUserId());
- newEntity.setOwnerUserName(oldEntity.getOwnerUserName());
- newEntity.setQuoteType(StringUtils.isNotBlank(oldEntity.getQuoteType()) ? oldEntity.getQuoteType() : "sales");
- newEntity.setPaymentTerms(oldEntity.getPaymentTerms());
- newEntity.setDeliveryTerms(oldEntity.getDeliveryTerms());
- newEntity.setRemark(remark);
- // 旧版本设为非最新
- oldEntity.setIsLatest(0);
- baseMapper.updateById(oldEntity);
- // 保存新版本
- baseMapper.insert(newEntity);
- // 复制产品明细
- List<CrmQuoteProductVo> oldProducts = quoteProductService.queryListByQuoteId(id);
- if (oldProducts != null && !oldProducts.isEmpty()) {
- List<CrmQuoteProductBo> newProducts = oldProducts.stream()
- .map(vo -> MapstructUtils.convert(vo, CrmQuoteProductBo.class))
- .toList();
- quoteProductService.batchInsert(newEntity.getId(), newProducts);
- }
- return newEntity.getId();
- }
- @Override
- public List<CrmQuoteVo> queryVersions(String quoteNo) {
- LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
- lqw.eq(CrmQuote::getQuoteNo, quoteNo);
- lqw.orderByDesc(CrmQuote::getCreateTime);
- return baseMapper.selectVoList(lqw);
- }
- /**
- * 生成报价单编号
- * 格式:{供方编码}DD-YYYYMMDD001(供方编码DD- + 报价日期年月日 + 3位序号)
- *
- * @param supplierId 供方主体ID,如果为null则使用默认编码"SX"
- * @param quoteDate 报价日期,如果为null则使用当前日期
- */
- private String generateQuoteNo(Long supplierId, LocalDate quoteDate) {
- // 获取供方编码,如果没有则使用默认值"SX"
- String supplierCode = "SX";
- if (supplierId != null) {
- try {
- var supplierInfo = supplierInfoService.queryById(supplierId);
- if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) {
- supplierCode = supplierInfo.getSupplierCode();
- }
- } catch (Exception e) {
- log.warn("查询供方信息失败,使用默认编码,供方ID: {}", supplierId, e);
- }
- }
- // 使用报价日期生成日期部分,如果报价日期为空则使用当前日期
- LocalDate date = quoteDate != null ? quoteDate : LocalDate.now();
- String dateStr = DateTimeFormatter.ofPattern("yyyyMMdd").format(date);
- String prefix = supplierCode + "DD-" + dateStr;
- // 查询该报价日期下的最大编号(只查询未删除的记录)
- // 同时需要匹配报价日期,确保序号按报价日期统计
- LambdaQueryWrapper<CrmQuote> wrapper = Wrappers.lambdaQuery();
- wrapper.likeRight(CrmQuote::getQuoteNo, prefix);
- wrapper.eq(CrmQuote::getQuoteDate, date); // 按报价日期查询
- wrapper.orderByDesc(CrmQuote::getQuoteNo);
- wrapper.last("LIMIT 1");
- CrmQuote lastQuote = baseMapper.selectOne(wrapper);
- int sequence = 1;
- if (lastQuote != null && StringUtils.isNotBlank(lastQuote.getQuoteNo())) {
- String lastNo = lastQuote.getQuoteNo();
- // {供方编码}DD-20251017001,前缀长度是{供方编码长度}+5(DD-yyyyMMdd),序号长度是3位
- if (lastNo.length() >= prefix.length() + 3) {
- String seqStr = lastNo.substring(prefix.length());
- try {
- sequence = Integer.parseInt(seqStr) + 1;
- } catch (NumberFormatException e) {
- sequence = 1;
- }
- }
- }
- // 最多尝试10次,避免无限循环
- for (int retry = 0; retry < 10; retry++) {
- String quoteNo = prefix + String.format("%03d", sequence + retry);
- // 检查编号是否已存在(只检查未删除的记录)
- Long count = baseMapper.selectCount(
- Wrappers.lambdaQuery(CrmQuote.class)
- .eq(CrmQuote::getQuoteNo, quoteNo));
- if (count == 0) {
- return quoteNo;
- }
- // 如果编号已存在,继续尝试下一个序号
- log.warn("报价单编号 {} 已存在,尝试生成下一个编号(第{}次重试)", quoteNo, retry + 1);
- }
- // 如果重试10次都失败,抛出异常
- throw new ServiceException("生成报价单编号失败,请稍后重试");
- }
- /**
- * 填充报价单被合同关联次数
- */
- private void fillContractLinkedCount(List<CrmQuoteVo> quoteList) {
- if (quoteList == null || quoteList.isEmpty()) {
- return;
- }
- Set<Long> quoteIds = quoteList.stream()
- .map(CrmQuoteVo::getId)
- .filter(java.util.Objects::nonNull)
- .collect(Collectors.toSet());
- if (quoteIds.isEmpty()) {
- return;
- }
- List<CrmContract> contracts = contractMapper.selectList(
- Wrappers.<CrmContract>lambdaQuery()
- .in(CrmContract::getQuoteId, quoteIds)
- .select(CrmContract::getQuoteId));
- Map<Long, Long> contractCountMap = contracts.stream()
- .map(CrmContract::getQuoteId)
- .filter(java.util.Objects::nonNull)
- .collect(Collectors.groupingBy(id -> id, Collectors.counting()));
- for (CrmQuoteVo vo : quoteList) {
- Long count = contractCountMap.getOrDefault(vo.getId(), 0L);
- vo.setContractLinkedCount(count.intValue());
- }
- }
- /**
- * 计算金额
- */
- private void calculateAmount(CrmQuote entity, List<CrmQuoteProductBo> products) {
- if (products == null || products.isEmpty()) {
- entity.setTotalAmount(BigDecimal.ZERO);
- entity.setDiscountAmount(BigDecimal.ZERO);
- entity.setFinalAmount(BigDecimal.ZERO);
- return;
- }
- // 计算总金额(未折扣前)
- BigDecimal totalAmount = BigDecimal.ZERO;
- for (CrmQuoteProductBo product : products) {
- BigDecimal subtotal = product.getPrice()
- .multiply(product.getQuantity())
- .multiply(BigDecimal.ONE.subtract(
- product.getDiscount() == null ? BigDecimal.ZERO
- : product.getDiscount().divide(BigDecimal.valueOf(100))));
- totalAmount = totalAmount.add(subtotal);
- }
- entity.setTotalAmount(totalAmount);
- // 计算整单折扣
- BigDecimal discountRate = entity.getDiscountRate() == null ? BigDecimal.ZERO : entity.getDiscountRate();
- BigDecimal discountAmount = totalAmount.multiply(discountRate.divide(BigDecimal.valueOf(100)));
- entity.setDiscountAmount(discountAmount);
- // 计算最终金额
- entity.setFinalAmount(totalAmount.subtract(discountAmount));
- }
- /**
- * 解析报价产品ID(按名称/规格/分类/单位/税率匹配,未命中自动新增)
- */
- private void resolveQuoteProductIds(List<CrmQuoteProductBo> products) {
- if (products == null || products.isEmpty()) {
- return;
- }
- for (CrmQuoteProductBo product : products) {
- Long productId = productService.resolveProductIdForQuoteItem(product);
- product.setProductId(productId);
- }
- }
- @Override
- public java.util.Map<String, Object> statisticsOverview(Long ownerUserId, String beginTime, String endTime) {
- java.util.Map<String, Object> result = new java.util.HashMap<>();
- LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
- if (ownerUserId != null) {
- lqw.eq(CrmQuote::getOwnerUserId, ownerUserId);
- }
- if (StringUtils.isNotBlank(beginTime)) {
- lqw.ge(CrmQuote::getCreateTime, beginTime);
- }
- if (StringUtils.isNotBlank(endTime)) {
- lqw.le(CrmQuote::getCreateTime, endTime);
- }
- // 总数量
- Long totalCount = baseMapper.selectCount(lqw);
- result.put("totalCount", totalCount);
- // 各状态数量
- LambdaQueryWrapper<CrmQuote> draftLqw = lqw.clone().eq(CrmQuote::getStatus, "draft");
- result.put("draftCount", baseMapper.selectCount(draftLqw));
- LambdaQueryWrapper<CrmQuote> submittedLqw = lqw.clone().eq(CrmQuote::getStatus, "submitted");
- result.put("submittedCount", baseMapper.selectCount(submittedLqw));
- LambdaQueryWrapper<CrmQuote> acceptedLqw = lqw.clone().eq(CrmQuote::getStatus, "accepted");
- Long acceptedCount = baseMapper.selectCount(acceptedLqw);
- result.put("acceptedCount", acceptedCount);
- LambdaQueryWrapper<CrmQuote> rejectedLqw = lqw.clone().eq(CrmQuote::getStatus, "rejected");
- result.put("rejectedCount", baseMapper.selectCount(rejectedLqw));
- LambdaQueryWrapper<CrmQuote> expiredLqw = lqw.clone().eq(CrmQuote::getStatus, "expired");
- result.put("expiredCount", baseMapper.selectCount(expiredLqw));
- // 总金额和已接受金额
- List<CrmQuote> allQuotes = baseMapper.selectList(lqw);
- BigDecimal totalAmount = allQuotes.stream()
- .map(CrmQuote::getFinalAmount)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- result.put("totalAmount", totalAmount);
- List<CrmQuote> acceptedQuotes = baseMapper.selectList(acceptedLqw);
- BigDecimal acceptedAmount = acceptedQuotes.stream()
- .map(CrmQuote::getFinalAmount)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- result.put("acceptedAmount", acceptedAmount);
- // 接受率
- double acceptRate = totalCount > 0 ? (acceptedCount * 100.0 / totalCount) : 0.0;
- result.put("acceptRate", String.format("%.2f", acceptRate));
- // 平均金额
- BigDecimal avgAmount = totalCount > 0 ? totalAmount.divide(BigDecimal.valueOf(totalCount), 2,
- java.math.RoundingMode.HALF_UP) : BigDecimal.ZERO;
- result.put("avgAmount", avgAmount);
- return result;
- }
- @Override
- public List<java.util.Map<String, Object>> statisticsStatus() {
- List<java.util.Map<String, Object>> result = new java.util.ArrayList<>();
- String[] statusArray = { "draft", "submitted", "accepted", "rejected", "expired", "contract" };
- String[] statusNameArray = { "草稿", "已提交", "已接受", "已拒绝", "已过期", "已转合同" };
- Long total = baseMapper.selectCount(Wrappers.lambdaQuery());
- for (int i = 0; i < statusArray.length; i++) {
- java.util.Map<String, Object> item = new java.util.HashMap<>();
- String status = statusArray[i];
- String statusName = statusNameArray[i];
- LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
- lqw.eq(CrmQuote::getStatus, status);
- Long count = baseMapper.selectCount(lqw);
- List<CrmQuote> quotes = baseMapper.selectList(lqw);
- BigDecimal amount = quotes.stream()
- .map(CrmQuote::getFinalAmount)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- double percent = total > 0 ? (count * 100.0 / total) : 0.0;
- item.put("status", status);
- item.put("statusName", statusName);
- item.put("count", count);
- item.put("amount", amount);
- item.put("percent", String.format("%.2f", percent));
- result.add(item);
- }
- return result;
- }
- @Override
- public List<java.util.Map<String, Object>> statisticsTrend(String type, String beginTime, String endTime) {
- List<java.util.Map<String, Object>> result = new java.util.ArrayList<>();
- // 1. 构建查询条件
- LambdaQueryWrapper<CrmQuote> lqw = Wrappers.lambdaQuery();
- if (StringUtils.isNotBlank(beginTime)) {
- lqw.ge(CrmQuote::getCreateTime, beginTime);
- }
- if (StringUtils.isNotBlank(endTime)) {
- lqw.le(CrmQuote::getCreateTime, endTime);
- }
- // 2. 查询所有数据
- List<CrmQuote> allQuotes = baseMapper.selectList(lqw);
- if (allQuotes.isEmpty()) {
- return result;
- }
- // 3. 按时间类型分组统计
- java.util.Map<String, List<CrmQuote>> groupedQuotes = new java.util.LinkedHashMap<>();
- for (CrmQuote quote : allQuotes) {
- // createTime继承自BaseEntity,是Date类型,需要转换为LocalDateTime
- java.util.Date createTimeDate = quote.getCreateTime();
- LocalDateTime createTime;
- if (createTimeDate != null) {
- createTime = DateUtil.toLocalDateTime(createTimeDate);
- } else if (quote.getQuoteDate() != null) {
- // 如果createTime为空,使用quoteDate
- createTime = quote.getQuoteDate().atStartOfDay();
- } else {
- createTime = LocalDateTime.now();
- }
- String period = getPeriodKey(createTime, type);
- groupedQuotes.computeIfAbsent(period, k -> new java.util.ArrayList<>()).add(quote);
- }
- // 4. 计算每个周期的统计数据
- for (java.util.Map.Entry<String, List<CrmQuote>> entry : groupedQuotes.entrySet()) {
- String period = entry.getKey();
- List<CrmQuote> periodQuotes = entry.getValue();
- java.util.Map<String, Object> item = new java.util.HashMap<>();
- item.put("period", period);
- // 总数量
- item.put("totalCount", periodQuotes.size());
- // 已接受数量
- long acceptedCount = periodQuotes.stream()
- .filter(q -> "accepted".equals(q.getStatus()) || "contract".equals(q.getStatus()))
- .count();
- item.put("acceptedCount", acceptedCount);
- // 总金额
- BigDecimal totalAmount = periodQuotes.stream()
- .map(CrmQuote::getFinalAmount)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- item.put("totalAmount", totalAmount);
- // 已接受金额
- BigDecimal acceptedAmount = periodQuotes.stream()
- .filter(q -> "accepted".equals(q.getStatus()) || "contract".equals(q.getStatus()))
- .map(CrmQuote::getFinalAmount)
- .reduce(BigDecimal.ZERO, BigDecimal::add);
- item.put("acceptedAmount", acceptedAmount);
- // 接受率
- double acceptRate = periodQuotes.size() > 0
- ? (acceptedCount * 100.0 / periodQuotes.size())
- : 0.0;
- item.put("acceptRate", String.format("%.2f", acceptRate));
- result.add(item);
- }
- log.info("报价单趋势分析: type={}, periods={}, totalRecords={}",
- type, result.size(), allQuotes.size());
- return result;
- }
- /**
- * 根据时间类型获取周期标识
- */
- private String getPeriodKey(LocalDateTime dateTime, String type) {
- if (dateTime == null) {
- return "未知";
- }
- DateTimeFormatter formatter;
- switch (type.toLowerCase()) {
- case "year":
- formatter = DateTimeFormatter.ofPattern("yyyy年");
- break;
- case "quarter":
- int quarter = (dateTime.getMonthValue() - 1) / 3 + 1;
- return dateTime.getYear() + "年Q" + quarter;
- case "month":
- default:
- formatter = DateTimeFormatter.ofPattern("yyyy年MM月");
- break;
- }
- return dateTime.format(formatter);
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Boolean withdrawApproval(Long id, String remark) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- // 只能撤回已提交未审批的报价单
- if (!"submitted".equals(entity.getStatus())) {
- throw new ServiceException("只能撤回已提交的报价单");
- }
- // 检查是否为创建人
- if (!entity.getCreateBy().equals(LoginHelper.getUserId())) {
- throw new ServiceException("只有创建人才能撤回审批");
- }
- // 更新状态为草稿
- entity.setStatus("draft");
- boolean flag = baseMapper.updateById(entity) > 0;
- if (flag) {
- // 删除所有pending状态的审批记录
- quoteApprovalService.deletePendingApprovals(id);
- log.info("撤回审批成功: quoteId={}, quoteNo={}, remark={}",
- id, entity.getQuoteNo(), remark);
- }
- return flag;
- }
- @Override
- @Transactional(rollbackFor = Exception.class)
- public Boolean invalidQuote(Long id, String remark) {
- CrmQuote entity = baseMapper.selectById(id);
- if (entity == null) {
- throw new ServiceException("报价单不存在");
- }
- // 任何状态都可以作废,但已作废的不能重复作废
- if ("invalid".equals(entity.getStatus())) {
- throw new ServiceException("报价单已作废,不能重复作废");
- }
- // 更新状态为已作废
- entity.setStatus("invalid");
- if (StringUtils.isNotBlank(remark)) {
- String currentRemark = entity.getRemark();
- if (StringUtils.isNotBlank(currentRemark)) {
- entity.setRemark(currentRemark + "\n【作废原因】" + remark);
- } else {
- entity.setRemark("【作废原因】" + remark);
- }
- }
- boolean flag = baseMapper.updateById(entity) > 0;
- if (flag) {
- log.info("作废报价单成功: quoteId={}, quoteNo={}, remark={}",
- id, entity.getQuoteNo(), remark);
- }
- return flag;
- }
- }
|