Browse Source

同步mainapp-sells.html和mainapp-supplys.html的通知弹窗功能

master
Trae AI 3 months ago
parent
commit
67d803bddc
  1. 2
      src/main/java/com/example/web/WebApplication.java
  2. 383
      src/main/java/com/example/web/aspect/CustomerDataChangeAspect.java
  3. 6
      src/main/java/com/example/web/config/MyBatisConfig.java
  4. 68
      src/main/java/com/example/web/controller/CustomerController.java
  5. 62
      src/main/java/com/example/web/controller/SupplyCustomerController.java
  6. 8
      src/main/java/com/example/web/controller/SupplyCustomerRecycleController.java
  7. 27
      src/main/java/com/example/web/entity/InformationTra.java
  8. 12
      src/main/java/com/example/web/entity/Users.java
  9. 5
      src/main/java/com/example/web/mapper/ContactsMapper.java
  10. 6
      src/main/java/com/example/web/mapper/SupplyUsersMapper.java
  11. 11
      src/main/java/com/example/web/mapper/UsersMapper.java
  12. 36
      src/main/java/com/example/web/service/CustomerReturnTaskService.java
  13. 105
      src/main/java/com/example/web/service/CustomerService.java
  14. 8
      src/main/java/com/example/web/service/FollowUpService.java
  15. 238
      src/main/java/com/example/web/service/InformationTraService.java
  16. 5
      src/main/java/com/example/web/service/SupplyCustomerRecycleService.java
  17. 14
      src/main/resources/application.yaml
  18. 7
      src/main/resources/mapper/InformationTraMapper.xml
  19. 32
      src/main/resources/mapper/SupplyUsersManagementsMapper.xml
  20. 40
      src/main/resources/mapper/SupplyUsersMapper.xml
  21. 14
      src/main/resources/mapper/UsersMapper.xml
  22. 918
      src/main/resources/static/mainapp-sells.html
  23. 484
      src/main/resources/static/mainapp-supplys.html
  24. 30
      test-update-customer.bat
  25. 16
      test.xml
  26. BIN
      test_method.xml
  27. 798
      客户数据变更追踪详细方案.md
  28. 839
      客户通知弹窗重新设计方案.md

2
src/main/java/com/example/web/WebApplication.java

@ -2,6 +2,7 @@ package com.example.web;
import me.paulschwarz.springdotenv.DotenvPropertySource; import me.paulschwarz.springdotenv.DotenvPropertySource;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
@ -9,6 +10,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@MapperScan("com.example.web.mapper") @MapperScan("com.example.web.mapper")
@EnableAspectJAutoProxy//启用AOP自动代理
@EnableScheduling//定时任务 @EnableScheduling//定时任务
/*@PropertySource(value = "classpath:.env", factory = DotenvPropertySource.class)*/ /*@PropertySource(value = "classpath:.env", factory = DotenvPropertySource.class)*/
public class WebApplication { public class WebApplication {

383
src/main/java/com/example/web/aspect/CustomerDataChangeAspect.java

@ -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);
}
}

6
src/main/java/com/example/web/config/MyBatisConfig.java

