28 changed files with 3848 additions and 384 deletions
@ -0,0 +1,383 @@ |
|||||
|
package com.example.web.aspect; |
||||
|
|
||||
|
import com.example.web.entity.Users; |
||||
|
import com.example.web.dto.ManagerAuthInfo; |
||||
|
import com.example.web.dto.UnifiedCustomerDTO; |
||||
|
import com.example.web.service.InformationTraService; |
||||
|
import com.example.web.service.CustomerService; |
||||
|
import com.example.web.service.SupplyCustomerService; |
||||
|
import org.aspectj.lang.JoinPoint; |
||||
|
import org.aspectj.lang.annotation.AfterReturning; |
||||
|
import org.aspectj.lang.annotation.Aspect; |
||||
|
import org.aspectj.lang.annotation.Before; |
||||
|
import org.aspectj.lang.annotation.Pointcut; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
/** |
||||
|
* 客户数据变更追踪拦截器 |
||||
|
*/ |
||||
|
@Aspect |
||||
|
@Component |
||||
|
public class CustomerDataChangeAspect { |
||||
|
|
||||
|
@Autowired |
||||
|
private InformationTraService informationTraService; |
||||
|
|
||||
|
@Autowired |
||||
|
private CustomerService customerService; |
||||
|
|
||||
|
@Autowired |
||||
|
private SupplyCustomerService supplyCustomerService; |
||||
|
|
||||
|
// 存储原始数据的临时缓存
|
||||
|
private ThreadLocal<Map<String, Object>> originalDataCache = new ThreadLocal<>(); |
||||
|
|
||||
|
// 定义切入点:客户修改相关的方法
|
||||
|
@Pointcut("execution(* com.example.web.service.CustomerService.update*(..)) || execution(* com.example.web.service.SupplyCustomerService.update*(..))") |
||||
|
public void customerUpdatePointcut() {} |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 修改前获取原始数据
|
||||
|
@Before("customerUpdatePointcut()") |
||||
|
public void beforeUpdate(JoinPoint joinPoint) { |
||||
|
// 获取方法参数
|
||||
|
Object[] args = joinPoint.getArgs(); |
||||
|
String customerId = null; |
||||
|
UnifiedCustomerDTO dto = null; |
||||
|
ManagerAuthInfo authInfo = null; |
||||
|
|
||||
|
// 遍历参数,查找UnifiedCustomerDTO和ManagerAuthInfo
|
||||
|
for (Object arg : args) { |
||||
|
if (arg instanceof UnifiedCustomerDTO) { |
||||
|
dto = (UnifiedCustomerDTO) arg; |
||||
|
customerId = dto.getId(); |
||||
|
} else if (arg instanceof ManagerAuthInfo) { |
||||
|
authInfo = (ManagerAuthInfo) arg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (customerId != null && dto != null) { |
||||
|
try { |
||||
|
System.out.println("📝 前置通知 - 获取原始数据,客户ID: " + customerId); |
||||
|
|
||||
|
// 如果没有找到ManagerAuthInfo,创建一个系统级的
|
||||
|
if (authInfo == null) { |
||||
|
authInfo = new ManagerAuthInfo("system", "system", "system", "system", "system", "system", "system"); |
||||
|
} |
||||
|
|
||||
|
// 获取原始数据 - 先尝试CustomerService,再尝试SupplyCustomerService
|
||||
|
UnifiedCustomerDTO originalDto = customerService.getCustomerById(customerId, authInfo); |
||||
|
|
||||
|
// 如果默认数据源没有,尝试wechat数据源
|
||||
|
if (originalDto == null) { |
||||
|
originalDto = customerService.getWechatCustomerById(customerId, authInfo); |
||||
|
} |
||||
|
|
||||
|
// 如果还是没有,尝试SupplyCustomerService
|
||||
|
if (originalDto == null) { |
||||
|
originalDto = supplyCustomerService.getCustomerById(customerId, authInfo); |
||||
|
if (originalDto == null) { |
||||
|
originalDto = supplyCustomerService.getWechatCustomerById(customerId, authInfo); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (originalDto != null) { |
||||
|
// 将原始数据转换为Map存储
|
||||
|
Map<String, Object> originalData = convertUnifiedCustomerDTOToMap(originalDto); |
||||
|
originalDataCache.set(originalData); |
||||
|
System.out.println("✅ 原始数据缓存成功 - 客户ID: " + customerId); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("❌ 前置通知执行失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 修改后记录变更
|
||||
|
@AfterReturning("customerUpdatePointcut()") |
||||
|
public void afterUpdate(JoinPoint joinPoint) { |
||||
|
// 获取方法参数
|
||||
|
Object[] args = joinPoint.getArgs(); |
||||
|
UnifiedCustomerDTO dto = null; |
||||
|
ManagerAuthInfo authInfo = null; |
||||
|
|
||||
|
// 遍历参数,查找UnifiedCustomerDTO和ManagerAuthInfo
|
||||
|
for (Object arg : args) { |
||||
|
if (arg instanceof UnifiedCustomerDTO) { |
||||
|
dto = (UnifiedCustomerDTO) arg; |
||||
|
} else if (arg instanceof ManagerAuthInfo) { |
||||
|
authInfo = (ManagerAuthInfo) arg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (dto != null && dto.getId() != null) { |
||||
|
try { |
||||
|
// 获取原始数据
|
||||
|
Map<String, Object> originalData = originalDataCache.get(); |
||||
|
if (originalData != null) { |
||||
|
// 如果没有找到ManagerAuthInfo,创建一个系统级的
|
||||
|
if (authInfo == null) { |
||||
|
authInfo = new ManagerAuthInfo("system", "system", "system", "system", "system", "system", "system"); |
||||
|
} |
||||
|
|
||||
|
// 从数据库中重新查询最新的数据 - 先尝试CustomerService,再尝试SupplyCustomerService
|
||||
|
UnifiedCustomerDTO modifiedDto = customerService.getCustomerById(dto.getId(), authInfo); |
||||
|
|
||||
|
// 如果默认数据源没有,尝试wechat数据源
|
||||
|
if (modifiedDto == null) { |
||||
|
modifiedDto = customerService.getWechatCustomerById(dto.getId(), authInfo); |
||||
|
} |
||||
|
|
||||
|
// 如果还是没有,尝试SupplyCustomerService
|
||||
|
if (modifiedDto == null) { |
||||
|
modifiedDto = supplyCustomerService.getCustomerById(dto.getId(), authInfo); |
||||
|
if (modifiedDto == null) { |
||||
|
modifiedDto = supplyCustomerService.getWechatCustomerById(dto.getId(), authInfo); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (modifiedDto != null) { |
||||
|
// 转换修改后的数据为Map
|
||||
|
Map<String, Object> modifiedData = convertUnifiedCustomerDTOToMap(modifiedDto); |
||||
|
// 比较并记录变更
|
||||
|
Map<String, Object> changedFields = compareData(originalData, modifiedData); |
||||
|
if (!changedFields.isEmpty()) { |
||||
|
System.out.println("📊 检测到数据变更: " + changedFields.keySet()); |
||||
|
|
||||
|
// 获取操作人信息
|
||||
|
String operatorId = authInfo.getManagerId() != null ? authInfo.getManagerId() : "system"; |
||||
|
String operatorName = authInfo.getUserName() != null ? authInfo.getUserName() : "系统"; |
||||
|
String company = authInfo.getManagercompany() != null ? authInfo.getManagercompany() : "系统"; |
||||
|
String department = authInfo.getManagerdepartment() != null ? authInfo.getManagerdepartment() : "系统"; |
||||
|
String organization = authInfo != null ? authInfo.getOrganization() : "系统"; |
||||
|
String role = authInfo != null ? authInfo.getRole() : "admin"; |
||||
|
String user = authInfo != null ? authInfo.getUserName() : "系统"; |
||||
|
String assistant = authInfo != null ? authInfo.getAssistant() : ""; |
||||
|
|
||||
|
// 记录变更到数据库 - 添加详细调试日志
|
||||
|
System.out.println("🔄 开始调用recordDetailedChange方法"); |
||||
|
System.out.println("📋 原始数据类型: " + (originalData != null ? originalData.getClass().getName() : "null")); |
||||
|
System.out.println("📋 修改后数据类型: " + (modifiedData != null ? modifiedData.getClass().getName() : "null")); |
||||
|
|
||||
|
try { |
||||
|
informationTraService.recordDetailedChange( |
||||
|
dto.getPhoneNumber(), |
||||
|
dto.getNickName(), |
||||
|
generateOperationEvent(changedFields), |
||||
|
company, |
||||
|
department, |
||||
|
organization, |
||||
|
role, |
||||
|
user, |
||||
|
assistant, |
||||
|
originalData, |
||||
|
modifiedData, |
||||
|
new ArrayList<>(changedFields.keySet()) |
||||
|
); |
||||
|
System.out.println("✅ 数据变更记录成功 - 客户ID: " + dto.getId()); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("❌ 调用recordDetailedChange方法失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} else { |
||||
|
System.out.println("ℹ️ 未检测到数据变更 - 客户ID: " + dto.getId()); |
||||
|
// 即使没有变更,也记录原始数据和修改后的数据,以便调试
|
||||
|
String operatorId = authInfo.getManagerId() != null ? authInfo.getManagerId() : "system"; |
||||
|
String operatorName = authInfo.getUserName() != null ? authInfo.getUserName() : "系统"; |
||||
|
String company = authInfo.getManagercompany() != null ? authInfo.getManagercompany() : "系统"; |
||||
|
String department = authInfo.getManagerdepartment() != null ? authInfo.getManagerdepartment() : "系统"; |
||||
|
String organization = authInfo != null ? authInfo.getOrganization() : "系统"; |
||||
|
String role = authInfo != null ? authInfo.getRole() : "admin"; |
||||
|
String user = authInfo != null ? authInfo.getUserName() : "系统"; |
||||
|
String assistant = authInfo != null ? authInfo.getAssistant() : ""; |
||||
|
|
||||
|
// 记录无变更数据 - 添加详细调试日志
|
||||
|
System.out.println("🔄 开始调用recordDetailedChange方法(无变更)"); |
||||
|
Map<String, Object> convertedModifiedDto = convertUnifiedCustomerDTOToMap(modifiedDto); |
||||
|
System.out.println("📋 原始数据类型: " + (originalData != null ? originalData.getClass().getName() : "null")); |
||||
|
System.out.println("📋 修改后数据类型: " + (convertedModifiedDto != null ? convertedModifiedDto.getClass().getName() : "null")); |
||||
|
|
||||
|
try { |
||||
|
informationTraService.recordDetailedChange( |
||||
|
dto.getPhoneNumber(), |
||||
|
dto.getNickName(), |
||||
|
"未检测到数据变更", |
||||
|
company, |
||||
|
department, |
||||
|
organization, |
||||
|
role, |
||||
|
user, |
||||
|
assistant, |
||||
|
originalData, |
||||
|
convertedModifiedDto, |
||||
|
new ArrayList<>() |
||||
|
); |
||||
|
System.out.println("✅ 无变更数据记录成功 - 客户ID: " + dto.getId()); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("❌ 调用recordDetailedChange方法(无变更)失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
System.out.println("❌ 无法获取修改后的数据 - 客户ID: " + dto.getId()); |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("❌ 数据变更记录失败: " + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} finally { |
||||
|
// 清除缓存
|
||||
|
originalDataCache.remove(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 将UnifiedCustomerDTO对象转换为Map
|
||||
|
private Map<String, Object> convertUnifiedCustomerDTOToMap(UnifiedCustomerDTO dto) { |
||||
|
Map<String, Object> map = new HashMap<>(); |
||||
|
map.put("id", dto.getId()); |
||||
|
map.put("company", dto.getCompany()); |
||||
|
map.put("region", dto.getRegion()); |
||||
|
map.put("level", dto.getLevel()); |
||||
|
map.put("type", dto.getType()); |
||||
|
map.put("demand", dto.getDemand()); |
||||
|
map.put("spec", dto.getSpec()); |
||||
|
map.put("nickName", dto.getNickName()); |
||||
|
map.put("phoneNumber", dto.getPhoneNumber()); |
||||
|
map.put("wechat", dto.getWechat()); |
||||
|
map.put("account", dto.getAccount()); |
||||
|
map.put("accountNumber", dto.getAccountNumber()); |
||||
|
map.put("bank", dto.getBank()); |
||||
|
map.put("address", dto.getAddress()); |
||||
|
map.put("managerId", dto.getManagerId()); |
||||
|
map.put("managercompany", dto.getManagercompany()); |
||||
|
map.put("managerdepartment", dto.getManagerdepartment()); |
||||
|
map.put("organization", dto.getOrganization()); |
||||
|
map.put("role", dto.getRole()); |
||||
|
map.put("userName", dto.getUserName()); |
||||
|
map.put("assistant", dto.getAssistant()); |
||||
|
map.put("created_at", dto.getCreated_at()); |
||||
|
map.put("updated_at", dto.getUpdated_at()); |
||||
|
map.put("productName", dto.getProductName()); |
||||
|
map.put("variety", dto.getVariety()); |
||||
|
map.put("specification", dto.getSpecification()); |
||||
|
map.put("quantity", dto.getQuantity()); |
||||
|
map.put("grossWeight", dto.getGrossWeight()); |
||||
|
map.put("yolk", dto.getYolk()); |
||||
|
map.put("dataSource", dto.getDataSource()); |
||||
|
return map; |
||||
|
} |
||||
|
|
||||
|
// 将Users对象转换为Map(备用方法)
|
||||
|
private Map<String, Object> convertUserToMap(Users user) { |
||||
|
Map<String, Object> map = new HashMap<>(); |
||||
|
map.put("id", user.getId()); |
||||
|
map.put("openid", user.getOpenid()); |
||||
|
map.put("userId", user.getUserId()); |
||||
|
map.put("nickName", user.getNickName()); |
||||
|
map.put("avatarUrl", user.getAvatarUrl()); |
||||
|
map.put("phoneNumber", user.getPhoneNumber()); |
||||
|
map.put("type", user.getType()); |
||||
|
map.put("gender", user.getGender()); |
||||
|
map.put("country", user.getCountry()); |
||||
|
map.put("province", user.getProvince()); |
||||
|
map.put("city", user.getCity()); |
||||
|
map.put("language", user.getLanguage()); |
||||
|
map.put("company", user.getCompany()); |
||||
|
map.put("region", user.getRegion()); |
||||
|
map.put("level", user.getLevel()); |
||||
|
map.put("demand", user.getDemand()); |
||||
|
map.put("spec", user.getSpec()); |
||||
|
map.put("followup", user.getFollowup()); |
||||
|
map.put("notice", user.getNotice()); |
||||
|
map.put("followup_at", user.getFollowup_at()); |
||||
|
map.put("updated_at", user.getUpdated_at()); |
||||
|
return map; |
||||
|
} |
||||
|
|
||||
|
// 比较原始数据和修改后的数据,返回变更的字段
|
||||
|
private Map<String, Object> compareData(Map<String, Object> original, Map<String, Object> modified) { |
||||
|
Map<String, Object> changes = new HashMap<>(); |
||||
|
|
||||
|
// 使用修改后的数据的key集合,确保覆盖所有可能变化的字段
|
||||
|
Set<String> allKeys = new HashSet<>(); |
||||
|
allKeys.addAll(original.keySet()); |
||||
|
allKeys.addAll(modified.keySet()); |
||||
|
|
||||
|
for (String key : allKeys) { |
||||
|
Object originalValue = original.get(key); |
||||
|
Object modifiedValue = modified.get(key); |
||||
|
|
||||
|
// 跳过不需要比较的字段
|
||||
|
if ("created_at".equals(key) || "updated_at".equals(key) || "id".equals(key) || "userId".equals(key)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (originalValue == null && modifiedValue != null) { |
||||
|
// 从null变为有值
|
||||
|
Map<String, Object> fieldChange = new HashMap<>(); |
||||
|
fieldChange.put("original", null); |
||||
|
fieldChange.put("modified", modifiedValue); |
||||
|
changes.put(key, fieldChange); |
||||
|
} else if (originalValue != null && modifiedValue == null) { |
||||
|
// 从有值变为null
|
||||
|
Map<String, Object> fieldChange = new HashMap<>(); |
||||
|
fieldChange.put("original", originalValue); |
||||
|
fieldChange.put("modified", null); |
||||
|
changes.put(key, fieldChange); |
||||
|
} else if (originalValue != null && !originalValue.equals(modifiedValue)) { |
||||
|
// 值发生了变化
|
||||
|
Map<String, Object> fieldChange = new HashMap<>(); |
||||
|
fieldChange.put("original", originalValue); |
||||
|
fieldChange.put("modified", modifiedValue); |
||||
|
changes.put(key, fieldChange); |
||||
|
} |
||||
|
} |
||||
|
return changes; |
||||
|
} |
||||
|
|
||||
|
// 生成操作事件描述
|
||||
|
private String generateOperationEvent(Map<String, Object> changedFields) { |
||||
|
StringBuilder sb = new StringBuilder("修改了客户信息:"); |
||||
|
for (String fieldName : changedFields.keySet()) { |
||||
|
// 转换为中文字段名
|
||||
|
String fieldLabel = getFieldLabel(fieldName); |
||||
|
sb.append(fieldLabel).append("、"); |
||||
|
} |
||||
|
// 移除最后一个顿号
|
||||
|
if (sb.length() > 5) { |
||||
|
sb.deleteCharAt(sb.length() - 1); |
||||
|
} |
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
// 字段名映射
|
||||
|
private String getFieldLabel(String fieldName) { |
||||
|
Map<String, String> fieldMap = new HashMap<>(); |
||||
|
fieldMap.put("nickName", "客户昵称"); |
||||
|
fieldMap.put("phoneNumber", "客户手机号"); |
||||
|
fieldMap.put("company", "客户公司"); |
||||
|
fieldMap.put("region", "客户地区"); |
||||
|
fieldMap.put("level", "客户等级"); |
||||
|
fieldMap.put("demand", "客户需求"); |
||||
|
fieldMap.put("spec", "客户规格"); |
||||
|
fieldMap.put("followup", "跟进信息"); |
||||
|
fieldMap.put("notice", "通知状态"); |
||||
|
fieldMap.put("followup_at", "跟进时间"); |
||||
|
fieldMap.put("updated_at", "更新时间"); |
||||
|
// 可以继续添加其他字段映射
|
||||
|
|
||||
|
return fieldMap.getOrDefault(fieldName, fieldName); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.example.web.service; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.scheduling.annotation.Scheduled; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
/** |
||||
|
* 客户回流定时任务服务 |
||||
|
* 定时检查客户回流情况 |
||||
|
*/ |
||||
|
@Service |
||||
|
@RequiredArgsConstructor |
||||
|
public class CustomerReturnTaskService { |
||||
|
|
||||
|
private static final Logger log = LoggerFactory.getLogger(CustomerReturnTaskService.class); |
||||
|
|
||||
|
@Autowired |
||||
|
private CustomerService customerService; |
||||
|
|
||||
|
/** |
||||
|
* 客户回流检查定时任务 - 每天凌晨1点执行 |
||||
|
*/ |
||||
|
@Scheduled(cron = "0 0 1 * * ?") |
||||
|
public void checkCustomerReturnFlowDaily() { |
||||
|
log.info("🎯 开始执行客户回流检查任务..."); |
||||
|
try { |
||||
|
customerService.checkCustomerReturnFlow(); |
||||
|
log.info("✅ 客户回流检查任务执行完成"); |
||||
|
} catch (Exception e) { |
||||
|
log.error("❌ 客户回流检查任务执行失败", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,30 @@ |
|||||
|
@echo off |
||||
|
setlocal |
||||
|
|
||||
|
echo === 测试客户信息更新API === |
||||
|
|
||||
|
set BASE_URL=http://localhost:8081/DL |
||||
|
set PHONE=17780155537 |
||||
|
set NEW_NAME=TestCustomerUpdate |
||||
|
|
||||
|
echo 🔄 正在调用更新客户信息API... |
||||
|
|
||||
|
curl -X POST ^ |
||||
|
"%BASE_URL%/supplyCustomer/updateCustomer" ^ |
||||
|
-H "Content-Type: application/json" ^ |
||||
|
-d "{ |
||||
|
\"phoneNumber\": \"%PHONE%\", |
||||
|
\"nickName\": \"%NEW_NAME%\", |
||||
|
\"type\": \"seller\", |
||||
|
\"dataSource\": \"wechat\", |
||||
|
\"managercompany\": \"测试公司\", |
||||
|
\"managerdepartment\": \"测试部门\", |
||||
|
\"organization\": \"测试组织\", |
||||
|
\"role\": \"测试角色\", |
||||
|
\"userName\": \"测试用户\", |
||||
|
\"assistant\": \"测试助手\" |
||||
|
}" |
||||
|
|
||||
|
echo. |
||||
|
echo === 测试完成 === |
||||
|
endlocal |
||||
@ -0,0 +1,16 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
<mapper namespace="com.example.web.mapper.UsersMapper"> |
||||
|
<select id="selectCustomersForReturnFlowCheck" resultType="com.example.web.entity.Users"> |
||||
|
SELECT user_id as userId, phone_number as phoneNumber, level, updated_at, followup_at FROM users WHERE updated_at >= DATE_SUB(NOW(), INTERVAL 3 DAY) AND (followup_at IS NULL OR followup_at < DATE_SUB(NOW(), INTERVAL 3 DAY)) AND level != 'organization-sea-pools' AND deleted = 0 |
||||
|
</select> |
||||
|
<update id="updateByUserId" parameterType="com.example.web.entity.Users"> |
||||
|
UPDATE users |
||||
|
<set> |
||||
|
<if test="level != null">level = #{level},</if> |
||||
|
<if test="updated_at != null">updated_at = #{updated_at},</if> |
||||
|
<if test="followup_at != null">followup_at = #{followup_at},</if> |
||||
|
</set> |
||||
|
WHERE user_id = #{userId} |
||||
|
</update> |
||||
|
</mapper> |
||||
Binary file not shown.
@ -0,0 +1,798 @@ |
|||||
|
# 客户数据变更追踪详细方案 |
||||
|
|
||||
|
## 一、需求分析 |
||||
|
|
||||
|
### 1.1 核心需求 |
||||
|
- 实现客户数据的详细变更追踪功能 |
||||
|
- 记录业务员修改客户的哪些字段数据 |
||||
|
- 详细展示原始数据和修改后的数据 |
||||
|
- 基于现有的 `informationtra` 表进行扩展 |
||||
|
- 实现客户回流功能,根据跟进情况自动调整客户等级 |
||||
|
|
||||
|
### 1.2 客户回流需求 |
||||
|
- 在 `users` 表中添加 `followup_at`(跟进时间)字段 |
||||
|
- `followup_at` 默认为 `updated_at` 的时间,只记录第一次之后便不更改 |
||||
|
- 与数据库 `updated_at` 字段做对比,判断客户是否需要回流 |
||||
|
- 回流条件:三天内 `updated_at` 发生变化但 `followup_at` 没有发生变化 |
||||
|
- 回流操作:将客户等级变为 `organization-sea-pools` |
||||
|
- 提交跟进时需要更新 `followup_at` |
||||
|
- 客户回流操作需要被记录到信息跟踪表中 |
||||
|
|
||||
|
### 1.2 技术现状 |
||||
|
- 已存在 `informationtra` 表用于记录操作跟踪 |
||||
|
- 已存在 `Users` 实体类表示客户信息 |
||||
|
- 已存在 `InformationTraService` 用于记录操作事件 |
||||
|
- 需要实现字段级别的数据变更追踪 |
||||
|
|
||||
|
## 二、方案设计 |
||||
|
|
||||
|
### 2.1 架构设计 |
||||
|
|
||||
|
``` |
||||
|
┌─────────────────┐ ┌───────────────────────┐ ┌─────────────────────┐ |
||||
|
│ 客户修改请求 │────▶│ 数据变更追踪拦截器 │────▶│ InformationTraService │ |
||||
|
└─────────────────┘ └───────────────────────┘ └─────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────┐ |
||||
|
│ informationtra表 │ |
||||
|
└─────────────────────┘ |
||||
|
|
||||
|
┌─────────────────┐ ┌───────────────────────┐ ┌─────────────────────┐ |
||||
|
│ 定时任务触发 │────▶│ 客户回流检查逻辑 │────▶│ CustomerService │ |
||||
|
└─────────────────┘ └───────────────────────┘ └─────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────┐ |
||||
|
│ users表更新 │ |
||||
|
└─────────────────────┘ |
||||
|
│ |
||||
|
▼ |
||||
|
┌─────────────────────┐ |
||||
|
│ informationtra表 │ |
||||
|
└─────────────────────┘ |
||||
|
``` |
||||
|
|
||||
|
### 2.2 数据结构设计 |
||||
|
|
||||
|
#### 2.2.1 现有 `informationtra` 表结构 |
||||
|
```sql |
||||
|
CREATE TABLE `informationtra` ( |
||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', |
||||
|
`tracompany` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改者公司', |
||||
|
`tradepartment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改者部门', |
||||
|
`traorganization` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改者组织', |
||||
|
`trarole` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改者角色', |
||||
|
`trauserName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改者名字', |
||||
|
`traassistant` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '修改协助人', |
||||
|
`userId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户ID', |
||||
|
`operationEvent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '操作事件', |
||||
|
`operationTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', |
||||
|
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
||||
|
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', |
||||
|
PRIMARY KEY (`id`), |
||||
|
KEY `idx_userId` (`userId`), |
||||
|
KEY `idx_operationTime` (`operationTime`), |
||||
|
KEY `idx_trauserName` (`trauserName`) |
||||
|
) ENGINE=InnoDB AUTO_INCREMENT=258 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='信息跟踪表'; |
||||
|
``` |
||||
|
|
||||
|
#### 2.2.2 `users` 表结构修改(添加followup_at字段) |
||||
|
```sql |
||||
|
ALTER TABLE `users` |
||||
|
ADD COLUMN `followup_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '跟进时间(只记录第一次)'; |
||||
|
``` |
||||
|
|
||||
|
#### 2.2.2 扩展设计 |
||||
|
|
||||
|
**方案1:在现有表中扩展字段** |
||||
|
```sql |
||||
|
ALTER TABLE `informationtra` |
||||
|
ADD COLUMN `originalData` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '原始数据JSON', |
||||
|
ADD COLUMN `modifiedData` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '修改后数据JSON', |
||||
|
ADD COLUMN `changedFields` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '变更字段列表JSON'; |
||||
|
``` |
||||
|
|
||||
|
**方案2:创建新的字段变更记录表** |
||||
|
```sql |
||||
|
CREATE TABLE `informationtra_fields` ( |
||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', |
||||
|
`traId` int NOT NULL COMMENT '关联informationtra表ID', |
||||
|
`fieldName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字段名', |
||||
|
`fieldLabel` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字段中文名称', |
||||
|
`originalValue` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '原始值', |
||||
|
`modifiedValue` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '修改后的值', |
||||
|
PRIMARY KEY (`id`), |
||||
|
KEY `idx_traId` (`traId`), |
||||
|
KEY `idx_fieldName` (`fieldName`) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='信息跟踪字段变更表'; |
||||
|
``` |
||||
|
**推荐方案**:方案1,直接在现有表中扩展字段,实现简单,查询方便。 |
||||
|
|
||||
|
## 三、核心功能实现 |
||||
|
|
||||
|
### 3.1 数据变更追踪拦截器 |
||||
|
|
||||
|
```java |
||||
|
package com.example.web.aspect; |
||||
|
|
||||
|
import com.example.web.entity.Users; |
||||
|
import com.example.web.service.InformationTraService; |
||||
|
import org.aspectj.lang.JoinPoint; |
||||
|
import org.aspectj.lang.annotation.AfterReturning; |
||||
|
import org.aspectj.lang.annotation.Aspect; |
||||
|
import org.aspectj.lang.annotation.Before; |
||||
|
import org.aspectj.lang.annotation.Pointcut; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 客户数据变更追踪拦截器 |
||||
|
*/ |
||||
|
@Aspect |
||||
|
@Component |
||||
|
public class CustomerDataChangeAspect { |
||||
|
|
||||
|
@Autowired |
||||
|
private InformationTraService informationTraService; |
||||
|
|
||||
|
@Autowired |
||||
|
private CustomerService customerService; |
||||
|
|
||||
|
// 存储原始数据的临时缓存 |
||||
|
private ThreadLocal<Map<String, Object>> originalDataCache = new ThreadLocal<>(); |
||||
|
|
||||
|
// 定义切入点:客户修改相关的方法 |
||||
|
@Pointcut("execution(* com.example.web.service.CustomerService.update*(..))") |
||||
|
public void customerUpdatePointcut() {} |
||||
|
|
||||
|
// 定义切入点:跟进信息更新方法 |
||||
|
@Pointcut("execution(* com.example.web.service.CustomerService.submitFollowup*(..))") |
||||
|
public void followupSubmitPointcut() {} |
||||
|
|
||||
|
// 修改前获取原始数据 |
||||
|
@Before("customerUpdatePointcut() || followupSubmitPointcut()") |
||||
|
public void beforeUpdate(JoinPoint joinPoint) { |
||||
|
// 获取方法参数 |
||||
|
Object[] args = joinPoint.getArgs(); |
||||
|
for (Object arg : args) { |
||||
|
if (arg instanceof Users) { |
||||
|
Users user = (Users) arg; |
||||
|
// 这里需要根据userId查询数据库获取原始数据 |
||||
|
Users originalUser = customerService.selectById(user.getUserId()); |
||||
|
if (originalUser != null) { |
||||
|
// 将原始数据转换为Map存储 |
||||
|
Map<String, Object> originalData = convertUserToMap(originalUser); |
||||
|
originalDataCache.set(originalData); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 修改后记录变更 |
||||
|
@AfterReturning("customerUpdatePointcut()") |
||||
|
public void afterUpdate(JoinPoint joinPoint) { |
||||
|
// 获取修改后的数据 |
||||
|
Object[] args = joinPoint.getArgs(); |
||||
|
for (Object arg : args) { |
||||
|
if (arg instanceof Users) { |
||||
|
Users modifiedUser = (Users) arg; |
||||
|
// 获取原始数据 |
||||
|
Map<String, Object> originalData = originalDataCache.get(); |
||||
|
if (originalData != null) { |
||||
|
// 转换修改后的数据为Map |
||||
|
Map<String, Object> modifiedData = convertUserToMap(modifiedUser); |
||||
|
// 比较并记录变更 |
||||
|
Map<String, Object> changedFields = compareData(originalData, modifiedData); |
||||
|
if (!changedFields.isEmpty()) { |
||||
|
// 记录变更到数据库 |
||||
|
informationTraService.recordDetailedChange( |
||||
|
originalData, |
||||
|
modifiedData, |
||||
|
changedFields, |
||||
|
modifiedUser.getUserId() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
// 清除缓存 |
||||
|
originalDataCache.remove(); |
||||
|
} |
||||
|
|
||||
|
// 跟进提交后更新followup_at并记录变更 |
||||
|
@AfterReturning("followupSubmitPointcut()") |
||||
|
public void afterFollowupSubmit(JoinPoint joinPoint) { |
||||
|
// 获取方法参数 |
||||
|
Object[] args = joinPoint.getArgs(); |
||||
|
for (Object arg : args) { |
||||
|
if (arg instanceof Users) { |
||||
|
Users modifiedUser = (Users) arg; |
||||
|
// 获取原始数据 |
||||
|
Map<String, Object> originalData = originalDataCache.get(); |
||||
|
if (originalData != null) { |
||||
|
// 转换修改后的数据为Map(包含更新后的followup_at) |
||||
|
Map<String, Object> modifiedData = convertUserToMap(modifiedUser); |
||||
|
// 比较并记录变更 |
||||
|
Map<String, Object> changedFields = compareData(originalData, modifiedData); |
||||
|
if (!changedFields.isEmpty()) { |
||||
|
// 记录变更到数据库 |
||||
|
informationTraService.recordDetailedChange( |
||||
|
originalData, |
||||
|
modifiedData, |
||||
|
changedFields, |
||||
|
modifiedUser.getUserId() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
// 清除缓存 |
||||
|
originalDataCache.remove(); |
||||
|
} |
||||
|
|
||||
|
// 将Users对象转换为Map |
||||
|
private Map<String, Object> convertUserToMap(Users user) { |
||||
|
Map<String, Object> map = new HashMap<>(); |
||||
|
map.put("id", user.getId()); |
||||
|
map.put("openid", user.getOpenid()); |
||||
|
map.put("userId", user.getUserId()); |
||||
|
map.put("nickName", user.getNickName()); |
||||
|
map.put("avatarUrl", user.getAvatarUrl()); |
||||
|
map.put("phoneNumber", user.getPhoneNumber()); |
||||
|
map.put("type", user.getType()); |
||||
|
map.put("gender", user.getGender()); |
||||
|
map.put("country", user.getCountry()); |
||||
|
map.put("province", user.getProvince()); |
||||
|
map.put("city", user.getCity()); |
||||
|
map.put("language", user.getLanguage()); |
||||
|
map.put("company", user.getCompany()); |
||||
|
map.put("region", user.getRegion()); |
||||
|
map.put("level", user.getLevel()); |
||||
|
map.put("demand", user.getDemand()); |
||||
|
map.put("spec", user.getSpec()); |
||||
|
map.put("followup", user.getFollowup()); |
||||
|
map.put("notice", user.getNotice()); |
||||
|
map.put("followup_at", user.getFollowupAt()); |
||||
|
map.put("updated_at", user.getUpdated_at()); |
||||
|
return map; |
||||
|
} |
||||
|
|
||||
|
// 比较原始数据和修改后的数据,返回变更的字段 |
||||
|
private Map<String, Object> compareData(Map<String, Object> original, Map<String, Object> modified) { |
||||
|
Map<String, Object> changes = new HashMap<>(); |
||||
|
for (String key : original.keySet()) { |
||||
|
Object originalValue = original.get(key); |
||||
|
Object modifiedValue = modified.get(key); |
||||
|
if (originalValue == null && modifiedValue != null) { |
||||
|
// 从null变为有值 |
||||
|
Map<String, Object> fieldChange = new HashMap<>(); |
||||
|
fieldChange.put("original", null); |
||||
|
fieldChange.put("modified", modifiedValue); |
||||
|
changes.put(key, fieldChange); |
||||
|
} else if (originalValue != null && modifiedValue == null) { |
||||
|
// 从有值变为null |
||||
|
Map<String, Object> fieldChange = new HashMap<>(); |
||||
|
fieldChange.put("original", originalValue); |
||||
|
fieldChange.put("modified", null); |
||||
|
changes.put(key, fieldChange); |
||||
|
} else if (originalValue != null && !originalValue.equals(modifiedValue)) { |
||||
|
// 值发生了变化 |
||||
|
Map<String, Object> fieldChange = new HashMap<>(); |
||||
|
fieldChange.put("original", originalValue); |
||||
|
fieldChange.put("modified", modifiedValue); |
||||
|
changes.put(key, fieldChange); |
||||
|
} |
||||
|
} |
||||
|
return changes; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 3.2 InformationTraService 扩展 |
||||
|
|
||||
|
```java |
||||
|
// 添加详细变更记录方法 |
||||
|
public boolean recordDetailedChange( |
||||
|
Map<String, Object> originalData, |
||||
|
Map<String, Object> modifiedData, |
||||
|
Map<String, Object> changedFields, |
||||
|
String userId) { |
||||
|
try { |
||||
|
// 获取当前登录用户信息(需要从上下文获取) |
||||
|
String tracompany = "当前公司"; |
||||
|
String tradepartment = "当前部门"; |
||||
|
String traorganization = "当前组织"; |
||||
|
String trarole = "当前角色"; |
||||
|
String trauserName = "当前用户"; |
||||
|
String traassistant = "当前协助人"; |
||||
|
LocalDateTime now = LocalDateTime.now(); |
||||
|
|
||||
|
// 构造信息跟踪记录 |
||||
|
InformationTra informationTra = new InformationTra(); |
||||
|
informationTra.setTracompany(tracompany); |
||||
|
informationTra.setTradepartment(tradepartment); |
||||
|
informationTra.setTraorganization(traorganization); |
||||
|
informationTra.setTrarole(trarole); |
||||
|
informationTra.setTrauserName(trauserName); |
||||
|
informationTra.setTraassistant(traassistant); |
||||
|
informationTra.setUserId(userId); |
||||
|
|
||||
|
// 生成操作事件描述 |
||||
|
String operationEvent = generateOperationEvent(changedFields); |
||||
|
informationTra.setOperationEvent(operationEvent); |
||||
|
|
||||
|
// 转换为JSON字符串存储 |
||||
|
ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
String originalDataJson = objectMapper.writeValueAsString(originalData); |
||||
|
String modifiedDataJson = objectMapper.writeValueAsString(modifiedData); |
||||
|
String changedFieldsJson = objectMapper.writeValueAsString(changedFields); |
||||
|
|
||||
|
informationTra.setOriginalData(originalDataJson); |
||||
|
informationTra.setModifiedData(modifiedDataJson); |
||||
|
informationTra.setChangedFields(changedFieldsJson); |
||||
|
|
||||
|
informationTra.setOperationTime(now); |
||||
|
informationTra.setCreatedAt(now); |
||||
|
informationTra.setUpdatedAt(now); |
||||
|
|
||||
|
// 写入数据库 |
||||
|
DynamicDataSource.setDataSourceKey("wechat"); |
||||
|
int result = informationTraMapper.insertInformationTra(informationTra); |
||||
|
return result > 0; |
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 记录客户回流操作 |
||||
|
public boolean recordCustomerReturn(String userId, Map<String, Object> originalData, Map<String, Object> modifiedData) { |
||||
|
try { |
||||
|
// 获取系统用户信息 |
||||
|
String tracompany = "系统自动"; |
||||
|
String tradepartment = "系统自动"; |
||||
|
String traorganization = "系统自动"; |
||||
|
String trarole = "系统自动"; |
||||
|
String trauserName = "系统自动"; |
||||
|
String traassistant = "系统自动"; |
||||
|
LocalDateTime now = LocalDateTime.now(); |
||||
|
|
||||
|
// 构造信息跟踪记录 |
||||
|
InformationTra informationTra = new InformationTra(); |
||||
|
informationTra.setTracompany(tracompany); |
||||
|
informationTra.setTradepartment(tradepartment); |
||||
|
informationTra.setTraorganization(traorganization); |
||||
|
informationTra.setTrarole(trarole); |
||||
|
informationTra.setTrauserName(trauserName); |
||||
|
informationTra.setTraassistant(traassistant); |
||||
|
informationTra.setUserId(userId); |
||||
|
|
||||
|
// 生成操作事件描述 |
||||
|
String operationEvent = "客户回流:由于三天内有数据更新但未跟进,客户等级变为organization-sea-pools"; |
||||
|
informationTra.setOperationEvent(operationEvent); |
||||
|
|
||||
|
// 转换为JSON字符串存储 |
||||
|
ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
String originalDataJson = objectMapper.writeValueAsString(originalData); |
||||
|
String modifiedDataJson = objectMapper.writeValueAsString(modifiedData); |
||||
|
|
||||
|
// 构造变更字段 |
||||
|
Map<String, Object> changedFields = new HashMap<>(); |
||||
|
Map<String, Object> levelChange = new HashMap<>(); |
||||
|
levelChange.put("original", originalData.get("level")); |
||||
|
levelChange.put("modified", "organization-sea-pools"); |
||||
|
changedFields.put("level", levelChange); |
||||
|
|
||||
|
String changedFieldsJson = objectMapper.writeValueAsString(changedFields); |
||||
|
|
||||
|
informationTra.setOriginalData(originalDataJson); |
||||
|
informationTra.setModifiedData(modifiedDataJson); |
||||
|
informationTra.setChangedFields(changedFieldsJson); |
||||
|
|
||||
|
informationTra.setOperationTime(now); |
||||
|
informationTra.setCreatedAt(now); |
||||
|
informationTra.setUpdatedAt(now); |
||||
|
|
||||
|
// 写入数据库 |
||||
|
DynamicDataSource.setDataSourceKey("wechat"); |
||||
|
int result = informationTraMapper.insertInformationTra(informationTra); |
||||
|
return result > 0; |
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 生成操作事件描述 |
||||
|
private String generateOperationEvent(Map<String, Object> changedFields) { |
||||
|
StringBuilder sb = new StringBuilder("修改了客户信息:"); |
||||
|
for (String fieldName : changedFields.keySet()) { |
||||
|
// 转换为中文字段名 |
||||
|
String fieldLabel = getFieldLabel(fieldName); |
||||
|
sb.append(fieldLabel).append("、"); |
||||
|
} |
||||
|
// 移除最后一个顿号 |
||||
|
if (sb.length() > 5) { |
||||
|
sb.deleteCharAt(sb.length() - 1); |
||||
|
} |
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
// 字段名映射 |
||||
|
private String getFieldLabel(String fieldName) { |
||||
|
Map<String, String> fieldMap = new HashMap<>(); |
||||
|
fieldMap.put("nickName", "客户昵称"); |
||||
|
fieldMap.put("phoneNumber", "客户手机号"); |
||||
|
fieldMap.put("company", "客户公司"); |
||||
|
fieldMap.put("region", "客户地区"); |
||||
|
fieldMap.put("level", "客户等级"); |
||||
|
fieldMap.put("demand", "客户需求"); |
||||
|
fieldMap.put("spec", "客户规格"); |
||||
|
fieldMap.put("followup", "跟进信息"); |
||||
|
fieldMap.put("notice", "通知状态"); |
||||
|
fieldMap.put("followup_at", "跟进时间"); |
||||
|
fieldMap.put("updated_at", "更新时间"); |
||||
|
// 可以继续添加其他字段映射 |
||||
|
|
||||
|
return fieldMap.getOrDefault(fieldName, fieldName); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 3.3 InformationTra 实体类扩展 |
||||
|
|
||||
|
```java |
||||
|
// 添加三个新字段 |
||||
|
private String originalData; // 原始数据JSON |
||||
|
private String modifiedData; // 修改后数据JSON |
||||
|
private String changedFields; // 变更字段列表JSON |
||||
|
|
||||
|
// 添加对应的getter和setter方法 |
||||
|
public String getOriginalData() { |
||||
|
return originalData; |
||||
|
} |
||||
|
|
||||
|
public void setOriginalData(String originalData) { |
||||
|
this.originalData = originalData; |
||||
|
} |
||||
|
|
||||
|
public String getModifiedData() { |
||||
|
return modifiedData; |
||||
|
} |
||||
|
|
||||
|
public void setModifiedData(String modifiedData) { |
||||
|
this.modifiedData = modifiedData; |
||||
|
} |
||||
|
|
||||
|
public String getChangedFields() { |
||||
|
return changedFields; |
||||
|
} |
||||
|
|
||||
|
public void setChangedFields(String changedFields) { |
||||
|
this.changedFields = changedFields; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 3.4 InformationTraMapper 扩展 |
||||
|
|
||||
|
```xml |
||||
|
<!-- 在XML映射文件中添加三个新字段 --> |
||||
|
<insert id="insertInformationTra" parameterType="com.example.web.entity.InformationTra"> |
||||
|
INSERT INTO informationtra ( |
||||
|
tracompany, tradepartment, traorganization, trarole, trauserName, |
||||
|
traassistant, userId, operationEvent, operationTime, created_at, |
||||
|
updated_at, originalData, modifiedData, changedFields |
||||
|
) VALUES ( |
||||
|
#{tracompany}, #{tradepartment}, #{traorganization}, #{trarole}, #{trauserName}, |
||||
|
#{traassistant}, #{userId}, #{operationEvent}, #{operationTime}, #{createdAt}, |
||||
|
#{updatedAt}, #{originalData}, #{modifiedData}, #{changedFields} |
||||
|
) |
||||
|
</insert> |
||||
|
``` |
||||
|
|
||||
|
### 3.5 客户回流功能实现 |
||||
|
|
||||
|
#### 3.5.1 Users实体类扩展 |
||||
|
|
||||
|
```java |
||||
|
// 添加followup_at字段 |
||||
|
private LocalDateTime followup_at; // 跟进时间(只记录第一次) |
||||
|
|
||||
|
// 添加对应的getter和setter方法 |
||||
|
public LocalDateTime getFollowup_at() { |
||||
|
return followup_at; |
||||
|
} |
||||
|
|
||||
|
public void setFollowup_at(LocalDateTime followup_at) { |
||||
|
this.followup_at = followup_at; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 3.5.2 CustomerService 回流功能实现 |
||||
|
|
||||
|
```java |
||||
|
// 提交跟进信息并更新followup_at |
||||
|
public boolean submitFollowup(Users user) { |
||||
|
try { |
||||
|
// 查询当前用户信息 |
||||
|
Users currentUser = this.selectById(user.getUserId()); |
||||
|
if (currentUser == null) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 设置更新时间 |
||||
|
LocalDateTime now = LocalDateTime.now(); |
||||
|
user.setUpdated_at(now); |
||||
|
|
||||
|
// 如果是第一次提交跟进或者followup_at为空,则设置followup_at |
||||
|
if (currentUser.getFollowup_at() == null) { |
||||
|
user.setFollowup_at(now); |
||||
|
} |
||||
|
|
||||
|
// 更新数据库 |
||||
|
DynamicDataSource.setDataSourceKey("wechat"); |
||||
|
int result = usersMapper.update(user); |
||||
|
return result > 0; |
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查并处理客户回流 |
||||
|
@Scheduled(cron = "0 0 0 * * ?") // 每天凌晨执行 |
||||
|
public void checkCustomerReturn() { |
||||
|
try { |
||||
|
DynamicDataSource.setDataSourceKey("wechat"); |
||||
|
// 查询所有用户 |
||||
|
List<Users> users = usersMapper.selectAll(); |
||||
|
|
||||
|
LocalDateTime now = LocalDateTime.now(); |
||||
|
LocalDateTime threeDaysAgo = now.minusDays(3); |
||||
|
|
||||
|
for (Users user : users) { |
||||
|
// 跳过空记录 |
||||
|
if (user == null || user.getUserId() == null) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 获取用户的updated_at和followup_at |
||||
|
LocalDateTime updatedAt = user.getUpdated_at(); |
||||
|
LocalDateTime followupAt = user.getFollowup_at(); |
||||
|
|
||||
|
// 如果followup_at为空,使用创建时间作为初始值 |
||||
|
if (followupAt == null) { |
||||
|
followupAt = user.getCreated_at(); |
||||
|
user.setFollowup_at(followupAt); |
||||
|
usersMapper.update(user); |
||||
|
} |
||||
|
|
||||
|
// 检查条件:三天内updated_at发生变化,但followup_at没有发生变化 |
||||
|
if (updatedAt != null && followupAt != null) { |
||||
|
// 判断updated_at是否在三天内 |
||||
|
if (updatedAt.isAfter(threeDaysAgo)) { |
||||
|
// 判断followup_at是否在三天内(如果在三天内说明有跟进) |
||||
|
if (!followupAt.isAfter(threeDaysAgo)) { |
||||
|
// 符合回流条件,将客户等级变为organization-sea-pools |
||||
|
Users originalUser = new Users(); |
||||
|
BeanUtils.copyProperties(user, originalUser); |
||||
|
|
||||
|
// 更新客户等级 |
||||
|
user.setLevel("organization-sea-pools"); |
||||
|
user.setUpdated_at(now); |
||||
|
|
||||
|
// 保存更新 |
||||
|
int result = usersMapper.update(user); |
||||
|
if (result > 0) { |
||||
|
// 记录回流操作 |
||||
|
Map<String, Object> originalData = convertUserToMap(originalUser); |
||||
|
Map<String, Object> modifiedData = convertUserToMap(user); |
||||
|
informationTraService.recordCustomerReturn(user.getUserId(), originalData, modifiedData); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 将Users对象转换为Map(辅助方法) |
||||
|
private Map<String, Object> convertUserToMap(Users user) { |
||||
|
Map<String, Object> map = new HashMap<>(); |
||||
|
map.put("id", user.getId()); |
||||
|
map.put("openid", user.getOpenid()); |
||||
|
map.put("userId", user.getUserId()); |
||||
|
map.put("nickName", user.getNickName()); |
||||
|
map.put("phoneNumber", user.getPhoneNumber()); |
||||
|
map.put("company", user.getCompany()); |
||||
|
map.put("region", user.getRegion()); |
||||
|
map.put("level", user.getLevel()); |
||||
|
map.put("followup", user.getFollowup()); |
||||
|
map.put("followup_at", user.getFollowup_at()); |
||||
|
map.put("updated_at", user.getUpdated_at()); |
||||
|
map.put("created_at", user.getCreated_at()); |
||||
|
return map; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 3.5.3 定时任务配置 |
||||
|
|
||||
|
```java |
||||
|
package com.example.web.config; |
||||
|
|
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import org.springframework.scheduling.annotation.EnableScheduling; |
||||
|
|
||||
|
/** |
||||
|
* 定时任务配置 |
||||
|
*/ |
||||
|
@Configuration |
||||
|
@EnableScheduling |
||||
|
public class SchedulingConfig { |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 四、前端展示方案 |
||||
|
|
||||
|
### 4.1 变更记录列表展示 |
||||
|
|
||||
|
```html |
||||
|
<table class="change-record-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>修改时间</th> |
||||
|
<th>修改人</th> |
||||
|
<th>部门</th> |
||||
|
<th>操作事件</th> |
||||
|
<th>操作</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr v-for="record in changeRecords" :key="record.id"> |
||||
|
<td>{{ record.operationTime }}</td> |
||||
|
<td>{{ record.trauserName }}</td> |
||||
|
<td>{{ record.tradepartment }}</td> |
||||
|
<td> |
||||
|
<span :class="{'system-operation': record.trauserName === '系统自动'}"> |
||||
|
{{ record.operationEvent }} |
||||
|
</span> |
||||
|
</td> |
||||
|
<td> |
||||
|
<button @click="showDetail(record)">查看详情</button> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
``` |
||||
|
|
||||
|
### 4.2 变更详情展示 |
||||
|
|
||||
|
```html |
||||
|
<div class="change-detail"> |
||||
|
<h3>修改详情</h3> |
||||
|
<div class="detail-info"> |
||||
|
<p><strong>修改时间:</strong>{{ currentRecord.operationTime }}</p> |
||||
|
<p><strong>修改人:</strong>{{ currentRecord.trauserName }}</p> |
||||
|
<p><strong>部门:</strong>{{ currentRecord.tradepartment }}</p> |
||||
|
<p><strong>公司:</strong>{{ currentRecord.tracompany }}</p> |
||||
|
</div> |
||||
|
|
||||
|
<h4>变更字段详情</h4> |
||||
|
<table class="field-change-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>字段名称</th> |
||||
|
<th>原始值</th> |
||||
|
<th>修改后的值</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr v-for="(change, fieldName) in changedFields" :key="fieldName"> |
||||
|
<td>{{ getFieldLabel(fieldName) }}</td> |
||||
|
<td class="original-value">{{ change.original || '空' }}</td> |
||||
|
<td class="modified-value">{{ change.modified || '空' }}</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
### 4.3 前端JavaScript处理 |
||||
|
|
||||
|
```javascript |
||||
|
// 解析变更字段 |
||||
|
parseChangedFields(record) { |
||||
|
if (record.changedFields) { |
||||
|
return JSON.parse(record.changedFields); |
||||
|
} |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
// 字段名映射 |
||||
|
getFieldLabel(fieldName) { |
||||
|
const fieldMap = { |
||||
|
'nickName': '客户昵称', |
||||
|
'phoneNumber': '客户手机号', |
||||
|
'company': '客户公司', |
||||
|
'region': '客户地区', |
||||
|
'level': '客户等级', |
||||
|
'demand': '客户需求', |
||||
|
'spec': '客户规格', |
||||
|
'followup': '跟进信息', |
||||
|
'notice': '通知状态', |
||||
|
'followup_at': '跟进时间', |
||||
|
'updated_at': '更新时间' |
||||
|
}; |
||||
|
return fieldMap[fieldName] || fieldName; |
||||
|
} |
||||
|
|
||||
|
// 格式化日期时间 |
||||
|
formatDateTime(dateTime) { |
||||
|
if (!dateTime) return ''; |
||||
|
const date = new Date(dateTime); |
||||
|
return date.toLocaleString('zh-CN', { |
||||
|
year: 'numeric', |
||||
|
month: '2-digit', |
||||
|
day: '2-digit', |
||||
|
hour: '2-digit', |
||||
|
minute: '2-digit', |
||||
|
second: '2-digit' |
||||
|
}); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 五、实施步骤 |
||||
|
|
||||
|
### 5.1 数据库修改 |
||||
|
1. 执行SQL语句扩展 `informationtra` 表 |
||||
|
- 添加三个新字段:`originalData`、`modifiedData`、`changedFields` |
||||
|
2. 执行SQL语句修改 `users` 表 |
||||
|
- 添加 `followup_at` 字段:`ALTER TABLE users ADD COLUMN followup_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT '跟进时间(只记录第一次)';` |
||||
|
|
||||
|
### 5.2 代码修改 |
||||
|
1. 扩展 `InformationTra` 实体类,添加三个新字段 |
||||
|
2. 扩展 `InformationTraMapper`,支持新字段的插入 |
||||
|
3. 实现 `CustomerDataChangeAspect` 拦截器 |
||||
|
4. 扩展 `InformationTraService`,添加详细变更记录和回流记录方法 |
||||
|
5. 扩展 `Users` 实体类,添加 `followup_at` 字段 |
||||
|
6. 在 `CustomerService` 中实现客户回流逻辑 |
||||
|
7. 添加定时任务配置,启用客户回流检查 |
||||
|
8. 修改前端代码,支持变更记录的展示和回流操作的标记 |
||||
|
|
||||
|
### 5.3 测试验证 |
||||
|
1. 修改客户信息,验证变更记录是否正确生成 |
||||
|
2. 查看变更详情,验证原始数据和修改后数据是否正确展示 |
||||
|
3. 测试提交跟进信息,验证 `followup_at` 是否正确设置 |
||||
|
4. 模拟三天内有数据更新但未跟进的情况,验证客户是否自动回流 |
||||
|
5. 检查回流操作是否被正确记录到信息跟踪表 |
||||
|
6. 测试不同字段的变更情况 |
||||
|
|
||||
|
## 六、注意事项 |
||||
|
|
||||
|
1. **性能考虑**:JSON存储会增加数据库存储量,但对于客户追踪场景,性能影响可接受 |
||||
|
2. **数据安全**:确保敏感字段的变更记录符合数据保护要求 |
||||
|
3. **兼容性**:新字段设置为NULL,确保不影响现有功能 |
||||
|
4. **扩展性**:设计时考虑未来可能的字段扩展需求 |
||||
|
5. **用户体验**:前端展示要清晰直观,便于业务员快速查看变更内容 |
||||
|
6. **定时任务性能**:客户回流检查逻辑应考虑数据量,避免影响系统性能 |
||||
|
7. **followup_at初始值**:对于现有数据,需要进行初始化,将followup_at设置为updated_at的值 |
||||
|
|
||||
|
## 七、总结 |
||||
|
|
||||
|
本方案通过扩展现有的 `informationtra` 表和 `users` 表,实现了客户数据的字段级变更追踪和客户回流功能。主要特点包括: |
||||
|
|
||||
|
1. **完整性**:详细记录原始数据和修改后的数据,包括客户回流操作 |
||||
|
2. **灵活性**:基于现有架构进行扩展,无需大量修改 |
||||
|
3. **易用性**:前端展示直观清晰,便于业务员查看变更记录和回流操作 |
||||
|
4. **智能性**:自动检测客户跟进情况,实现客户等级的自动调整 |
||||
|
5. **可扩展性**:设计考虑了未来的扩展需求 |
||||
|
|
||||
|
该方案可以满足需求,实现客户数据的详细变更追踪和客户回流功能。 |
||||
@ -0,0 +1,839 @@ |
|||||
|
# 客户通知弹窗重新设计方案 |
||||
|
|
||||
|
## 一、设计思路 |
||||
|
|
||||
|
### 1.1 整体设计理念 |
||||
|
- **简洁性**:去除冗余元素,突出核心信息 |
||||
|
- **一致性**:与现有网页风格保持统一,使用相同的配色方案和设计语言 |
||||
|
- **易用性**:优化交互流程,提升用户体验 |
||||
|
- **美观性**:采用现代化设计,增强视觉吸引力 |
||||
|
|
||||
|
### 1.2 设计目标 |
||||
|
- 重新设计通知按钮和数量徽章 |
||||
|
- 优化通知弹窗的布局和样式 |
||||
|
- 改进通知项的渲染格式 |
||||
|
- 增强用户交互反馈 |
||||
|
- 确保响应式设计适配不同屏幕尺寸 |
||||
|
|
||||
|
## 二、实现思路 |
||||
|
|
||||
|
### 2.1 技术栈 |
||||
|
- HTML5 + CSS3 + JavaScript |
||||
|
- 保持与现有代码结构的兼容性 |
||||
|
- 利用Font Awesome图标库 |
||||
|
|
||||
|
### 2.2 核心功能实现 |
||||
|
- 通知按钮的视觉增强和动画效果 |
||||
|
- 通知弹窗的布局优化 |
||||
|
- 通知项的卡片式设计 |
||||
|
- 通知状态的交互更新 |
||||
|
- 空状态的友好提示 |
||||
|
|
||||
|
## 三、设计图 |
||||
|
|
||||
|
### 3.1 通知按钮设计 |
||||
|
|
||||
|
#### 当前设计 |
||||
|
```html |
||||
|
<button class="notification-btn" id="notificationButton"> |
||||
|
<i class="fas fa-bell"></i> |
||||
|
</button> |
||||
|
``` |
||||
|
|
||||
|
#### 新设计 |
||||
|
```html |
||||
|
<button class="notification-btn" id="notificationButton"> |
||||
|
<i class="fas fa-bell"></i> |
||||
|
<span class="notification-count">3</span> |
||||
|
</button> |
||||
|
``` |
||||
|
|
||||
|
**设计说明**: |
||||
|
- 保留铃铛图标,但增加悬停效果 |
||||
|
- 优化数量徽章的位置和样式 |
||||
|
- 为有新通知的状态添加动画效果 |
||||
|
|
||||
|
### 3.2 通知弹窗设计 |
||||
|
|
||||
|
#### 当前设计 |
||||
|
```html |
||||
|
<div class="modal" id="notificationModal"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<h2 class="modal-title">客户通知</h2> |
||||
|
<button class="close-modal" id="closeNotificationModal">×</button> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<div id="notificationContent"> |
||||
|
<!-- 通知内容将通过JavaScript动态生成 --> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
#### 新设计 |
||||
|
```html |
||||
|
<div class="modal" id="notificationModal"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<div class="modal-header-left"> |
||||
|
<i class="fas fa-bell modal-header-icon"></i> |
||||
|
<h2 class="modal-title">客户通知</h2> |
||||
|
</div> |
||||
|
<div class="modal-header-right"> |
||||
|
<button class="secondary-btn" id="markAllAsRead">全部已读</button> |
||||
|
<button class="close-modal" id="closeNotificationModal">×</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<div id="notificationContent"> |
||||
|
<!-- 通知内容将通过JavaScript动态生成 --> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
**设计说明**: |
||||
|
- 优化头部布局,将标题和图标左对齐 |
||||
|
- 添加"全部已读"功能按钮 |
||||
|
- 改进关闭按钮的样式 |
||||
|
- 增加弹窗的阴影和圆角效果 |
||||
|
|
||||
|
### 3.3 通知项渲染格式 |
||||
|
|
||||
|
#### 当前渲染格式 |
||||
|
```html |
||||
|
<div class="notification-item new" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)"> |
||||
|
<div class="notification-header"> |
||||
|
<div class="notification-icon"> |
||||
|
<i class="fas fa-user-clock" style="color: #ff9800;"></i> |
||||
|
</div> |
||||
|
<div class="notification-content"> |
||||
|
<div class="notification-title"> |
||||
|
${customerName} |
||||
|
</div> |
||||
|
<div class="notification-meta"> |
||||
|
<span class="customer-id">客户ID: ${customer.id}</span> |
||||
|
<span class="customer-phone">${customer.phoneNumber || '无电话'}</span> |
||||
|
</div> |
||||
|
<div class="notification-footer"> |
||||
|
<div class="notification-time">${new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}</div> |
||||
|
<div class="notification-status">未读</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
#### 新渲染格式 |
||||
|
```html |
||||
|
<div class="notification-item new" data-customer-id="${customer.id}"> |
||||
|
<div class="notification-avatar"> |
||||
|
<i class="fas fa-user-tie"></i> |
||||
|
</div> |
||||
|
<div class="notification-main"> |
||||
|
<div class="notification-header"> |
||||
|
<h3 class="notification-title">${customerName}</h3> |
||||
|
<span class="notification-badge">新客户</span> |
||||
|
</div> |
||||
|
<div class="notification-info"> |
||||
|
<p class="notification-description">有新的客户信息需要您查看</p> |
||||
|
<div class="notification-meta"> |
||||
|
<span class="meta-item"><i class="fas fa-id-card"></i> ${customer.id}</span> |
||||
|
<span class="meta-item"><i class="fas fa-phone"></i> ${customer.phoneNumber || '无电话'}</span> |
||||
|
<span class="meta-item"><i class="fas fa-clock"></i> ${new Date(customer.created_at || Date.now()).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="notification-actions"> |
||||
|
<button class="action-btn view-btn" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)">查看详情</button> |
||||
|
<button class="action-btn ignore-btn" onclick="markNotificationAsRead('${customer.id}', this)">忽略</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
**设计说明**: |
||||
|
- 采用卡片式设计,增强视觉层次感 |
||||
|
- 添加客户头像区域(使用Font Awesome图标) |
||||
|
- 优化信息布局,突出客户名称 |
||||
|
- 增加通知类型徽章 |
||||
|
- 添加详细的元信息 |
||||
|
- 提供明确的操作按钮(查看详情/忽略) |
||||
|
- 为新通知添加视觉标识 |
||||
|
|
||||
|
### 3.4 空状态设计 |
||||
|
|
||||
|
#### 当前设计 |
||||
|
```html |
||||
|
<div class="notification-empty"><p>暂无通知</p></div> |
||||
|
``` |
||||
|
|
||||
|
#### 新设计 |
||||
|
```html |
||||
|
<div class="notification-empty"> |
||||
|
<div class="empty-icon"> |
||||
|
<i class="fas fa-inbox"></i> |
||||
|
</div> |
||||
|
<h3 class="empty-title">暂无通知</h3> |
||||
|
<p class="empty-description">当有新的客户信息时,会在这里显示通知</p> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
**设计说明**: |
||||
|
- 增加友好的空状态图标 |
||||
|
- 添加标题和描述文本 |
||||
|
- 提升视觉体验 |
||||
|
|
||||
|
## 四、代码实现 |
||||
|
|
||||
|
### 4.1 CSS样式实现 |
||||
|
|
||||
|
```css |
||||
|
/* 通知铃铛按钮样式 */ |
||||
|
.notification-btn { |
||||
|
background-color: #ffffff; |
||||
|
border: 1px solid #e0e0e0; |
||||
|
border-radius: 50%; |
||||
|
width: 44px; |
||||
|
height: 44px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
margin-right: 10px; |
||||
|
color: #666; |
||||
|
position: relative; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
||||
|
} |
||||
|
|
||||
|
.notification-btn:hover { |
||||
|
background-color: #f8f9fa; |
||||
|
border-color: #adb5bd; |
||||
|
color: #495057; |
||||
|
transform: scale(1.05); |
||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); |
||||
|
} |
||||
|
|
||||
|
.notification-btn i { |
||||
|
font-size: 20px; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
/* 通知铃铛激活状态样式 */ |
||||
|
.notification-btn.notification-active { |
||||
|
background-color: #ffebee; |
||||
|
border-color: #ffcdd2; |
||||
|
color: #d32f2f; |
||||
|
animation: ring 0.5s ease-in-out 3; |
||||
|
} |
||||
|
|
||||
|
/* 通知数量徽章样式 */ |
||||
|
.notification-count { |
||||
|
position: absolute; |
||||
|
top: -6px; |
||||
|
right: -6px; |
||||
|
background: linear-gradient(135deg, #d32f2f, #e53935); |
||||
|
color: white; |
||||
|
font-size: 11px; |
||||
|
font-weight: 600; |
||||
|
border-radius: 10px; |
||||
|
min-width: 20px; |
||||
|
height: 20px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 0 6px; |
||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); |
||||
|
animation: pulse 1.5s infinite; |
||||
|
} |
||||
|
|
||||
|
/* 铃铛摇晃动画 */ |
||||
|
@keyframes ring { |
||||
|
0% { transform: rotate(0deg); } |
||||
|
10% { transform: rotate(15deg); } |
||||
|
20% { transform: rotate(-15deg); } |
||||
|
30% { transform: rotate(15deg); } |
||||
|
40% { transform: rotate(-15deg); } |
||||
|
50% { transform: rotate(0deg); } |
||||
|
100% { transform: rotate(0deg); } |
||||
|
} |
||||
|
|
||||
|
/* 脉冲动画 */ |
||||
|
@keyframes pulse { |
||||
|
0% { transform: scale(1); } |
||||
|
50% { transform: scale(1.1); } |
||||
|
100% { transform: scale(1); } |
||||
|
} |
||||
|
|
||||
|
/* 通知弹窗样式 */ |
||||
|
#notificationModal .modal-content { |
||||
|
width: 420px; |
||||
|
max-width: 90vw; |
||||
|
max-height: 80vh; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 16px 20px; |
||||
|
background-color: #f8f9fa; |
||||
|
border-bottom: 1px solid #e0e0e0; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-header-left { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-header-icon { |
||||
|
color: #d32f2f; |
||||
|
font-size: 22px; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-title { |
||||
|
margin: 0; |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-header-right { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .secondary-btn { |
||||
|
background-color: #f0f0f0; |
||||
|
color: #666; |
||||
|
border: none; |
||||
|
padding: 6px 12px; |
||||
|
border-radius: 6px; |
||||
|
font-size: 13px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .secondary-btn:hover { |
||||
|
background-color: #e0e0e0; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-body { |
||||
|
padding: 0; |
||||
|
max-height: 60vh; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
/* 通知列表样式 */ |
||||
|
.notification-list { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 8px; |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
/* 通知项样式 */ |
||||
|
.notification-item { |
||||
|
background-color: #fff; |
||||
|
border: 1px solid #e0e0e0; |
||||
|
border-radius: 10px; |
||||
|
padding: 16px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.notification-item:hover { |
||||
|
border-color: #d32f2f; |
||||
|
box-shadow: 0 4px 12px rgba(211, 47, 47, 0.1); |
||||
|
transform: translateY(-2px); |
||||
|
} |
||||
|
|
||||
|
.notification-item.new { |
||||
|
border-left: 4px solid #d32f2f; |
||||
|
background-color: #fff8f8; |
||||
|
} |
||||
|
|
||||
|
/* 通知头像 */ |
||||
|
.notification-avatar { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
border-radius: 50%; |
||||
|
background: linear-gradient(135deg, #f5f7fa, #c3cfe2); |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.notification-avatar i { |
||||
|
font-size: 24px; |
||||
|
color: #d32f2f; |
||||
|
} |
||||
|
|
||||
|
/* 通知主内容 */ |
||||
|
.notification-main { |
||||
|
flex: 1; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.notification-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: flex-start; |
||||
|
margin-bottom: 6px; |
||||
|
} |
||||
|
|
||||
|
.notification-title { |
||||
|
margin: 0; |
||||
|
font-size: 16px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
word-break: break-word; |
||||
|
} |
||||
|
|
||||
|
.notification-badge { |
||||
|
background-color: #ffe0b2; |
||||
|
color: #f57c00; |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 10px; |
||||
|
font-size: 11px; |
||||
|
font-weight: 500; |
||||
|
text-transform: uppercase; |
||||
|
} |
||||
|
|
||||
|
.notification-description { |
||||
|
margin: 0 0 10px 0; |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
line-height: 1.4; |
||||
|
} |
||||
|
|
||||
|
.notification-meta { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 12px; |
||||
|
margin-bottom: 12px; |
||||
|
} |
||||
|
|
||||
|
.meta-item { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
font-size: 12px; |
||||
|
color: #999; |
||||
|
} |
||||
|
|
||||
|
.meta-item i { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
/* 通知操作 */ |
||||
|
.notification-actions { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
gap: 8px; |
||||
|
padding-top: 12px; |
||||
|
border-top: 1px solid #f0f0f0; |
||||
|
} |
||||
|
|
||||
|
.action-btn { |
||||
|
padding: 6px 12px; |
||||
|
border: none; |
||||
|
border-radius: 6px; |
||||
|
font-size: 13px; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.view-btn { |
||||
|
background-color: #d32f2f; |
||||
|
color: white; |
||||
|
} |
||||
|
|
||||
|
.view-btn:hover { |
||||
|
background-color: #b71c1c; |
||||
|
} |
||||
|
|
||||
|
.ignore-btn { |
||||
|
background-color: #f0f0f0; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
.ignore-btn:hover { |
||||
|
background-color: #e0e0e0; |
||||
|
} |
||||
|
|
||||
|
/* 空状态样式 */ |
||||
|
.notification-empty { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 60px 20px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.empty-icon { |
||||
|
width: 80px; |
||||
|
height: 80px; |
||||
|
border-radius: 50%; |
||||
|
background-color: #f5f5f5; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.empty-icon i { |
||||
|
font-size: 40px; |
||||
|
color: #ccc; |
||||
|
} |
||||
|
|
||||
|
.empty-title { |
||||
|
margin: 0 0 8px 0; |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.empty-description { |
||||
|
margin: 0; |
||||
|
font-size: 14px; |
||||
|
color: #999; |
||||
|
max-width: 300px; |
||||
|
} |
||||
|
|
||||
|
/* 滚动条样式 */ |
||||
|
#notificationModal .modal-body::-webkit-scrollbar { |
||||
|
width: 6px; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-body::-webkit-scrollbar-track { |
||||
|
background-color: #f5f5f5; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-body::-webkit-scrollbar-thumb { |
||||
|
background-color: #ccc; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
#notificationModal .modal-body::-webkit-scrollbar-thumb:hover { |
||||
|
background-color: #999; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 4.2 JavaScript实现 |
||||
|
|
||||
|
```javascript |
||||
|
// 显示通知弹窗 |
||||
|
async function showNotificationModal() { |
||||
|
console.log('🎯 显示通知弹窗'); |
||||
|
const notificationModal = document.getElementById('notificationModal'); |
||||
|
const notificationContent = document.getElementById('notificationContent'); |
||||
|
|
||||
|
// 显示加载状态 |
||||
|
notificationContent.innerHTML = '<div class="notification-loading"><div class="loading-spinner"></div><p>加载中...</p></div>'; |
||||
|
notificationModal.classList.add('active'); |
||||
|
|
||||
|
// 阻止背景滚动 |
||||
|
document.body.style.overflow = 'hidden'; |
||||
|
|
||||
|
try { |
||||
|
// 获取客户数据(保持原有的数据获取逻辑) |
||||
|
let allCustomers = []; |
||||
|
if (this.allPublicSeaCustomers) { |
||||
|
allCustomers = this.allPublicSeaCustomers; |
||||
|
} else { |
||||
|
const url = appendAuthParams(`${API_BASE_URL}/pool/all-customers`); |
||||
|
const response = await fetch(url); |
||||
|
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`); |
||||
|
|
||||
|
const result = await response.json(); |
||||
|
if (!result.success) throw new Error(result.message); |
||||
|
|
||||
|
const data = result.data || {}; |
||||
|
allCustomers = Array.isArray(data) ? data : Object.values(data); |
||||
|
this.allPublicSeaCustomers = allCustomers; |
||||
|
} |
||||
|
|
||||
|
// 获取notice为banold的客户 |
||||
|
const banoldCustomers = allCustomers.filter(customer => customer.notice === 'banold'); |
||||
|
|
||||
|
if (banoldCustomers.length === 0) { |
||||
|
// 渲染空状态 |
||||
|
notificationContent.innerHTML = ` |
||||
|
<div class="notification-empty"> |
||||
|
<div class="empty-icon"> |
||||
|
<i class="fas fa-inbox"></i> |
||||
|
</div> |
||||
|
<h3 class="empty-title">暂无通知</h3> |
||||
|
<p class="empty-description">当有新的客户信息时,会在这里显示通知</p> |
||||
|
</div> |
||||
|
`; |
||||
|
} else { |
||||
|
// 渲染通知列表 |
||||
|
let contentHTML = '<div class="notification-list">'; |
||||
|
banoldCustomers.forEach(customer => { |
||||
|
const customerName = customer.company || customer.companyName || '未知客户'; |
||||
|
const notificationClass = customer.notice === 'banold' ? 'notification-item new' : 'notification-item'; |
||||
|
|
||||
|
contentHTML += ` |
||||
|
<div class="${notificationClass}" data-customer-id="${customer.id}"> |
||||
|
<div class="notification-avatar"> |
||||
|
<i class="fas fa-user-tie"></i> |
||||
|
</div> |
||||
|
<div class="notification-main"> |
||||
|
<div class="notification-header"> |
||||
|
<h3 class="notification-title">${customerName}</h3> |
||||
|
<span class="notification-badge">新客户</span> |
||||
|
</div> |
||||
|
<div class="notification-info"> |
||||
|
<p class="notification-description">有新的客户信息需要您查看</p> |
||||
|
<div class="notification-meta"> |
||||
|
<span class="meta-item"><i class="fas fa-id-card"></i> ${customer.id}</span> |
||||
|
<span class="meta-item"><i class="fas fa-phone"></i> ${customer.phoneNumber || '无电话'}</span> |
||||
|
<span class="meta-item"><i class="fas fa-clock"></i> ${new Date(customer.created_at || Date.now()).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="notification-actions"> |
||||
|
<button class="action-btn view-btn" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)">查看详情</button> |
||||
|
<button class="action-btn ignore-btn" onclick="markNotificationAsRead('${customer.id}', this)">忽略</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
`; |
||||
|
}); |
||||
|
contentHTML += '</div>'; |
||||
|
notificationContent.innerHTML = contentHTML; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('❌ 加载通知失败:', error); |
||||
|
notificationContent.innerHTML = ` |
||||
|
<div class="notification-error"> |
||||
|
<div class="error-icon"> |
||||
|
<i class="fas fa-exclamation-circle"></i> |
||||
|
</div> |
||||
|
<h3 class="error-title">加载失败</h3> |
||||
|
<p class="error-description">加载通知时发生错误,请稍后重试</p> |
||||
|
<button class="retry-btn" onclick="showNotificationModal()">重试</button> |
||||
|
</div> |
||||
|
`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 标记通知为已读 |
||||
|
async function markNotificationAsRead(customerId, element) { |
||||
|
console.log('📌 标记通知为已读,客户ID:', customerId); |
||||
|
|
||||
|
try { |
||||
|
// 发送请求更新通知状态 |
||||
|
const response = await fetch(`/DL/pool/customers/${customerId}/notice`, { |
||||
|
method: 'PUT', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
}, |
||||
|
body: JSON.stringify({ notice: 'old' }) |
||||
|
}); |
||||
|
|
||||
|
const data = await response.json(); |
||||
|
|
||||
|
if (data.success) { |
||||
|
console.log('✅ 标记通知为已读成功'); |
||||
|
|
||||
|
// 移除通知项 |
||||
|
const notificationItem = element.closest('.notification-item'); |
||||
|
if (notificationItem) { |
||||
|
notificationItem.remove(); |
||||
|
} |
||||
|
|
||||
|
// 检查是否还有通知 |
||||
|
const notificationContent = document.getElementById('notificationContent'); |
||||
|
const remainingNotifications = notificationContent.querySelectorAll('.notification-item').length; |
||||
|
|
||||
|
if (remainingNotifications === 0) { |
||||
|
notificationContent.innerHTML = ` |
||||
|
<div class="notification-empty"> |
||||
|
<div class="empty-icon"> |
||||
|
<i class="fas fa-inbox"></i> |
||||
|
</div> |
||||
|
<h3 class="empty-title">暂无通知</h3> |
||||
|
<p class="empty-description">当有新的客户信息时,会在这里显示通知</p> |
||||
|
</div> |
||||
|
`; |
||||
|
} |
||||
|
|
||||
|
// 更新通知按钮状态 |
||||
|
updateNotificationStatus(this.allPublicSeaCustomers); |
||||
|
} else { |
||||
|
throw new Error('标记通知失败'); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('❌ 标记通知失败:', error); |
||||
|
showToast('标记通知失败,请稍后重试', 'error'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 全部标记为已读 |
||||
|
async function markAllAsRead() { |
||||
|
console.log('📌 全部标记为已读'); |
||||
|
|
||||
|
try { |
||||
|
// 获取所有未读通知 |
||||
|
const notificationItems = document.querySelectorAll('.notification-item.new'); |
||||
|
|
||||
|
if (notificationItems.length === 0) { |
||||
|
showToast('没有未读通知', 'info'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 批量更新通知状态 |
||||
|
const updatePromises = Array.from(notificationItems).map(item => { |
||||
|
const customerId = item.dataset.customerId; |
||||
|
return fetch(`/DL/pool/customers/${customerId}/notice`, { |
||||
|
method: 'PUT', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
}, |
||||
|
body: JSON.stringify({ notice: 'old' }) |
||||
|
}).then(response => response.json()); |
||||
|
}); |
||||
|
|
||||
|
const results = await Promise.all(updatePromises); |
||||
|
const successCount = results.filter(r => r.success).length; |
||||
|
|
||||
|
if (successCount > 0) { |
||||
|
console.log('✅ 全部标记为已读成功'); |
||||
|
|
||||
|
// 清空通知列表 |
||||
|
const notificationContent = document.getElementById('notificationContent'); |
||||
|
notificationContent.innerHTML = ` |
||||
|
<div class="notification-empty"> |
||||
|
<div class="empty-icon"> |
||||
|
<i class="fas fa-inbox"></i> |
||||
|
</div> |
||||
|
<h3 class="empty-title">暂无通知</h3> |
||||
|
<p class="empty-description">当有新的客户信息时,会在这里显示通知</p> |
||||
|
</div> |
||||
|
`; |
||||
|
|
||||
|
// 更新通知按钮状态 |
||||
|
updateNotificationStatus(this.allPublicSeaCustomers); |
||||
|
|
||||
|
showToast(`已标记 ${successCount} 条通知为已读`, 'success'); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('❌ 全部标记为已读失败:', error); |
||||
|
showToast('批量标记失败,请稍后重试', 'error'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 隐藏通知弹窗 |
||||
|
function hideNotificationModal() { |
||||
|
const notificationModal = document.getElementById('notificationModal'); |
||||
|
notificationModal.classList.remove('active'); |
||||
|
document.body.style.overflow = ''; |
||||
|
} |
||||
|
|
||||
|
// 更新通知状态 |
||||
|
function updateNotificationStatus(customers) { |
||||
|
// 统计notice为banold的客户数量 |
||||
|
const banoldCount = customers.filter(customer => customer.notice === 'banold').length; |
||||
|
|
||||
|
const notificationButton = document.getElementById('notificationButton'); |
||||
|
if (notificationButton) { |
||||
|
if (banoldCount > 0) { |
||||
|
// 有新通知 |
||||
|
notificationButton.classList.add('notification-active'); |
||||
|
|
||||
|
let countBadge = notificationButton.querySelector('.notification-count'); |
||||
|
if (!countBadge) { |
||||
|
countBadge = document.createElement('span'); |
||||
|
countBadge.className = 'notification-count'; |
||||
|
notificationButton.appendChild(countBadge); |
||||
|
} |
||||
|
countBadge.textContent = banoldCount; |
||||
|
} else { |
||||
|
// 无新通知 |
||||
|
notificationButton.classList.remove('notification-active'); |
||||
|
|
||||
|
const countBadge = notificationButton.querySelector('.notification-count'); |
||||
|
if (countBadge) { |
||||
|
countBadge.remove(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 绑定通知事件 |
||||
|
function bindNotificationEvents() { |
||||
|
const notificationButton = document.getElementById('notificationButton'); |
||||
|
const notificationModal = document.getElementById('notificationModal'); |
||||
|
const closeNotificationModal = document.getElementById('closeNotificationModal'); |
||||
|
const markAllAsReadBtn = document.getElementById('markAllAsRead'); |
||||
|
|
||||
|
if (notificationButton) { |
||||
|
notificationButton.addEventListener('click', showNotificationModal); |
||||
|
} |
||||
|
|
||||
|
if (closeNotificationModal) { |
||||
|
closeNotificationModal.addEventListener('click', hideNotificationModal); |
||||
|
} |
||||
|
|
||||
|
if (markAllAsReadBtn) { |
||||
|
markAllAsReadBtn.addEventListener('click', markAllAsRead); |
||||
|
} |
||||
|
|
||||
|
if (notificationModal) { |
||||
|
notificationModal.addEventListener('click', (e) => { |
||||
|
if (e.target === notificationModal) { |
||||
|
hideNotificationModal(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 初始化通知功能 |
||||
|
document.addEventListener('DOMContentLoaded', function() { |
||||
|
bindNotificationEvents(); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## 五、部署与测试 |
||||
|
|
||||
|
### 5.1 部署步骤 |
||||
|
1. 备份现有代码 |
||||
|
2. 将新的CSS样式添加到样式文件中 |
||||
|
3. 更新JavaScript函数 |
||||
|
4. 修改HTML结构 |
||||
|
5. 测试功能完整性 |
||||
|
|
||||
|
### 5.2 测试要点 |
||||
|
- 通知按钮的显示和动画效果 |
||||
|
- 通知弹窗的打开和关闭 |
||||
|
- 通知列表的渲染 |
||||
|
- 通知状态的更新 |
||||
|
- 空状态的显示 |
||||
|
- 响应式设计的适配 |
||||
|
|
||||
|
## 六、总结 |
||||
|
|
||||
|
本方案通过重新设计客户通知弹窗和通知渲染格式,实现了以下改进: |
||||
|
|
||||
|
1. **视觉提升**:采用现代化的卡片式设计,增强视觉层次感 |
||||
|
2. **用户体验优化**:提供更清晰的操作按钮和状态反馈 |
||||
|
3. **功能增强**:增加了批量标记已读和忽略功能 |
||||
|
4. **友好性提升**:优化了空状态和错误状态的提示 |
||||
|
5. **性能优化**:保持了与现有代码的兼容性,无需大规模重构 |
||||
|
|
||||
|
新设计简洁美观,与整体网页风格契合,同时提升了用户的操作效率和体验。 |
||||
Loading…
Reference in new issue