src/main/java/com/ruoyi/basic/controller/CustomerController.java
@@ -32,7 +32,7 @@ * æ¥è¯¢å®¢æ·æ¡£æ¡å表 */ @GetMapping("/list") public IPage<Customer> list(Page page, Customer customer) { public IPage<Customer> list(Page<Customer> page, Customer customer) { return customerService.selectCustomerList(page, customer); } @@ -76,7 +76,7 @@ */ @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Long id) { return success(customerService.selectCustomerById(id)); return success(customerService.selectCustomerDetailById(id)); } /** src/main/java/com/ruoyi/basic/controller/CustomerFollowUpController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,144 @@ package com.ruoyi.basic.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.basic.pojo.CustomerFollowUp; import com.ruoyi.basic.pojo.CustomerReturnVisit; import com.ruoyi.basic.service.CustomerFollowUpService; import com.ruoyi.basic.service.CustomerReturnVisitService; import com.ruoyi.framework.aspectj.lang.annotation.Log; import com.ruoyi.framework.aspectj.lang.enums.BusinessType; import com.ruoyi.framework.web.controller.BaseController; import com.ruoyi.framework.web.domain.AjaxResult; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; /** * <br> * 客æ·è·è¿æ§å¶å± * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:45 */ @RestController @RequestMapping("/basic/customer-follow") public class CustomerFollowUpController extends BaseController { @Autowired private CustomerFollowUpService customerFollowUpService; @Autowired private CustomerReturnVisitService customerReturnVisitService; /** * æ¥è¯¢å®¢æ·è·è¿å表 */ @GetMapping("/list") @ApiOperation("æ¥è¯¢å®¢æ·è·è¿å表") public IPage<CustomerFollowUp> list(Page<CustomerFollowUp> page, CustomerFollowUp customerFollowUp) { LambdaQueryWrapper<CustomerFollowUp> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(customerFollowUp.getCustomerId() != null, CustomerFollowUp::getCustomerId, customerFollowUp.getCustomerId()) .like(customerFollowUp.getFollowerUserName() != null, CustomerFollowUp::getFollowerUserName, customerFollowUp.getFollowerUserName()) .orderByDesc(CustomerFollowUp::getFollowUpTime); return customerFollowUpService.page(page, queryWrapper); } /** * è·å客æ·è·è¿è¯¦ç»ä¿¡æ¯ */ @ApiOperation("è·å客æ·è·è¿è¯¦ç»ä¿¡æ¯") @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") Integer id) { return AjaxResult.success(customerFollowUpService.getFollowUpWithFiles(id)); } /** * æ°å¢å®¢æ·è·è¿ */ @PostMapping("/add") @ApiOperation("æ°å¢å®¢æ·è·è¿") @Log(title = "客æ·è·è¿-æ°å¢", businessType = BusinessType.INSERT) public AjaxResult add(@RequestBody CustomerFollowUp customerFollowUp) { return toAjax(customerFollowUpService.insertCustomerFollowUp(customerFollowUp)); } /** * ä¿®æ¹å®¢æ·è·è¿ */ @PutMapping("/edit") @ApiOperation("ä¿®æ¹å®¢æ·è·è¿") @Log(title = "客æ·è·è¿-ä¿®æ¹", businessType = BusinessType.UPDATE) public AjaxResult edit(@RequestBody CustomerFollowUp customerFollowUp) { return toAjax(customerFollowUpService.updateCustomerFollowUp(customerFollowUp)); } /** * ä¸ä¼ è·è¿éä»¶ */ @ApiOperation("ä¸ä¼ è·è¿éä»¶") @PostMapping("/upload/{followUpId}") @Log(title = "客æ·è·è¿-ä¸ä¼ éä»¶", businessType = BusinessType.INSERT) public AjaxResult uploadFiles(@RequestParam("files") List<MultipartFile> files, @PathVariable Integer followUpId) { customerFollowUpService.addFollowUpFiles(files, followUpId); return AjaxResult.success(); } /** * å é¤è·è¿éä»¶ */ @ApiOperation("å é¤è·è¿éä»¶") @DeleteMapping("/file/{fileId}") @Log(title = "客æ·è·è¿-å é¤éä»¶", businessType = BusinessType.DELETE) public AjaxResult deleteFile(@PathVariable Integer fileId) { customerFollowUpService.deleteFollowUpFile(fileId); return AjaxResult.success(); } /** * å é¤å®¢æ·è·è¿ */ @ApiOperation("å é¤å®¢æ·è·è¿") @DeleteMapping("/{id}") @Log(title = "客æ·è·è¿-å é¤", businessType = BusinessType.DELETE) public AjaxResult remove(@PathVariable Integer id) { return toAjax(customerFollowUpService.deleteCustomerFollowUpById(id)); } /** * æ°å¢/æ´æ°å访æé */ @ApiOperation("æ°å¢/æ´æ°å访æé") @PostMapping("/return-visit") @Log(title = "å访æé-æ°å¢/æ´æ°", businessType = BusinessType.UPDATE) public AjaxResult saveReturnVisit(@RequestBody CustomerReturnVisit customerReturnVisit) { return toAjax(customerReturnVisitService.saveOrUpdateReturnVisit(customerReturnVisit)); } /** * è·åå访æé详æ */ @ApiOperation("è·åå访æé详æ ") @GetMapping("/return-visit/{customerId}") public AjaxResult getReturnVisit(@PathVariable Integer customerId) { return AjaxResult.success(customerReturnVisitService.getByCustomerId(customerId)); } /** * æ è®°å访æé已读 */ @ApiOperation("æ è®°å访æé已读") @PutMapping("/return-visit/read/{id}") @Log(title = "å访æé-æ 记已读", businessType = BusinessType.UPDATE) public AjaxResult markAsRead(@PathVariable Long id) { customerReturnVisitService.markAsRead(id); return AjaxResult.success(); } } src/main/java/com/ruoyi/basic/dto/CustomerDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,24 @@ package com.ruoyi.basic.dto; import com.ruoyi.basic.pojo.Customer; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; /** * <br> * å®¢æ·æ¡£æ¡DTOï¼å å«è·è¿è®°å½ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/05 09:40 */ @Data @EqualsAndHashCode(callSuper = true) public class CustomerDto extends Customer { private List<CustomerFollowUpDto> followUpList; } src/main/java/com/ruoyi/basic/dto/CustomerFollowUpDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,25 @@ package com.ruoyi.basic.dto; import com.ruoyi.basic.pojo.CustomerFollowUp; import com.ruoyi.basic.pojo.CustomerFollowUpFile; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; /** * <br> * 客æ·è·è¿DTOï¼å å«éä»¶å表 * </br> * * @author deslrey * @version 1.0 * @since 2026/03/05 09:41 */ @Data @EqualsAndHashCode(callSuper = true) public class CustomerFollowUpDto extends CustomerFollowUp { private List<CustomerFollowUpFile> fileList; } src/main/java/com/ruoyi/basic/dto/CustomerReturnVisitDto.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,54 @@ package com.ruoyi.basic.dto; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.time.LocalDateTime; /** * <br> * 客æ·å访æéDto * </br> * * @author deslrey * @version 1.0 * @since 2026/03/05 10:26 */ @Data public class CustomerReturnVisitDto { /** * å访æé主é®ID */ private Long id; /** * å ³è客æ·ID (å¯¹åº customer 表ç id) */ private Integer customerId; /** * æéå¼å ³ç¶æ (0:å ³é, 1:å¼å¯) */ private Integer isEnabled; /** * æéçå ·ä½å 容 */ private String content; /** * 设å®çæéè§¦åæ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime reminderTime; /** * å¤çç¶æ (0:å¾ æé, 1:å·²æé) */ private Integer isCompleted; /** * æ¥æ¶æéçç¨æ·ID */ private Long remindUserId; } src/main/java/com/ruoyi/basic/mapper/CustomerFollowUpFileMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,16 @@ package com.ruoyi.basic.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.basic.pojo.CustomerFollowUpFile; /** * <br> * 客æ·è·è¿éä»¶mapper * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:52 */ public interface CustomerFollowUpFileMapper extends BaseMapper<CustomerFollowUpFile> { } src/main/java/com/ruoyi/basic/mapper/CustomerFollowUpMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,16 @@ package com.ruoyi.basic.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.basic.pojo.CustomerFollowUp; /** * <br> * 客æ·è·è¿mapper * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:46 */ public interface CustomerFollowUpMapper extends BaseMapper<CustomerFollowUp> { } src/main/java/com/ruoyi/basic/mapper/CustomerReturnVisitMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,16 @@ package com.ruoyi.basic.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.ruoyi.basic.pojo.CustomerReturnVisit; /** * <br> * 客æ·å访æémapper * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 17:57 */ public interface CustomerReturnVisitMapper extends BaseMapper<CustomerReturnVisit> { } src/main/java/com/ruoyi/basic/pojo/Customer.java
@@ -1,6 +1,7 @@ package com.ruoyi.basic.pojo; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Date; import com.baomidou.mybatisplus.annotation.*; @@ -33,6 +34,21 @@ private String customerName; /** 客æ·åç±»ï¼é¶å®å®¢æ·ï¼è¿éåå®¢æ· */ /** * è·è¿ç¨åº¦ */ @Excel(name = "è·è¿ç¨åº¦") @TableField(exist = false) private String followUpLevel; /** * è·è¿æ¶é´ */ @Excel(name = "è·è¿æ¶é´") @TableField(exist = false) private LocalDateTime followUpTime; @Excel(name = "客æ·åç±»") private String customerType; src/main/java/com/ruoyi/basic/pojo/CustomerFollowUp.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,85 @@ package com.ruoyi.basic.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * <br> * 客æ·è·è¿è¿åº¦è¡¨ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:37 */ @Data @TableName(value = "customer_follow_up") public class CustomerFollowUp implements Serializable { private static final long serialVersionUID = 1L; /** * 主é®ID */ @TableId(type = IdType.AUTO) private Integer id; /** * å ³èç客æ·ID */ private Integer customerId; /** * è·è¿æ¹å¼ */ private String followUpMethod; /** * è·è¿ç¨åº¦ */ private String followUpLevel; /** * è·è¿æ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime followUpTime; /** * è·è¿äººå§å */ private String followerUserName; /** * è·è¿å 容 */ private String content; /** * è·è¿äººID */ private Long followerUserId; /** * ç§æ·ID */ private Long tenantId; /** * å建æ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; /** * æ´æ°æ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; } src/main/java/com/ruoyi/basic/pojo/CustomerFollowUpFile.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,81 @@ package com.ruoyi.basic.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * <br> * 客æ·è·è¿é件表 * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:41 */ @Data @TableName(value = "customer_follow_up_file") public class CustomerFollowUpFile implements Serializable { private static final long serialVersionUID = 1L; /** * ä¸»é® */ @TableId(type = IdType.AUTO) private Long id; /** * è·è¿è®°å½ID */ private Integer followUpId; /** * æä»¶åç§° */ private String fileName; /** * æä»¶è®¿é®å°å */ private String fileUrl; /** * æä»¶å¤§å°ï¼åä½ï¼åèï¼ */ private Long fileSize; /** * æä»¶åç¼ */ private String fileSuffix; /** * ä¸ä¼ è */ private Long createUser; /** * å建æ¶é´ */ private LocalDateTime createTime; /** * ä¿®æ¹è */ private Long updateUser; /** * ä¿®æ¹æ¶é´ */ private LocalDateTime updateTime; /** * ç§æ·ID */ private Long tenantId; } src/main/java/com/ruoyi/basic/pojo/CustomerReturnVisit.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,92 @@ package com.ruoyi.basic.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** * <br> * 客æ·å访æé * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 17:50 */ @Data @TableName(value = "customer_return_visit") public class CustomerReturnVisit implements Serializable { private static final long serialVersionUID = 1L; /** * 主é®ID */ @TableId(type = IdType.AUTO) private Long id; /** * å ³è客æ·ID */ private Integer customerId; /** * æéå¼å ³ (0:å ³é, 1:å¼å¯) */ private Integer isEnabled; /** * æéå 容 */ private String content; /** * æéæ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime reminderTime; /** * å¤çç¶æ (0:å¾ æé, 1:å·²æé) */ private Integer isCompleted; /** * æ¥æ¶æéçç¨æ·ID */ private Long remindUserId; /** * ç§æ·ID */ private Long tenantId; /** * å建è */ private Long createUser; /** * å建æ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime; /** * ä¿®æ¹è */ private Long updateUser; /** * ä¿®æ¹æ¶é´ */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime updateTime; } src/main/java/com/ruoyi/basic/service/CustomerFollowUpFileService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,16 @@ package com.ruoyi.basic.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.basic.pojo.CustomerFollowUpFile; /** * <br> * 客æ·è·è¿éä»¶æ¥å£ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:52 */ public interface CustomerFollowUpFileService extends IService<CustomerFollowUpFile> { } src/main/java/com/ruoyi/basic/service/CustomerFollowUpService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,55 @@ package com.ruoyi.basic.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.basic.dto.CustomerFollowUpDto; import com.ruoyi.basic.pojo.CustomerFollowUp; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.List; /** * <br> * 客æ·è·è¿æ¥å£ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:48 */ public interface CustomerFollowUpService extends IService<CustomerFollowUp> { /** * æ°å¢å®¢æ·è·è¿ */ boolean insertCustomerFollowUp(CustomerFollowUp customerFollowUp); /** * ä¿®æ¹å®¢æ·è·è¿ */ int updateCustomerFollowUp(CustomerFollowUp customerFollowUp); /** * æ¹éå é¤å®¢æ·è·è¿ */ int deleteCustomerFollowUpById(Integer id); /** * æ ¹æ®å®¢æ·IDå 餿æè·è¿è®°å½ */ void deleteByCustomerId(Long customerId); /** * æ·»å è·è¿éä»¶ */ void addFollowUpFiles(List<MultipartFile> files, Integer followUpId); /** * å é¤è·è¿éä»¶ */ void deleteFollowUpFile(Integer fileId); /** * è·åè·è¿è¯¦æ */ CustomerFollowUpDto getFollowUpWithFiles(Integer id); } src/main/java/com/ruoyi/basic/service/CustomerReturnVisitService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,25 @@ package com.ruoyi.basic.service; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.basic.dto.CustomerReturnVisitDto; import com.ruoyi.basic.pojo.CustomerReturnVisit; /** * <br> * 客æ·å访æéæ¥å£ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 17:58 */ public interface CustomerReturnVisitService extends IService<CustomerReturnVisit> { int saveOrUpdateReturnVisit(CustomerReturnVisit customerReturnVisit); CustomerReturnVisitDto getByCustomerId(Integer customerId); void deleteByCustomerId(Long customerId); void markAsRead(Long id); } src/main/java/com/ruoyi/basic/service/ICustomerService.java
@@ -3,11 +3,13 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.ruoyi.basic.dto.CustomerDto; import com.ruoyi.basic.pojo.Customer; import com.ruoyi.framework.web.domain.AjaxResult; import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Map; /** * å®¢æ·æ¡£æ¡Serviceæ¥å£ @@ -25,12 +27,20 @@ Customer selectCustomerById(Long id); /** * æ¥è¯¢å®¢æ·è¯¦æ ï¼å«è·è¿è®°å½åéä»¶ï¼ * * @param id å®¢æ·æ¡£æ¡ä¸»é® * @return 客æ·è¯¦æ DTO */ CustomerDto selectCustomerDetailById(Long id); /** * æ¥è¯¢å®¢æ·æ¡£æ¡å表 * * @param customer å®¢æ·æ¡£æ¡ * @return å®¢æ·æ¡£æ¡éå */ IPage<Customer> selectCustomerList(Page page, Customer customer); IPage<Customer> selectCustomerList(Page<Customer> page, Customer customer); /** * æ°å¢å®¢æ·æ¡£æ¡ @@ -63,7 +73,7 @@ * * @return ç»æ */ List customerList(Customer customer); List<Map<String, Object>> customerList(Customer customer); List<Customer> selectCustomerLists(Customer customer); src/main/java/com/ruoyi/basic/service/impl/CustomerFollowUpFileServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,20 @@ package com.ruoyi.basic.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.basic.mapper.CustomerFollowUpFileMapper; import com.ruoyi.basic.pojo.CustomerFollowUpFile; import com.ruoyi.basic.service.CustomerFollowUpFileService; import org.springframework.stereotype.Service; /** * <br> * å®¢æ·æ ¹æ®éä»¶æ¥å£å®ç°ç±» * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:53 */ @Service public class CustomerFollowUpFileServiceImpl extends ServiceImpl<CustomerFollowUpFileMapper, CustomerFollowUpFile> implements CustomerFollowUpFileService { } src/main/java/com/ruoyi/basic/service/impl/CustomerFollowUpServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,227 @@ package com.ruoyi.basic.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.basic.dto.CustomerFollowUpDto; import com.ruoyi.basic.mapper.CustomerFollowUpMapper; import com.ruoyi.basic.pojo.CustomerFollowUp; import com.ruoyi.basic.pojo.CustomerFollowUpFile; import com.ruoyi.basic.service.CustomerFollowUpFileService; import com.ruoyi.basic.service.CustomerFollowUpService; import com.ruoyi.basic.service.ICustomerService; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import org.apache.commons.io.FilenameUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; /** * <br> * å®¢æ·æ ¹æ®æ¥å£å®ç°ç±» * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 14:48 */ @Service public class CustomerFollowUpServiceImpl extends ServiceImpl<CustomerFollowUpMapper, CustomerFollowUp> implements CustomerFollowUpService { @Autowired private CustomerFollowUpFileService customerFollowUpFileService; @Value("${file.upload-dir}") private String uploadDir; @Override @Transactional(rollbackFor = Exception.class) public boolean insertCustomerFollowUp(CustomerFollowUp customerFollowUp) { validateFollowUp(customerFollowUp); Long currentUserId = SecurityUtils.getUserId(); Long currentTenantId = SecurityUtils.getLoginUser().getTenantId(); customerFollowUp.setFollowerUserId(currentUserId); customerFollowUp.setCreateTime(LocalDateTime.now()); customerFollowUp.setTenantId(currentTenantId); int result = baseMapper.insert(customerFollowUp); if (result < 1) { throw new ServiceException("客æ·è·è¿æ°æ®æ·»å 失败"); } return true; } @Override public int updateCustomerFollowUp(CustomerFollowUp customerFollowUp) { validateFollowUp(customerFollowUp); customerFollowUp.setUpdateTime(LocalDateTime.now()); return baseMapper.updateById(customerFollowUp); } @Override @Transactional(rollbackFor = Exception.class) public void addFollowUpFiles(List<MultipartFile> files, Integer followUpId) { handleFollowUpFiles(files, followUpId); } @Override @Transactional(rollbackFor = Exception.class) public void deleteFollowUpFile(Integer fileId) { CustomerFollowUpFile file = customerFollowUpFileService.getById(fileId); if (file != null) { try { Files.deleteIfExists(Paths.get(file.getFileUrl())); } catch (Exception e) { throw new ServiceException("å é¤æä»¶å¤±è´¥ï¼" + e.getMessage()); } customerFollowUpFileService.removeById(fileId); } } @Override @Transactional(rollbackFor = Exception.class) public int deleteCustomerFollowUpById(Integer id) { if (id == null) { throw new ServiceException("è·è¿IDä¸è½ä¸ºç©º"); } List<CustomerFollowUpFile> files = customerFollowUpFileService.list(new LambdaQueryWrapper<CustomerFollowUpFile>() .eq(CustomerFollowUpFile::getFollowUpId, id)); if (files != null && !files.isEmpty()) { for (CustomerFollowUpFile file : files) { deleteFollowUpFile(file.getId().intValue()); } } int result = baseMapper.deleteById(id); if (result < 1) { throw new ServiceException("客æ·è·è¿è®°å½å é¤å¤±è´¥"); } return result; } @Override @Transactional(rollbackFor = Exception.class) public void deleteByCustomerId(Long customerId) { if (customerId == null) { throw new ServiceException("客æ·IDä¸è½ä¸ºç©º"); } List<CustomerFollowUp> followUps = list(new LambdaQueryWrapper<CustomerFollowUp>() .eq(CustomerFollowUp::getCustomerId, customerId)); if (followUps != null && !followUps.isEmpty()) { for (CustomerFollowUp followUp : followUps) { deleteCustomerFollowUpById(followUp.getId()); } } } private void handleFollowUpFiles(List<MultipartFile> multipartFiles, Integer followUpId) { if (multipartFiles == null || multipartFiles.isEmpty()) { return; } Long currentUserId = SecurityUtils.getUserId(); Long currentTenantId = SecurityUtils.getLoginUser().getTenantId(); List<CustomerFollowUpFile> fileList = new ArrayList<>(); for (MultipartFile file : multipartFiles) { if (file == null || file.isEmpty()) { continue; } try { String formalDir = uploadDir + "/" + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); Path formalDirPath = Paths.get(formalDir); if (!Files.exists(formalDirPath)) { Files.createDirectories(formalDirPath); } String originalFilename = file.getOriginalFilename(); String fileExtension = FilenameUtils.getExtension(originalFilename); String formalFilename = followUpId + "_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8) + (StringUtils.hasText(fileExtension) ? "." + fileExtension : ""); Path formalFilePath = formalDirPath.resolve(formalFilename); file.transferTo(formalFilePath.toFile()); CustomerFollowUpFile followUpFile = new CustomerFollowUpFile(); followUpFile.setFollowUpId(followUpId); followUpFile.setFileName(originalFilename); followUpFile.setFileUrl(formalFilePath.toString()); followUpFile.setFileSize(file.getSize()); followUpFile.setFileSuffix(fileExtension); followUpFile.setCreateUser(currentUserId); followUpFile.setCreateTime(LocalDateTime.now()); followUpFile.setTenantId(currentTenantId); fileList.add(followUpFile); } catch (Exception e) { throw new ServiceException("æä»¶ [" + file.getOriginalFilename() + "] ä¸ä¼ 失败ï¼" + e.getMessage()); } } if (!fileList.isEmpty()) { customerFollowUpFileService.saveBatch(fileList); } } private void validateFollowUp(CustomerFollowUp followUp) { if (followUp == null) { throw new ServiceException("è·è¿æ°æ®ä¸è½ä¸ºç©º"); } if (StringUtils.isEmpty(followUp.getFollowUpMethod())) { throw new ServiceException("è·è¿æ¹å¼ä¸è½ä¸ºç©º"); } if (StringUtils.isEmpty(followUp.getFollowUpLevel())) { throw new ServiceException("è·è¿ç¨åº¦ä¸è½ä¸ºç©º"); } if (followUp.getFollowUpTime() == null) { throw new ServiceException("è·è¿æ¶é´ä¸è½ä¸ºç©º"); } if (StringUtils.isEmpty(followUp.getFollowerUserName())) { throw new ServiceException("è·è¿äººä¸è½ä¸ºç©º"); } if (StringUtils.isEmpty(followUp.getContent())) { throw new ServiceException("è·è¿å 容ä¸è½ä¸ºç©º"); } } @Override public CustomerFollowUpDto getFollowUpWithFiles(Integer id) { CustomerFollowUp followUp = baseMapper.selectById(id); if (followUp == null) { return null; } CustomerFollowUpDto dto = new CustomerFollowUpDto(); BeanUtils.copyProperties(followUp, dto); List<CustomerFollowUpFile> fileList = customerFollowUpFileService.list(new LambdaQueryWrapper<CustomerFollowUpFile>() .eq(CustomerFollowUpFile::getFollowUpId, id)); dto.setFileList(fileList); return dto; } } src/main/java/com/ruoyi/basic/service/impl/CustomerReturnVisitServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,139 @@ package com.ruoyi.basic.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.basic.dto.CustomerReturnVisitDto; import com.ruoyi.basic.mapper.CustomerReturnVisitMapper; import com.ruoyi.basic.pojo.CustomerReturnVisit; import com.ruoyi.basic.service.CustomerReturnVisitService; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.SecurityUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; /** * <br> * 客æ·å访æéæ¥å£å®ç°ç±» * </br> * * @author deslrey * @version 1.0 * @since 2026/03/04 17:58 */ @Service public class CustomerReturnVisitServiceImpl extends ServiceImpl<CustomerReturnVisitMapper, CustomerReturnVisit> implements CustomerReturnVisitService { @Autowired private ReturnVisitReminderService returnVisitReminderService; @Override @Transactional(rollbackFor = Exception.class) public int saveOrUpdateReturnVisit(CustomerReturnVisit customerReturnVisit) { validateReturnVisit(customerReturnVisit); Long currentUserId = SecurityUtils.getUserId(); Long currentTenantId = SecurityUtils.getLoginUser().getTenantId(); if (customerReturnVisit.getId() != null) { CustomerReturnVisit existing = baseMapper.selectById(customerReturnVisit.getId()); if (existing == null) { throw new ServiceException("å访æéä¸åå¨"); } if (!existing.getTenantId().equals(currentTenantId)) { throw new ServiceException("æ æéä¿®æ¹æ¤å访æé"); } customerReturnVisit.setUpdateUser(currentUserId); customerReturnVisit.setUpdateTime(LocalDateTime.now()); int result = baseMapper.updateById(customerReturnVisit); // æ ¹æ®æéå¼å ³æ¥å¤æéåä¿¡æ¯æ°å¢æåæ¶ if (customerReturnVisit.getIsEnabled() == 1) { returnVisitReminderService.scheduleReminder(customerReturnVisit.getId()); } else { returnVisitReminderService.cancelReminder(customerReturnVisit.getId()); } return result; } else { customerReturnVisit.setCreateUser(currentUserId); customerReturnVisit.setCreateTime(LocalDateTime.now()); customerReturnVisit.setTenantId(currentTenantId); int result = baseMapper.insert(customerReturnVisit); if (customerReturnVisit.getIsEnabled() == 1) { returnVisitReminderService.scheduleReminder(customerReturnVisit.getId()); } return result; } } @Override public CustomerReturnVisitDto getByCustomerId(Integer customerId) { if (customerId == null) { throw new ServiceException("客æ·IDä¸è½ä¸ºç©º"); } LambdaQueryWrapper<CustomerReturnVisit> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(CustomerReturnVisit::getCustomerId, customerId); CustomerReturnVisit returnVisit = baseMapper.selectOne(queryWrapper); if (returnVisit == null) { return null; } CustomerReturnVisitDto dto = new CustomerReturnVisitDto(); BeanUtils.copyProperties(returnVisit, dto); return dto; } @Override @Transactional(rollbackFor = Exception.class) public void deleteByCustomerId(Long customerId) { if (customerId == null) { throw new ServiceException("客æ·IDä¸è½ä¸ºç©º"); } LambdaQueryWrapper<CustomerReturnVisit> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(CustomerReturnVisit::getCustomerId, customerId); List<CustomerReturnVisit> returnVisits = baseMapper.selectList(queryWrapper); for (CustomerReturnVisit returnVisit : returnVisits) { returnVisitReminderService.cancelReminder(returnVisit.getId()); } baseMapper.delete(queryWrapper); } @Override @Transactional(rollbackFor = Exception.class) public void markAsRead(Long id) { if (id == null) { throw new ServiceException("å访æéIDä¸è½ä¸ºç©º"); } CustomerReturnVisit returnVisit = baseMapper.selectById(id); if (returnVisit == null) { throw new ServiceException("å访æéä¸åå¨"); } CustomerReturnVisit updateObj = new CustomerReturnVisit(); updateObj.setId(id); updateObj.setIsCompleted(1); baseMapper.updateById(updateObj); } private void validateReturnVisit(CustomerReturnVisit returnVisit) { if (returnVisit == null) { throw new ServiceException("å访æéæ°æ®ä¸è½ä¸ºç©º"); } if (returnVisit.getCustomerId() == null) { throw new ServiceException("客æ·IDä¸è½ä¸ºç©º"); } if (returnVisit.getReminderTime() == null) { throw new ServiceException("æéæ¶é´ä¸è½ä¸ºç©º"); } if (returnVisit.getIsEnabled() != null && returnVisit.getIsEnabled() == 1) { if (returnVisit.getReminderTime().isBefore(LocalDateTime.now())) { throw new ServiceException("æéæ¶é´ä¸è½æ©äºå½åæ¶é´"); } } } } src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -7,8 +7,15 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.basic.dto.CustomerDto; import com.ruoyi.basic.dto.CustomerFollowUpDto; import com.ruoyi.basic.mapper.CustomerMapper; import com.ruoyi.basic.pojo.Customer; import com.ruoyi.basic.pojo.CustomerFollowUp; import com.ruoyi.basic.pojo.CustomerFollowUpFile; import com.ruoyi.basic.service.CustomerFollowUpFileService; import com.ruoyi.basic.service.CustomerFollowUpService; import com.ruoyi.basic.service.CustomerReturnVisitService; import com.ruoyi.basic.service.ICustomerService; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; @@ -20,7 +27,9 @@ import com.ruoyi.sales.pojo.SalesLedger; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; @@ -44,6 +53,12 @@ private final SalesLedgerMapper salesLedgerMapper; private CustomerMapper customerMapper; private CustomerFollowUpService customerFollowUpService; private CustomerFollowUpFileService customerFollowUpFileService; private CustomerReturnVisitService customerReturnVisitService; /** * æ¥è¯¢å®¢æ·æ¡£æ¡ * @@ -56,13 +71,57 @@ } /** * æ¥è¯¢å®¢æ·è¯¦æ ï¼å«è·è¿è®°å½åéä»¶ï¼ * * @param id å®¢æ·æ¡£æ¡ä¸»é® * @return 客æ·è¯¦æ DTO */ @Override public CustomerDto selectCustomerDetailById(Long id) { Customer customer = customerMapper.selectById(id); if (customer == null) { return null; } CustomerDto dto = new CustomerDto(); BeanUtils.copyProperties(customer, dto); // æ¥è¯¢è·è¿è®°å½ List<CustomerFollowUp> followUpList = customerFollowUpService.list( new LambdaQueryWrapper<CustomerFollowUp>() .eq(CustomerFollowUp::getCustomerId, id) .orderByDesc(CustomerFollowUp::getFollowUpTime) ); if (!CollectionUtils.isEmpty(followUpList)) { List<CustomerFollowUpDto> followUpDtoList = followUpList.stream().map(followUp -> { CustomerFollowUpDto followUpDto = new CustomerFollowUpDto(); BeanUtils.copyProperties(followUp, followUpDto); // æ¥è¯¢éä»¶ List<CustomerFollowUpFile> fileList = customerFollowUpFileService.list( new LambdaQueryWrapper<CustomerFollowUpFile>() .eq(CustomerFollowUpFile::getFollowUpId, followUp.getId()) ); followUpDto.setFileList(fileList); return followUpDto; }).collect(Collectors.toList()); dto.setFollowUpList(followUpDtoList); } return dto; } /** * æ¥è¯¢å®¢æ·æ¡£æ¡å表 * * @param customer å®¢æ·æ¡£æ¡ * @return å®¢æ·æ¡£æ¡ */ @Override public IPage<Customer> selectCustomerList(Page page, Customer customer) { public IPage<Customer> selectCustomerList(Page<Customer> page, Customer customer) { // 1. å¤ç空å¼åºæ¯ï¼åæ°æ ¡éªï¼ if (page == null) { page = Page.of(1, 10); // é»è®¤ç¬¬1é¡µï¼æ¯é¡µ10æ¡æ°æ® @@ -92,12 +151,26 @@ // å®å ¨è·ååæ®µï¼é¿å null弿¼æ¥ String address = StringUtils.defaultString(c.getCompanyAddress(), ""); String phone = StringUtils.defaultString(c.getCompanyPhone(), ""); c.setAddressPhone(address + "(" + phone + ")"); // ä¼ååç¬¦ä¸²æ¼æ¥ c.setAddressPhone(address + "(" + phone + ")"); // æ¥è¯¢ææ°çè·è¿è®°å½ CustomerFollowUp followUp = customerFollowUpService.getOne( new LambdaQueryWrapper<CustomerFollowUp>() .eq(CustomerFollowUp::getCustomerId, c.getId()) .orderByDesc(CustomerFollowUp::getFollowUpTime) .last("LIMIT 1") ); if (followUp != null) { c.setFollowUpLevel(followUp.getFollowUpLevel()); c.setFollowUpTime(followUp.getFollowUpTime()); } }) .collect(Collectors.toList()); // 5. æ´æ°åé¡µç»æä¸çæ°æ®ï¼ä¿æå页信æ¯å®æ´ï¼ customerPage.setRecords(processedList); IPage<Customer> resultPage = new Page<>(customerPage.getCurrent(), customerPage.getSize(), customerPage.getTotal()); resultPage.setRecords(processedList); return customerPage; // è¿åå å«å页信æ¯çIPage对象 } @@ -137,12 +210,19 @@ * @return ç»æ */ @Override @Transactional(rollbackFor = Exception.class) public int deleteCustomerByIds(Long[] ids) { List<Long> idList = Arrays.asList(ids); List<SalesLedger> salesLedgers = salesLedgerMapper.selectList(new QueryWrapper<SalesLedger>().lambda().in(SalesLedger::getCustomerId, idList)); if (!salesLedgers.isEmpty()) { throw new RuntimeException("å®¢æ·æ¡£æ¡ä¸æéå®ååï¼è¯·å å é¤éå®åå"); } // å é¤å®¢æ·çåæ¶ä¹éè¦å é¤å¯¹åºç客æ·è·éãéä»¶åå访æé for (Long id : ids) { customerFollowUpService.deleteByCustomerId(id); customerReturnVisitService.deleteByCustomerId(id); } return customerMapper.deleteBatchIds(idList); } src/main/java/com/ruoyi/basic/service/impl/ReturnVisitReminderService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,53 @@ package com.ruoyi.basic.service.impl; import com.ruoyi.basic.pojo.CustomerReturnVisit; import com.ruoyi.basic.service.CustomerReturnVisitService; import com.ruoyi.framework.redis.RedisCache; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.ZoneId; /** * <br> * 客æ·å访æéæå¡ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/05 10:45 */ @Slf4j @Service public class ReturnVisitReminderService { private static final String REMINDER_QUEUE_KEY = "return_visit:reminder:queue"; @Autowired private RedisCache redisCache; @Autowired private CustomerReturnVisitService customerReturnVisitService; @SuppressWarnings("unchecked") public void scheduleReminder(Long returnVisitId) { CustomerReturnVisit returnVisit = customerReturnVisitService.getById(returnVisitId); if (returnVisit == null || returnVisit.getIsEnabled() == 0) { return; } long timestamp = returnVisit.getReminderTime() .atZone(ZoneId.systemDefault()) .toInstant() .toEpochMilli(); redisCache.redisTemplate.opsForZSet().add(REMINDER_QUEUE_KEY, returnVisitId, timestamp); log.info("已添å å访æéå°éå: ID={}, æéæ¶é´={}", returnVisitId, returnVisit.getReminderTime()); } @SuppressWarnings("unchecked") public void cancelReminder(Long returnVisitId) { redisCache.redisTemplate.opsForZSet().remove(REMINDER_QUEUE_KEY, returnVisitId); log.info("已忶å访æé: ID={}", returnVisitId); } } src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,92 @@ package com.ruoyi.basic.task; import com.ruoyi.basic.pojo.CustomerReturnVisit; import com.ruoyi.basic.service.CustomerReturnVisitService; import com.ruoyi.framework.redis.RedisCache; import com.ruoyi.project.system.domain.SysUserClient; import com.ruoyi.project.system.service.SysUserClientService; import com.ruoyi.project.system.service.impl.UnipushService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Set; /** * <br> * 客æ·å访æé宿¶ä»»å¡ * </br> * * @author deslrey * @version 1.0 * @since 2026/03/05 9:04 */ @Slf4j @Component public class ReturnVisitReminderTask { private static final String REMINDER_QUEUE_KEY = "return_visit:reminder:queue"; @Autowired private RedisCache redisCache; @Autowired private CustomerReturnVisitService customerReturnVisitService; @Autowired private UnipushService unipushService; @Autowired private SysUserClientService userClientService; @SuppressWarnings("unchecked") @Scheduled(fixedDelay = 60000) public void processReminders() { long now = System.currentTimeMillis(); Set<Object> dueReminders = redisCache.redisTemplate.opsForZSet().rangeByScore(REMINDER_QUEUE_KEY, 0, now); if (dueReminders == null || dueReminders.isEmpty()) { return; } for (Object obj : dueReminders) { Long returnVisitId = Long.valueOf(obj.toString()); try { Long removeCount = redisCache.redisTemplate.opsForZSet().remove(REMINDER_QUEUE_KEY, obj); if (removeCount != null && removeCount > 0) { processReminder(returnVisitId); } } catch (Exception e) { log.error("å¤çå访æé失败: ID={}", returnVisitId, e); } } } @SuppressWarnings("unchecked") private void processReminder(Long returnVisitId) { CustomerReturnVisit returnVisit = customerReturnVisitService.getById(returnVisitId); if (returnVisit == null || returnVisit.getIsEnabled() == 0 || returnVisit.getIsCompleted() == 1) { return; } SysUserClient client = userClientService.getById(returnVisit.getRemindUserId()); if (client == null || client.getCid() == null) { log.warn("ç¨æ·æªç»å®CID, æ æ³åéUnipushæ¨é: userId={}", returnVisit.getRemindUserId()); return; } try { unipushService.sendReturnVisitReminder(returnVisitId, client.getCid(), returnVisit.getContent(), returnVisit.getCustomerId()); CustomerReturnVisit updateObj = new CustomerReturnVisit(); updateObj.setId(returnVisitId); updateObj.setIsCompleted(1); customerReturnVisitService.updateById(updateObj); log.info("å访æéå·²éè¿ Unipush åé: ID={}", returnVisitId); } catch (Exception e) { log.error("åéå访æé失败ï¼éæ°å å ¥éå: ID={}", returnVisitId, e); long retryTime = System.currentTimeMillis() + 60000; redisCache.redisTemplate.opsForZSet().add(REMINDER_QUEUE_KEY, returnVisitId, retryTime); } } } src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java
@@ -80,16 +80,19 @@ log.warn("ç¨æ· {} æªç»å®ç§»å¨ç«¯ CID,è·³è¿æ¨é", sysNotice.getConsigneeId()); continue; } // 转æ¢è·¯å¾ String appPath = convertWebPathToAppPath(sysNotice.getJumpPath()); String content = sysNotice.getNoticeContent(); if (StringUtils.isNotEmpty(sysNotice.getRemark())) { content = content + " " + sysNotice.getRemark(); } // æ¨é sendRoutingPush( sysNotice.getNoticeId(), client.getCid(), sysNotice.getNoticeTitle(), sysNotice.getRemark() != null ? sysNotice.getRemark() : sysNotice.getNoticeContent(), content, appPath ); } @@ -118,31 +121,41 @@ } else { lastSegment = pathOnly; } if (StringUtils.isEmpty(lastSegment)) { return DEFAULT_APP_PAGE; } SysMenu menu = sysMenuMapper.selectMenuByPath(lastSegment); if (menu != null && StringUtils.isNotEmpty(menu.getAppComponent())) { String appPath = menu.getAppComponent(); if (appPath.startsWith("/")) { appPath = appPath.substring(1); } // æ¼æ¥ Web 端åå§åæ°å¹¶è¿å return appPath + queryString; } return DEFAULT_APP_PAGE; } /** * åéå访æé */ public void sendReturnVisitReminder(Long returnVisitId, String cid, String content, Integer customerId) { String targetPath = "pages/cooperativeOffice/customerManage/detail?customerId=" + customerId; sendRoutingPush(returnVisitId, cid, "客æ·å访æé", content, targetPath, false); } /** * åéåäººè·¯ç±æ¨é */ private void sendRoutingPush(Long noticeId, String cid, String title, String content, String targetPath) { sendRoutingPush(noticeId, cid, title, content, targetPath, true); } /** * åéåäººè·¯ç±æ¨é */ private void sendRoutingPush(Long noticeId, String cid, String title, String content, String targetPath, boolean needMarkRead) { log.info("å夿¨éæ¶æ¯:NoticeId={}, CID={}, Title={}, TargetPath={}", noticeId, cid, title, targetPath); PushDTO<Audience> pushDTO = new PushDTO<>(); @@ -156,6 +169,7 @@ pushMessageMap.put("content", content); payloadMap.put("url", targetPath); payloadMap.put("noticeId", noticeId); payloadMap.put("needMarkRead", needMarkRead); pushMessageMap.put("payload", JSON.toJSONString(payloadMap)); String transmissionContent = JSON.toJSONString(pushMessageMap);