CrmContractServiceImpl.java 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014
  1. package org.dromara.crm.service.impl;
  2. import cn.hutool.core.date.DateUtil;
  3. import cn.hutool.json.JSONArray;
  4. import cn.hutool.json.JSONObject;
  5. import cn.hutool.json.JSONUtil;
  6. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  7. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  8. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  9. import lombok.RequiredArgsConstructor;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.dromara.common.core.exception.ServiceException;
  12. import org.dromara.common.core.service.UserService;
  13. import org.dromara.common.core.utils.MapstructUtils;
  14. import org.dromara.common.core.utils.StringUtils;
  15. import org.dromara.common.mybatis.core.page.PageQuery;
  16. import org.dromara.common.mybatis.core.page.TableDataInfo;
  17. import org.dromara.common.satoken.utils.LoginHelper;
  18. import org.dromara.crm.domain.*;
  19. import org.dromara.crm.domain.bo.CrmContractBo;
  20. import org.dromara.crm.domain.bo.CrmContractProductBo;
  21. import org.dromara.crm.domain.bo.CrmContractProductPriceTierBo;
  22. import org.dromara.crm.domain.bo.CrmProductBo;
  23. import org.dromara.crm.domain.vo.*;
  24. import org.dromara.crm.mapper.*;
  25. import org.dromara.crm.service.ICrmContractApprovalService;
  26. import org.dromara.crm.service.ICrmContractProductService;
  27. import org.dromara.crm.service.ICrmContractService;
  28. import org.dromara.crm.service.ICrmPaymentPlanService;
  29. import org.dromara.crm.service.ICrmProductService;
  30. import org.dromara.crm.service.ICrmSupplierInfoService;
  31. import org.springframework.stereotype.Service;
  32. import org.springframework.transaction.annotation.Transactional;
  33. import java.math.BigDecimal;
  34. import java.math.RoundingMode;
  35. import java.time.LocalDate;
  36. import java.time.LocalDateTime;
  37. import java.time.format.DateTimeFormatter;
  38. import java.util.*;
  39. import java.util.stream.Collectors;
  40. /**
  41. * 合同管理Service实现
  42. *
  43. * @author AI Assistant
  44. * @date 2025-10-04
  45. */
  46. @Slf4j
  47. @RequiredArgsConstructor
  48. @Service
  49. public class CrmContractServiceImpl implements ICrmContractService {
  50. private final CrmContractMapper baseMapper;
  51. private final ICrmContractProductService contractProductService;
  52. private final CrmContractApprovalMapper approvalMapper;
  53. private final ICrmContractApprovalService contractApprovalService;
  54. private final ICrmPaymentPlanService paymentPlanService;
  55. private final CrmPaymentMapper paymentMapper;
  56. private final CrmOpportunityMapper opportunityMapper;
  57. private final CrmQuoteMapper quoteMapper;
  58. private final CrmContractProductMapper contractProductMapper;
  59. private final CrmContractTemplateMapper contractTemplateMapper;
  60. private final CrmInvoiceMapper invoiceMapper;
  61. private final CrmCustomerMapper customerMapper;
  62. private final CrmContactMapper contactMapper;
  63. private final UserService userService;
  64. private final ICrmSupplierInfoService supplierInfoService;
  65. private final ICrmProductService productService;
  66. private final CrmProductMapper productMapper;
  67. private final CrmContractProductPriceTierMapper contractProductPriceTierMapper;
  68. /**
  69. * 构建查询条件
  70. */
  71. private LambdaQueryWrapper<CrmContract> buildQueryWrapper(CrmContractBo bo) {
  72. LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
  73. // 合同编号、合同名称和客户名称的OR查询
  74. // 如果三个字段都有值且相同,则认为是联合搜索(OR关系)
  75. if (StringUtils.isNotBlank(bo.getContractNo()) && StringUtils.isNotBlank(bo.getContractName())
  76. && StringUtils.isNotBlank(bo.getCustomerName())
  77. && bo.getContractNo().equals(bo.getContractName())
  78. && bo.getContractName().equals(bo.getCustomerName())) {
  79. // 合同编号、合同名称或客户名称包含关键字
  80. String keyword = bo.getContractNo();
  81. lqw.and(wrapper -> wrapper
  82. .like(CrmContract::getContractNo, keyword)
  83. .or()
  84. .like(CrmContract::getContractName, keyword)
  85. .or()
  86. .like(CrmContract::getCustomerName, keyword));
  87. } else {
  88. // 分别查询
  89. lqw.like(StringUtils.isNotBlank(bo.getContractNo()), CrmContract::getContractNo, bo.getContractNo());
  90. lqw.like(StringUtils.isNotBlank(bo.getContractName()), CrmContract::getContractName, bo.getContractName());
  91. lqw.like(StringUtils.isNotBlank(bo.getCustomerName()), CrmContract::getCustomerName, bo.getCustomerName());
  92. }
  93. lqw.eq(bo.getCustomerId() != null, CrmContract::getCustomerId, bo.getCustomerId());
  94. lqw.eq(bo.getOpportunityId() != null, CrmContract::getOpportunityId, bo.getOpportunityId());
  95. lqw.eq(bo.getQuoteId() != null, CrmContract::getQuoteId, bo.getQuoteId());
  96. lqw.eq(StringUtils.isNotBlank(bo.getContractType()), CrmContract::getContractType, bo.getContractType());
  97. lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CrmContract::getStatus, bo.getStatus());
  98. // 排除指定状态(如默认排除已作废的记录)
  99. lqw.ne(StringUtils.isNotBlank(bo.getExcludeStatus()), CrmContract::getStatus, bo.getExcludeStatus());
  100. lqw.eq(StringUtils.isNotBlank(bo.getApprovalStatus()), CrmContract::getApprovalStatus, bo.getApprovalStatus());
  101. lqw.eq(bo.getOwnerUserId() != null, CrmContract::getOwnerUserId, bo.getOwnerUserId());
  102. // 如果指定了待审批人ID,查询该用户待审批的合同
  103. if (bo.getPendingApprovalUserId() != null) {
  104. // 使用SQL子查询:查询审批记录中审批人是当前用户且审批结果是pending且isCurrent=1的合同
  105. lqw.inSql(CrmContract::getId,
  106. "SELECT DISTINCT contract_id FROM crm_contract_approval " +
  107. "WHERE approver_id = " + bo.getPendingApprovalUserId() +
  108. " AND approval_result = 'pending' " +
  109. " AND is_current = 1 " +
  110. " AND del_flag = 0");
  111. }
  112. // 如果指定了审批人ID,查询该用户审批过的合同
  113. if (bo.getApprovedByUserId() != null) {
  114. // 查询该审批人已审批过的合同ID列表
  115. LambdaQueryWrapper<CrmContractApproval> approvalLqw = Wrappers.lambdaQuery();
  116. approvalLqw.eq(CrmContractApproval::getApproverId, bo.getApprovedByUserId());
  117. approvalLqw.in(CrmContractApproval::getApprovalResult, "approved", "rejected");
  118. List<CrmContractApproval> approvalList = approvalMapper.selectList(approvalLqw);
  119. if (!approvalList.isEmpty()) {
  120. List<Long> contractIds = approvalList.stream()
  121. .map(CrmContractApproval::getContractId)
  122. .distinct()
  123. .collect(java.util.stream.Collectors.toList());
  124. lqw.in(CrmContract::getId, contractIds);
  125. } else {
  126. // 如果没有审批记录,返回空结果
  127. lqw.eq(CrmContract::getId, -1L);
  128. }
  129. }
  130. lqw.ge(bo.getParams().get("minAmount") != null, CrmContract::getAmount, bo.getParams().get("minAmount"));
  131. lqw.le(bo.getParams().get("maxAmount") != null, CrmContract::getAmount, bo.getParams().get("maxAmount"));
  132. // 签订日期范围查询
  133. if (bo.getSignDateBegin() != null && !bo.getSignDateBegin().isEmpty()) {
  134. lqw.ge(CrmContract::getSignDate, LocalDate.parse(bo.getSignDateBegin()));
  135. }
  136. if (bo.getSignDateEnd() != null && !bo.getSignDateEnd().isEmpty()) {
  137. lqw.le(CrmContract::getSignDate, LocalDate.parse(bo.getSignDateEnd()));
  138. }
  139. lqw.between(bo.getParams().get("beginTime") != null && bo.getParams().get("endTime") != null,
  140. CrmContract::getCreateTime, bo.getParams().get("beginTime"), bo.getParams().get("endTime"));
  141. lqw.orderByDesc(CrmContract::getCreateTime);
  142. return lqw;
  143. }
  144. @Override
  145. public TableDataInfo<CrmContractVo> queryPageList(CrmContractBo bo, PageQuery pageQuery) {
  146. LambdaQueryWrapper<CrmContract> lqw = buildQueryWrapper(bo);
  147. Page<CrmContractVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
  148. // 设置每个合同的审批权限
  149. try {
  150. Long currentUserId = LoginHelper.getUserId();
  151. for (CrmContractVo vo : result.getRecords()) {
  152. vo.setCanApprove(contractApprovalService.canUserApprove(vo.getId(), currentUserId));
  153. }
  154. } catch (Exception e) {
  155. log.warn("获取当前用户审批权限失败: {}", e.getMessage());
  156. }
  157. return TableDataInfo.build(result);
  158. }
  159. @Override
  160. public List<CrmContractVo> queryList(CrmContractBo bo) {
  161. LambdaQueryWrapper<CrmContract> lqw = buildQueryWrapper(bo);
  162. return baseMapper.selectVoList(lqw);
  163. }
  164. @Override
  165. public CrmContractVo queryById(Long id) {
  166. CrmContractVo vo = baseMapper.selectVoById(id);
  167. if (vo != null) {
  168. // 设置当前用户是否可以审批
  169. try {
  170. Long currentUserId = LoginHelper.getUserId();
  171. vo.setCanApprove(contractApprovalService.canUserApprove(id, currentUserId));
  172. } catch (Exception e) {
  173. log.warn("获取当前用户审批权限失败: {}", e.getMessage());
  174. vo.setCanApprove(false);
  175. }
  176. // 关联查询供方主体名称
  177. if (vo.getSupplierId() != null) {
  178. try {
  179. CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(vo.getSupplierId());
  180. if (supplierInfo != null) {
  181. vo.setSupplierName(supplierInfo.getSupplierName());
  182. }
  183. } catch (Exception e) {
  184. log.warn("获取供方主体信息失败: {}", e.getMessage());
  185. }
  186. }
  187. // 查询并设置产品明细列表
  188. try {
  189. List<CrmContractProductVo> products = contractProductService.queryListByContractId(id);
  190. if (vo.getProducts() == null || vo.getProducts().isEmpty()) {
  191. vo.setProducts(products);
  192. }
  193. if ("framework".equals(vo.getContractType()) && products != null && !products.isEmpty()) {
  194. List<CrmContractProductPriceTier> tierEntities = contractProductPriceTierMapper.selectList(
  195. Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
  196. .eq(CrmContractProductPriceTier::getContractId, id)
  197. .orderByAsc(CrmContractProductPriceTier::getContractProductId)
  198. .orderByAsc(CrmContractProductPriceTier::getSort));
  199. List<CrmContractProductPriceTierVo> allTiers = new ArrayList<>();
  200. for (CrmContractProductPriceTier en : tierEntities) {
  201. allTiers.add(convertTierEntityToVo(en));
  202. }
  203. Map<Long, List<CrmContractProductPriceTierVo>> tierMap = new HashMap<>();
  204. for (CrmContractProductPriceTierVo t : allTiers) {
  205. tierMap.computeIfAbsent(t.getContractProductId(), k -> new ArrayList<>()).add(t);
  206. }
  207. for (CrmContractProductVo p : products) {
  208. p.setPriceTiers(tierMap.getOrDefault(p.getId(), new ArrayList<>()));
  209. }
  210. }
  211. log.info("查询合同详情成功,合同ID: {}, 产品数量: {}", id, products != null ? products.size() : 0);
  212. } catch (Exception e) {
  213. log.error("查询合同产品明细失败,合同ID: {}", id, e);
  214. }
  215. }
  216. return vo;
  217. }
  218. @Override
  219. @Transactional(rollbackFor = Exception.class)
  220. public Boolean insertByBo(CrmContractBo bo) {
  221. validateContractTypeAndProductsPresence(bo);
  222. normalizeFrameworkContractData(bo);
  223. validateProductPricesByContractType(bo);
  224. validateSalePurchaseContractAmount(bo);
  225. CrmContract entity = MapstructUtils.convert(bo, CrmContract.class);
  226. // 查询供方信息(用于生成合同编号和合同名称)
  227. String supplierShortName = "山峡"; // 默认简称
  228. if (entity.getSupplierId() != null) {
  229. try {
  230. CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
  231. if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) {
  232. supplierShortName = supplierInfo.getShortName();
  233. }
  234. } catch (Exception e) {
  235. log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e);
  236. }
  237. }
  238. // 生成合同编号
  239. if (StringUtils.isBlank(entity.getContractNo())) {
  240. entity.setContractNo(generateContractNo(entity.getSupplierId(), entity.getSignDate()));
  241. }
  242. // 查询客户信息
  243. CrmCustomer customer = null;
  244. if (entity.getCustomerId() != null) {
  245. customer = customerMapper.selectById(entity.getCustomerId());
  246. if (customer != null) {
  247. entity.setCustomerName(customer.getCustomerName());
  248. }
  249. }
  250. String contractType = entity.getContractType();
  251. // 自动生成合同名称
  252. String contractNo = entity.getContractNo();
  253. String customerShortName = customer != null ? customer.getShortName() : "";
  254. entity.setContractName(generateContractName(contractNo, contractType, supplierShortName,
  255. customerShortName, entity.getContractName()));
  256. // 查询并设置联系人信息
  257. if (entity.getContactId() != null) {
  258. CrmContact contact = contactMapper.selectById(entity.getContactId());
  259. if (contact != null) {
  260. entity.setContactName(contact.getContactName());
  261. entity.setContactMobile(contact.getMobile());
  262. }
  263. }
  264. // 设置默认值
  265. if (entity.getStatus() == null) {
  266. entity.setStatus("draft");
  267. }
  268. // 草稿状态下,审批状态应该为空,只有提交审批后才设置为pending
  269. if ("draft".equals(entity.getStatus())) {
  270. entity.setApprovalStatus(null);
  271. }
  272. if (entity.getOwnerUserId() == null) {
  273. entity.setOwnerUserId(LoginHelper.getUserId());
  274. }
  275. // 设置负责人姓名
  276. if (entity.getOwnerUserId() != null) {
  277. try {
  278. String ownerUserName = userService.selectNicknameById(entity.getOwnerUserId());
  279. entity.setOwnerUserName(ownerUserName);
  280. } catch (Exception e) {
  281. log.warn("获取负责人姓名失败,用户ID: {}", entity.getOwnerUserId(), e);
  282. entity.setOwnerUserName(null);
  283. }
  284. } else {
  285. entity.setOwnerUserName(null);
  286. }
  287. if (entity.getReceivedAmount() == null) {
  288. entity.setReceivedAmount(BigDecimal.ZERO);
  289. }
  290. if (entity.getInvoiceAmount() == null) {
  291. entity.setInvoiceAmount(BigDecimal.ZERO);
  292. }
  293. // 计算未回款金额和未开票金额
  294. entity.setUnreceivedAmount(entity.getAmount().subtract(entity.getReceivedAmount()));
  295. entity.setUninvoiceAmount(entity.getAmount().subtract(entity.getInvoiceAmount()));
  296. // 保存合同
  297. resolveContractProductIds(bo.getProducts());
  298. boolean flag = baseMapper.insert(entity) > 0;
  299. if (flag) {
  300. bo.setId(entity.getId());
  301. // 保存产品明细
  302. if (bo.getProducts() != null && !bo.getProducts().isEmpty()) {
  303. int sort = 1;
  304. for (CrmContractProductBo product : bo.getProducts()) {
  305. product.setContractId(entity.getId());
  306. // 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突
  307. product.setId(null);
  308. // 如果没有设置排序号,则自动设置为循环索引
  309. if (product.getSort() == null) {
  310. product.setSort(sort);
  311. }
  312. sort++;
  313. contractProductService.insertByBo(product);
  314. if ("framework".equals(entity.getContractType())) {
  315. savePriceTiersForProduct(product.getId(), entity.getId(), product.getPriceTiers());
  316. }
  317. }
  318. }
  319. // 生成回款计划
  320. if (bo.getPaymentPlans() != null && !bo.getPaymentPlans().isEmpty()) {
  321. try {
  322. paymentPlanService.batchGenerate(entity.getId(), bo.getPaymentPlans());
  323. log.info("合同创建成功,已生成回款计划,合同ID: {}, 期数: {}", entity.getId(), bo.getPaymentPlans().size());
  324. } catch (Exception e) {
  325. log.error("生成回款计划失败,合同ID: {}", entity.getId(), e);
  326. // 不影响合同创建,只记录日志
  327. }
  328. }
  329. }
  330. return flag;
  331. }
  332. @Override
  333. @Transactional(rollbackFor = Exception.class)
  334. public Boolean updateByBo(CrmContractBo bo) {
  335. // 只有草稿状态的合同才能修改
  336. CrmContract existContract = baseMapper.selectById(bo.getId());
  337. if (existContract == null) {
  338. throw new ServiceException("合同不存在");
  339. }
  340. if (!"draft".equals(existContract.getStatus())) {
  341. throw new ServiceException("只有草稿状态的合同才能修改");
  342. }
  343. validateContractTypeAndProductsPresence(bo);
  344. normalizeFrameworkContractData(bo);
  345. validateProductPricesByContractType(bo);
  346. validateSalePurchaseContractAmount(bo);
  347. CrmContract entity = MapstructUtils.convert(bo, CrmContract.class);
  348. // 草稿状态下,审批状态应该为空
  349. if ("draft".equals(existContract.getStatus())) {
  350. entity.setApprovalStatus(null);
  351. }
  352. // 查询并设置客户名称(如果客户ID有变化)
  353. CrmCustomer customer = null;
  354. if (entity.getCustomerId() != null && !entity.getCustomerId().equals(existContract.getCustomerId())) {
  355. customer = customerMapper.selectById(entity.getCustomerId());
  356. if (customer != null) {
  357. entity.setCustomerName(customer.getCustomerName());
  358. }
  359. } else if (entity.getCustomerId() != null) {
  360. // 客户ID没变化,但可能需要用于生成合同名称
  361. customer = customerMapper.selectById(entity.getCustomerId());
  362. }
  363. // 查询并设置联系人信息(如果联系人ID有变化)
  364. if (entity.getContactId() != null && !entity.getContactId().equals(existContract.getContactId())) {
  365. CrmContact contact = contactMapper.selectById(entity.getContactId());
  366. if (contact != null) {
  367. entity.setContactName(contact.getContactName());
  368. entity.setContactMobile(contact.getMobile());
  369. }
  370. }
  371. // 检查签订日期是否变化,如果变化则重新生成合同编号和合同名称
  372. boolean signDateChanged = entity.getSignDate() != null
  373. && !entity.getSignDate().equals(existContract.getSignDate());
  374. boolean supplierIdChanged = entity.getSupplierId() != null
  375. && !entity.getSupplierId().equals(existContract.getSupplierId());
  376. if (signDateChanged || supplierIdChanged) {
  377. // 查询供方信息(用于生成合同编号和合同名称)
  378. String supplierShortName = "山峡"; // 默认简称
  379. if (entity.getSupplierId() != null) {
  380. try {
  381. CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(entity.getSupplierId());
  382. if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getShortName())) {
  383. supplierShortName = supplierInfo.getShortName();
  384. }
  385. } catch (Exception e) {
  386. log.warn("查询供方信息失败,使用默认简称,供方ID: {}", entity.getSupplierId(), e);
  387. }
  388. }
  389. // 重新生成合同编号
  390. entity.setContractNo(generateContractNo(entity.getSupplierId(), entity.getSignDate()));
  391. // 重新生成合同名称
  392. String contractNo = entity.getContractNo();
  393. String customerShortName = customer != null ? customer.getShortName() : "";
  394. // 判断用户是否修改了合同名称
  395. // 如果用户修改了合同名称,使用用户的新名称;否则使用旧名称并更新编号
  396. String userContractName = null;
  397. if (StringUtils.isNotBlank(entity.getContractName())
  398. && !entity.getContractName().equals(existContract.getContractName())) {
  399. // 用户修改了合同名称,使用用户的新名称(移除可能存在的旧编号)
  400. userContractName = entity.getContractName().replaceAll("([^)]+)$", "");
  401. } else {
  402. // 用户没有修改合同名称,使用旧名称(移除旧的合同编号)
  403. String oldContractName = existContract.getContractName();
  404. if (StringUtils.isNotBlank(oldContractName)) {
  405. userContractName = oldContractName.replaceAll("([^)]+)$", "");
  406. }
  407. }
  408. entity.setContractName(generateContractName(contractNo, entity.getContractType(), supplierShortName,
  409. customerShortName, userContractName));
  410. }
  411. // 重新计算未回款金额和未开票金额
  412. if (entity.getAmount() != null) {
  413. BigDecimal receivedAmount = existContract.getReceivedAmount() != null
  414. ? existContract.getReceivedAmount()
  415. : BigDecimal.ZERO;
  416. BigDecimal invoiceAmount = existContract.getInvoiceAmount() != null
  417. ? existContract.getInvoiceAmount()
  418. : BigDecimal.ZERO;
  419. entity.setUnreceivedAmount(entity.getAmount().subtract(receivedAmount));
  420. entity.setUninvoiceAmount(entity.getAmount().subtract(invoiceAmount));
  421. }
  422. boolean flag = baseMapper.updateById(entity) > 0;
  423. if (flag && bo.getProducts() != null) {
  424. resolveContractProductIds(bo.getProducts());
  425. contractProductPriceTierMapper.delete(Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
  426. .eq(CrmContractProductPriceTier::getContractId, bo.getId()));
  427. // 更新产品明细(简化处理:先删除再新增)
  428. contractProductService.deleteByIds(
  429. contractProductService.queryListByContractId(bo.getId())
  430. .stream().map(p -> p.getId()).toList());
  431. int sort = 1;
  432. for (CrmContractProductBo product : bo.getProducts()) {
  433. product.setContractId(bo.getId());
  434. // 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突
  435. product.setId(null);
  436. // 如果没有设置排序号,则自动设置为循环索引
  437. if (product.getSort() == null) {
  438. product.setSort(sort);
  439. }
  440. sort++;
  441. contractProductService.insertByBo(product);
  442. if ("framework".equals(bo.getContractType())) {
  443. savePriceTiersForProduct(product.getId(), bo.getId(), product.getPriceTiers());
  444. }
  445. }
  446. }
  447. return flag;
  448. }
  449. @Override
  450. @Transactional(rollbackFor = Exception.class)
  451. public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
  452. if (isValid) {
  453. // 校验:只能删除草稿状态的合同
  454. for (Long id : ids) {
  455. CrmContract contract = baseMapper.selectById(id);
  456. if (contract != null && !"draft".equals(contract.getStatus())) {
  457. throw new ServiceException("只能删除草稿状态的合同");
  458. }
  459. }
  460. }
  461. return baseMapper.deleteByIds(ids) > 0;
  462. }
  463. @Override
  464. @Transactional(rollbackFor = Exception.class)
  465. public Boolean submitApproval(Long id, List<Long> approverIds, String remark) {
  466. CrmContract contract = baseMapper.selectById(id);
  467. if (contract == null) {
  468. throw new ServiceException("合同不存在");
  469. }
  470. if (!"draft".equals(contract.getStatus())) {
  471. throw new ServiceException("只有草稿状态的合同才能提交审批");
  472. }
  473. validateFrameworkContractForSubmitApproval(id, contract);
  474. // 更新合同状态
  475. CrmContract update = new CrmContract();
  476. update.setId(id);
  477. update.setStatus("approving");
  478. update.setApprovalStatus("pending");
  479. baseMapper.updateById(update);
  480. // 创建审批记录
  481. if (approverIds != null && !approverIds.isEmpty()) {
  482. for (int i = 0; i < approverIds.size(); i++) {
  483. Long approverId = approverIds.get(i);
  484. CrmContractApproval approval = new CrmContractApproval();
  485. approval.setContractId(id);
  486. approval.setStep(i + 1);
  487. approval.setStepName("第" + (i + 1) + "步审批");
  488. approval.setApproverId(approverId);
  489. // TODO: 这里应该查询用户信息填充审批人姓名和部门
  490. // 由于跨模块调用复杂性,暂时在审批时填充
  491. // 或者可以通过远程API调用System模块的用户服务
  492. approval.setApproverName("审批人" + approverId); // 临时占位
  493. approval.setApproverDept("待审批时填充"); // 临时占位
  494. approval.setApprovalResult("pending");
  495. approval.setIsCurrent(i == 0 ? 1 : 0);
  496. approval.setSort(i + 1);
  497. approvalMapper.insert(approval);
  498. }
  499. }
  500. return true;
  501. }
  502. @Override
  503. @Transactional(rollbackFor = Exception.class)
  504. public int batchSubmitApproval(List<Long> ids, List<Long> approverIds, String remark) {
  505. int successCount = 0;
  506. for (Long id : ids) {
  507. try {
  508. submitApproval(id, approverIds, remark);
  509. successCount++;
  510. } catch (Exception e) {
  511. // 记录错误但继续处理其他合同
  512. log.error("合同ID {} 提交审批失败: {}", id, e.getMessage());
  513. }
  514. }
  515. return successCount;
  516. }
  517. @Override
  518. public Boolean saveTemplate(Long id, String templateContent, String settlementClauseDrafts) {
  519. // 检查合同是否存在
  520. CrmContract entity = baseMapper.selectById(id);
  521. if (entity == null) {
  522. throw new ServiceException("合同不存在");
  523. }
  524. String finalSettlementClauseDrafts = settlementClauseDrafts;
  525. if (StringUtils.isBlank(finalSettlementClauseDrafts)) {
  526. String method = StringUtils.isNotBlank(entity.getSettlementMethod()) ? entity.getSettlementMethod() : "arrive_payment";
  527. String defaultClause = "advance_payment".equals(method)
  528. ? "款到发货。需方付款后安排发货,需方收到货物后,供方开具13%增值税专用发票。"
  529. : "货到付款,需方收到供方货物后,供方开具13%增值税专用发票,无异议后,以现汇方式月结。";
  530. JSONObject drafts = new JSONObject();
  531. drafts.set(method, defaultClause);
  532. finalSettlementClauseDrafts = drafts.toString();
  533. }
  534. log.info("========== 开始保存合同模板内容 ==========");
  535. log.info("contractId={}", id);
  536. log.info("contentLength={}", templateContent != null ? templateContent.length() : 0);
  537. log.info("content前200字符={}", templateContent != null && templateContent.length() > 200
  538. ? templateContent.substring(0, 200)
  539. : templateContent);
  540. log.info("包含HTML标签={}", templateContent != null && templateContent.contains("<"));
  541. log.info("第五条草稿长度={}", finalSettlementClauseDrafts != null ? finalSettlementClauseDrafts.length() : 0);
  542. // 使用 LambdaUpdateWrapper 只更新模板相关字段,避免乐观锁问题
  543. int result = baseMapper.update(null,
  544. Wrappers.<CrmContract>lambdaUpdate()
  545. .eq(CrmContract::getId, id)
  546. .set(CrmContract::getTemplateContent, templateContent)
  547. .set(CrmContract::getSettlementClauseDrafts, finalSettlementClauseDrafts));
  548. log.info("保存合同模板内容完成: contractId={}, success={}, affectedRows={}",
  549. id, result > 0, result);
  550. // 验证保存结果
  551. CrmContract savedEntity = baseMapper.selectById(id);
  552. if (savedEntity != null && savedEntity.getTemplateContent() != null) {
  553. log.info("验证保存结果: 数据库中的内容长度={}", savedEntity.getTemplateContent().length());
  554. log.info("验证保存结果: 数据库中的内容前200字符={}",
  555. savedEntity.getTemplateContent().length() > 200
  556. ? savedEntity.getTemplateContent().substring(0, 200)
  557. : savedEntity.getTemplateContent());
  558. } else {
  559. log.warn("验证保存结果: 数据库中未找到模板内容!");
  560. }
  561. return result > 0;
  562. }
  563. @Override
  564. public String getTemplate(Long id) {
  565. log.info("========== 开始获取合同模板内容 ==========");
  566. log.info("contractId={}", id);
  567. // 直接查询,确保获取最新数据
  568. CrmContract entity = baseMapper.selectById(id);
  569. if (entity == null) {
  570. throw new ServiceException("合同不存在");
  571. }
  572. String templateContent = entity.getTemplateContent();
  573. log.info("获取合同模板内容完成:");
  574. log.info("- hasTemplate={}", templateContent != null);
  575. log.info("- contentLength={}", templateContent != null ? templateContent.length() : 0);
  576. log.info("- content前200字符={}", templateContent != null && templateContent.length() > 200
  577. ? templateContent.substring(0, 200)
  578. : templateContent);
  579. log.info("- 包含HTML标签={}", templateContent != null && templateContent.contains("<"));
  580. return templateContent;
  581. }
  582. @Override
  583. @Transactional(rollbackFor = Exception.class)
  584. public Boolean withdrawApproval(Long id, String remark) {
  585. CrmContract contract = baseMapper.selectById(id);
  586. if (contract == null) {
  587. throw new ServiceException("合同不存在");
  588. }
  589. if (!"approving".equals(contract.getStatus())) {
  590. throw new ServiceException("只有审批中的合同才能撤回");
  591. }
  592. // 使用 UpdateWrapper 来明确更新 null 值
  593. com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<CrmContract> updateWrapper = Wrappers
  594. .lambdaUpdate();
  595. updateWrapper.eq(CrmContract::getId, id)
  596. .set(CrmContract::getStatus, "draft")
  597. .set(CrmContract::getApprovalStatus, null); // 明确设置为 null
  598. baseMapper.update(null, updateWrapper);
  599. // 删除待审批的审批记录
  600. LambdaQueryWrapper<CrmContractApproval> wrapper = Wrappers.lambdaQuery();
  601. wrapper.eq(CrmContractApproval::getContractId, id);
  602. wrapper.eq(CrmContractApproval::getApprovalResult, "pending");
  603. approvalMapper.delete(wrapper);
  604. log.info("撤回审批成功,合同ID: {}, 撤回原因: {}", id, remark);
  605. return true;
  606. }
  607. @Override
  608. @Transactional(rollbackFor = Exception.class)
  609. public Boolean startExecute(Long id, String remark) {
  610. CrmContract contract = baseMapper.selectById(id);
  611. if (contract == null) {
  612. throw new ServiceException("合同不存在");
  613. }
  614. if (!"approved".equals(contract.getStatus())) {
  615. throw new ServiceException("只有已审批的合同才能开始执行");
  616. }
  617. CrmContract update = new CrmContract();
  618. update.setId(id);
  619. update.setStatus("executing");
  620. return baseMapper.updateById(update) > 0;
  621. }
  622. @Override
  623. @Transactional(rollbackFor = Exception.class)
  624. public Boolean finishContract(Long id, String remark) {
  625. CrmContract contract = baseMapper.selectById(id);
  626. if (contract == null) {
  627. throw new ServiceException("合同不存在");
  628. }
  629. if (!"executing".equals(contract.getStatus())) {
  630. throw new ServiceException("只有执行中的合同才能完成");
  631. }
  632. CrmContract update = new CrmContract();
  633. update.setId(id);
  634. update.setStatus("finished");
  635. if (StringUtils.isNotBlank(remark)) {
  636. update.setRemark(remark);
  637. }
  638. return baseMapper.updateById(update) > 0;
  639. }
  640. @Override
  641. @Transactional(rollbackFor = Exception.class)
  642. public Boolean invalidContract(Long id, String invalidReason, String remark) {
  643. if (StringUtils.isBlank(invalidReason)) {
  644. throw new ServiceException("作废原因不能为空");
  645. }
  646. CrmContract contract = baseMapper.selectById(id);
  647. if (contract == null) {
  648. throw new ServiceException("合同不存在");
  649. }
  650. if ("finished".equals(contract.getStatus())) {
  651. throw new ServiceException("已完成的合同不能作废");
  652. }
  653. CrmContract update = new CrmContract();
  654. update.setId(id);
  655. update.setStatus("invalid");
  656. update.setInvalidReason(invalidReason);
  657. update.setInvalidTime(LocalDateTime.now());
  658. update.setRemark(remark);
  659. return baseMapper.updateById(update) > 0;
  660. }
  661. @Override
  662. public Boolean uploadFile(Long id, String fileUrl) {
  663. CrmContract update = new CrmContract();
  664. update.setId(id);
  665. update.setContractFile(fileUrl);
  666. return baseMapper.updateById(update) > 0;
  667. }
  668. @Override
  669. @Transactional(rollbackFor = Exception.class)
  670. public Boolean uploadAttachments(Long id, String ossIds) {
  671. if (StringUtils.isBlank(ossIds)) {
  672. throw new ServiceException("附件ossId不能为空");
  673. }
  674. CrmContract contract = baseMapper.selectById(id);
  675. if (contract == null) {
  676. throw new ServiceException("合同不存在");
  677. }
  678. // 追加附件ossId
  679. String existingFiles = contract.getAttachmentFiles();
  680. String newFiles;
  681. if (StringUtils.isBlank(existingFiles)) {
  682. newFiles = ossIds;
  683. } else {
  684. // 将现有附件和新附件合并(去重)
  685. String[] existingArr = existingFiles.split(",");
  686. String[] newArr = ossIds.split(",");
  687. java.util.Set<String> fileSet = new java.util.HashSet<>(java.util.Arrays.asList(existingArr));
  688. fileSet.addAll(java.util.Arrays.asList(newArr));
  689. newFiles = String.join(",", fileSet);
  690. }
  691. CrmContract update = new CrmContract();
  692. update.setId(id);
  693. update.setAttachmentFiles(newFiles);
  694. return baseMapper.updateById(update) > 0;
  695. }
  696. @Override
  697. @Transactional(rollbackFor = Exception.class)
  698. public Boolean uploadAttachmentsWithCategory(Long id, java.util.List<java.util.Map<String, Object>> attachments) {
  699. if (attachments == null || attachments.isEmpty()) {
  700. throw new ServiceException("附件列表不能为空");
  701. }
  702. CrmContract contract = baseMapper.selectById(id);
  703. if (contract == null) {
  704. throw new ServiceException("合同不存在");
  705. }
  706. // 解析现有附件(支持JSON格式和逗号分隔格式)
  707. JSONArray existingAttachments = new JSONArray();
  708. String existingFiles = contract.getAttachmentFiles();
  709. if (StringUtils.isNotBlank(existingFiles)) {
  710. // 尝试解析为JSON格式
  711. if (existingFiles.trim().startsWith("[")) {
  712. existingAttachments = JSONUtil.parseArray(existingFiles);
  713. } else {
  714. // 兼容旧格式:逗号分隔的ossId,转换为JSON格式
  715. String[] ossIds = existingFiles.split(",");
  716. for (String ossId : ossIds) {
  717. if (StringUtils.isNotBlank(ossId)) {
  718. JSONObject item = new JSONObject();
  719. item.set("ossId", ossId.trim());
  720. item.set("category", "other"); // 旧数据默认分类为"其他附件"
  721. existingAttachments.add(item);
  722. }
  723. }
  724. }
  725. }
  726. // 添加新附件(去重:基于ossId)
  727. java.util.Set<String> existingOssIds = new java.util.HashSet<>();
  728. for (int i = 0; i < existingAttachments.size(); i++) {
  729. JSONObject item = existingAttachments.getJSONObject(i);
  730. String ossId = item.getStr("ossId");
  731. if (StringUtils.isNotBlank(ossId)) {
  732. existingOssIds.add(ossId);
  733. }
  734. }
  735. for (java.util.Map<String, Object> attachment : attachments) {
  736. String ossId = attachment.get("ossId") != null ? attachment.get("ossId").toString() : null;
  737. String category = attachment.get("category") != null ? attachment.get("category").toString() : "other";
  738. if (StringUtils.isBlank(ossId)) {
  739. continue;
  740. }
  741. // 如果已存在,更新分类;否则添加新附件
  742. if (existingOssIds.contains(ossId)) {
  743. // 更新现有附件的分类
  744. for (int i = 0; i < existingAttachments.size(); i++) {
  745. JSONObject item = existingAttachments.getJSONObject(i);
  746. if (ossId.equals(item.getStr("ossId"))) {
  747. item.set("category", category);
  748. break;
  749. }
  750. }
  751. } else {
  752. // 添加新附件
  753. JSONObject newItem = new JSONObject();
  754. newItem.set("ossId", ossId);
  755. newItem.set("category", category);
  756. existingAttachments.add(newItem);
  757. existingOssIds.add(ossId);
  758. }
  759. }
  760. // 保存为JSON格式
  761. String newFiles = JSONUtil.toJsonStr(existingAttachments);
  762. CrmContract update = new CrmContract();
  763. update.setId(id);
  764. update.setAttachmentFiles(newFiles);
  765. return baseMapper.updateById(update) > 0;
  766. }
  767. @Override
  768. public Map<String, Object> statisticsOverview(Long ownerUserId, String beginTime, String endTime) {
  769. Map<String, Object> result = new HashMap<>();
  770. // 构建查询条件
  771. LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
  772. // 负责人筛选
  773. if (ownerUserId != null) {
  774. lqw.eq(CrmContract::getOwnerUserId, ownerUserId);
  775. }
  776. // 时间范围筛选
  777. if (StringUtils.isNotBlank(beginTime) && StringUtils.isNotBlank(endTime)) {
  778. lqw.between(CrmContract::getCreateTime, beginTime, endTime);
  779. }
  780. // 查询所有合同
  781. List<CrmContract> contracts = baseMapper.selectList(lqw);
  782. // 统计各状态数量
  783. long totalCount = contracts.size();
  784. long draftCount = contracts.stream().filter(c -> "draft".equals(c.getStatus())).count();
  785. long approvingCount = contracts.stream().filter(c -> "approving".equals(c.getStatus())).count();
  786. long executingCount = contracts.stream().filter(c -> "executing".equals(c.getStatus())).count();
  787. long finishedCount = contracts.stream().filter(c -> "finished".equals(c.getStatus())).count();
  788. long invalidCount = contracts.stream().filter(c -> "invalid".equals(c.getStatus())).count();
  789. // 统计金额
  790. BigDecimal totalAmount = contracts.stream()
  791. .map(CrmContract::getAmount)
  792. .filter(Objects::nonNull)
  793. .reduce(BigDecimal.ZERO, BigDecimal::add);
  794. BigDecimal receivedAmount = contracts.stream()
  795. .map(CrmContract::getReceivedAmount)
  796. .filter(Objects::nonNull)
  797. .reduce(BigDecimal.ZERO, BigDecimal::add);
  798. BigDecimal unreceivedAmount = contracts.stream()
  799. .map(CrmContract::getUnreceivedAmount)
  800. .filter(Objects::nonNull)
  801. .reduce(BigDecimal.ZERO, BigDecimal::add);
  802. BigDecimal invoiceAmount = contracts.stream()
  803. .map(CrmContract::getInvoiceAmount)
  804. .filter(Objects::nonNull)
  805. .reduce(BigDecimal.ZERO, BigDecimal::add);
  806. BigDecimal uninvoiceAmount = contracts.stream()
  807. .map(CrmContract::getUninvoiceAmount)
  808. .filter(Objects::nonNull)
  809. .reduce(BigDecimal.ZERO, BigDecimal::add);
  810. // 计算回款率和开票率
  811. BigDecimal receivedRate = totalAmount.compareTo(BigDecimal.ZERO) > 0
  812. ? receivedAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"))
  813. : BigDecimal.ZERO;
  814. BigDecimal invoiceRate = totalAmount.compareTo(BigDecimal.ZERO) > 0
  815. ? invoiceAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"))
  816. : BigDecimal.ZERO;
  817. // 组装结果
  818. result.put("totalCount", totalCount);
  819. result.put("draftCount", draftCount);
  820. result.put("approvingCount", approvingCount);
  821. result.put("executingCount", executingCount);
  822. result.put("finishedCount", finishedCount);
  823. result.put("invalidCount", invalidCount);
  824. result.put("totalAmount", totalAmount);
  825. result.put("receivedAmount", receivedAmount);
  826. result.put("unreceivedAmount", unreceivedAmount);
  827. result.put("receivedRate", receivedRate.setScale(2, RoundingMode.HALF_UP));
  828. result.put("invoiceAmount", invoiceAmount);
  829. result.put("uninvoiceAmount", uninvoiceAmount);
  830. result.put("invoiceRate", invoiceRate.setScale(2, RoundingMode.HALF_UP));
  831. return result;
  832. }
  833. @Override
  834. public List<Map<String, Object>> statisticsByStatus() {
  835. List<Map<String, Object>> result = new ArrayList<>();
  836. // 查询所有合同
  837. List<CrmContract> contracts = baseMapper.selectList(Wrappers.lambdaQuery());
  838. // 计算总数和总金额
  839. long totalCount = contracts.size();
  840. BigDecimal totalAmount = contracts.stream()
  841. .map(CrmContract::getAmount)
  842. .filter(Objects::nonNull)
  843. .reduce(BigDecimal.ZERO, BigDecimal::add);
  844. // 定义状态映射
  845. Map<String, String> statusMap = new LinkedHashMap<>();
  846. statusMap.put("draft", "草稿");
  847. statusMap.put("approving", "审批中");
  848. statusMap.put("approved", "已审批");
  849. statusMap.put("executing", "执行中");
  850. statusMap.put("finished", "已完成");
  851. statusMap.put("invalid", "已作废");
  852. // 按状态统计
  853. for (Map.Entry<String, String> entry : statusMap.entrySet()) {
  854. String status = entry.getKey();
  855. String statusName = entry.getValue();
  856. List<CrmContract> statusContracts = contracts.stream()
  857. .filter(c -> status.equals(c.getStatus()))
  858. .toList();
  859. long count = statusContracts.size();
  860. BigDecimal amount = statusContracts.stream()
  861. .map(CrmContract::getAmount)
  862. .filter(Objects::nonNull)
  863. .reduce(BigDecimal.ZERO, BigDecimal::add);
  864. // 计算百分比
  865. BigDecimal percent = totalCount > 0
  866. ? new BigDecimal(count).divide(new BigDecimal(totalCount), 4, RoundingMode.HALF_UP)
  867. .multiply(new BigDecimal("100"))
  868. : BigDecimal.ZERO;
  869. Map<String, Object> item = new HashMap<>();
  870. item.put("status", status);
  871. item.put("statusName", statusName);
  872. item.put("count", count);
  873. item.put("amount", amount);
  874. item.put("percent", percent.setScale(2, RoundingMode.HALF_UP));
  875. result.add(item);
  876. }
  877. return result;
  878. }
  879. @Override
  880. public List<Map<String, Object>> statisticsByType() {
  881. List<Map<String, Object>> result = new ArrayList<>();
  882. // 查询所有合同
  883. List<CrmContract> contracts = baseMapper.selectList(Wrappers.lambdaQuery());
  884. // 计算总数和总金额
  885. long totalCount = contracts.size();
  886. BigDecimal totalAmount = contracts.stream()
  887. .map(CrmContract::getAmount)
  888. .filter(Objects::nonNull)
  889. .reduce(BigDecimal.ZERO, BigDecimal::add);
  890. // 定义类型映射
  891. Map<String, String> typeMap = new LinkedHashMap<>();
  892. typeMap.put("sale", "销售合同");
  893. typeMap.put("service", "服务合同");
  894. typeMap.put("purchase", "采购合同");
  895. typeMap.put("framework", "框架合同");
  896. typeMap.put("supplement", "补充协议");
  897. typeMap.put("lease", "租赁合同");
  898. typeMap.put("other", "其他");
  899. // 按类型统计
  900. for (Map.Entry<String, String> entry : typeMap.entrySet()) {
  901. String type = entry.getKey();
  902. String typeName = entry.getValue();
  903. List<CrmContract> typeContracts = contracts.stream()
  904. .filter(c -> type.equals(c.getContractType()))
  905. .toList();
  906. long count = typeContracts.size();
  907. // 如果该类型没有合同,跳过
  908. if (count == 0) {
  909. continue;
  910. }
  911. BigDecimal amount = typeContracts.stream()
  912. .map(CrmContract::getAmount)
  913. .filter(Objects::nonNull)
  914. .reduce(BigDecimal.ZERO, BigDecimal::add);
  915. // 计算百分比
  916. BigDecimal percent = totalCount > 0
  917. ? new BigDecimal(count).divide(new BigDecimal(totalCount), 4, RoundingMode.HALF_UP)
  918. .multiply(new BigDecimal("100"))
  919. : BigDecimal.ZERO;
  920. Map<String, Object> item = new HashMap<>();
  921. item.put("contractType", type);
  922. item.put("typeName", typeName);
  923. item.put("count", count);
  924. item.put("amount", amount);
  925. item.put("percent", percent.setScale(2, RoundingMode.HALF_UP));
  926. result.add(item);
  927. }
  928. return result;
  929. }
  930. @Override
  931. public List<Map<String, Object>> statisticsTrend(String type, String beginTime, String endTime) {
  932. List<Map<String, Object>> result = new ArrayList<>();
  933. // 解析开始和结束时间
  934. LocalDateTime startDateTime;
  935. LocalDateTime endDateTime;
  936. DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  937. try {
  938. startDateTime = LocalDateTime.parse(beginTime, formatter);
  939. endDateTime = LocalDateTime.parse(endTime, formatter);
  940. } catch (Exception e) {
  941. log.error("日期格式错误", e);
  942. return result;
  943. }
  944. // 查询时间范围内的合同
  945. LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
  946. lqw.between(CrmContract::getCreateTime, startDateTime, endDateTime);
  947. List<CrmContract> contracts = baseMapper.selectList(lqw);
  948. // 根据type确定统计粒度
  949. String period;
  950. if ("month".equals(type)) {
  951. period = "month";
  952. } else if ("quarter".equals(type)) {
  953. period = "quarter";
  954. } else if ("year".equals(type)) {
  955. period = "year";
  956. } else {
  957. period = "month"; // 默认按月统计
  958. }
  959. // 按时间分组统计
  960. Map<String, List<CrmContract>> groupedContracts = new LinkedHashMap<>();
  961. for (CrmContract contract : contracts) {
  962. if (contract.getCreateTime() == null) {
  963. continue;
  964. }
  965. // 将Date转换为LocalDateTime
  966. LocalDateTime createTime = DateUtil.toLocalDateTime(contract.getCreateTime());
  967. String periodKey;
  968. if ("month".equals(period)) {
  969. periodKey = createTime.format(DateTimeFormatter.ofPattern("yyyy-MM"));
  970. } else if ("quarter".equals(period)) {
  971. int quarter = (createTime.getMonthValue() - 1) / 3 + 1;
  972. periodKey = createTime.getYear() + "-Q" + quarter;
  973. } else { // year
  974. periodKey = String.valueOf(createTime.getYear());
  975. }
  976. groupedContracts.computeIfAbsent(periodKey, k -> new ArrayList<>()).add(contract);
  977. }
  978. // 生成统计结果
  979. for (Map.Entry<String, List<CrmContract>> entry : groupedContracts.entrySet()) {
  980. String periodKey = entry.getKey();
  981. List<CrmContract> periodContracts = entry.getValue();
  982. long totalCount = periodContracts.size();
  983. BigDecimal totalAmount = periodContracts.stream()
  984. .map(CrmContract::getAmount)
  985. .filter(Objects::nonNull)
  986. .reduce(BigDecimal.ZERO, BigDecimal::add);
  987. BigDecimal receivedAmount = periodContracts.stream()
  988. .map(CrmContract::getReceivedAmount)
  989. .filter(Objects::nonNull)
  990. .reduce(BigDecimal.ZERO, BigDecimal::add);
  991. // 计算回款率
  992. BigDecimal receivedRate = totalAmount.compareTo(BigDecimal.ZERO) > 0
  993. ? receivedAmount.divide(totalAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"))
  994. : BigDecimal.ZERO;
  995. Map<String, Object> item = new HashMap<>();
  996. item.put("period", periodKey);
  997. item.put("totalCount", totalCount);
  998. item.put("totalAmount", totalAmount);
  999. item.put("receivedAmount", receivedAmount);
  1000. item.put("receivedRate", receivedRate.setScale(2, RoundingMode.HALF_UP));
  1001. result.add(item);
  1002. }
  1003. return result;
  1004. }
  1005. @Override
  1006. public List<CrmContractVo> queryExpiring(Integer days) {
  1007. if (days == null || days <= 0) {
  1008. days = 30;
  1009. }
  1010. LocalDate endDate = LocalDate.now().plusDays(days);
  1011. LambdaQueryWrapper<CrmContract> lqw = Wrappers.lambdaQuery();
  1012. lqw.eq(CrmContract::getStatus, "executing");
  1013. lqw.le(CrmContract::getEndDate, endDate);
  1014. lqw.ge(CrmContract::getEndDate, LocalDate.now());
  1015. lqw.orderByAsc(CrmContract::getEndDate);
  1016. return baseMapper.selectVoList(lqw);
  1017. }
  1018. /**
  1019. * 生成合同名称
  1020. * <p>销售合同、框架合同:供方简称-客户简称(合同编号);采购合同:客户简称-供方简称(合同编号);其余类型未填名称时为「合同(编号)」。</p>
  1021. *
  1022. * @param contractNo 合同编号
  1023. * @param contractType 合同类型
  1024. * @param supplierShortName 供方简称
  1025. * @param customerShortName 客户简称
  1026. * @param userContractName 用户填写的合同名称(如果为空则自动生成)
  1027. * @return 合同名称
  1028. */
  1029. private String generateContractName(String contractNo, String contractType,
  1030. String supplierShortName, String customerShortName,
  1031. String userContractName) {
  1032. if (StringUtils.isBlank(userContractName)) {
  1033. // 用户未填写合同名称,根据类型自动生成
  1034. if ("sale".equals(contractType) || "framework".equals(contractType)) {
  1035. // 销售合同、框架合同:供方简称-客户简称(合同编号)
  1036. return supplierShortName + "-" + customerShortName + "(" + contractNo + ")";
  1037. } else if ("purchase".equals(contractType)) {
  1038. // 采购合同:客户简称-供方简称(合同编号)
  1039. return customerShortName + "-" + supplierShortName + "(" + contractNo + ")";
  1040. } else {
  1041. // 其他类型:使用合同编号作为名称
  1042. return "合同(" + contractNo + ")";
  1043. }
  1044. } else {
  1045. // 用户手工填写了合同名称,自动补充合同编号
  1046. // 先移除旧的合同编号(如果存在)
  1047. String cleanedName = userContractName.replaceAll("([^)]+)$", "");
  1048. // 检查是否已经包含新的合同编号,避免重复添加
  1049. if (!cleanedName.contains("(" + contractNo + ")")) {
  1050. return cleanedName + "(" + contractNo + ")";
  1051. }
  1052. return cleanedName;
  1053. }
  1054. }
  1055. /**
  1056. * 生成合同编号
  1057. * 格式:{供方编码}/X-JT + yyyyMMdd + 3位流水号
  1058. * 示例:SX/X-JT20250926002
  1059. *
  1060. * @param supplierId 供方主体ID,如果为null则使用默认编码"SX"
  1061. * @param signDate 签订日期,如果为null则使用当前日期
  1062. */
  1063. @Override
  1064. public String generateContractNo(Long supplierId, LocalDate signDate) {
  1065. // 获取供方编码,如果没有则使用默认值"SX"
  1066. String supplierCode = "SX";
  1067. if (supplierId != null) {
  1068. try {
  1069. CrmSupplierInfoVo supplierInfo = supplierInfoService.queryById(supplierId);
  1070. if (supplierInfo != null && StringUtils.isNotBlank(supplierInfo.getSupplierCode())) {
  1071. supplierCode = supplierInfo.getSupplierCode();
  1072. }
  1073. } catch (Exception e) {
  1074. log.warn("查询供方信息失败,使用默认编码,供方ID: {}", supplierId, e);
  1075. }
  1076. }
  1077. String prefix = supplierCode + "/X-JT";
  1078. // 使用签订日期,如果为null则使用当前日期
  1079. LocalDate dateToUse = signDate != null ? signDate : LocalDate.now();
  1080. String date = dateToUse.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
  1081. // 查询该签订日期最大流水号(使用动态前缀)
  1082. LambdaQueryWrapper<CrmContract> wrapper = Wrappers.lambdaQuery();
  1083. wrapper.likeRight(CrmContract::getContractNo, prefix + date);
  1084. wrapper.orderByDesc(CrmContract::getContractNo);
  1085. wrapper.last("LIMIT 1");
  1086. CrmContract lastContract = baseMapper.selectOne(wrapper);
  1087. int serialNum = 1;
  1088. if (lastContract != null && lastContract.getContractNo() != null) {
  1089. String lastNo = lastContract.getContractNo();
  1090. // 完整编号格式:{供方编码}/X-JT20250926002
  1091. // 前缀长度可能不同,需要动态计算
  1092. int prefixLength = prefix.length();
  1093. int dateLength = 8; // yyyyMMdd
  1094. int totalPrefixLength = prefixLength + dateLength;
  1095. if (lastNo.length() >= totalPrefixLength + 3) {
  1096. try {
  1097. String serialStr = lastNo.substring(totalPrefixLength);
  1098. serialNum = Integer.parseInt(serialStr) + 1;
  1099. } catch (NumberFormatException e) {
  1100. log.warn("解析合同编号流水号失败,使用默认值1,合同编号: {}", lastNo);
  1101. serialNum = 1;
  1102. }
  1103. }
  1104. }
  1105. return String.format("%s%s%03d", prefix, date, serialNum);
  1106. }
  1107. @Override
  1108. @Transactional(rollbackFor = Exception.class)
  1109. public Boolean updateReceivedAmount(Long contractId) {
  1110. // 查询该合同的所有回款记录
  1111. LambdaQueryWrapper<CrmPayment> lqw = Wrappers.lambdaQuery();
  1112. lqw.eq(CrmPayment::getContractId, contractId);
  1113. lqw.eq(CrmPayment::getDelFlag, 0);
  1114. List<CrmPayment> payments = paymentMapper.selectList(lqw);
  1115. // 计算已回款金额
  1116. BigDecimal receivedAmount = payments.stream()
  1117. .map(CrmPayment::getAmount)
  1118. .filter(Objects::nonNull)
  1119. .reduce(BigDecimal.ZERO, BigDecimal::add);
  1120. // 查询合同
  1121. CrmContract contract = baseMapper.selectById(contractId);
  1122. if (contract == null) {
  1123. log.error("合同不存在,无法更新回款金额,合同ID: {}", contractId);
  1124. return false;
  1125. }
  1126. // 更新合同回款金额
  1127. CrmContract update = new CrmContract();
  1128. update.setId(contractId);
  1129. update.setReceivedAmount(receivedAmount);
  1130. update.setUnreceivedAmount(contract.getAmount().subtract(receivedAmount));
  1131. boolean flag = baseMapper.updateById(update) > 0;
  1132. if (flag) {
  1133. log.info("更新合同回款金额成功,合同ID: {}, 已回款: {}", contractId, receivedAmount);
  1134. }
  1135. return flag;
  1136. }
  1137. @Override
  1138. @Transactional(rollbackFor = Exception.class)
  1139. public Long renewContract(Long parentContractId, CrmContractBo bo) {
  1140. // 1. 查询原合同
  1141. CrmContract parentContract = baseMapper.selectById(parentContractId);
  1142. if (parentContract == null) {
  1143. throw new ServiceException("原合同不存在");
  1144. }
  1145. // 2. 创建续签合同
  1146. CrmContract newContract = new CrmContract();
  1147. // 从原合同复制基础信息
  1148. newContract.setCustomerId(parentContract.getCustomerId());
  1149. newContract.setCustomerName(parentContract.getCustomerName());
  1150. newContract.setContactId(parentContract.getContactId());
  1151. newContract.setContactName(parentContract.getContactName());
  1152. newContract.setContactMobile(parentContract.getContactMobile());
  1153. newContract.setContractType(parentContract.getContractType());
  1154. newContract.setOwnerUserId(parentContract.getOwnerUserId());
  1155. newContract.setOwnerUserName(parentContract.getOwnerUserName());
  1156. // 复制供方主体ID
  1157. newContract.setSupplierId(bo.getSupplierId() != null ? bo.getSupplierId() : parentContract.getSupplierId());
  1158. // 设置续签相关字段
  1159. newContract.setContractNature("renewal");
  1160. newContract.setParentContractId(parentContractId);
  1161. newContract
  1162. .setRenewalCount((parentContract.getRenewalCount() == null ? 0 : parentContract.getRenewalCount()) + 1);
  1163. // 使用传入的新信息覆盖
  1164. if (StringUtils.isNotBlank(bo.getContractName())) {
  1165. newContract.setContractName(bo.getContractName());
  1166. } else {
  1167. newContract.setContractName(parentContract.getContractName() + "-续签" + newContract.getRenewalCount());
  1168. }
  1169. newContract.setAmount(bo.getAmount() != null ? bo.getAmount() : parentContract.getAmount());
  1170. newContract.setCurrency(bo.getCurrency() != null ? bo.getCurrency() : parentContract.getCurrency());
  1171. newContract.setTaxRate(bo.getTaxRate() != null ? bo.getTaxRate() : parentContract.getTaxRate());
  1172. // 计算含税金额
  1173. if (newContract.getTaxRate() != null && newContract.getAmount() != null) {
  1174. newContract.setAmountWithTax(
  1175. newContract.getAmount().multiply(
  1176. BigDecimal.ONE.add(
  1177. newContract.getTaxRate().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP))));
  1178. }
  1179. newContract.setSignDate(bo.getSignDate());
  1180. newContract.setStartDate(bo.getStartDate());
  1181. newContract.setEndDate(bo.getEndDate());
  1182. newContract.setContractPeriod(bo.getContractPeriod());
  1183. // 复制甲乙方信息
  1184. newContract.setPartyA(parentContract.getPartyA());
  1185. newContract.setPartyAAddress(parentContract.getPartyAAddress());
  1186. newContract.setPartyAContact(parentContract.getPartyAContact());
  1187. newContract.setPartyAMobile(parentContract.getPartyAMobile());
  1188. newContract.setPartyB(parentContract.getPartyB());
  1189. newContract.setPartyBAddress(parentContract.getPartyBAddress());
  1190. newContract.setPartyBContact(parentContract.getPartyBContact());
  1191. newContract.setPartyBMobile(parentContract.getPartyBMobile());
  1192. // 复制合同条款
  1193. newContract.setPaymentTerms(
  1194. bo.getPaymentTerms() != null ? bo.getPaymentTerms() : parentContract.getPaymentTerms());
  1195. newContract.setDeliveryTerms(
  1196. bo.getDeliveryTerms() != null ? bo.getDeliveryTerms() : parentContract.getDeliveryTerms());
  1197. newContract.setContractTerms(
  1198. bo.getContractTerms() != null ? bo.getContractTerms() : parentContract.getContractTerms());
  1199. // 生成合同编号(续签合同使用原合同的供方主体)
  1200. newContract.setContractNo(generateContractNo(newContract.getSupplierId(), newContract.getSignDate()));
  1201. // 设置默认状态
  1202. newContract.setStatus("draft");
  1203. newContract.setApprovalStatus(null); // 草稿状态下,审批状态应该为空
  1204. newContract.setPerformanceStatus("normal");
  1205. newContract.setPerformanceRate(BigDecimal.ZERO);
  1206. newContract.setReceivedAmount(BigDecimal.ZERO);
  1207. newContract.setUnreceivedAmount(newContract.getAmount());
  1208. newContract.setInvoiceAmount(BigDecimal.ZERO);
  1209. newContract.setUninvoiceAmount(newContract.getAmount());
  1210. // 保存续签合同
  1211. boolean flag = baseMapper.insert(newContract) > 0;
  1212. if (!flag) {
  1213. throw new ServiceException("创建续签合同失败");
  1214. }
  1215. // 复制产品明细
  1216. if (bo.getProducts() != null && !bo.getProducts().isEmpty()) {
  1217. resolveContractProductIds(bo.getProducts());
  1218. // 使用传入的产品
  1219. for (CrmContractProductBo product : bo.getProducts()) {
  1220. product.setContractId(newContract.getId());
  1221. // 清空ID,让MyBatis-Plus自动生成新ID,避免主键冲突
  1222. product.setId(null);
  1223. contractProductService.insertByBo(product);
  1224. if ("framework".equals(parentContract.getContractType())) {
  1225. savePriceTiersForProduct(product.getId(), newContract.getId(), product.getPriceTiers());
  1226. }
  1227. }
  1228. } else {
  1229. // 复制原合同的产品
  1230. LambdaQueryWrapper<CrmContractProduct> productWrapper = Wrappers.lambdaQuery();
  1231. productWrapper.eq(CrmContractProduct::getContractId, parentContractId);
  1232. List<CrmContractProduct> parentProducts = contractProductMapper.selectList(productWrapper);
  1233. for (CrmContractProduct parentProduct : parentProducts) {
  1234. CrmContractProduct newProduct = new CrmContractProduct();
  1235. newProduct.setContractId(newContract.getId());
  1236. newProduct.setProductId(parentProduct.getProductId());
  1237. newProduct.setProductName(parentProduct.getProductName());
  1238. newProduct.setProductNo(parentProduct.getProductNo());
  1239. newProduct.setCategoryId(parentProduct.getCategoryId());
  1240. newProduct.setCategoryName(parentProduct.getCategoryName());
  1241. newProduct.setModel(parentProduct.getModel());
  1242. newProduct.setUnit(parentProduct.getUnit());
  1243. newProduct.setQuantity(parentProduct.getQuantity());
  1244. newProduct.setPrice(parentProduct.getPrice());
  1245. newProduct.setDiscount(parentProduct.getDiscount());
  1246. newProduct.setSubtotal(parentProduct.getSubtotal());
  1247. newProduct.setDescription(parentProduct.getDescription());
  1248. newProduct.setSort(parentProduct.getSort());
  1249. contractProductMapper.insert(newProduct);
  1250. if ("framework".equals(parentContract.getContractType())) {
  1251. copyPriceTiersToNewProduct(parentProduct.getId(), newContract.getId(), newProduct.getId());
  1252. }
  1253. }
  1254. }
  1255. log.info("合同续签成功,原合同ID: {}, 新合同ID: {}, 续签次数: {}", parentContractId, newContract.getId(),
  1256. newContract.getRenewalCount());
  1257. return newContract.getId();
  1258. }
  1259. @Override
  1260. @Transactional(rollbackFor = Exception.class)
  1261. public Boolean createFromOpportunity(Long opportunityId, CrmContractBo bo) {
  1262. // 1. 查询商机
  1263. CrmOpportunity opportunity = opportunityMapper.selectById(opportunityId);
  1264. if (opportunity == null) {
  1265. throw new ServiceException("商机不存在");
  1266. }
  1267. // 2. 自动填充信息
  1268. bo.setOpportunityId(opportunityId);
  1269. bo.setCustomerId(opportunity.getCustomerId());
  1270. if (bo.getAmount() == null) {
  1271. bo.setAmount(opportunity.getAmount());
  1272. }
  1273. if (bo.getOwnerUserId() == null) {
  1274. bo.setOwnerUserId(opportunity.getOwnerUserId());
  1275. }
  1276. // 3. 创建合同
  1277. boolean flag = insertByBo(bo);
  1278. // 4. 更新商机状态为"已转合同"
  1279. if (flag) {
  1280. CrmOpportunity updateOpp = new CrmOpportunity();
  1281. updateOpp.setId(opportunityId);
  1282. updateOpp.setStage("won"); // 赢单
  1283. opportunityMapper.updateById(updateOpp);
  1284. log.info("从商机创建合同成功,商机ID: {}, 合同ID: {}", opportunityId, bo.getId());
  1285. }
  1286. return flag;
  1287. }
  1288. /**
  1289. * 保存合同前解析产品ID(按名称/规格/分类/单位/税率匹配,未命中自动新增)
  1290. */
  1291. private void resolveContractProductIds(List<CrmContractProductBo> products) {
  1292. if (products == null || products.isEmpty()) {
  1293. return;
  1294. }
  1295. for (CrmContractProductBo product : products) {
  1296. product.setProductId(resolveProductIdForContractItem(product));
  1297. }
  1298. }
  1299. private Long resolveProductIdForContractItem(CrmContractProductBo contractProductBo) {
  1300. if (contractProductBo == null) {
  1301. throw new ServiceException("产品明细不能为空");
  1302. }
  1303. String productName = normalizeText(contractProductBo.getProductName());
  1304. if (productName == null) {
  1305. throw new ServiceException("产品名称不能为空");
  1306. }
  1307. String model = normalizeText(contractProductBo.getModel());
  1308. String unit = normalizeText(contractProductBo.getUnit());
  1309. BigDecimal taxRateForCompare = normalizeTaxRateForCompare(contractProductBo.getTaxRate());
  1310. BigDecimal taxRateForSave = normalizeTaxRateForSave(contractProductBo.getTaxRate());
  1311. LambdaQueryWrapper<CrmProduct> lqw = Wrappers.lambdaQuery();
  1312. // 名称/规格/单位:trim + 不区分大小写 + null与空字符串视为相同
  1313. lqw.apply("LOWER(TRIM(COALESCE(product_name, ''))) = {0}", toLower(productName));
  1314. lqw.apply("LOWER(TRIM(COALESCE(model, ''))) = {0}", toLower(model));
  1315. lqw.apply("LOWER(TRIM(COALESCE(unit, ''))) = {0}", toLower(unit));
  1316. // 税率:统一保留2位后比较
  1317. lqw.apply("ROUND(COALESCE(tax_rate, 0), 2) = {0}", taxRateForCompare);
  1318. lqw.orderByDesc(CrmProduct::getId);
  1319. List<CrmProduct> candidates = productMapper.selectList(lqw);
  1320. if (!candidates.isEmpty()) {
  1321. Long categoryId = contractProductBo.getCategoryId();
  1322. // 分类为空允许匹配;若有分类,优先取分类一致
  1323. if (categoryId != null) {
  1324. for (CrmProduct candidate : candidates) {
  1325. if (categoryId.equals(candidate.getCategoryId())) {
  1326. return candidate.getId();
  1327. }
  1328. }
  1329. }
  1330. // 命中多条取最新一条(id最大)
  1331. return candidates.get(0).getId();
  1332. }
  1333. // 未命中则自动新增产品
  1334. CrmProductBo newBo = new CrmProductBo();
  1335. newBo.setProductNo(productService.generateAutoProductNo());
  1336. newBo.setProductName(productName);
  1337. newBo.setCategoryId(contractProductBo.getCategoryId());
  1338. newBo.setCategoryName(normalizeText(contractProductBo.getCategoryName()));
  1339. newBo.setModel(model);
  1340. newBo.setUnit(unit);
  1341. newBo.setTaxRate(taxRateForSave);
  1342. // 合同保存自动新增产品时,标准价格默认0
  1343. newBo.setPrice(BigDecimal.ZERO);
  1344. newBo.setStatus("on");
  1345. productService.insertByBo(newBo);
  1346. return newBo.getId();
  1347. }
  1348. private String normalizeText(String value) {
  1349. if (value == null) {
  1350. return null;
  1351. }
  1352. String trimmed = value.trim();
  1353. return trimmed.isEmpty() ? null : trimmed;
  1354. }
  1355. private String toLower(String value) {
  1356. return value == null ? "" : value.toLowerCase();
  1357. }
  1358. private BigDecimal normalizeTaxRateForCompare(BigDecimal value) {
  1359. BigDecimal taxRate = value == null ? BigDecimal.ZERO : value;
  1360. return taxRate.setScale(2, RoundingMode.HALF_UP);
  1361. }
  1362. /**
  1363. * 保存税率时尽量保留前端传入形态:
  1364. * - 传整数则保存整数
  1365. * - 传小数则保存小数
  1366. */
  1367. private BigDecimal normalizeTaxRateForSave(BigDecimal value) {
  1368. if (value == null) {
  1369. return BigDecimal.ZERO;
  1370. }
  1371. BigDecimal normalized = value.stripTrailingZeros();
  1372. if (normalized.scale() <= 0) {
  1373. return normalized.setScale(0, RoundingMode.UNNECESSARY);
  1374. }
  1375. return normalized;
  1376. }
  1377. private void validateContractTypeAndProductsPresence(CrmContractBo bo) {
  1378. if (bo == null || StringUtils.isBlank(bo.getContractType())) {
  1379. return;
  1380. }
  1381. String contractType = bo.getContractType();
  1382. if ("sale".equals(contractType) || "purchase".equals(contractType) || "framework".equals(contractType)) {
  1383. if (bo.getProducts() == null || bo.getProducts().isEmpty()) {
  1384. throw new ServiceException("销售合同、采购合同或框架合同必须至少添加一个产品");
  1385. }
  1386. }
  1387. }
  1388. private void normalizeFrameworkContractData(CrmContractBo bo) {
  1389. if (bo == null || !"framework".equals(bo.getContractType())) {
  1390. return;
  1391. }
  1392. bo.setAmount(BigDecimal.ZERO);
  1393. bo.setAmountWithTax(BigDecimal.ZERO);
  1394. if (bo.getProducts() == null) {
  1395. return;
  1396. }
  1397. for (CrmContractProductBo p : bo.getProducts()) {
  1398. p.setPrice(BigDecimal.ZERO);
  1399. p.setSubtotal(BigDecimal.ZERO);
  1400. p.setTaxAmount(BigDecimal.ZERO);
  1401. }
  1402. }
  1403. private void validateProductPricesByContractType(CrmContractBo bo) {
  1404. if (bo == null || StringUtils.isBlank(bo.getContractType())) {
  1405. return;
  1406. }
  1407. String contractType = bo.getContractType();
  1408. if (!"sale".equals(contractType) && !"purchase".equals(contractType)) {
  1409. return;
  1410. }
  1411. List<CrmContractProductBo> products = bo.getProducts();
  1412. if (products == null) {
  1413. return;
  1414. }
  1415. for (CrmContractProductBo p : products) {
  1416. if (p.getPrice() == null || p.getPrice().compareTo(new BigDecimal("0.01")) < 0) {
  1417. throw new ServiceException("销售/采购合同产品的含税单价必须大于0");
  1418. }
  1419. if (p.getSubtotal() == null || p.getSubtotal().compareTo(new BigDecimal("0.01")) < 0) {
  1420. throw new ServiceException("销售/采购合同产品的含税总价必须大于0");
  1421. }
  1422. }
  1423. }
  1424. private void validateSalePurchaseContractAmount(CrmContractBo bo) {
  1425. if (bo == null || StringUtils.isBlank(bo.getContractType())) {
  1426. return;
  1427. }
  1428. String contractType = bo.getContractType();
  1429. if (!"sale".equals(contractType) && !"purchase".equals(contractType)) {
  1430. return;
  1431. }
  1432. if (bo.getAmount() == null || bo.getAmount().compareTo(new BigDecimal("0.01")) < 0) {
  1433. throw new ServiceException("销售合同或采购合同的合同金额必须大于0");
  1434. }
  1435. }
  1436. private List<String> parseLetterOssIdsJson(String json) {
  1437. if (StringUtils.isBlank(json)) {
  1438. return Collections.emptyList();
  1439. }
  1440. try {
  1441. List<String> list = JSONUtil.toList(json, String.class);
  1442. return list.stream().filter(StringUtils::isNotBlank).map(String::trim).toList();
  1443. } catch (Exception e) {
  1444. log.warn("解析 letter_oss_ids JSON 失败: {}", json);
  1445. return Collections.emptyList();
  1446. }
  1447. }
  1448. private List<String> parseLetterOssIdsFromEntity(CrmContractProductPriceTier en) {
  1449. if (StringUtils.isNotBlank(en.getLetterOssIds())) {
  1450. List<String> parsed = parseLetterOssIdsJson(en.getLetterOssIds());
  1451. if (!parsed.isEmpty()) {
  1452. return new ArrayList<>(parsed);
  1453. }
  1454. }
  1455. if (en.getLetterOssId() != null) {
  1456. return new ArrayList<>(List.of(en.getLetterOssId().toString()));
  1457. }
  1458. return new ArrayList<>();
  1459. }
  1460. private CrmContractProductPriceTierVo convertTierEntityToVo(CrmContractProductPriceTier en) {
  1461. CrmContractProductPriceTierVo vo = new CrmContractProductPriceTierVo();
  1462. vo.setId(en.getId());
  1463. vo.setContractId(en.getContractId());
  1464. vo.setContractProductId(en.getContractProductId());
  1465. vo.setStartDate(en.getStartDate());
  1466. vo.setEndDate(en.getEndDate());
  1467. vo.setPrice(en.getPrice());
  1468. vo.setSort(en.getSort());
  1469. vo.setLetterOssIds(parseLetterOssIdsFromEntity(en));
  1470. return vo;
  1471. }
  1472. private void savePriceTiersForProduct(Long contractProductId, Long contractId, List<CrmContractProductPriceTierBo> tiers) {
  1473. if (tiers == null || tiers.isEmpty()) {
  1474. return;
  1475. }
  1476. int sort = 1;
  1477. for (CrmContractProductPriceTierBo t : tiers) {
  1478. if (t.getStartDate() == null || t.getEndDate() == null || t.getPrice() == null) {
  1479. continue;
  1480. }
  1481. List<String> ossIds = t.getLetterOssIds();
  1482. if (ossIds == null || ossIds.isEmpty()) {
  1483. continue;
  1484. }
  1485. List<String> normalized = ossIds.stream()
  1486. .filter(StringUtils::isNotBlank)
  1487. .map(String::trim)
  1488. .toList();
  1489. if (normalized.isEmpty()) {
  1490. continue;
  1491. }
  1492. CrmContractProductPriceTier e = new CrmContractProductPriceTier();
  1493. e.setContractId(contractId);
  1494. e.setContractProductId(contractProductId);
  1495. e.setStartDate(t.getStartDate());
  1496. e.setEndDate(t.getEndDate());
  1497. e.setPrice(t.getPrice());
  1498. e.setLetterOssIds(JSONUtil.toJsonStr(normalized));
  1499. e.setLetterOssId(null);
  1500. if (t.getSort() == null) {
  1501. e.setSort(sort);
  1502. } else {
  1503. e.setSort(t.getSort());
  1504. }
  1505. sort++;
  1506. contractProductPriceTierMapper.insert(e);
  1507. }
  1508. }
  1509. private void copyPriceTiersToNewProduct(Long oldContractProductId, Long newContractId, Long newContractProductId) {
  1510. List<CrmContractProductPriceTier> rows = contractProductPriceTierMapper.selectList(
  1511. Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
  1512. .eq(CrmContractProductPriceTier::getContractProductId, oldContractProductId));
  1513. if (rows == null || rows.isEmpty()) {
  1514. return;
  1515. }
  1516. int s = 1;
  1517. for (CrmContractProductPriceTier row : rows) {
  1518. CrmContractProductPriceTier n = new CrmContractProductPriceTier();
  1519. n.setContractId(newContractId);
  1520. n.setContractProductId(newContractProductId);
  1521. n.setStartDate(row.getStartDate());
  1522. n.setEndDate(row.getEndDate());
  1523. n.setPrice(row.getPrice());
  1524. n.setLetterOssIds(row.getLetterOssIds());
  1525. n.setLetterOssId(row.getLetterOssId());
  1526. n.setSort(row.getSort() != null ? row.getSort() : s);
  1527. contractProductPriceTierMapper.insert(n);
  1528. s++;
  1529. }
  1530. }
  1531. private void validateFrameworkContractForSubmitApproval(Long contractId, CrmContract contract) {
  1532. if (!"framework".equals(contract.getContractType())) {
  1533. return;
  1534. }
  1535. List<CrmContractProductVo> products = contractProductService.queryListByContractId(contractId);
  1536. if (products == null || products.isEmpty()) {
  1537. throw new ServiceException("框架合同必须维护产品明细");
  1538. }
  1539. List<CrmContractProductPriceTier> tiers = contractProductPriceTierMapper.selectList(
  1540. Wrappers.lambdaQuery(CrmContractProductPriceTier.class)
  1541. .eq(CrmContractProductPriceTier::getContractId, contractId));
  1542. Map<Long, List<CrmContractProductPriceTier>> byProduct = tiers.stream()
  1543. .collect(Collectors.groupingBy(CrmContractProductPriceTier::getContractProductId));
  1544. for (CrmContractProductVo prod : products) {
  1545. List<CrmContractProductPriceTier> list = byProduct.get(prod.getId());
  1546. if (list == null || list.isEmpty()) {
  1547. throw new ServiceException("框架合同产品「" + prod.getProductName() + "」至少维护一条调价区间(含调价函)");
  1548. }
  1549. for (CrmContractProductPriceTier t : list) {
  1550. if (t.getStartDate() == null || t.getEndDate() == null) {
  1551. throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价区间日期不能为空");
  1552. }
  1553. if (t.getStartDate().isAfter(t.getEndDate())) {
  1554. throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价开始日期不能晚于结束日期");
  1555. }
  1556. if (t.getPrice() == null || t.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
  1557. throw new ServiceException("框架合同产品「" + prod.getProductName() + "」调价含税单价必须大于0");
  1558. }
  1559. List<String> letterIds = parseLetterOssIdsFromEntity(t);
  1560. if (letterIds.isEmpty()) {
  1561. throw new ServiceException("框架合同产品「" + prod.getProductName() + "」每条调价区间须上传调价函附件");
  1562. }
  1563. }
  1564. }
  1565. }
  1566. @Override
  1567. @Transactional(rollbackFor = Exception.class)
  1568. public Boolean createFromQuote(Long quoteId, CrmContractBo bo) {
  1569. // 1. 查询报价单
  1570. CrmQuote quote = quoteMapper.selectById(quoteId);
  1571. if (quote == null) {
  1572. throw new ServiceException("报价单不存在");
  1573. }
  1574. // 2. 自动填充信息
  1575. bo.setQuoteId(quoteId);
  1576. bo.setCustomerId(quote.getCustomerId());
  1577. bo.setOpportunityId(quote.getOpportunityId());
  1578. if (bo.getAmount() == null) {
  1579. bo.setAmount(quote.getTotalAmount());
  1580. }
  1581. if (bo.getOwnerUserId() == null) {
  1582. bo.setOwnerUserId(quote.getOwnerUserId());
  1583. }
  1584. // 从报价单复制产品明细(如果没有传入)
  1585. if ((bo.getProducts() == null || bo.getProducts().isEmpty()) && quote.getId() != null) {
  1586. // TODO: 从报价单产品表复制产品明细
  1587. log.info("从报价单复制产品明细,报价单ID: {}", quoteId);
  1588. }
  1589. // 3. 创建合同
  1590. boolean flag = insertByBo(bo);
  1591. // 4. 更新报价单状态为"已转合同"
  1592. if (flag) {
  1593. CrmQuote updateQuote = new CrmQuote();
  1594. updateQuote.setId(quoteId);
  1595. updateQuote.setStatus("converted"); // 已转化
  1596. quoteMapper.updateById(updateQuote);
  1597. log.info("从报价单创建合同成功,报价单ID: {}, 合同ID: {}", quoteId, bo.getId());
  1598. }
  1599. return flag;
  1600. }
  1601. @Override
  1602. @Transactional(rollbackFor = Exception.class)
  1603. public Boolean createFromTemplate(Long templateId, CrmContractBo bo) {
  1604. // 1. 查询模板
  1605. CrmContractTemplate template = contractTemplateMapper.selectById(templateId);
  1606. if (template == null) {
  1607. throw new ServiceException("合同模板不存在");
  1608. }
  1609. if (template.getIsActive() == null || template.getIsActive() != 1) {
  1610. throw new ServiceException("该模板已停用,无法使用");
  1611. }
  1612. // 2. 从模板填充信息
  1613. if (bo.getContractType() == null) {
  1614. bo.setContractType(template.getContractType());
  1615. }
  1616. if (bo.getCurrency() == null) {
  1617. bo.setCurrency(template.getDefaultCurrency());
  1618. }
  1619. if (bo.getTaxRate() == null) {
  1620. bo.setTaxRate(template.getDefaultTaxRate());
  1621. }
  1622. if (bo.getContractPeriod() == null) {
  1623. bo.setContractPeriod(template.getDefaultPeriod());
  1624. }
  1625. // 填充合同条款
  1626. if (StringUtils.isBlank(bo.getPaymentTerms())) {
  1627. bo.setPaymentTerms(template.getPaymentTerms());
  1628. }
  1629. if (StringUtils.isBlank(bo.getDeliveryTerms())) {
  1630. bo.setDeliveryTerms(template.getDeliveryTerms());
  1631. }
  1632. if (StringUtils.isBlank(bo.getContractTerms())) {
  1633. bo.setContractTerms(template.getContractTerms());
  1634. }
  1635. // 填充乙方信息
  1636. if (StringUtils.isBlank(bo.getPartyB())) {
  1637. bo.setPartyB(template.getPartyB());
  1638. }
  1639. if (StringUtils.isBlank(bo.getPartyBAddress())) {
  1640. bo.setPartyBAddress(template.getPartyBAddress());
  1641. }
  1642. if (StringUtils.isBlank(bo.getPartyBContact())) {
  1643. bo.setPartyBContact(template.getPartyBContact());
  1644. }
  1645. if (StringUtils.isBlank(bo.getPartyBMobile())) {
  1646. bo.setPartyBMobile(template.getPartyBMobile());
  1647. }
  1648. // 3. 创建合同
  1649. boolean flag = insertByBo(bo);
  1650. // 4. 更新模板使用次数
  1651. if (flag) {
  1652. CrmContractTemplate updateTemplate = new CrmContractTemplate();
  1653. updateTemplate.setId(templateId);
  1654. updateTemplate.setUsageCount((template.getUsageCount() == null ? 0 : template.getUsageCount()) + 1);
  1655. contractTemplateMapper.updateById(updateTemplate);
  1656. log.info("从模板创建合同成功,模板ID: {}, 合同ID: {}", templateId, bo.getId());
  1657. }
  1658. return flag;
  1659. }
  1660. @Override
  1661. @Transactional(rollbackFor = Exception.class)
  1662. public Boolean signContract(Long id, LocalDate actualSignDate, String contractFile, String remark) {
  1663. // 1. 查询合同
  1664. CrmContract contract = baseMapper.selectById(id);
  1665. if (contract == null) {
  1666. throw new ServiceException("合同不存在");
  1667. }
  1668. // 2. 校验合同状态
  1669. if (!"approved".equals(contract.getStatus())) {
  1670. throw new ServiceException("只有审批通过的合同才能签订");
  1671. }
  1672. // 3. 更新合同信息
  1673. CrmContract update = new CrmContract();
  1674. update.setId(id);
  1675. update.setActualSignDate(actualSignDate);
  1676. update.setContractFile(contractFile);
  1677. update.setStatus("executing"); // 签订后进入执行中状态
  1678. update.setPerformanceStatus("normal");
  1679. update.setPerformanceRate(BigDecimal.ZERO);
  1680. if (StringUtils.isNotBlank(remark)) {
  1681. update.setRemark(remark);
  1682. }
  1683. boolean flag = baseMapper.updateById(update) > 0;
  1684. if (flag) {
  1685. log.info("合同签订成功,合同ID: {}, 签订日期: {}", id, actualSignDate);
  1686. // TODO: 发送通知给负责人和财务
  1687. // TODO: 激活回款计划
  1688. }
  1689. return flag;
  1690. }
  1691. @Override
  1692. @Transactional(rollbackFor = Exception.class)
  1693. public Boolean batchAssignOwner(Collection<Long> ids, Long ownerUserId, String remark) {
  1694. if (ids == null || ids.isEmpty()) {
  1695. throw new ServiceException("合同ID列表不能为空");
  1696. }
  1697. if (ownerUserId == null) {
  1698. throw new ServiceException("负责人ID不能为空");
  1699. }
  1700. // 批量更新
  1701. int successCount = 0;
  1702. for (Long id : ids) {
  1703. CrmContract contract = baseMapper.selectById(id);
  1704. if (contract == null) {
  1705. log.warn("合同不存在,跳过,ID: {}", id);
  1706. continue;
  1707. }
  1708. CrmContract update = new CrmContract();
  1709. update.setId(id);
  1710. update.setOwnerUserId(ownerUserId);
  1711. // 设置负责人姓名
  1712. try {
  1713. String ownerUserName = userService.selectNicknameById(ownerUserId);
  1714. update.setOwnerUserName(ownerUserName);
  1715. } catch (Exception e) {
  1716. log.warn("获取负责人姓名失败,用户ID: {}", ownerUserId, e);
  1717. update.setOwnerUserName(null);
  1718. }
  1719. if (StringUtils.isNotBlank(remark)) {
  1720. update.setRemark(remark);
  1721. }
  1722. if (baseMapper.updateById(update) > 0) {
  1723. successCount++;
  1724. log.info("合同分配成功,合同ID: {}, 新负责人ID: {}", id, ownerUserId);
  1725. }
  1726. }
  1727. log.info("批量分配合同负责人完成,成功: {}, 总数: {}", successCount, ids.size());
  1728. return successCount > 0;
  1729. }
  1730. @Override
  1731. public String downloadContractPdf(Long id) {
  1732. // 查询合同
  1733. CrmContractVo contract = queryById(id);
  1734. if (contract == null) {
  1735. throw new ServiceException("合同不存在");
  1736. }
  1737. // 返回合同文件路径(如果存在)
  1738. if (StringUtils.isNotBlank(contract.getContractFile())) {
  1739. return contract.getContractFile();
  1740. }
  1741. throw new ServiceException("合同PDF文件不存在");
  1742. }
  1743. @Override
  1744. @Transactional(rollbackFor = Exception.class)
  1745. public Boolean updateInvoiceAmount(Long contractId) {
  1746. // 查询该合同的所有发票记录
  1747. LambdaQueryWrapper<CrmInvoice> lqw = Wrappers.lambdaQuery();
  1748. lqw.eq(CrmInvoice::getContractId, contractId);
  1749. lqw.eq(CrmInvoice::getDelFlag, 0);
  1750. List<CrmInvoice> invoices = invoiceMapper.selectList(lqw);
  1751. // 计算已开票金额
  1752. BigDecimal invoiceAmount = invoices.stream()
  1753. .map(CrmInvoice::getAmount)
  1754. .filter(Objects::nonNull)
  1755. .reduce(BigDecimal.ZERO, BigDecimal::add);
  1756. // 查询合同
  1757. CrmContract contract = baseMapper.selectById(contractId);
  1758. if (contract == null) {
  1759. log.error("合同不存在,无法更新开票金额,合同ID: {}", contractId);
  1760. return false;
  1761. }
  1762. // 更新合同开票金额
  1763. CrmContract update = new CrmContract();
  1764. update.setId(contractId);
  1765. update.setInvoiceAmount(invoiceAmount);
  1766. update.setUninvoiceAmount(contract.getAmount().subtract(invoiceAmount));
  1767. boolean flag = baseMapper.updateById(update) > 0;
  1768. if (flag) {
  1769. log.info("更新合同开票金额成功,合同ID: {}, 已开票: {}", contractId, invoiceAmount);
  1770. }
  1771. return flag;
  1772. }
  1773. }