@ -29,6 +29,12 @@ public class MyBatisConfig {
// 设置Mapper XML文件路径 // 设置Mapper XML文件路径
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml")); .getResources("classpath:mapper/*.xml"));
// 添加配置:开启下划线到驼峰命名的自动转换
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject(); return factoryBean.getObject();
} }

68
src/main/java/com/example/web/controller/CustomerController.java

@ -120,18 +120,7 @@ public class CustomerController {
response.put("success", true); response.put("success", true);
response.put("data", customer); response.put("data", customer);
// 记录查看客户详情操作
informationTraService.recordOperationEvent(
customer.getPhoneNumber(),
customer.getNickName() != null ? customer.getNickName() : "",
customer.getPhoneNumber() + "-查看客户",
authInfo.getManagercompany(),
authInfo.getManagerdepartment(),
authInfo.getOrganization(),
authInfo.getRole(),
authInfo.getUserName(),
authInfo.getAssistant()
);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@ -171,28 +160,17 @@ public class CustomerController {
} }
// 2. 如果wechat数据源没找到或无权限,再尝试查询默认数据源 // 2. 如果wechat数据源没找到或无权限,再尝试查询默认数据源
System.out.println("📊 查询 DEFAULT 数据源..."); System.out.println("📊 查询 DEFAULT 数据源...");
UnifiedCustomerDTO defaultCustomer = customerService.getCustomerById(id, authInfo); UnifiedCustomerDTO defaultCustomer = customerService.getCustomerById(id, authInfo);
if (defaultCustomer != null) { if (defaultCustomer != null) {
System.out.println("✅ 在默认数据源中找到客户"); System.out.println("✅ 在默认数据源中找到客户");
response.put("success", true); response.put("success", true);
response.put("data", defaultCustomer); response.put("data", defaultCustomer);
// 记录查看客户详情操作
informationTraService.recordOperationEvent(
defaultCustomer.getPhoneNumber(),
defaultCustomer.getNickName() != null ? defaultCustomer.getNickName() : "",
defaultCustomer.getPhoneNumber() + "-查看客户",
authInfo.getManagercompany(),
authInfo.getManagerdepartment(),
authInfo.getOrganization(),
authInfo.getRole(),
authInfo.getUserName(),
authInfo.getAssistant()
);
return ResponseEntity.ok(response);
return ResponseEntity.ok(response);
} }
// 3. 如果两个数据源都没找到 // 3. 如果两个数据源都没找到
@ -728,18 +706,7 @@ public class CustomerController {
response.put("success", true); response.put("success", true);
response.put("message", "客户信息更新成功"); response.put("message", "客户信息更新成功");
// 记录修改客户信息操作 // 📝 注意:不再需要手动记录操作事件,因为CustomerDataChangeAspect切面会自动记录详细的变更信息
informationTraService.recordOperationEvent(
updatedDTO.getPhoneNumber(),
updatedDTO.getUserName() != null ? updatedDTO.getUserName() : "",
updatedDTO.getPhoneNumber() + "-更新客户",
authInfo.getManagercompany(),
authInfo.getManagerdepartment(),
authInfo.getOrganization(),
authInfo.getRole(),
authInfo.getUserName(),
authInfo.getAssistant()
);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} else { } else {
@ -936,18 +903,7 @@ public class CustomerController {
return ResponseEntity.badRequest().body(response); return ResponseEntity.badRequest().body(response);
} }
// 记录更新客户信息操作 // 📝 注意:不再需要手动记录操作事件,因为CustomerDataChangeAspect切面会自动记录详细的变更信息
informationTraService.recordOperationEvent(
updatedDTO.getPhoneNumber(),
updatedDTO.getNickName() != null ? updatedDTO.getNickName() : "",
updatedDTO.getPhoneNumber() + "-更新客户",
updatedDTO.getManagercompany() != null ? updatedDTO.getManagercompany() : "",
updatedDTO.getManagerdepartment() != null ? updatedDTO.getManagerdepartment() : "",
updatedDTO.getOrganization() != null ? updatedDTO.getOrganization() : "",
updatedDTO.getRole() != null ? updatedDTO.getRole() : "",
updatedDTO.getUserName() != null ? updatedDTO.getUserName() : "",
updatedDTO.getAssistant() != null ? updatedDTO.getAssistant() : ""
);
response.put("success", true); response.put("success", true);
response.put("message", "客户信息更新成功"); response.put("message", "客户信息更新成功");

62
src/main/java/com/example/web/controller/SupplyCustomerController.java

@ -116,18 +116,7 @@ public class SupplyCustomerController {
response.put("success", true); response.put("success", true);
response.put("data", customer); response.put("data", customer);
// 记录查看客户详情操作
informationTraService.recordOperationEvent(
customer.getPhoneNumber(),
customer.getUserName() != null ? customer.getUserName() : "",
customer.getPhoneNumber() + "-查看客户",
authInfo.getManagercompany(),
authInfo.getManagerdepartment(),
authInfo.getOrganization(),
authInfo.getRole(),
authInfo.getUserName(),
authInfo.getAssistant()
);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@ -171,24 +160,13 @@ public class SupplyCustomerController {
UnifiedCustomerDTO defaultCustomer = supplyCustomerService.getCustomerById(id, authInfo); UnifiedCustomerDTO defaultCustomer = supplyCustomerService.getCustomerById(id, authInfo);
if (defaultCustomer != null) { if (defaultCustomer != null) {
System.out.println("✅ 在默认数据源中找到客户"); System.out.println("✅ 在默认数据源中找到客户");
response.put("success", true); response.put("success", true);
response.put("data", defaultCustomer); response.put("data", defaultCustomer);
// 记录查看客户详情操作
informationTraService.recordOperationEvent(
defaultCustomer.getPhoneNumber(),
defaultCustomer.getUserName() != null ? defaultCustomer.getUserName() : "",
defaultCustomer.getPhoneNumber() + "-查看客户",
authInfo.getManagercompany(),
authInfo.getManagerdepartment(),
authInfo.getOrganization(),
authInfo.getRole(),
authInfo.getUserName(),
authInfo.getAssistant()
);
return ResponseEntity.ok(response);
return ResponseEntity.ok(response);
} }
// 3. 如果两个数据源都没找到 // 3. 如果两个数据源都没找到
@ -691,18 +669,7 @@ public class SupplyCustomerController {
response.put("success", true); response.put("success", true);
response.put("message", "客户信息更新成功"); response.put("message", "客户信息更新成功");
// 记录修改客户信息操作 // 📝 注意:不再需要手动记录操作事件,因为CustomerDataChangeAspect切面会自动记录详细的变更信息
informationTraService.recordOperationEvent(
updatedDTO.getPhoneNumber(),
updatedDTO.getUserName() != null ? updatedDTO.getUserName() : "",
updatedDTO.getPhoneNumber() + "-更新客户",
authInfo.getManagercompany(),
authInfo.getManagerdepartment(),
authInfo.getOrganization(),
authInfo.getRole(),
authInfo.getUserName(),
authInfo.getAssistant()
);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} else { } else {
@ -908,18 +875,7 @@ public class SupplyCustomerController {
return ResponseEntity.badRequest().body(response); return ResponseEntity.badRequest().body(response);
} }
// 记录更新客户信息操作 // 📝 注意:不再需要手动记录操作事件,因为CustomerDataChangeAspect切面会自动记录详细的变更信息
informationTraService.recordOperationEvent(
updatedDTO.getPhoneNumber(),
updatedDTO.getNickName() != null ? updatedDTO.getNickName() : "",
updatedDTO.getPhoneNumber() + "-更新客户",
updatedDTO.getManagercompany() != null ? updatedDTO.getManagercompany() : "",
updatedDTO.getManagerdepartment() != null ? updatedDTO.getManagerdepartment() : "",
updatedDTO.getOrganization() != null ? updatedDTO.getOrganization() : "",
updatedDTO.getRole() != null ? updatedDTO.getRole() : "",
updatedDTO.getUserName() != null ? updatedDTO.getUserName() : "",
updatedDTO.getAssistant() != null ? updatedDTO.getAssistant() : ""
);
// 🎯 关键修复:返回修正后的数据源信息给前端 // 🎯 关键修复:返回修正后的数据源信息给前端
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();

8
src/main/java/com/example/web/controller/SupplyCustomerRecycleController.java

@ -16,7 +16,7 @@ import java.util.Map;
public class SupplyCustomerRecycleController { public class SupplyCustomerRecycleController {
@Autowired @Autowired
private SupplyCustomerRecycleService supplycustomerRecycleService; private SupplyCustomerRecycleService supplyCustomerRecycleService;
/** /**
* 手动触发客户回流 * 手动触发客户回流
@ -27,11 +27,11 @@ public class SupplyCustomerRecycleController {
Map<String, Object> response = new HashMap<>(); Map<String, Object> response = new HashMap<>();
try { try {
supplycustomerRecycleService.manualRecycle(); supplyCustomerRecycleService.manualRecycle();
response.put("success", true); response.put("success", true);
response.put("message", "客户回流任务执行成功"); response.put("message", "客户回流任务执行成功");
response.put("config", supplycustomerRecycleService.getRecycleConfigInfo()); response.put("config", supplyCustomerRecycleService.getRecycleConfigInfo());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} catch (Exception e) { } catch (Exception e) {
@ -51,7 +51,7 @@ public class SupplyCustomerRecycleController {
try { try {
response.put("success", true); response.put("success", true);
response.put("config", supplycustomerRecycleService.getRecycleConfigInfo()); response.put("config", supplyCustomerRecycleService.getRecycleConfigInfo());
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} catch (Exception e) { } catch (Exception e) {

27
src/main/java/com/example/web/entity/InformationTra.java

@ -15,6 +15,9 @@ public class InformationTra {
private LocalDateTime operationTime; private LocalDateTime operationTime;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
private String originalData; // 原始数据JSON
private String modifiedData; // 修改后数据JSON
private String changedFields; // 变更字段列表JSON
// Getters and Setters // Getters and Setters
public Integer getId() { public Integer getId() {
@ -112,4 +115,28 @@ public class InformationTra {
public void setUpdatedAt(LocalDateTime updatedAt) { public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
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;
}
} }

12
src/main/java/com/example/web/entity/Users.java

@ -34,8 +34,9 @@ public class Users {
private String spec;//规格 private String spec;//规格
private String followup;//跟进信息 private String followup;//跟进信息
private String notice;//通知状态 private String notice;//通知状态
private LocalDateTime followup_at;//最后跟进时间
public Users(Integer id, String openid, String userId, String nickName, String avatarUrl, String phoneNumber, String type, Integer gender, String country, String province, String city, String language, String session_key, LocalDateTime created_at, LocalDateTime updated_at, String company, String region, String level, String demand, String spec, String followup, String notice) { public Users(Integer id, String openid, String userId, String nickName, String avatarUrl, String phoneNumber, String type, Integer gender, String country, String province, String city, String language, String session_key, LocalDateTime created_at, LocalDateTime updated_at, String company, String region, String level, String demand, String spec, String followup, String notice, LocalDateTime followup_at) {
this.id = id; this.id = id;
this.openid = openid; this.openid = openid;
this.userId = userId; this.userId = userId;
@ -58,6 +59,7 @@ public class Users {
this.spec = spec; this.spec = spec;
this.followup = followup; this.followup = followup;
this.notice = notice; this.notice = notice;
this.followup_at = followup_at;
} }
public Integer getId() { public Integer getId() {
@ -235,4 +237,12 @@ public class Users {
public void setNotice(String notice) { public void setNotice(String notice) {
this.notice = notice; this.notice = notice;
} }
public LocalDateTime getFollowup_at() {
return followup_at;
}
public void setFollowup_at(LocalDateTime followup_at) {
this.followup_at = followup_at;
}
} }

5
src/main/java/com/example/web/mapper/ContactsMapper.java

@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update; import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@Mapper @Mapper
@ -30,6 +31,6 @@ public interface ContactsMapper {
String getFollowUpByPhone(@Param("phoneNumber") String phoneNumber); String getFollowUpByPhone(@Param("phoneNumber") String phoneNumber);
// 更新跟进信息 // 更新跟进信息
@Update("UPDATE contacts SET followup = #{followup} WHERE phoneNumber = #{phoneNumber}") @Update("UPDATE contacts SET followup = #{followup}, updated_at = #{updatedAt} WHERE phoneNumber = #{phoneNumber}")
int updateFollowUpByPhone(@Param("phoneNumber") String phoneNumber, @Param("followup") String followup); int updateFollowUpByPhone(@Param("phoneNumber") String phoneNumber, @Param("followup") String followup, @Param("updatedAt") LocalDateTime updatedAt);
} }

6
src/main/java/com/example/web/mapper/SupplyUsersMapper.java

@ -223,7 +223,7 @@ public interface SupplyUsersMapper {
/** /**
* 更新客户等级 * 更新客户等级
*/ */
@Update("UPDATE users SET level = #{level}, updated_at = #{updateTime} WHERE user_id = #{userId}") @Update("UPDATE wechat_app.users SET level = #{level}, updated_at = #{updateTime} WHERE userId = #{userId}")
boolean updateCustomerLevel(@Param("userId") String userId, boolean updateCustomerLevel(@Param("userId") String userId,
@Param("level") String level, @Param("level") String level,
@Param("updateTime") LocalDateTime updateTime); @Param("updateTime") LocalDateTime updateTime);
@ -261,8 +261,8 @@ public interface SupplyUsersMapper {
String getFollowUpByPhone(@Param("phoneNumber") String phoneNumber); String getFollowUpByPhone(@Param("phoneNumber") String phoneNumber);
// 更新跟进信息 // 更新跟进信息
@Update("UPDATE users SET followup = #{followup} WHERE phoneNumber = #{phoneNumber}") @Update("UPDATE users SET followup = #{followup}, updated_at = #{updatedAt}, followup_at = COALESCE(followup_at, #{updatedAt}) WHERE phoneNumber = #{phoneNumber}")
int updateFollowUpByPhone(@Param("phoneNumber") String phoneNumber, @Param("followup") String followup); int updateFollowUpByPhone(@Param("phoneNumber") String phoneNumber, @Param("followup") String followup, @Param("updatedAt") LocalDateTime updatedAt);
// 🔥 新增:更新用户通知状态 // 🔥 新增:更新用户通知状态
@Update("UPDATE users SET notice = #{notice} WHERE userId = #{userId}") @Update("UPDATE users SET notice = #{notice} WHERE userId = #{userId}")

11
src/main/java/com/example/web/mapper/UsersMapper.java

@ -3,6 +3,7 @@ package com.example.web.mapper;
import com.example.web.annotation.DataSource; import com.example.web.annotation.DataSource;
import com.example.web.dto.ManagerAuthInfo; import com.example.web.dto.ManagerAuthInfo;
import com.example.web.dto.UserProductCartDTO; import com.example.web.dto.UserProductCartDTO;
import com.example.web.entity.Users;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update; import org.apache.ibatis.annotations.Update;
@ -273,4 +274,14 @@ public interface UsersMapper {
// 🔥 新增:更新用户通知状态 // 🔥 新增:更新用户通知状态
@Update("UPDATE users SET notice = #{notice} WHERE userId = #{userId}") @Update("UPDATE users SET notice = #{notice} WHERE userId = #{userId}")
int updateNotice(@Param("userId") String userId, @Param("notice") String notice); int updateNotice(@Param("userId") String userId, @Param("notice") String notice);
/**
* 查询需要检查回流的客户列表
*/
List<Users> selectCustomersForReturnFlowCheck();
/**
* 根据用户ID更新用户信息
*/
int updateByUserId(Users user);
} }

36
src/main/java/com/example/web/service/CustomerReturnTaskService.java

@ -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);
}
}
}

105
src/main/java/com/example/web/service/CustomerService.java

@ -5,8 +5,10 @@ import com.example.web.dto.ManagerAuthInfo;
import com.example.web.dto.UnifiedCustomerDTO; import com.example.web.dto.UnifiedCustomerDTO;
import com.example.web.dto.UserProductCartDTO; import com.example.web.dto.UserProductCartDTO;
import com.example.web.entity.Managers; import com.example.web.entity.Managers;
import com.example.web.entity.Users;
import com.example.web.entity.UsersManagements; import com.example.web.entity.UsersManagements;
import com.example.web.mapper.*; import com.example.web.mapper.*;
import com.example.web.config.DynamicDataSource;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -42,6 +44,9 @@ public class CustomerService {
@Autowired @Autowired
private PoolCustomerService poolCustomerService; private PoolCustomerService poolCustomerService;
@Autowired
private InformationTraService informationTraService;
// ==================== 精确更新方法 ==================== // ==================== 精确更新方法 ====================
@ -1325,4 +1330,104 @@ public class CustomerService {
return false; return false;
} }
} }
/**
* 检查客户回流 - 定时任务调用
*/
public void checkCustomerReturnFlow() {
// 保存原始数据源,以便在finally块中恢复
String originalDataSource = DynamicDataSource.getCurrentDataSourceKey();
try {
// 设置为wechat数据源
DynamicDataSource.setDataSourceKey("wechat");
System.out.println("🔄 开始执行客户回流检查任务");
// 获取需要检查回流的客户列表
List<Users> customers = getCustomersForReturnFlowCheck();
System.out.println("📊 找到需要检查回流的客户数量: " + (customers != null ? customers.size() : 0));
if (customers != null && !customers.isEmpty()) {
for (Users customer : customers) {
try {
// 执行回流操作
updateCustomerToSeaPool(customer);
} catch (Exception e) {
System.err.println("❌ 处理客户回流失败,客户ID: " + customer.getUserId() + ", 错误: " + e.getMessage());
e.printStackTrace();
}
}
}
System.out.println("✅ 客户回流检查任务执行完成");
} catch (Exception e) {
System.err.println("❌ 执行客户回流检查任务失败: " + e.getMessage());
e.printStackTrace();
} finally {
// 恢复原始数据源,避免线程池复用问题
if (originalDataSource != null) {
DynamicDataSource.setDataSourceKey(originalDataSource);
} else {
DynamicDataSource.clearDataSourceKey();
}
System.out.println("🔄 恢复数据源为原始值: " + (originalDataSource != null ? originalDataSource : "默认数据源"));
}
}
/**
* 获取需要检查回流的客户列表
*/
private List<Users> getCustomersForReturnFlowCheck() {
try {
// 查询符合条件的客户:
// 1. updated_at在过去3天内
// 2. followup_at为空或超过3天未更新
// 3. 等级不是organization-sea-pools
List<Users> customers = usersMapper.selectCustomersForReturnFlowCheck();
return customers;
} catch (Exception e) {
System.err.println("❌ 获取客户列表失败: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* 将客户更新为公海池等级并记录回流信息
*/
private void updateCustomerToSeaPool(Users customer) {
try {
System.out.println("🔄 开始处理客户回流,客户ID: " + customer.getUserId() + ", 手机号: " + customer.getPhoneNumber());
// 1. 记录原始等级
String originalLevel = customer.getLevel();
// 2. 更新客户等级为organization-sea-pools
Users updateUser = new Users();
updateUser.setUserId(customer.getUserId());
updateUser.setLevel("organization-sea-pools");
updateUser.setUpdated_at(LocalDateTime.now());
int rows = usersMapper.updateByUserId(updateUser);
if (rows > 0) {
System.out.println("✅ 客户等级更新成功,原始等级: " + originalLevel + ", 新等级: organization-sea-pools");
// 3. 记录客户回流信息
informationTraService.recordCustomerReturn(
customer.getUserId(),
"系统自动操作",
"系统自动操作",
"系统自动操作",
"系统自动操作",
"系统自动操作",
"系统自动操作"
);
} else {
System.err.println("❌ 客户等级更新失败,客户ID: " + customer.getUserId());
}
} catch (Exception e) {
System.err.println("❌ 处理客户回流失败,客户ID: " + customer.getUserId() + ", 错误: " + e.getMessage());
e.printStackTrace();
}
}
} }

8
src/main/java/com/example/web/service/FollowUpService.java

@ -9,6 +9,7 @@ import com.example.web.mapper.UsersMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service @Service
public class FollowUpService { public class FollowUpService {
@ -52,12 +53,15 @@ public class FollowUpService {
*/ */
public boolean saveFollowUp(String phoneNumber, String followup) { public boolean saveFollowUp(String phoneNumber, String followup) {
try { try {
// 获取当前时间作为updated_at
LocalDateTime now = LocalDateTime.now();
// 先尝试在contacts表中保存 // 先尝试在contacts表中保存
if (contactsMapper.updateFollowUpByPhone(phoneNumber, followup) > 0) { if (contactsMapper.updateFollowUpByPhone(phoneNumber, followup, now) > 0) {
return true; return true;
} }
// 如果contacts表中没有,尝试在users表中保存 // 如果contacts表中没有,尝试在users表中保存
return supplyUsersMapper.updateFollowUpByPhone(phoneNumber, followup) > 0; return supplyUsersMapper.updateFollowUpByPhone(phoneNumber, followup, now) > 0;
} catch (Exception e) { } catch (Exception e) {
throw e; throw e;
} }

238
src/main/java/com/example/web/service/InformationTraService.java

@ -3,12 +3,18 @@ package com.example.web.service;
import com.example.web.dto.UserProductCartDTO; import com.example.web.dto.UserProductCartDTO;
import com.example.web.entity.Contacts; import com.example.web.entity.Contacts;
import com.example.web.entity.InformationTra; import com.example.web.entity.InformationTra;
import com.example.web.entity.Users;
import com.example.web.mapper.*; import com.example.web.mapper.*;
import com.example.web.config.DynamicDataSource; import com.example.web.config.DynamicDataSource;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*;
@Service @Service
public class InformationTraService { public class InformationTraService {
@ -102,4 +108,236 @@ public class InformationTraService {
System.out.println("🔄 恢复数据源为原始值: " + (originalDataSource != null ? originalDataSource : "默认数据源")); System.out.println("🔄 恢复数据源为原始值: " + (originalDataSource != null ? originalDataSource : "默认数据源"));
} }
} }
/**
* 记录详细变更信息
*/
public boolean recordDetailedChange(String phoneNumber, String customerName, String operationEvent,
String managerCompany, String managerDepartment, String organization,
String role, String userName, String assistant,
Object originalData, Object modifiedData, List<String> changedFields) {
// 保存原始数据源,以便在finally块中恢复
String originalDataSource = DynamicDataSource.getCurrentDataSourceKey();
try {
System.out.println("\n===========================================");
System.out.println("🚀 开始记录详细变更信息");
System.out.println("📱 手机号: " + phoneNumber);
System.out.println("👤 客户名: " + customerName);
System.out.println("📝 操作事件: " + operationEvent);
// 记录参数类型信息
System.out.println("📊 参数类型信息:");
System.out.println(" - originalData类型: " + (originalData != null ? originalData.getClass().getName() : "null"));
System.out.println(" - modifiedData类型: " + (modifiedData != null ? modifiedData.getClass().getName() : "null"));
System.out.println(" - changedFields类型: " + (changedFields != null ? changedFields.getClass().getName() : "null"));
// 1. 从两个数据源查询客户信息
String userId = null;
// 查询wechat数据源的users表
DynamicDataSource.setDataSourceKey("wechat");
System.out.println("🔄 查询wechat数据源...");
UserProductCartDTO wechatUser = usersMapper.selectByPhone(phoneNumber);
if (wechatUser != null) {
userId = wechatUser.getUserId();
System.out.println("✅ 从wechat数据源获取到userId: " + userId);
} else {
System.out.println("⚠️ 在wechat数据源中未找到客户");
// 查询primary数据源的contacts表
DynamicDataSource.setDataSourceKey("primary");
System.out.println("🔄 查询primary数据源...");
Contacts contact = contactsMapper.selectByPhoneNumber(phoneNumber);
if (contact != null) {
System.out.println("✅ 在primary数据源中找到客户: " + contact.getNickName());
userId = contact.getId();
System.out.println("ℹ️ 使用primary数据源的contact.id作为userId: " + userId);
}
}
// 如果都没找到,返回失败
if (userId == null) {
System.err.println("❌ 无法获取客户ID,操作记录失败");
return false;
}
// 2. 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 3. 构造操作记录
InformationTra informationTra = new InformationTra();
informationTra.setTracompany(managerCompany);
informationTra.setTradepartment(managerDepartment);
informationTra.setTraorganization(organization);
informationTra.setTrarole(role);
informationTra.setTrauserName(userName);
informationTra.setTraassistant(assistant);
informationTra.setUserId(userId);
informationTra.setOperationEvent(operationEvent);
informationTra.setOperationTime(now);
informationTra.setCreatedAt(now);
informationTra.setUpdatedAt(now);
// 4. 处理变更数据
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
// 详细记录原始数据
try {
if (originalData != null) {
System.out.println("\n📥 原始数据详细信息:");
System.out.println(" - 类型: " + originalData.getClass().getName());
System.out.println(" - 内容: " + originalData.toString());
// 尝试序列化
String originalJson = objectMapper.writeValueAsString(originalData);
informationTra.setOriginalData(originalJson);
System.out.println("✅ originalData序列化成功,长度: " + originalJson.length() + " 字符");
// 打印前100个字符预览
System.out.println(" - 预览: " + (originalJson.length() > 100 ? originalJson.substring(0, 100) + "..." : originalJson));
} else {
System.out.println("⚠️ originalData为null");
}
} catch (Exception e) {
System.err.println("❌ originalData序列化失败: " + e.getMessage());
e.printStackTrace();
// 尝试使用toString方法作为备选
if (originalData != null) {
System.out.println("🔄 尝试使用toString()作为备选: " + originalData.toString());
informationTra.setOriginalData(originalData.toString());
}
}
// 详细记录修改后数据
try {
if (modifiedData != null) {
System.out.println("\n📤 修改后数据详细信息:");
System.out.println(" - 类型: " + modifiedData.getClass().getName());
System.out.println(" - 内容: " + modifiedData.toString());
// 尝试序列化
String modifiedJson = objectMapper.writeValueAsString(modifiedData);
informationTra.setModifiedData(modifiedJson);
System.out.println("✅ modifiedData序列化成功,长度: " + modifiedJson.length() + " 字符");
// 打印前100个字符预览
System.out.println(" - 预览: " + (modifiedJson.length() > 100 ? modifiedJson.substring(0, 100) + "..." : modifiedJson));
} else {
System.out.println("⚠️ modifiedData为null");
}
} catch (Exception e) {
System.err.println("❌ modifiedData序列化失败: " + e.getMessage());
e.printStackTrace();
// 尝试使用toString方法作为备选
if (modifiedData != null) {
System.out.println("🔄 尝试使用toString()作为备选: " + modifiedData.toString());
informationTra.setModifiedData(modifiedData.toString());
}
}
// 详细记录变更字段
try {
if (changedFields != null && !changedFields.isEmpty()) {
System.out.println("\n🔄 变更字段详细信息:");
System.out.println(" - 数量: " + changedFields.size());
System.out.println(" - 内容: " + changedFields);
// 尝试序列化
String changedFieldsJson = objectMapper.writeValueAsString(changedFields);
informationTra.setChangedFields(changedFieldsJson);
System.out.println("✅ changedFields序列化成功");
} else {
System.out.println("⚠️ changedFields为null或空");
}
} catch (Exception e) {
System.err.println("❌ changedFields序列化失败: " + e.getMessage());
e.printStackTrace();
// 尝试使用toString方法作为备选
if (changedFields != null) {
System.out.println("🔄 尝试使用toString()作为备选: " + changedFields.toString());
informationTra.setChangedFields(changedFields.toString());
}
}
// 5. 始终写入wechat数据源的informationtra表
DynamicDataSource.setDataSourceKey("wechat");
System.out.println("\n💾 准备插入详细变更记录到wechat数据源");
System.out.println(" - userId: " + userId);
int result = informationTraMapper.insertInformationTra(informationTra);
if (result > 0) {
System.out.println("✅ 插入详细变更记录成功,影响行数: " + result);
System.out.println("===========================================");
return true;
} else {
System.err.println("❌ 插入详细变更记录失败,影响行数: " + result);
return false;
}
} catch (Exception e) {
System.err.println("❌ 详细变更记录异常: " + e.getMessage());
e.printStackTrace();
return false;
} finally {
// 恢复原始数据源,避免线程池复用问题
if (originalDataSource != null) {
DynamicDataSource.setDataSourceKey(originalDataSource);
} else {
DynamicDataSource.clearDataSourceKey();
}
System.out.println("🔄 恢复数据源为原始值: " + (originalDataSource != null ? originalDataSource : "默认数据源"));
}
}
/**
* 记录客户回流信息
*/
public boolean recordCustomerReturn(String userId, String managerCompany, String managerDepartment,
String organization, String role, String userName, String assistant) {
// 保存原始数据源,以便在finally块中恢复
String originalDataSource = DynamicDataSource.getCurrentDataSourceKey();
try {
// 1. 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 2. 构造操作记录
InformationTra informationTra = new InformationTra();
informationTra.setTracompany(managerCompany);
informationTra.setTradepartment(managerDepartment);
informationTra.setTraorganization(organization);
informationTra.setTrarole(role);
informationTra.setTrauserName(userName);
informationTra.setTraassistant(assistant);
informationTra.setUserId(userId);
informationTra.setOperationEvent("客户回流:系统自动降级至organization-sea-pools等级");
informationTra.setOperationTime(now);
informationTra.setCreatedAt(now);
informationTra.setUpdatedAt(now);
// 3. 始终写入wechat数据源的informationtra表
DynamicDataSource.setDataSourceKey("wechat");
System.out.println("🔄 设置数据源为wechat,准备插入客户回流记录");
int result = informationTraMapper.insertInformationTra(informationTra);
if (result > 0) {
System.out.println("✅ 插入客户回流记录成功,影响行数: " + result);
return true;
} else {
System.err.println("❌ 插入客户回流记录失败,影响行数: " + result);
return false;
}
} catch (Exception e) {
e.printStackTrace();
System.err.println("❌ 客户回流记录异常: " + e.getMessage());
return false;
} finally {
// 恢复原始数据源,避免线程池复用问题
if (originalDataSource != null) {
DynamicDataSource.setDataSourceKey(originalDataSource);
} else {
DynamicDataSource.clearDataSourceKey();
}
System.out.println("🔄 恢复数据源为原始值: " + (originalDataSource != null ? originalDataSource : "默认数据源"));
}
}
} }

5
src/main/java/com/example/web/service/SupplyCustomerRecycleService.java

@ -34,9 +34,9 @@ public class SupplyCustomerRecycleService {
private int organizationToDepartmentDays; private int organizationToDepartmentDays;
/** /**
* 客户回流定时任务 - 天凌晨2点执行 * 客户回流定时任务 - 15分钟执行一次
*/ */
@Scheduled(cron = "0 0 2 * * ?") @Scheduled(cron = "0 */15 * * * ?")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void autoRecycleCustomers() { public void autoRecycleCustomers() {
log.info("🎯 开始执行客户回流任务..."); log.info("🎯 开始执行客户回流任务...");
@ -197,7 +197,6 @@ public class SupplyCustomerRecycleService {
log.info("✅ 客户 {} 回流后负责人信息: {}", customer.getUserId(), manager.getUserName()); log.info("✅ 客户 {} 回流后负责人信息: {}", customer.getUserId(), manager.getUserName());
} }
} }
log.info("📊 获取到 {} 个回流客户的完整信息", recycledCustomers.size()); log.info("📊 获取到 {} 个回流客户的完整信息", recycledCustomers.size());
return recycledCustomers; return recycledCustomers;
} }

14
src/main/resources/application.yaml

@ -17,12 +17,12 @@ spring:
app: app:
recycle: recycle:
# 未分级客户回流到组织公海池的天数阈值 # 未分级客户回流到组织公海池的天数阈值
unclassified-to-organization-days: 30 unclassified-to-organization-days: 3
# 组织公海池客户回流到部门公海池的天数阈值 # 组织公海池客户回流到部门公海池的天数阈值
organization-to-department-days: 30 organization-to-department-days: 3
server: server:
port: 8081 port: 8080
servlet: servlet:
context-path: /DL context-path: /DL
# 在Tomcat中部署时,端口由Tomcat配置决定,这里不需要指定 # 在Tomcat中部署时,端口由Tomcat配置决定,这里不需要指定
@ -39,7 +39,9 @@ mybatis:
logging: logging:
level: level:
com.example.web.mapper: DEBUG com.example.web.mapper: TRACE
com.example.web.config: DEBUG com.example.web.config: TRACE
com.example.web.aspect: DEBUG com.example.web.aspect: TRACE
org.apache.ibatis: TRACE
org.mybatis.spring: TRACE

7
src/main/resources/mapper/InformationTraMapper.xml

@ -15,17 +15,20 @@
<result property="operationTime" column="operationTime"/> <result property="operationTime" column="operationTime"/>
<result property="createdAt" column="created_at"/> <result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/> <result property="updatedAt" column="updated_at"/>
<result property="originalData" column="originalData"/>
<result property="modifiedData" column="modifiedData"/>
<result property="changedFields" column="changedFields"/>
</resultMap> </resultMap>
<insert id="insertInformationTra" parameterType="com.example.web.entity.InformationTra"> <insert id="insertInformationTra" parameterType="com.example.web.entity.InformationTra">
INSERT INTO informationtra ( INSERT INTO informationtra (
tracompany, tradepartment, traorganization, trarole, tracompany, tradepartment, traorganization, trarole,
trauserName, traassistant, userId, operationEvent, trauserName, traassistant, userId, operationEvent,
operationTime, created_at, updated_at operationTime, created_at, updated_at, originalData, modifiedData, changedFields
) VALUES ( ) VALUES (
#{tracompany}, #{tradepartment}, #{traorganization}, #{trarole}, #{tracompany}, #{tradepartment}, #{traorganization}, #{trarole},
#{trauserName}, #{traassistant}, #{userId}, #{operationEvent}, #{trauserName}, #{traassistant}, #{userId}, #{operationEvent},
#{operationTime}, #{createdAt}, #{updatedAt} #{operationTime}, #{createdAt}, #{updatedAt}, #{originalData}, #{modifiedData}, #{changedFields}
) )
</insert> </insert>

32
src/main/resources/mapper/SupplyUsersManagementsMapper.xml

@ -22,7 +22,7 @@
<select id="findByUserNameAndManagerId" parameterType="map" resultMap="UsersManagementsResultMap"> <select id="findByUserNameAndManagerId" parameterType="map" resultMap="UsersManagementsResultMap">
SELECT id, userId, managerId, managercompany, managerdepartment, SELECT id, userId, managerId, managercompany, managerdepartment,
organization, role, root, created_at, updated_at, userName, assistant organization, role, root, created_at, updated_at, userName, assistant
FROM usermanagements FROM wechat_app.usermanagements
WHERE userName = #{userName} AND managerId = #{managerId} WHERE userName = #{userName} AND managerId = #{managerId}
</select> </select>
@ -30,7 +30,7 @@
<select id="findByDepartment" parameterType="string" resultMap="UsersManagementsResultMap"> <select id="findByDepartment" parameterType="string" resultMap="UsersManagementsResultMap">
SELECT id, userId, managerId, managercompany, managerdepartment, SELECT id, userId, managerId, managercompany, managerdepartment,
organization, role, root, created_at, updated_at, userName, assistant organization, role, root, created_at, updated_at, userName, assistant
FROM usermanagements FROM wechat_app.usermanagements
WHERE managerdepartment = #{department} WHERE managerdepartment = #{department}
</select> </select>
@ -38,7 +38,7 @@
<select id="findByConditions" parameterType="map" resultMap="UsersManagementsResultMap"> <select id="findByConditions" parameterType="map" resultMap="UsersManagementsResultMap">
SELECT id, userId, managerId, managercompany, managerdepartment, SELECT id, userId, managerId, managercompany, managerdepartment,
organization, role, root, created_at, updated_at, userName, assistant organization, role, root, created_at, updated_at, userName, assistant
FROM usermanagements FROM wechat_app.usermanagements
<where> <where>
<if test="userName != null">AND userName = #{userName}</if> <if test="userName != null">AND userName = #{userName}</if>
<if test="department != null">AND managerdepartment = #{department}</if> <if test="department != null">AND managerdepartment = #{department}</if>
@ -49,14 +49,14 @@
</select> </select>
<!-- 根据用户ID查询负责人信息 --> <!-- 根据用户ID查询负责人信息 -->
<select id="findByUserId" parameterType="string" resultMap="UsersManagementsResultMap"> <select id="findByUserId" parameterType="string" resultMap="UsersManagementsResultMap">
SELECT * FROM usermanagements SELECT * FROM wechat_app.usermanagements
WHERE userId = #{userId} WHERE userId = #{userId}
ORDER BY id DESC ORDER BY id DESC
</select> </select>
<!-- 插入负责人信息 --> <!-- 插入负责人信息 -->
<insert id="insertUsersManagements" parameterType="com.example.web.entity.UsersManagements"> <insert id="insertUsersManagements" parameterType="com.example.web.entity.UsersManagements">
INSERT INTO usermanagements ( INSERT INTO wechat_app.usermanagements (
userId, managerId, managercompany, managerdepartment, userId, managerId, managercompany, managerdepartment,
organization, role, root, created_at, updated_at, organization, role, root, created_at, updated_at,
userName, assistant userName, assistant
@ -69,7 +69,7 @@
<!-- 更新负责人信息 --> <!-- 更新负责人信息 -->
<update id="updateUsersManagements" parameterType="com.example.web.entity.UsersManagements"> <update id="updateUsersManagements" parameterType="com.example.web.entity.UsersManagements">
UPDATE usermanagements UPDATE wechat_app.usermanagements
SET managerId = #{managerId}, SET managerId = #{managerId},
managercompany = #{managercompany}, managercompany = #{managercompany},
managerdepartment = #{managerdepartment}, managerdepartment = #{managerdepartment},
@ -84,28 +84,28 @@
<!-- 更新负责人信息的更新时间 --> <!-- 更新负责人信息的更新时间 -->
<update id="updateManagerUpdateTime"> <update id="updateManagerUpdateTime">
UPDATE users_managements UPDATE wechat_app.usermanagements
SET updated_at = #{updatedAt} SET updated_at = #{updatedAt}
WHERE user_id = #{userId} WHERE userId = #{userId}
</update> </update>
<!-- 查询完整负责人信息 --> <!-- 查询完整负责人信息 -->
<select id="findCompleteManagerInfoByUserId" resultType="com.example.web.entity.UsersManagements"> <select id="findCompleteManagerInfoByUserId" resultType="com.example.web.entity.UsersManagements">
SELECT SELECT
manager_id, managerId,
managercompany, managercompany,
managerdepartment, managerdepartment,
organization, organization,
role, role,
user_name, userName,
assistant, assistant,
created_at, created_at,
updated_at updated_at
FROM users_managements FROM wechat_app.usermanagements
WHERE user_id = #{userId} WHERE userId = #{userId}
</select> </select>
<select id="findByUserIdAndAuthInfo" resultMap="UsersManagementsResultMap"> <select id="findByUserIdAndAuthInfo" resultMap="UsersManagementsResultMap">
SELECT * FROM usermanagements SELECT * FROM wechat_app.usermanagements
WHERE userId = #{userId} WHERE userId = #{userId}
AND (managercompany = #{authInfo.managercompany} OR #{authInfo.managercompany} IS NULL) AND (managercompany = #{authInfo.managercompany} OR #{authInfo.managercompany} IS NULL)
AND (managerdepartment = #{authInfo.managerdepartment} OR #{authInfo.managerdepartment} IS NULL) AND (managerdepartment = #{authInfo.managerdepartment} OR #{authInfo.managerdepartment} IS NULL)
@ -115,7 +115,7 @@
</select> </select>
<select id="findAuthorizedUserIds" resultType="string"> <select id="findAuthorizedUserIds" resultType="string">
SELECT userId FROM usermanagements SELECT userId FROM wechat_app.usermanagements
WHERE (managercompany = #{authInfo.managercompany} OR #{authInfo.managercompany} IS NULL) WHERE (managercompany = #{authInfo.managercompany} OR #{authInfo.managercompany} IS NULL)
AND (managerdepartment = #{authInfo.managerdepartment} OR #{authInfo.managerdepartment} IS NULL) AND (managerdepartment = #{authInfo.managerdepartment} OR #{authInfo.managerdepartment} IS NULL)
AND (organization = #{authInfo.organization} OR #{authInfo.organization} IS NULL) AND (organization = #{authInfo.organization} OR #{authInfo.organization} IS NULL)
@ -126,7 +126,7 @@
<select id="findByUserIds" resultMap="UsersManagementsResultMap"> <select id="findByUserIds" resultMap="UsersManagementsResultMap">
SELECT id, userId, managerId, managercompany, managerdepartment, SELECT id, userId, managerId, managercompany, managerdepartment,
organization, role, root, created_at, updated_at, userName, assistant organization, role, root, created_at, updated_at, userName, assistant
FROM usermanagements FROM wechat_app.usermanagements
WHERE userId IN WHERE userId IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")"> <foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId} #{userId}

40
src/main/resources/mapper/SupplyUsersMapper.xml

@ -86,7 +86,7 @@
u.created_at, u.created_at,
u.updated_at, u.updated_at,
u.notice u.notice
FROM users u FROM wechat_app.users u
LEFT JOIN usermanagements um ON u.userId = um.userId LEFT JOIN usermanagements um ON u.userId = um.userId
WHERE u.phoneNumber IS NOT NULL WHERE u.phoneNumber IS NOT NULL
AND u.phoneNumber != '' AND u.phoneNumber != ''
@ -122,7 +122,7 @@
<!-- 🚀 新增:采购端获取授权客户总数 --> <!-- 🚀 新增:采购端获取授权客户总数 -->
<select id="getAuthorizedCustomersCount" resultType="int" parameterType="map"> <select id="getAuthorizedCustomersCount" resultType="int" parameterType="map">
SELECT COUNT(DISTINCT u.userId) SELECT COUNT(DISTINCT u.userId)
FROM users u FROM wechat_app.users u
LEFT JOIN usermanagements um ON u.userId = um.userId LEFT JOIN usermanagements um ON u.userId = um.userId
WHERE u.phoneNumber IS NOT NULL WHERE u.phoneNumber IS NOT NULL
AND u.phoneNumber != '' AND u.phoneNumber != ''
@ -236,7 +236,7 @@
created_at, created_at,
updated_at, updated_at,
notice notice
FROM users FROM wechat_app.users
WHERE userId = #{userId} WHERE userId = #{userId}
</select> </select>
@ -275,7 +275,7 @@
notice, notice,
created_at, created_at,
updated_at updated_at
FROM users FROM wechat_app.users
WHERE phoneNumber IS NOT NULL WHERE phoneNumber IS NOT NULL
AND phoneNumber != '' AND phoneNumber != ''
AND (type = 'seller' OR type = 'both') AND (type = 'seller' OR type = 'both')
@ -297,7 +297,7 @@
created_at, created_at,
updated_at, updated_at,
notice notice
FROM users FROM wechat_app.users
WHERE phoneNumber = #{phoneNumber} WHERE phoneNumber = #{phoneNumber}
AND (type = 'seller' OR type = 'both') AND (type = 'seller' OR type = 'both')
</select> </select>
@ -317,7 +317,7 @@
created_at, created_at,
updated_at, updated_at,
notice notice
FROM users FROM wechat_app.users
WHERE userId = #{userId} WHERE userId = #{userId}
AND (type = 'seller' OR type = 'both') AND (type = 'seller' OR type = 'both')
</select> </select>
@ -341,7 +341,7 @@
<!-- 根据手机号更新用户信息 --> <!-- 根据手机号更新用户信息 -->
<update id="updateByPhone" parameterType="com.example.web.dto.UserProductCartDTO"> <update id="updateByPhone" parameterType="com.example.web.dto.UserProductCartDTO">
UPDATE users UPDATE wechat_app.users
SET SET
<if test="nickName != null and nickName != ''"> <if test="nickName != null and nickName != ''">
nickName = #{nickName}, nickName = #{nickName},
@ -622,7 +622,7 @@
created_at, created_at,
updated_at, updated_at,
notice notice
FROM users FROM wechat_app.users
WHERE userId IN WHERE userId IN
<foreach collection="userIds" item="userId" open="(" separator="," close=")"> <foreach collection="userIds" item="userId" open="(" separator="," close=")">
#{userId} #{userId}
@ -650,10 +650,10 @@
<!-- 查询超过指定时间的未分级客户 --> <!-- 查询超过指定时间的未分级客户 -->
<select id="findUnclassifiedCustomersOlderThan" resultType="com.example.web.dto.UserProductCartDTO"> <select id="findUnclassifiedCustomersOlderThan" resultType="com.example.web.dto.UserProductCartDTO">
SELECT SELECT
user_id as userId, userId,
phone_number as phoneNumber, phoneNumber,
type, type,
nick_name as nickName, nickName,
company, company,
region, region,
level, level,
@ -661,19 +661,18 @@
spec, spec,
created_at, created_at,
updated_at updated_at
FROM users FROM wechat_app.users
WHERE level = 'unclassified' WHERE level = 'unclassified'
AND updated_at &lt;= #{thresholdTime} AND updated_at &lt;= #{thresholdTime}
AND deleted = 0
</select> </select>
<!-- 查询超过指定时间的组织公海池客户 --> <!-- 查询超过指定时间的组织公海池客户 -->
<select id="findOrganizationSeaPoolsCustomersOlderThan" resultType="com.example.web.dto.UserProductCartDTO"> <select id="findOrganizationSeaPoolsCustomersOlderThan" resultType="com.example.web.dto.UserProductCartDTO">
SELECT SELECT
user_id as userId, userId,
phone_number as phoneNumber, phoneNumber,
type, type,
nick_name as nickName, nickName,
company, company,
region, region,
level, level,
@ -681,18 +680,17 @@
spec, spec,
created_at, created_at,
updated_at updated_at
FROM users FROM wechat_app.users
WHERE level = 'organization-sea-pools' WHERE level = 'organization-sea-pools'
AND updated_at &lt;= #{thresholdTime} AND updated_at &lt;= #{thresholdTime}
AND deleted = 0
</select> </select>
<select id="findRecentlyRecycledCustomers" resultType="com.example.web.dto.UserProductCartDTO"> <select id="findRecentlyRecycledCustomers" resultType="com.example.web.dto.UserProductCartDTO">
SELECT SELECT
u.user_id, u.userId,
u.phone_number, u.phoneNumber,
u.type, u.type,
u.nick_name, u.nickName,
u.company, u.company,
u.region, u.region,
u.level, u.level,

14
src/main/resources/mapper/UsersMapper.xml

@ -719,4 +719,18 @@
ORDER BY u.updated_at DESC ORDER BY u.updated_at DESC
</select> </select>
<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 &lt; 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="updatedAt != null">updated_at = #{updatedAt},</if>
<if test="followupAt != null">followup_at = #{followupAt},</if>
</set>
WHERE user_id = #{userId}
</update>
</mapper> </mapper>

918
src/main/resources/static/mainapp-sells.html

File diff suppressed because it is too large

484
src/main/resources/static/mainapp-supplys.html

@ -592,56 +592,62 @@
/* 通知铃铛按钮样式 */ /* 通知铃铛按钮样式 */
.notification-btn { .notification-btn {
background-color: #f8f9fa; background-color: #ffffff;
border: 1px solid #e0e0e0; border: 1px solid #e0e7ff;
border-radius: 50%; border-radius: 50%;
width: 40px; width: 48px;
height: 40px; height: 48px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
margin-right: 10px; margin-right: 10px;
color: #666; color: #6366f1;
position: relative; position: relative;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.15);
} }
.notification-btn:hover { .notification-btn:hover {
background-color: #e9ecef; background-color: #f0f4ff;
border-color: #adb5bd; border-color: #818cf8;
color: #495057; color: #4f46e5;
transform: scale(1.05); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.25);
} }
.notification-btn i { .notification-btn i {
font-size: 18px; font-size: 20px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* 通知铃铛激活状态样式 */ /* 通知铃铛激活状态样式 */
.notification-btn.notification-active { .notification-btn.notification-active {
background-color: #fee2e2; background-color: #fee2e2;
border-color: #fecaca; border-color: #ef4444;
color: #ef4444; color: #ef4444;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.25);
animation: ring 1s ease-in-out;
} }
/* 通知数量徽章样式 */ /* 通知数量徽章样式 */
.notification-count { .notification-count {
position: absolute; position: absolute;
top: -5px; top: -2px;
right: -5px; right: -2px;
background: #ef4444; background: #ef4444;
color: white; color: white;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
border-radius: 50%; border-radius: 10px;
min-width: 18px; min-width: 20px;
height: 18px; height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 6px; padding: 0 6px;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3);
animation: pulse 2s infinite;
} }
/* 铃铛摇晃动画 */ /* 铃铛摇晃动画 */
@ -659,6 +665,279 @@
100% { transform: rotate(0deg); } 100% { transform: rotate(0deg); }
} }
/* 徽章脉冲动画 */
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
/* 通知弹窗样式 */
.notification-modal {
position: fixed;
top: 0;
right: 0;
width: 360px;
max-height: 500px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: none;
flex-direction: column;
overflow: hidden;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.notification-modal.show {
display: flex;
}
.modal-content {
background: white;
border-radius: 20px;
width: 90%;
max-width: 900px;
max-height: 90vh;
overflow-y: auto;
padding: 0;
position: relative;
animation: fadeIn 0.3s ease;
}
.modal-header {
position: sticky;
top: 0;
z-index: 1001;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 20px 20px 0 0;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.modal-header-left {
display: flex;
align-items: center;
gap: 10px;
}
.modal-header-left i {
font-size: 20px;
}
.modal-header-right {
display: flex;
align-items: center;
gap: 10px;
}
.modal-title {
font-size: 24px;
margin: 0;
color: #333;
font-weight: 600;
}
.mark-all-read-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}
.mark-all-read-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.4);
}
.close-modal {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
}
.close-modal:hover {
background: rgba(255, 255, 255, 0.3);
}
.modal-body {
padding: 30px;
}
/* 通知项样式 */
.notification-item {
padding: 16px 20px;
border-bottom: 1px solid #f3f4f6;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: flex-start;
gap: 12px;
background: white;
position: relative;
}
.notification-item:hover {
background: #f9fafb;
transform: translateX(4px);
}
.notification-item.unread {
background: #f0f9ff;
border-left: 3px solid #3b82f6;
}
.notification-item.unread:hover {
background: #e0f2fe;
}
.notification-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: #e5e7eb;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
flex-shrink: 0;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
}
.customer-name {
font-weight: 600;
color: #111827;
font-size: 15px;
}
.notification-time {
font-size: 12px;
color: #9ca3af;
white-space: nowrap;
}
.notification-message {
font-size: 14px;
color: #4b5563;
line-height: 1.5;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.customer-info {
font-size: 12px;
color: #6b7280;
}
.read-status {
font-size: 12px;
color: #3b82f6;
}
/* 空状态样式 */
.notification-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
color: #9ca3af;
}
.notification-empty i {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.notification-empty h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #4b5563;
}
.notification-empty p {
margin: 0;
font-size: 14px;
}
/* 加载状态样式 */
.notification-loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
color: #6b7280;
}
/* 滚动条样式 */
.modal-body::-webkit-scrollbar {
width: 4px;
}
.modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
}
.modal-body::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2px;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* 头部操作容器样式 */ /* 头部操作容器样式 */
.header-actions-container { .header-actions-container {
display: flex; display: flex;
@ -2803,8 +3082,16 @@
<div class="modal" id="notificationModal"> <div class="modal" id="notificationModal">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">客户通知</h2> <div class="modal-header-left">
<button class="close-modal" id="closeNotificationModal">&times;</button> <i class="fas fa-bell"></i>
<h2 class="modal-title">客户通知</h2>
</div>
<div class="modal-header-right">
<button class="mark-all-read-btn" id="markAllAsRead">
<i class="fas fa-check-double"></i> 全部已读
</button>
<button class="close-modal" id="closeNotificationModal">&times;</button>
</div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="notificationContent"> <div id="notificationContent">
@ -4405,8 +4692,17 @@
updateNotificationStatus(customers) { updateNotificationStatus(customers) {
// 统计notice为banold的客户数量 // 统计notice为banold的客户数量
console.log('📊 更新通知状态 - 客户列表:', customers.length, '个客户'); console.log('📊 更新通知状态 - 客户列表:', customers.length, '个客户');
const banoldCount = customers.filter(customer => customer.notice === 'banold').length; const banoldCustomers = customers.filter(customer => customer.notice === 'banold');
console.log('🔔 待处理通知数量:', banoldCount);
// 计算实际未读数量(考虑localStorage中的已读标记)
let unreadCount = 0;
banoldCustomers.forEach(customer => {
if (localStorage.getItem(`notification_read_${customer.id}`) !== 'true') {
unreadCount++;
}
});
console.log('🔔 待处理通知数量:', unreadCount);
// 更新通知铃铛样式 // 更新通知铃铛样式
const notificationButton = document.getElementById('notificationButton'); const notificationButton = document.getElementById('notificationButton');
@ -4414,7 +4710,7 @@
if (notificationButton) { if (notificationButton) {
const bellIcon = notificationButton.querySelector('i'); const bellIcon = notificationButton.querySelector('i');
if (bellIcon) { if (bellIcon) {
if (banoldCount > 0) { if (unreadCount > 0) {
notificationButton.classList.add('notification-active'); notificationButton.classList.add('notification-active');
bellIcon.style.animation = 'ring 1s ease-in-out'; bellIcon.style.animation = 'ring 1s ease-in-out';
@ -4425,7 +4721,7 @@
countBadge.className = 'notification-count'; countBadge.className = 'notification-count';
notificationButton.appendChild(countBadge); notificationButton.appendChild(countBadge);
} }
countBadge.textContent = banoldCount; countBadge.textContent = unreadCount;
} else { } else {
notificationButton.classList.remove('notification-active'); notificationButton.classList.remove('notification-active');
bellIcon.style.animation = 'none'; bellIcon.style.animation = 'none';
@ -4442,7 +4738,7 @@
this.bindNotificationEvents(); this.bindNotificationEvents();
} }
return banoldCount; return unreadCount;
} }
async fetchPublicSeaData(loginInfo, level) { async fetchPublicSeaData(loginInfo, level) {
@ -4474,11 +4770,15 @@
const notificationButton = document.getElementById('notificationButton'); const notificationButton = document.getElementById('notificationButton');
const notificationModal = document.getElementById('notificationModal'); const notificationModal = document.getElementById('notificationModal');
const closeNotificationModal = document.getElementById('closeNotificationModal'); const closeNotificationModal = document.getElementById('closeNotificationModal');
const markAllAsReadBtn = document.getElementById('markAllAsRead');
if (notificationButton && notificationModal && closeNotificationModal) { if (notificationButton && notificationModal && closeNotificationModal) {
// 移除现有的点击事件监听器,避免重复绑定 // 移除现有的点击事件监听器,避免重复绑定
notificationButton.onclick = null; notificationButton.onclick = null;
closeNotificationModal.onclick = null; closeNotificationModal.onclick = null;
if (markAllAsReadBtn) {
markAllAsReadBtn.onclick = null;
}
// 点击通知按钮显示弹窗 // 点击通知按钮显示弹窗
notificationButton.addEventListener('click', () => { notificationButton.addEventListener('click', () => {
@ -4490,6 +4790,14 @@
this.hideNotificationModal(); this.hideNotificationModal();
}); });
// 点击"全部已读"按钮
if (markAllAsReadBtn) {
markAllAsReadBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.markAllAsRead();
});
}
// 点击弹窗外部关闭弹窗 // 点击弹窗外部关闭弹窗
notificationModal.addEventListener('click', (e) => { notificationModal.addEventListener('click', (e) => {
if (e.target === notificationModal) { if (e.target === notificationModal) {
@ -4506,8 +4814,8 @@
const notificationContent = document.getElementById('notificationContent'); const notificationContent = document.getElementById('notificationContent');
// 显示加载状态 // 显示加载状态
notificationContent.innerHTML = '<p style="text-align: center; color: #666;">加载中...</p>'; notificationContent.innerHTML = '<div class="notification-loading">正在加载通知...</div>';
notificationModal.classList.add('active'); notificationModal.style.display = 'block';
// 阻止背景滚动和操作 // 阻止背景滚动和操作
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@ -4519,7 +4827,6 @@
if (this.allPublicSeaCustomers) { if (this.allPublicSeaCustomers) {
allCustomers = this.allPublicSeaCustomers; allCustomers = this.allPublicSeaCustomers;
console.log('📋 从缓存获取客户数据:', allCustomers.length, '条'); console.log('📋 从缓存获取客户数据:', allCustomers.length, '条');
console.log('📋 缓存完整数据:', JSON.stringify(this.allPublicSeaCustomers));
} else { } else {
// 如果缓存中没有数据,直接从API获取 // 如果缓存中没有数据,直接从API获取
const url = appendAuthParams(`${API_BASE_URL}/supply/pool/all-customers`); const url = appendAuthParams(`${API_BASE_URL}/supply/pool/all-customers`);
@ -4535,7 +4842,6 @@
const data = result.data || {}; const data = result.data || {};
allCustomers = Array.isArray(data) ? data : Object.values(data); allCustomers = Array.isArray(data) ? data : Object.values(data);
console.log('🔄 转换后客户数组:', allCustomers.length, '条'); console.log('🔄 转换后客户数组:', allCustomers.length, '条');
console.log('🔄 转换后完整数据:', JSON.stringify(allCustomers));
// 更新缓存 // 更新缓存
this.allPublicSeaCustomers = allCustomers; this.allPublicSeaCustomers = allCustomers;
@ -4544,59 +4850,135 @@
// 获取notice为banold的客户 // 获取notice为banold的客户
const banoldCustomers = allCustomers.filter(customer => customer.notice === 'banold'); const banoldCustomers = allCustomers.filter(customer => customer.notice === 'banold');
console.log('🔔 符合条件的banold客户:', banoldCustomers.length, '条'); console.log('🔔 符合条件的banold客户:', banoldCustomers.length, '条');
console.log('🔔 banold客户详情:', JSON.stringify(banoldCustomers));
if (banoldCustomers.length === 0) { if (banoldCustomers.length === 0) {
notificationContent.innerHTML = '<div class="notification-empty"><p>暂无通知</p></div>'; notificationContent.innerHTML = '<div class="notification-empty"><i class="fas fa-bell-slash"></i><h3>暂无通知</h3><p>目前没有需要处理的客户通知</p></div>';
} else { } else {
// 生成通知内容 // 生成通知内容
let contentHTML = '<div class="notification-list">'; let contentHTML = '';
banoldCustomers.forEach(customer => { banoldCustomers.forEach(customer => {
// 检查客户是否已读(通过localStorage标记)
const isRead = localStorage.getItem(`notification_read_${customer.id}`) === 'true';
const customerName = customer.company || customer.companyName || '未知'; const customerName = customer.company || customer.companyName || '未知';
// 为banold状态(未读)添加new类
const notificationClass = customer.notice === 'banold' ? 'notification-item new' : 'notification-item';
contentHTML += ` contentHTML += `
<div class="${notificationClass}" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)"> <div class="notification-item ${isRead ? '' : 'unread'}" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)">
<div class="notification-header"> <div class="notification-icon">
<div class="notification-icon"> <i class="fas fa-user-alt"></i>
<i class="fas fa-user-clock" style="color: #ff9800;"></i> </div>
<div class="notification-content">
<div class="notification-header">
<div class="customer-name">${customerName}</div>
<div class="notification-time">${new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</div>
</div> </div>
<div class="notification-content"> <div class="notification-message">客户信息已更新,需要您的关注</div>
<div class="notification-title"> <div class="notification-footer">
${customerName} <div class="customer-info">ID: ${customer.id} | 电话: ${customer.phoneNumber || '无'}</div>
</div> ${isRead ? '' : '<div class="read-status">未读</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> </div>
</div> </div>
`; `;
}); });
contentHTML += '</div>';
notificationContent.innerHTML = contentHTML; notificationContent.innerHTML = contentHTML;
} }
} catch (error) { } catch (error) {
console.error('❌ 获取通知数据失败:', error); console.error('❌ 获取通知数据失败:', error);
console.error('❌ 错误详情:', error.stack); console.error('❌ 错误详情:', error.stack);
notificationContent.innerHTML = '<p style="text-align: center; color: #ff6b6b;">加载通知失败,请稍后重试</p>'; notificationContent.innerHTML = '<div class="notification-empty"><i class="fas fa-exclamation-circle"></i><h3>获取通知失败</h3><p>请稍后重试或检查网络连接</p></div>';
} }
} }
// 隐藏通知弹窗 // 隐藏通知弹窗
hideNotificationModal() { hideNotificationModal() {
const notificationModal = document.getElementById('notificationModal'); const notificationModal = document.getElementById('notificationModal');
notificationModal.classList.remove('active'); notificationModal.style.display = 'none';
// 恢复背景滚动 // 恢复背景滚动
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
} }
// 标记通知为已读
markNotificationAsRead(customerId) {
// 标记通知为已读
localStorage.setItem(`notification_read_${customerId}`, 'true');
// 更新界面显示
const notificationItem = document.querySelector(`[data-customer-id="${customerId}"]`);
if (notificationItem) {
notificationItem.classList.remove('unread');
const readStatus = notificationItem.querySelector('.read-status');
if (readStatus) {
readStatus.remove();
}
}
// 更新未读数量
this.updateNotificationStatus(this.allPublicSeaCustomers);
}
// 标记所有通知为已读
markAllAsRead() {
// 获取所有通知项
const notificationItems = document.querySelectorAll('.notification-item');
if (notificationItems.length === 0) return;
// 标记所有通知为已读
notificationItems.forEach(item => {
// 从点击事件中获取customerId
const customerId = item.getAttribute('onclick').match(/'([^']+)'/)[1];
localStorage.setItem(`notification_read_${customerId}`, 'true');
item.classList.remove('unread');
const readStatus = item.querySelector('.read-status');
if (readStatus) {
readStatus.remove();
}
});
// 更新未读数量
this.updateNotificationStatus(this.allPublicSeaCustomers);
// 显示提示
this.showToast('所有通知已标记为已读');
}
// 显示提示消息
showToast(message) {
// 创建简单的提示消息
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #10b981;
color: white;
padding: 12px 24px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
animation: slideIn 0.3s ease;
`;
toast.textContent = message;
// 添加动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
document.body.appendChild(toast);
// 3秒后移除提示
setTimeout(() => {
toast.remove();
style.remove();
}, 3000);
}
// 获取普通等级数据的辅助方法 // 获取普通等级数据的辅助方法

30
test-update-customer.bat

@ -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

16
test.xml

@ -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>

BIN
test_method.xml

Binary file not shown.

798
客户数据变更追踪详细方案.md

@ -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. **可扩展性**:设计考虑了未来的扩展需求
该方案可以满足需求,实现客户数据的详细变更追踪和客户回流功能。

839
客户通知弹窗重新设计方案.md

@ -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">&times;</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">&times;</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…
Cancel
Save