Browse Source

修复客户通知弹窗问题,包括弹窗闪烁、无数据和无法关闭等问题

master
Trae AI 3 months ago
parent
commit
e29eb036ff
  1. 5
      pom.xml
  2. 28
      src/main/java/com/example/web/config/WebSocketConfig.java
  3. 7
      src/main/java/com/example/web/controller/LoginController.java
  4. 52
      src/main/java/com/example/web/dto/NotificationDTO.java
  5. 6
      src/main/java/com/example/web/dto/UnifiedCustomerDTO.java
  6. 62
      src/main/java/com/example/web/listener/DatabaseChangeListener.java
  7. 32
      src/main/java/com/example/web/service/CustomerService.java
  8. 11
      src/main/java/com/example/web/service/EnterpriseService.java
  9. 7
      src/main/java/com/example/web/service/InformationTraService.java
  10. 8
      src/main/java/com/example/web/service/PoolCustomerService.java
  11. 52
      src/main/java/com/example/web/service/RealTimeDataService.java
  12. 17
      src/main/java/com/example/web/service/SupplyCustomerRecycleService.java
  13. 33
      src/main/java/com/example/web/service/SupplyCustomerService.java
  14. 11
      src/main/java/com/example/web/service/SupplyEnterpriseService.java
  15. 8
      src/main/java/com/example/web/service/SupplyPoolCustomerService.java
  16. 31
      src/main/java/com/example/web/task/DataRefreshTask.java
  17. 45
      src/main/resources/application.yaml
  18. 2
      src/main/resources/static/loginmm.html
  19. 678
      src/main/resources/static/mainapp-sells.html
  20. 445
      src/main/resources/static/mainapp-supplys.html
  21. 465
      实时数据接收与缓存优化方案.md
  22. 314
      预加载解决方案.md

5
pom.xml

@ -40,6 +40,7 @@
<dependency> <dependency>
<groupId>com.mysql</groupId> <groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
@ -116,6 +117,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies> </dependencies>

28
src/main/java/com/example/web/config/WebSocketConfig.java

@ -0,0 +1,28 @@
package com.example.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 配置消息代理,用于广播消息
config.enableSimpleBroker("/topic");
// 配置应用程序前缀,用于处理客户端发送的消息
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册WebSocket端点,允许前端连接
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // 支持SockJS,兼容不支持WebSocket的浏览器
}
}

7
src/main/java/com/example/web/controller/LoginController.java

@ -30,8 +30,13 @@ public class LoginController {
System.out.println("权限等级: " + result.get("root")); System.out.println("权限等级: " + result.get("root"));
System.out.println("Token: " + result.get("token")); System.out.println("Token: " + result.get("token"));
System.out.println("系统类型: " + (Boolean.TRUE.equals(result.get("isSupplySide")) ? "采购端" : "销售端")); System.out.println("系统类型: " + (Boolean.TRUE.equals(result.get("isSupplySide")) ? "采购端" : "销售端"));
Map<String, Object> user = (Map<String, Object>) result.get("user"); Object userObj = result.get("user");
if (userObj instanceof Map) {
Map<String, Object> user = (Map<String, Object>) userObj;
System.out.println("用户信息: " + user); System.out.println("用户信息: " + user);
} else {
System.out.println("用户信息: " + userObj);
}
} }
System.out.println(result.get("user")); System.out.println(result.get("user"));
return result; return result;

52
src/main/java/com/example/web/dto/NotificationDTO.java

@ -0,0 +1,52 @@
package com.example.web.dto;
import java.time.LocalDateTime;
public class NotificationDTO {
private String type; // 通知类型
private String title; // 通知标题
private String message; // 通知内容
private String customerId; // 相关客户ID
private LocalDateTime timestamp; // 时间戳
// getter和setter方法
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getCustomerId() {
return customerId;
}
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
}

6
src/main/java/com/example/web/dto/UnifiedCustomerDTO.java

@ -193,31 +193,37 @@ public class UnifiedCustomerDTO {
/** /**
* @deprecated 请使用 getUpdateContactData() * @deprecated 请使用 getUpdateContactData()
*/ */
@Deprecated
public ContactInfo getUpdateContact() { return updateContactData; } public ContactInfo getUpdateContact() { return updateContactData; }
/** /**
* @deprecated 请使用 setUpdateContactData() * @deprecated 请使用 setUpdateContactData()
*/ */
@Deprecated
public void setUpdateContact(ContactInfo updateContact) { this.updateContactData = updateContact; } public void setUpdateContact(ContactInfo updateContact) { this.updateContactData = updateContact; }
/** /**
* @deprecated 请使用 getUpdateCartItemData() * @deprecated 请使用 getUpdateCartItemData()
*/ */
@Deprecated
public CartItem getUpdateCartItem() { return updateCartItemData; } public CartItem getUpdateCartItem() { return updateCartItemData; }
/** /**
* @deprecated 请使用 setUpdateCartItemData() * @deprecated 请使用 setUpdateCartItemData()
*/ */
@Deprecated
public void setUpdateCartItem(CartItem updateCartItem) { this.updateCartItemData = updateCartItem; } public void setUpdateCartItem(CartItem updateCartItem) { this.updateCartItemData = updateCartItem; }
/** /**
* @deprecated 请使用 getUpdateProductItemData() * @deprecated 请使用 getUpdateProductItemData()
*/ */
@Deprecated
public ProductItem getUpdateProductItem() { return updateProductItemData; } public ProductItem getUpdateProductItem() { return updateProductItemData; }
/** /**
* @deprecated 请使用 setUpdateProductItemData() * @deprecated 请使用 setUpdateProductItemData()
*/ */
@Deprecated
public void setUpdateProductItem(ProductItem updateProductItem) { this.updateProductItemData = updateProductItem; } public void setUpdateProductItem(ProductItem updateProductItem) { this.updateProductItemData = updateProductItem; }
// 基础字段的getter/setter // 基础字段的getter/setter

62
src/main/java/com/example/web/listener/DatabaseChangeListener.java

@ -0,0 +1,62 @@
package com.example.web.listener;
import com.example.web.dto.UserProductCartDTO;
import com.example.web.mapper.SupplyUsersMapper;
import com.example.web.mapper.UsersMapper;
import com.example.web.service.RealTimeDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DatabaseChangeListener {
@Autowired
private RealTimeDataService realTimeDataService;
@Autowired
private UsersMapper usersMapper;
@Autowired
private SupplyUsersMapper supplyUsersMapper;
/**
* 手动触发数据更新推送供外部调用或定时任务调用
*/
public void triggerDataUpdate() {
// 推送销售端公海池数据更新
List<UserProductCartDTO> publicSeaCustomers = usersMapper.getAllUserBasicInfo();
realTimeDataService.pushPublicSeaUpdate(publicSeaCustomers);
realTimeDataService.pushAllCustomersUpdate(publicSeaCustomers);
// 推送采购端数据更新
List<UserProductCartDTO> supplyCustomers = supplyUsersMapper.getAllUserBasicInfo();
realTimeDataService.pushPublicSeaUpdate(supplyCustomers);
realTimeDataService.pushAllCustomersUpdate(supplyCustomers);
// 推送notice为banold的客户通知
pushBanoldCustomerNotifications(publicSeaCustomers);
pushBanoldCustomerNotifications(supplyCustomers);
}
/**
* 推送notice为banold的客户通知
*/
private void pushBanoldCustomerNotifications(List<UserProductCartDTO> customers) {
// 筛选出notice为banold的客户
List<UserProductCartDTO> banoldCustomers = customers.stream()
.filter(customer -> "banold".equals(customer.getNotice()))
.collect(java.util.stream.Collectors.toList());
// 为每个banold客户推送通知
for (UserProductCartDTO customer : banoldCustomers) {
realTimeDataService.pushNotification(
"CUSTOMER_NOTICE",
"客户通知",
"客户" + (customer.getCompany() != null ? customer.getCompany() : customer.getNickName()) + "需要您的关注",
customer.getUserId()
);
}
}
}

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

@ -17,6 +17,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -47,6 +48,9 @@ public class CustomerService {
@Autowired @Autowired
private InformationTraService informationTraService; private InformationTraService informationTraService;
@Autowired
private RealTimeDataService realTimeDataService;
// ==================== 精确更新方法 ==================== // ==================== 精确更新方法 ====================
@ -97,6 +101,16 @@ public class CustomerService {
checkAndUpdateManagerInfo(originalLevel, newLevel, dto, userId, authInfo); checkAndUpdateManagerInfo(originalLevel, newLevel, dto, userId, authInfo);
System.out.println("✅ 公海池客户信息精确更新成功"); System.out.println("✅ 公海池客户信息精确更新成功");
// 实时推送客户更新
UserProductCartDTO updatedCustomer = usersMapper.selectByUserId(userId);
realTimeDataService.pushCustomerUpdate(userId, updatedCustomer);
// 推送公海池数据更新
List<UserProductCartDTO> allCustomers = usersMapper.getAllUserBasicInfo();
realTimeDataService.pushPublicSeaUpdate(allCustomers);
realTimeDataService.pushAllCustomersUpdate(allCustomers);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@ -291,7 +305,7 @@ public class CustomerService {
existingManager.setRoot("3"); existingManager.setRoot("3");
} }
existingManager.setUpdated_at(LocalDateTime.now()); existingManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = usersManagementsMapper.updateUsersManagements(existingManager); int rows = usersManagementsMapper.updateUsersManagements(existingManager);
System.out.println("✅ 更新负责人记录影响行数: " + rows); System.out.println("✅ 更新负责人记录影响行数: " + rows);
@ -327,8 +341,8 @@ public class CustomerService {
newManager.setManagerId(getSafeString(authInfo.getManagerId())); newManager.setManagerId(getSafeString(authInfo.getManagerId()));
newManager.setRoot("3"); // 默认权限 newManager.setRoot("3"); // 默认权限
newManager.setCreated_at(LocalDateTime.now()); newManager.setCreated_at(LocalDateTime.now(ZoneOffset.UTC));
newManager.setUpdated_at(LocalDateTime.now()); newManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = usersManagementsMapper.insertUsersManagements(newManager); int rows = usersManagementsMapper.insertUsersManagements(newManager);
System.out.println("✅ 插入负责人记录影响行数: " + rows); System.out.println("✅ 插入负责人记录影响行数: " + rows);
@ -480,7 +494,7 @@ public class CustomerService {
existingManager.setRoot("3"); existingManager.setRoot("3");
} }
existingManager.setUpdated_at(LocalDateTime.now()); existingManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = usersManagementsMapper.updateUsersManagements(existingManager); int rows = usersManagementsMapper.updateUsersManagements(existingManager);
System.out.println("✅ 更新负责人记录影响行数: " + rows); System.out.println("✅ 更新负责人记录影响行数: " + rows);
@ -503,8 +517,8 @@ public class CustomerService {
newManager.setUserName(getSafeString(authInfo.getUserName())); newManager.setUserName(getSafeString(authInfo.getUserName()));
newManager.setAssistant(getSafeString(authInfo.getAssistant())); newManager.setAssistant(getSafeString(authInfo.getAssistant()));
newManager.setCreated_at(LocalDateTime.now()); newManager.setCreated_at(LocalDateTime.now(ZoneOffset.UTC));
newManager.setUpdated_at(LocalDateTime.now()); newManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = usersManagementsMapper.insertUsersManagements(newManager); int rows = usersManagementsMapper.insertUsersManagements(newManager);
System.out.println("✅ 插入负责人记录影响行数: " + rows); System.out.println("✅ 插入负责人记录影响行数: " + rows);
@ -860,7 +874,7 @@ public class CustomerService {
updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand())); updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand()));
updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec())); updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec()));
// 优先使用前端传递的更新时间,如果没有则使用当前时间 // 优先使用前端传递的更新时间,如果没有则使用当前时间
updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now()); updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now(ZoneOffset.UTC));
int rows = usersMapper.updateByPhone(updateUser); int rows = usersMapper.updateByPhone(updateUser);
System.out.println("更新用户基本信息影响行数: " + rows); System.out.println("更新用户基本信息影响行数: " + rows);
@ -1104,7 +1118,7 @@ public class CustomerService {
updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand())); updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand()));
updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec())); updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec()));
// 优先使用前端传递的更新时间,如果没有则使用当前时间 // 优先使用前端传递的更新时间,如果没有则使用当前时间
updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now()); updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now(ZoneOffset.UTC));
int rows = usersMapper.updateByPhone(updateUser); int rows = usersMapper.updateByPhone(updateUser);
System.out.println("更新用户基本信息影响行数: " + rows); System.out.println("更新用户基本信息影响行数: " + rows);
@ -1406,7 +1420,7 @@ public class CustomerService {
Users updateUser = new Users(); Users updateUser = new Users();
updateUser.setUserId(customer.getUserId()); updateUser.setUserId(customer.getUserId());
updateUser.setLevel("organization-sea-pools"); updateUser.setLevel("organization-sea-pools");
updateUser.setUpdated_at(LocalDateTime.now()); updateUser.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = usersMapper.updateByUserId(updateUser); int rows = usersMapper.updateByUserId(updateUser);
if (rows > 0) { if (rows > 0) {

11
src/main/java/com/example/web/service/EnterpriseService.java

@ -17,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -175,7 +176,7 @@ public class EnterpriseService {
contacts.setBank(dto.getBank()); contacts.setBank(dto.getBank());
contacts.setAddress(dto.getAddress()); contacts.setAddress(dto.getAddress());
contacts.setCreated_at(dto.getCreated_at() != null ? dto.getCreated_at() : LocalDateTime.now()); contacts.setCreated_at(dto.getCreated_at() != null ? dto.getCreated_at() : LocalDateTime.now());
contacts.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now()); contacts.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now(ZoneOffset.UTC));
int contactsRows = contactsMapper.insertContacts(contacts); int contactsRows = contactsMapper.insertContacts(contacts);
System.out.println("✅ 联系人信息保存结果: " + contactsRows + " 行受影响"); System.out.println("✅ 联系人信息保存结果: " + contactsRows + " 行受影响");
@ -193,7 +194,7 @@ public class EnterpriseService {
managers.setUserName(StringUtils.hasText(dto.getUserName()) ? dto.getUserName() : "未分配"); managers.setUserName(StringUtils.hasText(dto.getUserName()) ? dto.getUserName() : "未分配");
managers.setAssistant(StringUtils.hasText(dto.getAssistant()) ? dto.getAssistant() : "无"); managers.setAssistant(StringUtils.hasText(dto.getAssistant()) ? dto.getAssistant() : "无");
managers.setCreated_at(LocalDateTime.now()); managers.setCreated_at(LocalDateTime.now());
managers.setUpdated_at(LocalDateTime.now()); managers.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int managersRows = managersMapper.insertManagers(managers); int managersRows = managersMapper.insertManagers(managers);
System.out.println("✅ 负责人信息保存结果: " + managersRows + " 行受影响"); System.out.println("✅ 负责人信息保存结果: " + managersRows + " 行受影响");
@ -376,7 +377,7 @@ public class EnterpriseService {
contacts.setBank(dto.getBank()); contacts.setBank(dto.getBank());
contacts.setAddress(dto.getAddress()); contacts.setAddress(dto.getAddress());
contacts.setCreated_at(LocalDateTime.now()); contacts.setCreated_at(LocalDateTime.now());
contacts.setUpdated_at(LocalDateTime.now()); contacts.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int contactsRows = contactsMapper.insertContacts(contacts); int contactsRows = contactsMapper.insertContacts(contacts);
if (contactsRows <= 0) { if (contactsRows <= 0) {
@ -416,7 +417,7 @@ public class EnterpriseService {
managers.setUserName(dto.getUserName()); managers.setUserName(dto.getUserName());
managers.setAssistant(dto.getAssistant()); managers.setAssistant(dto.getAssistant());
managers.setUpdated_at(LocalDateTime.now()); managers.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int managersRows = managersMapper.updateManagers(managers); int managersRows = managersMapper.updateManagers(managers);
if (managersRows <= 0) { if (managersRows <= 0) {
@ -474,7 +475,7 @@ public class EnterpriseService {
updateUser.setPhoneNumber(dto.getPhoneNumber()); updateUser.setPhoneNumber(dto.getPhoneNumber());
updateUser.setNickName(dto.getNickName()); updateUser.setNickName(dto.getNickName());
updateUser.setType(dto.getType()); updateUser.setType(dto.getType());
updateUser.setUpdated_at(LocalDateTime.now()); updateUser.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = usersMapper.updateByPhone(updateUser); int rows = usersMapper.updateByPhone(updateUser);
System.out.println("更新用户基本信息影响行数: " + rows); System.out.println("更新用户基本信息影响行数: " + rows);

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

@ -14,6 +14,7 @@ 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.time.ZoneOffset;
import java.util.*; import java.util.*;
@Service @Service
@ -67,7 +68,7 @@ public class InformationTraService {
} }
// 2. 获取当前时间 // 2. 获取当前时间
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
// 3. 构造操作记录 // 3. 构造操作记录
InformationTra informationTra = new InformationTra(); InformationTra informationTra = new InformationTra();
@ -163,7 +164,7 @@ public class InformationTraService {
} }
// 2. 获取当前时间 // 2. 获取当前时间
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
// 3. 构造操作记录 // 3. 构造操作记录
InformationTra informationTra = new InformationTra(); InformationTra informationTra = new InformationTra();
@ -299,7 +300,7 @@ public class InformationTraService {
String originalDataSource = DynamicDataSource.getCurrentDataSourceKey(); String originalDataSource = DynamicDataSource.getCurrentDataSourceKey();
try { try {
// 1. 获取当前时间 // 1. 获取当前时间
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
// 2. 构造操作记录 // 2. 构造操作记录
InformationTra informationTra = new InformationTra(); InformationTra informationTra = new InformationTra();

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

@ -19,6 +19,8 @@ public class PoolCustomerService {
private UsersMapper usersMapper; private UsersMapper usersMapper;
@Autowired @Autowired
private UsersManagementsMapper usersManagementsMapper; private UsersManagementsMapper usersManagementsMapper;
@Autowired
private RealTimeDataService realTimeDataService;
/** /**
* 公共方法判断是否为公海池客户供Controller调用 * 公共方法判断是否为公海池客户供Controller调用
@ -449,6 +451,12 @@ public class PoolCustomerService {
System.out.println("🔄 更新通知状态, userId: " + userId + ", 从banold改为old"); System.out.println("🔄 更新通知状态, userId: " + userId + ", 从banold改为old");
usersMapper.updateNotice(userId, "old"); usersMapper.updateNotice(userId, "old");
userInfo.setNotice("old"); userInfo.setNotice("old");
// 实时推送更新,确保前端通知计数正确
System.out.println("📡 推送实时更新,通知状态已变更");
List<UserProductCartDTO> allCustomers = usersMapper.getAllUserBasicInfo();
realTimeDataService.pushPublicSeaUpdate(allCustomers);
realTimeDataService.pushAllCustomersUpdate(allCustomers);
} }
// 销售端权限校验 // 销售端权限校验

52
src/main/java/com/example/web/service/RealTimeDataService.java

@ -0,0 +1,52 @@
package com.example.web.service;
import com.example.web.dto.NotificationDTO;
import com.example.web.dto.UserProductCartDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
@Service
public class RealTimeDataService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 推送客户数据更新
*/
public void pushCustomerUpdate(String customerId, UserProductCartDTO customerData) {
messagingTemplate.convertAndSend("/topic/customers/" + customerId, customerData);
}
/**
* 推送公海池数据更新
*/
public void pushPublicSeaUpdate(List<UserProductCartDTO> customerList) {
messagingTemplate.convertAndSend("/topic/public-sea", customerList);
}
/**
* 推送所有客户数据更新
*/
public void pushAllCustomersUpdate(List<UserProductCartDTO> customerList) {
messagingTemplate.convertAndSend("/topic/all-customers", customerList);
}
/**
* 推送通知
*/
public void pushNotification(String type, String title, String message, String customerId) {
NotificationDTO notification = new NotificationDTO();
notification.setType(type);
notification.setTitle(title);
notification.setMessage(message);
notification.setCustomerId(customerId);
notification.setTimestamp(LocalDateTime.now(ZoneOffset.UTC));
messagingTemplate.convertAndSend("/topic/notifications", notification);
}
}

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

@ -13,6 +13,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
@Service @Service
@ -36,7 +37,7 @@ public class SupplyCustomerRecycleService {
/** /**
* 客户回流定时任务 - 每15分钟执行一次 * 客户回流定时任务 - 每15分钟执行一次
*/ */
@Scheduled(cron = "0 */15 * * * ?") @Scheduled(cron = "0 */15 * * * ?")// 每15分钟执行一次
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void autoRecycleCustomers() { public void autoRecycleCustomers() {
log.info("🎯 开始执行客户回流任务..."); log.info("🎯 开始执行客户回流任务...");
@ -63,7 +64,7 @@ public class SupplyCustomerRecycleService {
private void recycleUnclassifiedToOrganization() { private void recycleUnclassifiedToOrganization() {
log.info("🔄 开始处理未分级客户回流..."); log.info("🔄 开始处理未分级客户回流...");
LocalDateTime thresholdTime = LocalDateTime.now().minusDays(unclassifiedToOrganizationDays); LocalDateTime thresholdTime = LocalDateTime.now(ZoneOffset.UTC).minusDays(unclassifiedToOrganizationDays);
// 查询超过指定天数未更新的未分级客户 // 查询超过指定天数未更新的未分级客户
List<UserProductCartDTO> unclassifiedCustomers = supplyUsersMapper.findUnclassifiedCustomersOlderThan(thresholdTime); List<UserProductCartDTO> unclassifiedCustomers = supplyUsersMapper.findUnclassifiedCustomersOlderThan(thresholdTime);
@ -84,7 +85,7 @@ public class SupplyCustomerRecycleService {
boolean success = supplyUsersMapper.updateCustomerLevel( boolean success = supplyUsersMapper.updateCustomerLevel(
customer.getUserId(), customer.getUserId(),
"organization-sea-pools", "organization-sea-pools",
LocalDateTime.now() LocalDateTime.now(ZoneOffset.UTC)
); );
if (success) { if (success) {
@ -92,7 +93,7 @@ public class SupplyCustomerRecycleService {
if (currentManager != null) { if (currentManager != null) {
// 更新负责人信息的更新时间,但不改变负责人本身 // 更新负责人信息的更新时间,但不改变负责人本身
boolean managerUpdated = supplyUsersManagementsMapper.updateManagerUpdateTime( boolean managerUpdated = supplyUsersManagementsMapper.updateManagerUpdateTime(
customer.getUserId(), LocalDateTime.now()); customer.getUserId(), LocalDateTime.now(ZoneOffset.UTC));
log.info("✅ 客户 {} 负责人信息已保留: {}", customer.getUserId(), log.info("✅ 客户 {} 负责人信息已保留: {}", customer.getUserId(),
managerUpdated ? "成功" : "失败"); managerUpdated ? "成功" : "失败");
} }
@ -116,7 +117,7 @@ public class SupplyCustomerRecycleService {
private void recycleOrganizationToDepartment() { private void recycleOrganizationToDepartment() {
log.info("🔄 开始处理组织公海池客户回流..."); log.info("🔄 开始处理组织公海池客户回流...");
LocalDateTime thresholdTime = LocalDateTime.now().minusDays(organizationToDepartmentDays); LocalDateTime thresholdTime = LocalDateTime.now(ZoneOffset.UTC).minusDays(organizationToDepartmentDays);
// 查询超过指定天数未更新的组织公海池客户 // 查询超过指定天数未更新的组织公海池客户
List<UserProductCartDTO> organizationCustomers = supplyUsersMapper.findOrganizationSeaPoolsCustomersOlderThan(thresholdTime); List<UserProductCartDTO> organizationCustomers = supplyUsersMapper.findOrganizationSeaPoolsCustomersOlderThan(thresholdTime);
@ -137,7 +138,7 @@ public class SupplyCustomerRecycleService {
boolean success = supplyUsersMapper.updateCustomerLevel( boolean success = supplyUsersMapper.updateCustomerLevel(
customer.getUserId(), customer.getUserId(),
"department-sea-pools", "department-sea-pools",
LocalDateTime.now() LocalDateTime.now(ZoneOffset.UTC)
); );
if (success) { if (success) {
@ -145,7 +146,7 @@ public class SupplyCustomerRecycleService {
if (currentManager != null) { if (currentManager != null) {
// 更新负责人信息的更新时间,但不改变负责人本身 // 更新负责人信息的更新时间,但不改变负责人本身
boolean managerUpdated = supplyUsersManagementsMapper.updateManagerUpdateTime( boolean managerUpdated = supplyUsersManagementsMapper.updateManagerUpdateTime(
customer.getUserId(), LocalDateTime.now()); customer.getUserId(), LocalDateTime.now(ZoneOffset.UTC));
log.info("✅ 客户 {} 负责人信息已保留: {}", customer.getUserId(), log.info("✅ 客户 {} 负责人信息已保留: {}", customer.getUserId(),
managerUpdated ? "成功" : "失败"); managerUpdated ? "成功" : "失败");
} }
@ -186,7 +187,7 @@ public class SupplyCustomerRecycleService {
log.info("🔍 获取回流客户完整信息(包含负责人信息)"); log.info("🔍 获取回流客户完整信息(包含负责人信息)");
// 获取最近回流的客户(例如最近1天内回流的) // 获取最近回流的客户(例如最近1天内回流的)
LocalDateTime sinceTime = LocalDateTime.now().minusDays(1); LocalDateTime sinceTime = LocalDateTime.now(ZoneOffset.UTC).minusDays(1);
List<UserProductCartDTO> recycledCustomers = supplyUsersMapper.findRecentlyRecycledCustomers(sinceTime); List<UserProductCartDTO> recycledCustomers = supplyUsersMapper.findRecentlyRecycledCustomers(sinceTime);
// 为每个客户加载负责人信息 // 为每个客户加载负责人信息

33
src/main/java/com/example/web/service/SupplyCustomerService.java

@ -14,6 +14,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -41,6 +42,12 @@ public class SupplyCustomerService {
@Autowired @Autowired
private SupplyPoolCustomerService supplyPoolCustomerService; private SupplyPoolCustomerService supplyPoolCustomerService;
@Autowired
private InformationTraService informationTraService;
@Autowired
private RealTimeDataService realTimeDataService;
// ==================== 精确更新方法 ==================== // ==================== 精确更新方法 ====================
/** /**
@ -94,6 +101,16 @@ public class SupplyCustomerService {
checkAndUpdateManagerInfo(originalLevel, newLevel, dto, userId, authInfo); checkAndUpdateManagerInfo(originalLevel, newLevel, dto, userId, authInfo);
System.out.println("✅ 公海池客户信息精确更新成功"); System.out.println("✅ 公海池客户信息精确更新成功");
// 实时推送客户更新
UserProductCartDTO updatedCustomer = supplyUsersMapper.selectByUserId(userId);
realTimeDataService.pushCustomerUpdate(userId, updatedCustomer);
// 推送公海池数据更新
List<UserProductCartDTO> allCustomers = supplyUsersMapper.getAllUserBasicInfo();
realTimeDataService.pushPublicSeaUpdate(allCustomers);
realTimeDataService.pushAllCustomersUpdate(allCustomers);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
@ -289,7 +306,7 @@ public class SupplyCustomerService {
existingManager.setRoot("2"); existingManager.setRoot("2");
} }
existingManager.setUpdated_at(LocalDateTime.now()); existingManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyUsersManagementsMapper.updateUsersManagements(existingManager); int rows = supplyUsersManagementsMapper.updateUsersManagements(existingManager);
System.out.println("✅ 更新负责人记录影响行数: " + rows); System.out.println("✅ 更新负责人记录影响行数: " + rows);
@ -324,8 +341,8 @@ public class SupplyCustomerService {
newManager.setManagerId(getSafeString(authInfo.getManagerId())); newManager.setManagerId(getSafeString(authInfo.getManagerId()));
newManager.setRoot("2"); // 采购端默认权限 newManager.setRoot("2"); // 采购端默认权限
newManager.setCreated_at(LocalDateTime.now()); newManager.setCreated_at(LocalDateTime.now(ZoneOffset.UTC));
newManager.setUpdated_at(LocalDateTime.now()); newManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyUsersManagementsMapper.insertUsersManagements(newManager); int rows = supplyUsersManagementsMapper.insertUsersManagements(newManager);
System.out.println("✅ 插入负责人记录影响行数: " + rows); System.out.println("✅ 插入负责人记录影响行数: " + rows);
@ -434,7 +451,7 @@ public class SupplyCustomerService {
existingManager.setRoot("2"); existingManager.setRoot("2");
} }
existingManager.setUpdated_at(LocalDateTime.now()); existingManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyUsersManagementsMapper.updateUsersManagements(existingManager); int rows = supplyUsersManagementsMapper.updateUsersManagements(existingManager);
System.out.println("✅ 更新负责人记录影响行数: " + rows); System.out.println("✅ 更新负责人记录影响行数: " + rows);
@ -457,8 +474,8 @@ public class SupplyCustomerService {
newManager.setUserName(getSafeString(authInfo.getUserName())); newManager.setUserName(getSafeString(authInfo.getUserName()));
newManager.setAssistant(getSafeString(authInfo.getAssistant())); newManager.setAssistant(getSafeString(authInfo.getAssistant()));
newManager.setCreated_at(LocalDateTime.now()); newManager.setCreated_at(LocalDateTime.now(ZoneOffset.UTC));
newManager.setUpdated_at(LocalDateTime.now()); newManager.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyUsersManagementsMapper.insertUsersManagements(newManager); int rows = supplyUsersManagementsMapper.insertUsersManagements(newManager);
System.out.println("✅ 插入负责人记录影响行数: " + rows); System.out.println("✅ 插入负责人记录影响行数: " + rows);
@ -821,7 +838,7 @@ public class SupplyCustomerService {
updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand())); updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand()));
updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec())); updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec()));
// 优先使用前端传递的更新时间,如果没有则使用当前时间 // 优先使用前端传递的更新时间,如果没有则使用当前时间
updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now()); updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyUsersMapper.updateByPhone(updateUser); int rows = supplyUsersMapper.updateByPhone(updateUser);
System.out.println("更新用户基本信息影响行数: " + rows); System.out.println("更新用户基本信息影响行数: " + rows);
@ -1101,7 +1118,7 @@ public class SupplyCustomerService {
updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand())); updateUser.setDemand(getUpdateValue(dto.getDemand(), existingUser.getDemand()));
updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec())); updateUser.setSpec(getUpdateValue(dto.getSpec(), existingUser.getSpec()));
// 优先使用前端传递的更新时间,如果没有则使用当前时间 // 优先使用前端传递的更新时间,如果没有则使用当前时间
updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now()); updateUser.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyUsersMapper.updateByPhone(updateUser); int rows = supplyUsersMapper.updateByPhone(updateUser);
System.out.println("更新用户基本信息影响行数: " + rows); System.out.println("更新用户基本信息影响行数: " + rows);

11
src/main/java/com/example/web/service/SupplyEnterpriseService.java

@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -175,7 +176,7 @@ public class SupplyEnterpriseService {
contacts.setBank(dto.getBank()); contacts.setBank(dto.getBank());
contacts.setAddress(dto.getAddress()); contacts.setAddress(dto.getAddress());
contacts.setCreated_at(dto.getCreated_at() != null ? dto.getCreated_at() : LocalDateTime.now()); contacts.setCreated_at(dto.getCreated_at() != null ? dto.getCreated_at() : LocalDateTime.now());
contacts.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now()); contacts.setUpdated_at(dto.getUpdated_at() != null ? dto.getUpdated_at() : LocalDateTime.now(ZoneOffset.UTC));
int contactsRows = supplycontactsMapper.insertContacts(contacts); int contactsRows = supplycontactsMapper.insertContacts(contacts);
System.out.println("✅ [采购端] 联系人信息保存结果: " + contactsRows + " 行受影响"); System.out.println("✅ [采购端] 联系人信息保存结果: " + contactsRows + " 行受影响");
@ -193,7 +194,7 @@ public class SupplyEnterpriseService {
managers.setUserName(StringUtils.hasText(dto.getUserName()) ? dto.getUserName() : "未分配"); managers.setUserName(StringUtils.hasText(dto.getUserName()) ? dto.getUserName() : "未分配");
managers.setAssistant(StringUtils.hasText(dto.getAssistant()) ? dto.getAssistant() : "无"); managers.setAssistant(StringUtils.hasText(dto.getAssistant()) ? dto.getAssistant() : "无");
managers.setCreated_at(LocalDateTime.now()); managers.setCreated_at(LocalDateTime.now());
managers.setUpdated_at(LocalDateTime.now()); managers.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int managersRows = supplymanagersMapper.insertManagers(managers); int managersRows = supplymanagersMapper.insertManagers(managers);
System.out.println("✅ [采购端] 负责人信息保存结果: " + managersRows + " 行受影响"); System.out.println("✅ [采购端] 负责人信息保存结果: " + managersRows + " 行受影响");
@ -384,7 +385,7 @@ public class SupplyEnterpriseService {
contacts.setBank(dto.getBank()); contacts.setBank(dto.getBank());
contacts.setAddress(dto.getAddress()); contacts.setAddress(dto.getAddress());
contacts.setCreated_at(LocalDateTime.now()); contacts.setCreated_at(LocalDateTime.now());
contacts.setUpdated_at(LocalDateTime.now()); contacts.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int contactsRows = supplycontactsMapper.insertContacts(contacts); int contactsRows = supplycontactsMapper.insertContacts(contacts);
if (contactsRows <= 0) { if (contactsRows <= 0) {
@ -426,7 +427,7 @@ public class SupplyEnterpriseService {
managers.setUserName(dto.getUserName()); managers.setUserName(dto.getUserName());
managers.setAssistant(dto.getAssistant()); managers.setAssistant(dto.getAssistant());
managers.setUpdated_at(LocalDateTime.now()); managers.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int managersRows = supplymanagersMapper.updateManagers(managers); int managersRows = supplymanagersMapper.updateManagers(managers);
if (managersRows <= 0) { if (managersRows <= 0) {
@ -488,7 +489,7 @@ public class SupplyEnterpriseService {
updateUser.setPhoneNumber(dto.getPhoneNumber()); updateUser.setPhoneNumber(dto.getPhoneNumber());
updateUser.setNickName(dto.getNickName()); updateUser.setNickName(dto.getNickName());
updateUser.setType(dto.getType()); updateUser.setType(dto.getType());
updateUser.setUpdated_at(LocalDateTime.now()); updateUser.setUpdated_at(LocalDateTime.now(ZoneOffset.UTC));
int rows = supplyusersMapper.updateByPhone(updateUser); int rows = supplyusersMapper.updateByPhone(updateUser);
System.out.println("[采购端] 更新用户基本信息影响行数: " + rows); System.out.println("[采购端] 更新用户基本信息影响行数: " + rows);

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

@ -21,6 +21,8 @@ public class SupplyPoolCustomerService {
private SupplyUsersMapper supplyusersMapper; private SupplyUsersMapper supplyusersMapper;
@Autowired @Autowired
private SupplyUsersManagementsMapper supplyUsersManagementsMapper; private SupplyUsersManagementsMapper supplyUsersManagementsMapper;
@Autowired
private RealTimeDataService realTimeDataService;
/** /**
* 根据手机号查询微信用户信息采购端权限只处理seller和both类型- 支持一对多 * 根据手机号查询微信用户信息采购端权限只处理seller和both类型- 支持一对多
@ -468,6 +470,12 @@ public class SupplyPoolCustomerService {
System.out.println("🔄 更新通知状态: " + userId + " 从banold改为old"); System.out.println("🔄 更新通知状态: " + userId + " 从banold改为old");
supplyusersMapper.updateNotice(userId, "old"); supplyusersMapper.updateNotice(userId, "old");
userInfo.setNotice("old"); userInfo.setNotice("old");
// 实时推送更新,确保前端通知计数正确
System.out.println("📡 推送实时更新,通知状态已变更");
List<UserProductCartDTO> allCustomers = supplyusersMapper.getAllUserBasicInfo();
realTimeDataService.pushPublicSeaUpdate(allCustomers);
realTimeDataService.pushAllCustomersUpdate(allCustomers);
} }
// 🔥 新增:查询负责人信息 // 🔥 新增:查询负责人信息
try { try {

31
src/main/java/com/example/web/task/DataRefreshTask.java

@ -0,0 +1,31 @@
package com.example.web.task;
import com.example.web.listener.DatabaseChangeListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class DataRefreshTask {
private static final Logger log = LoggerFactory.getLogger(DataRefreshTask.class);
@Autowired
private DatabaseChangeListener databaseChangeListener;
/**
* 每30秒刷新一次数据提高通知实时性
*/
@Scheduled(fixedRate = 30000) // 30000毫秒 = 30秒
public void refreshDataPeriodically() {
log.info("🔄 开始定期刷新数据");
try {
databaseChangeListener.triggerDataUpdate();
log.info("✅ 数据刷新完成");
} catch (Exception e) {
log.error("❌ 数据刷新失败: {}", e.getMessage(), e);
}
}
}

45
src/main/resources/application.yaml

@ -2,16 +2,42 @@ spring:
datasource: datasource:
# userlogin数据库 # userlogin数据库
primary: primary:
jdbc-url: jdbc:mysql://1.95.162.61:3306/userlogin?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true jdbc-url: jdbc:mysql://1.95.162.61:3306/userlogin?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&maxReconnects=10&allowPublicKeyRetrieval=true
username: root username: root
password: schl@2025 password: schl@2025
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-timeout: 120000
max-lifetime: 3600000
maximum-pool-size: 15
minimum-idle: 8
validation-timeout: 5000
test-while-idle: true
test-on-borrow: true
test-on-return: true
connection-test-query: SELECT 1
initialization-fail-timeout: -1
keepalive-time: 300000
leak-detection-threshold: 30000
# wechat_app数据库 # wechat_app数据库
wechat: wechat:
jdbc-url: jdbc:mysql://1.95.162.61:3306/wechat_app?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true jdbc-url: jdbc:mysql://1.95.162.61:3306/wechat_app?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&maxReconnects=10&allowPublicKeyRetrieval=true
username: root username: root
password: schl@2025 password: schl@2025
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-timeout: 120000
max-lifetime: 3600000
maximum-pool-size: 20
minimum-idle: 10
validation-timeout: 5000
test-while-idle: true
test-on-borrow: true
test-on-return: true
connection-test-query: SELECT 1
initialization-fail-timeout: -1
keepalive-time: 300000
leak-detection-threshold: 30000
# 应用配置 # 应用配置
app: app:
@ -21,8 +47,21 @@ app:
# 组织公海池客户回流到部门公海池的天数阈值 # 组织公海池客户回流到部门公海池的天数阈值
organization-to-department-days: 3 organization-to-department-days: 3
# Spring Boot Actuator配置
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
health:
datasource:
enabled: true
server: server:
port: 8080 port: 8081
servlet: servlet:
context-path: /DL context-path: /DL
# 在Tomcat中部署时,端口由Tomcat配置决定,这里不需要指定 # 在Tomcat中部署时,端口由Tomcat配置决定,这里不需要指定

2
src/main/resources/static/loginmm.html

@ -520,7 +520,7 @@
errorElement.classList.remove('show'); errorElement.classList.remove('show');
} }
const API_BASE_URL = 'http://8.137.125.67:8080/DL'; // 服务器API地址 const API_BASE_URL = 'http://localhost:8081/DL'; // 服务器API地址
async function sendLoginRequest(projectName, userName, password) { async function sendLoginRequest(projectName, userName, password) {
try { try {
// 使用URL编码的表单数据 // 使用URL编码的表单数据

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

@ -4763,14 +4763,34 @@
// 更新通知铃铛状态 // 更新通知铃铛状态
updateNotificationStatus(customers) { updateNotificationStatus(customers) {
// 统计notice为banold的客户数量 // 统计notice为banold的客户数量
let banoldCustomers = [];
// 如果没有提供customers参数,尝试使用缓存数据
if (customers) {
console.log('📊 更新通知状态 - 客户列表:', customers.length, '个客户'); console.log('📊 更新通知状态 - 客户列表:', customers.length, '个客户');
const totalNotifications = customers.filter(customer => customer.notice === 'banold').length; banoldCustomers = customers.filter(customer => customer.notice === 'banold');
console.log('🔔 待处理通知数量:', totalNotifications); } else if (this.allPublicSeaCustomers) {
console.log('📊 更新通知状态 - 使用缓存数据:', this.allPublicSeaCustomers.length, '个客户');
banoldCustomers = this.allPublicSeaCustomers.filter(customer => customer.notice === 'banold');
} else {
console.log('📊 更新通知状态 - 无客户数据,直接返回');
// 如果没有数据,不更新通知状态
return;
}
// 获取已读通知ID // 计算实际未读数量(考虑localStorage中的已读标记)
const readNotifications = JSON.parse(localStorage.getItem('readNotifications') || '[]'); let unreadCount = 0;
// 计算未读通知数量 banoldCustomers.forEach(customer => {
const unreadCount = Math.max(0, totalNotifications - readNotifications.length); const isRead = localStorage.getItem(`notification_read_${customer.id}`) === 'true';
if (!isRead) {
unreadCount++;
console.log(`🔍 客户 ${customer.id} 未读`);
} else {
console.log(`✅ 客户 ${customer.id} 已读`);
}
});
console.log('🔔 待处理通知数量:', unreadCount);
// 更新通知铃铛样式 // 更新通知铃铛样式
const notificationButton = document.getElementById('notificationButton'); const notificationButton = document.getElementById('notificationButton');
@ -4780,7 +4800,16 @@
if (bellIcon) { if (bellIcon) {
if (unreadCount > 0) { if (unreadCount > 0) {
notificationButton.classList.add('notification-active'); notificationButton.classList.add('notification-active');
// 只在首次激活时添加动画,避免重复闪烁
if (!notificationButton.classList.contains('animation-added')) {
bellIcon.style.animation = 'ring 1s ease-in-out'; bellIcon.style.animation = 'ring 1s ease-in-out';
notificationButton.classList.add('animation-added');
// 动画结束后移除标记
setTimeout(() => {
notificationButton.classList.remove('animation-added');
bellIcon.style.animation = 'none';
}, 1000);
}
// 添加或更新通知数量显示 // 添加或更新通知数量显示
let countBadge = notificationButton.querySelector('.notification-count'); let countBadge = notificationButton.querySelector('.notification-count');
@ -4793,6 +4822,7 @@
} else { } else {
notificationButton.classList.remove('notification-active'); notificationButton.classList.remove('notification-active');
bellIcon.style.animation = 'none'; bellIcon.style.animation = 'none';
notificationButton.classList.remove('animation-added');
// 移除通知数量显示 // 移除通知数量显示
const countBadge = notificationButton.querySelector('.notification-count'); const countBadge = notificationButton.querySelector('.notification-count');
@ -4809,6 +4839,25 @@
return unreadCount; return unreadCount;
} }
// 标记通知为已读
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 || []);
}
// 获取公海池数据的辅助方法 // 获取公海池数据的辅助方法
async fetchPublicSeaData(loginInfo, level) { async fetchPublicSeaData(loginInfo, level) {
const url = appendAuthParams(`${API_BASE_URL}/pool/all-customers`); const url = appendAuthParams(`${API_BASE_URL}/pool/all-customers`);
@ -4891,13 +4940,8 @@
notificationModal.style.zIndex = '1000'; // 确保弹窗在最上层 notificationModal.style.zIndex = '1000'; // 确保弹窗在最上层
try { try {
// 先尝试从缓存获取数据 // 每次打开通知弹窗时,强制从API获取最新数据,不依赖缓存
let allCustomers = []; const url = appendAuthParams(`${API_BASE_URL}/pool/all-customers`);
if (this.allPublicSeaCustomers) {
allCustomers = this.allPublicSeaCustomers;
} else {
// 如果缓存中没有数据,直接从API获取
const url = appendAuthParams(`${API_BASE_URL}/supply/pool/all-customers`);
console.log('🌐 请求API地址:', url); console.log('🌐 请求API地址:', url);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`); if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);
@ -4908,15 +4952,15 @@
if (!result.success) throw new Error(result.message); if (!result.success) throw new Error(result.message);
const data = result.data || {}; const data = result.data || {};
allCustomers = Array.isArray(data) ? data : Object.values(data); const allCustomers = Array.isArray(data) ? data : Object.values(data);
console.log('🔄 转换后客户数组:', allCustomers.length, '条'); console.log('🔄 转换后客户数组:', allCustomers.length, '条');
// 更新缓存 // 更新缓存
this.allPublicSeaCustomers = allCustomers; this.allPublicSeaCustomers = allCustomers;
}
// 获取notice为banold的客户 // 获取notice为banold的客户 - 添加更灵活的过滤条件
const banoldCustomers = allCustomers.filter(customer => customer.notice === 'banold'); const banoldCustomers = allCustomers.filter(customer => customer.notice && customer.notice === 'banold');
console.log('🔍 过滤后banold客户:', banoldCustomers.length, '条');
if (banoldCustomers.length === 0) { if (banoldCustomers.length === 0) {
// 渲染空状态 // 渲染空状态
@ -4928,16 +4972,16 @@
</div> </div>
`; `;
} else { } else {
// 获取已读通知ID // 获取已读通知ID - 使用更可靠的存储方式
const readNotifications = JSON.parse(localStorage.getItem('readNotifications') || '[]'); const readNotifications = JSON.parse(localStorage.getItem('readNotifications') || '[]');
// 渲染通知列表 // 渲染通知列表
let contentHTML = ''; let contentHTML = '';
banoldCustomers.forEach(customer => { banoldCustomers.forEach(customer => {
const customerName = customer.company || customer.companyName || '未知客户'; const customerName = customer.company || customer.companyName || customer.name || '未知客户';
const isRead = readNotifications.includes(customer.id); const isRead = readNotifications.includes(customer.id);
const notificationClass = isRead ? 'notification-item' : 'notification-item unread'; const notificationClass = isRead ? 'notification-item' : 'notification-item new';
const timeStr = customer.created_at ? new Date(customer.created_at).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '未知时间'; const timeStr = customer.created_at || customer.updateTime ? new Date(customer.created_at || customer.updateTime).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '未知时间';
contentHTML += ` contentHTML += `
<div class="${notificationClass}" data-customer-id="${customer.id}" data-phone="${customer.phoneNumber || ''}" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)"> <div class="${notificationClass}" data-customer-id="${customer.id}" data-phone="${customer.phoneNumber || ''}" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)">
@ -4959,6 +5003,7 @@
`; `;
}); });
notificationContent.innerHTML = contentHTML; notificationContent.innerHTML = contentHTML;
console.log('📋 渲染完成通知列表:', banoldCustomers.length, '条');
// 为每个通知项添加点击事件(保持原有功能) // 为每个通知项添加点击事件(保持原有功能)
notificationContent.querySelectorAll('.notification-item').forEach(item => { notificationContent.querySelectorAll('.notification-item').forEach(item => {
@ -4997,7 +5042,7 @@
async markNotificationAsRead(customerId, notificationItem) { async markNotificationAsRead(customerId, notificationItem) {
try { try {
// 调用API更新通知状态 // 调用API更新通知状态
const url = appendAuthParams(`${API_BASE_URL}/supply/pool/update-customer-notice`); const url = appendAuthParams(`${API_BASE_URL}/pool/update-customer-notice`);
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -5041,6 +5086,9 @@
} }
} }
// 更新控制面板统计卡片
updateDashboardStats();
// 显示成功提示 // 显示成功提示
this.showToast('通知已标记为已读', 'success'); this.showToast('通知已标记为已读', 'success');
@ -5073,7 +5121,7 @@
const customerIds = Array.from(unreadNotifications).map(item => item.dataset.customerId); const customerIds = Array.from(unreadNotifications).map(item => item.dataset.customerId);
for (const customerId of customerIds) { for (const customerId of customerIds) {
const url = appendAuthParams(`${API_BASE_URL}/supply/pool/update-customer-notice`); const url = appendAuthParams(`${API_BASE_URL}/pool/update-customer-notice`);
await fetch(url, { await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -5109,6 +5157,9 @@
// 更新通知数量 // 更新通知数量
this.updateNotificationStatus(this.allPublicSeaCustomers || []); this.updateNotificationStatus(this.allPublicSeaCustomers || []);
// 更新控制面板统计卡片
updateDashboardStats();
// 恢复按钮状态 // 恢复按钮状态
if (markAllBtn) { if (markAllBtn) {
markAllBtn.disabled = false; markAllBtn.disabled = false;
@ -5376,21 +5427,365 @@
console.log('🧹 缓存已清空'); console.log('🧹 缓存已清空');
} }
} }
// 初始化缓存系统 // 初始化缓存系统将在后面使用OptimizedCustomerDataCache完成
window.customerCache = new CustomerDataCache();
// WebSocket客户端实现
class WebSocketClient {
constructor() {
this.socket = null;
this.isConnected = false;
this.reconnectInterval = 5000;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.callbacks = {};
}
// 页面加载时预加载所有等级数据 // 初始化WebSocket连接
init() {
// 添加SockJS和STOMP依赖
const script1 = document.createElement('script');
script1.src = 'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js';
document.head.appendChild(script1);
const script2 = document.createElement('script');
script2.src = 'https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js';
script2.onload = () => this.connect();
document.head.appendChild(script2);
}
// 连接WebSocket服务器
connect() {
try {
const socket = new SockJS('/DL/ws');
this.stompClient = Stomp.over(socket);
this.stompClient.connect({},
(frame) => {
console.log('✅ WebSocket连接成功:', frame);
this.isConnected = true;
this.reconnectAttempts = 0;
// 订阅公海池数据更新
this.stompClient.subscribe('/topic/public-sea', (message) => {
const data = JSON.parse(message.body);
this.handlePublicSeaUpdate(data);
});
// 订阅所有客户数据更新
this.stompClient.subscribe('/topic/all-customers', (message) => {
const data = JSON.parse(message.body);
this.handleAllCustomersUpdate(data);
});
// 订阅通知
this.stompClient.subscribe('/topic/notifications', (message) => {
const notification = JSON.parse(message.body);
this.handleNotification(notification);
});
},
(error) => {
console.error('❌ WebSocket连接错误:', error);
this.isConnected = false;
this.attemptReconnect();
}
);
} catch (error) {
console.error('❌ WebSocket初始化失败:', error);
this.attemptReconnect();
}
}
// 尝试重连
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`⏱️ 尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => this.connect(), this.reconnectInterval);
} else {
console.error('❌ 达到最大重连次数,停止尝试');
// 回退到轮询模式
console.log('🔄 回退到轮询模式');
window.customerCache.startAutoRefresh();
}
}
// 处理公海池数据更新
handlePublicSeaUpdate(data) {
console.log('🔄 收到公海池数据更新');
// 清除现有缓存,强制更新
window.customerCache.clear();
// 更新缓存
window.customerCache.updatePublicSeaCache(data);
// 重新加载当前视图
this.reloadCurrentView();
}
// 处理所有客户数据更新
handleAllCustomersUpdate(data) {
console.log('🔄 收到所有客户数据更新');
// 清除现有缓存,强制更新
window.customerCache.clear();
// 更新缓存
window.customerCache.updateAllCustomersCache(data);
// 重新加载当前视图
this.reloadCurrentView();
}
// 重新加载当前视图
reloadCurrentView() {
const currentActiveLevel = document.querySelector('.level-tab.active')?.getAttribute('data-level');
if (currentActiveLevel) {
console.log(`🔄 重新加载当前视图: ${currentActiveLevel}`);
// 触发当前视图的数据加载 - 检查loadCustomerData函数是否存在
if (typeof loadCustomerData === 'function') {
loadCustomerData(currentActiveLevel);
} else {
console.warn('⚠️ loadCustomerData函数未定义,跳过视图重新加载');
}
}
}
// 处理通知
handleNotification(notification) {
console.log('📢 收到通知:', notification);
// 显示通知
this.showNotification(notification);
// 收到通知后,清除缓存并更新通知状态,确保通知列表实时更新
this.allPublicSeaCustomers = null;
// 更新通知状态
this.updateNotificationStatus();
// 调用回调函数
if (this.callbacks['notification']) {
this.callbacks['notification'].forEach(callback => callback(notification));
}
}
// 显示通知
showNotification(notification) {
const notificationEl = document.createElement('div');
notificationEl.className = 'websocket-notification';
notificationEl.innerHTML = `
<div class="notification-header">
<h4>${notification.title}</h4>
<button class="close-btn">&times;</button>
</div>
<div class="notification-body">
<p>${notification.message}</p>
<small>${new Date(notification.timestamp).toLocaleString()}</small>
</div>
`;
// 添加样式
notificationEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 16px;
width: 320px;
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
// 添加关闭事件
const closeBtn = notificationEl.querySelector('.close-btn');
closeBtn.addEventListener('click', () => notificationEl.remove());
// 添加到页面
document.body.appendChild(notificationEl);
// 3秒后自动关闭
setTimeout(() => notificationEl.remove(), 3000);
}
// 订阅事件
on(event, callback) {
if (!this.callbacks[event]) {
this.callbacks[event] = [];
}
this.callbacks[event].push(callback);
}
// 取消订阅
off(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
}
}
// 断开连接
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect();
this.isConnected = false;
}
}
}
// 初始化WebSocket客户端
window.wsClient = new WebSocketClient();
// 优化CustomerDataCache,添加WebSocket支持
class OptimizedCustomerDataCache extends CustomerDataCache {
constructor() {
super();
this.isWebSocketEnabled = true;
}
// 更新公海池缓存
updatePublicSeaCache(data) {
try {
const loginInfo = getLoginInfo();
const levels = ['company-sea-pools', 'organization-sea-pools', 'department-sea-pools'];
// 保存所有公海客户数据到缓存中,用于通知弹窗
this.allPublicSeaCustomers = data;
// 统计notice为banold的客户数量并更新通知铃铛
this.updateNotificationStatus(data);
// 对每个等级分别过滤和缓存
for (const level of levels) {
const filteredCustomers = this.filterPublicSeaCustomersByLoginInfo(
data, loginInfo, level
).sort((a, b) => {
const aTime = new Date(a.updated_at || a.created_at);
const bTime = new Date(b.updated_at || b.created_at);
return bTime - aTime;
});
this.set(level, filteredCustomers);
this.updateUIIfActive(level, filteredCustomers);
}
console.log('✅ 公海池缓存已更新');
} catch (error) {
console.error('❌ 更新公海池缓存失败:', error);
}
}
// 更新所有客户缓存
updateAllCustomersCache(data) {
try {
const loginInfo = getLoginInfo();
const levels = ['important', 'normal', 'low-value', 'logistics', 'unclassified'];
// 根据登录信息过滤一次
const filteredByLogin = this.filterCustomersByLoginInfo(
data, loginInfo, null
);
// 对每个等级分别过滤和缓存
for (const level of levels) {
const levelMap = {
'important': 'important',
'normal': 'normal',
'low-value': 'low-value',
'logistics': 'logistics',
'unclassified': 'unclassified'
};
const backendLevel = levelMap[level] || level;
const customersForLevel = filteredByLogin.filter(customer => {
const customerLevel = standardizeCustomerLevel(customer.level);
return customerLevel === backendLevel;
}).sort((a, b) => {
const aTime = new Date(a.updated_at || a.created_at);
const bTime = new Date(b.updated_at || b.created_at);
return bTime - aTime;
});
this.set(level, customersForLevel);
this.updateUIIfActive(level, customersForLevel);
}
console.log('✅ 所有客户缓存已更新');
} catch (error) {
console.error('❌ 更新所有客户缓存失败:', error);
}
}
// 重写startAutoRefresh方法,使用WebSocket时不启动轮询
startAutoRefresh() {
if (!window.wsClient || !window.wsClient.isConnected) {
console.log('🔄 启动自动刷新');
super.startAutoRefresh();
} else {
console.log('✅ WebSocket已连接,跳过轮询启动');
}
}
}
// 替换原有的缓存系统
window.customerCache = new OptimizedCustomerDataCache();
// 页面加载时预加载所有等级数据并初始化WebSocket
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
console.log('🚀 页面加载完成,开始预加载客户数据'); console.log('🚀 页面加载完成,开始初始化系统');
try { try {
// 并行预加载所有等级数据,提升性能 // 1. 初始化WebSocket
window.wsClient.init();
// 2. 初始化UI组件
initUserInfoDropdown();
initAllCustomersPagination();
initLevelPagination();
setupEnhancedEventDelegation(); // 关键:确保事件委托优先设置
setupLevelTabs();
initAutoRefresh();
initTimeFilter();
console.log('✅ UI组件初始化完成');
// 3. 并行预加载所有等级数据,提升性能
await window.customerCache.preloadAllLevels(); await window.customerCache.preloadAllLevels();
console.log('✅ 所有等级数据预加载完成'); console.log('✅ 所有等级数据预加载完成');
// 4. 初始渲染当前活跃标签页
const activeLevel = document.querySelector('.level-tab.active')?.getAttribute('data-level') || 'all';
refreshCustomerData(activeLevel);
// 5. WebSocket连接成功后停止轮询
if (window.wsClient && window.wsClient.isConnected) {
window.customerCache.stopAutoRefresh();
}
// 6. 添加调试信息按钮
setTimeout(addRefreshButtons, 2000);
console.log('✅ 系统初始化完成');
} catch (error) { } catch (error) {
console.error('❌ 预加载数据失败:', error); console.error('❌ 初始化失败:', error);
}
});
// 页面关闭前断开连接
window.addEventListener('beforeunload', () => {
if (window.wsClient) {
window.wsClient.disconnect();
} }
}); });
// 添加动画样式
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);
function optimizedProcessFilteredCustomers(customers, level) { function optimizedProcessFilteredCustomers(customers, level) {
// 快速检查空数据 // 快速检查空数据
@ -7520,19 +7915,10 @@
this.updateDemandDisplay(demand); this.updateDemandDisplay(demand);
this.setHiddenCartItemId(this.currentCartItemId); this.setHiddenCartItemId(this.currentCartItemId);
// 🔥 关键修复:重新调用viewCustomerDetails以刷新数据 // 直接更新当前客户数据的选中需求ID,不重新调用viewCustomerDetails
if (window.currentCustomerData) { if (window.currentCustomerData) {
const customerId = window.currentCustomerData.id; window.currentCustomerData._currentSelectedDemandId = this.currentCartItemId;
const phoneNumber = window.currentCustomerData.phoneNumber; console.log('🔄 更新当前客户数据的选中需求ID:', this.currentCartItemId);
console.log('🔄 重新加载客户详情以更新选中状态', {
customerId,
phoneNumber,
targetCartItemId: this.currentCartItemId
});
// 重新调用viewCustomerDetails,传递选中的购物车项ID
viewCustomerDetails(customerId, phoneNumber, this.currentCartItemId);
} }
console.log('✅ 需求选择完成,当前ID:', this.currentCartItemId); console.log('✅ 需求选择完成,当前ID:', this.currentCartItemId);
@ -11612,9 +11998,80 @@
// 更新控制面板统计卡片 // 更新控制面板统计卡片
function updateDashboardStats() { function updateDashboardStats() {
// 这里可以添加统计数据的更新逻辑
// 例如从API获取最新的统计数据
console.log('更新控制面板统计卡片'); console.log('更新控制面板统计卡片');
// 尝试从多种来源获取客户数据
let allCustomers = [];
// 1. 尝试直接从customerCache获取(如果存在)
if (typeof customerCache !== 'undefined' && customerCache) {
if (customerCache['all'] && customerCache['all'].data && Array.isArray(customerCache['all'].data)) {
allCustomers = customerCache['all'].data;
} else if (typeof customerCache.get === 'function') {
const cachedData = customerCache.get('all');
if (cachedData && Array.isArray(cachedData)) {
allCustomers = cachedData;
}
}
}
// 2. 尝试从全局变量获取
if (allCustomers.length === 0) {
if (window.allCustomersData && Array.isArray(window.allCustomersData)) {
allCustomers = window.allCustomersData;
} else if (window.customers && Array.isArray(window.customers)) {
allCustomers = window.customers;
}
}
console.log('用于更新统计的数据数量:', allCustomers.length);
// 更新总客户数
const totalCustomers = allCustomers.length;
console.log('总客户数:', totalCustomers);
// 更新活跃客户数(这里使用一个简单的计算,实际应该根据业务逻辑计算)
const activeCustomers = Math.floor(totalCustomers * 0.7); // 假设70%是活跃客户
console.log('活跃客户数:', activeCustomers);
// 更新客户留存率
const retentionRate = totalCustomers > 0 ? '100%' : '0%';
console.log('客户留存率:', retentionRate);
// 更新本月销售额(模拟数据)
const monthlySales = '¥' + (Math.floor(Math.random() * 1000000) + 500000).toLocaleString();
console.log('本月销售额:', monthlySales);
// 更新统计卡片
try {
// 更新总客户数卡片
const totalCustomersElement = document.querySelector('.stats-cards .stat-card:nth-child(1) .value');
if (totalCustomersElement) {
totalCustomersElement.textContent = totalCustomers;
}
// 更新活跃客户卡片
const activeCustomersElement = document.querySelector('.stats-cards .stat-card:nth-child(2) .value');
if (activeCustomersElement) {
activeCustomersElement.textContent = activeCustomers;
}
// 更新客户留存率卡片
const retentionRateElement = document.querySelector('.stats-cards .stat-card:nth-child(3) .value');
if (retentionRateElement) {
retentionRateElement.textContent = retentionRate;
}
// 更新本月销售额卡片
const monthlySalesElement = document.querySelector('.stats-cards .stat-card:nth-child(4) .value');
if (monthlySalesElement) {
monthlySalesElement.textContent = monthlySales;
}
console.log('✅ 控制面板统计卡片更新完成');
} catch (error) {
console.error('❌ 更新控制面板统计卡片失败:', error);
}
} }
// 获取等级显示名称 // 获取等级显示名称
@ -12295,6 +12752,9 @@
currentCustomerId = customerId || phoneNumber; currentCustomerId = customerId || phoneNumber;
resetEditStateToInitial(); resetEditStateToInitial();
// 初始化模态框事件,确保关闭功能正常
initModalEvents();
try { try {
// 尝试按优先级查询客户信息:API查询 -> 本地缓存 // 尝试按优先级查询客户信息:API查询 -> 本地缓存
const customerData = await fetchCustomerData(customerId, phoneNumber, targetCartItemId); const customerData = await fetchCustomerData(customerId, phoneNumber, targetCartItemId);
@ -13059,17 +13519,23 @@
return; return;
} }
// 移除旧的事件监听器,避免重复绑定 - 只处理关闭按钮,不替换整个模态框
const newCloseModal = closeModal.cloneNode(true);
closeModal.parentNode.replaceChild(newCloseModal, closeModal);
// 重新绑定关闭事件 // 重新绑定关闭事件
closeModal.addEventListener('click', function (e) { newCloseModal.addEventListener('click', function (e) {
e.stopPropagation(); e.stopPropagation();
const modal = document.getElementById('customerModal');
modal.classList.remove('active'); modal.classList.remove('active');
document.body.style.overflow = 'auto'; document.body.style.overflow = 'auto';
console.log("关闭模态框,重置编辑状态"); console.log("关闭模态框,重置编辑状态");
resetEditStateToInitial(); resetEditStateToInitial();
}); });
// 点击模态框外部关闭 // 只在模态框没有外部点击事件监听器时添加
// 避免重复添加导致的多次触发问题
if (!modal.dataset.hasOutsideClickListener) {
// 绑定模态框外部点击事件
modal.addEventListener('click', function (e) { modal.addEventListener('click', function (e) {
if (e.target === modal) { if (e.target === modal) {
modal.classList.remove('active'); modal.classList.remove('active');
@ -13077,6 +13543,8 @@
resetEditStateToInitial(); resetEditStateToInitial();
} }
}); });
modal.dataset.hasOutsideClickListener = 'true';
}
// 初始化编辑按钮 // 初始化编辑按钮
initEditButton(); initEditButton();
@ -14701,17 +15169,7 @@
} }
}); });
closeModal.addEventListener('click', function () {
modal.classList.remove('active');
document.body.style.overflow = 'auto';
});
modal.addEventListener('click', function (e) {
if (e.target === modal) {
modal.classList.remove('active');
document.body.style.overflow = 'auto';
}
});
// 新增详情按钮事件 // 新增详情按钮事件
addDetailBtn.addEventListener('click', function () { addDetailBtn.addEventListener('click', function () {
@ -14878,115 +15336,13 @@
}); });
} }
// 确保数据缓存系统在页面加载时正确初始化 // 第二个DOMContentLoaded事件监听器已删除,避免与第一个冲突
document.addEventListener('DOMContentLoaded', async function() {
console.log('🚀 初始化客户数据系统');
// 1. 先初始化缓存系统 - 修复:确保即使没有CustomerDataCache类也能正常工作
if (!window.customerCache) {
console.log('创建简单缓存系统');
window.customerCache = {
data: {},
set: function(key, value) {
this.data[key] = value;
console.log('缓存已更新:', key, '数量:', Array.isArray(value) ? value.length : 1);
},
get: function(key) {
return this.data[key] || null;
},
clear: function() {
this.data = {};
},
init: async function() {
// 简单初始化
console.log('缓存系统初始化完成');
},
preloadAllLevels: async function() {
// 尝试预加载数据
try {
const response = await fetch(appendAuthParams(`${API_BASE_URL}/customers/all`));
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
this.set('all', data.data);
allCustomersData = data.data;
console.log('预加载客户数据成功,数量:', data.data.length);
}
}
} catch (error) {
console.error('预加载数据失败:', error);
}
}
};
}
try {
await window.customerCache.init();
} catch (error) {
console.error('缓存系统初始化失败:', error);
}
// 2. 初始化UI组件
try {
initUserInfoDropdown();
initAllCustomersPagination();
initLevelPagination();
setupEnhancedEventDelegation(); // 关键:确保事件委托优先设置
setupLevelTabs();
initAutoRefresh();
initTimeFilter();
console.log('UI组件初始化完成');
} catch (error) {
console.error('UI组件初始化失败:', error);
}
// 3. 预加载关键数据 - 修复:添加更健壮的数据加载逻辑
try {
await window.customerCache.preloadAllLevels();
// 获取缓存的数据
const allCustomers = window.customerCache.get('all') || [];
console.log('✅ 所有等级数据预加载完成,数量:', allCustomers.length);
// 更新全局数据
if (allCustomers.length > 0) {
allCustomersData = allCustomers;
}
// 4. 初始渲染当前活跃标签页
const activeLevel = document.querySelector('.level-tab.active')?.getAttribute('data-level') || 'all';
refreshCustomerData(activeLevel);
// 移除直接调用updateRecentCustomers()
// 因为已经在refreshCustomerData函数中添加了对refreshRecentCustomers()的调用
// 这样可以确保最近客户与全部客户、重要客户、普通客户等一起渲染
} catch (error) {
console.error('❌ 数据预加载失败:', error);
// 失败时尝试使用备用数据源
if (window.customers && Array.isArray(window.customers)) {
console.log('使用备用数据源,数量:', window.customers.length);
allCustomersData = window.customers;
window.customerCache.set('all', window.customers);
updateAllCustomersPagination(window.customers);
// 移除直接调用updateRecentCustomers()
// 依赖refreshCustomerData中的refreshRecentCustomers()调用来同步更新
}
}
// 修复:移除重复的bindTableRowClickEvents调用,使用事件委托
console.log('初始化完成,依赖事件委托机制处理交互');
// 添加调试信息按钮
setTimeout(addRefreshButtons, 2000);
});
// 绑定行点击事件的功能已在setupEnhancedEventDelegation中实现 // 绑定行点击事件的功能已在setupEnhancedEventDelegation中实现
// 添加通知弹窗样式 // 添加通知弹窗样式
const style = document.createElement('style'); const notificationStyle = document.createElement('style');
style.textContent = ` notificationStyle.textContent = `
/* 通知弹窗内容区域 */ /* 通知弹窗内容区域 */
#notificationModal .modal-body { #notificationModal .modal-body {
flex: 1; flex: 1;

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

@ -1,4 +1,4 @@

<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
@ -4419,6 +4419,9 @@
console.log('🔄 初始化客户数据缓存系统'); console.log('🔄 初始化客户数据缓存系统');
this.startAutoRefresh(); this.startAutoRefresh();
// 绑定通知事件
this.bindNotificationEvents();
// 移除预加载所有等级数据,改为按需加载 // 移除预加载所有等级数据,改为按需加载
} }
@ -4697,8 +4700,12 @@
// 计算实际未读数量(考虑localStorage中的已读标记) // 计算实际未读数量(考虑localStorage中的已读标记)
let unreadCount = 0; let unreadCount = 0;
banoldCustomers.forEach(customer => { banoldCustomers.forEach(customer => {
if (localStorage.getItem(`notification_read_${customer.id}`) !== 'true') { const isRead = localStorage.getItem(`notification_read_${customer.id}`) === 'true';
if (!isRead) {
unreadCount++; unreadCount++;
console.log(`🔍 客户 ${customer.id} 未读`);
} else {
console.log(`✅ 客户 ${customer.id} 已读`);
} }
}); });
@ -4712,7 +4719,16 @@
if (bellIcon) { if (bellIcon) {
if (unreadCount > 0) { if (unreadCount > 0) {
notificationButton.classList.add('notification-active'); notificationButton.classList.add('notification-active');
// 只在首次激活时添加动画,避免重复闪烁
if (!notificationButton.classList.contains('animation-added')) {
bellIcon.style.animation = 'ring 1s ease-in-out'; bellIcon.style.animation = 'ring 1s ease-in-out';
notificationButton.classList.add('animation-added');
// 动画结束后移除标记
setTimeout(() => {
notificationButton.classList.remove('animation-added');
bellIcon.style.animation = 'none';
}, 1000);
}
// 添加或更新通知数量显示 // 添加或更新通知数量显示
let countBadge = notificationButton.querySelector('.notification-count'); let countBadge = notificationButton.querySelector('.notification-count');
@ -4725,6 +4741,7 @@
} else { } else {
notificationButton.classList.remove('notification-active'); notificationButton.classList.remove('notification-active');
bellIcon.style.animation = 'none'; bellIcon.style.animation = 'none';
notificationButton.classList.remove('animation-added');
// 移除通知数量显示 // 移除通知数量显示
const countBadge = notificationButton.querySelector('.notification-count'); const countBadge = notificationButton.querySelector('.notification-count');
@ -4734,8 +4751,8 @@
} }
} }
// 绑定通知点击事件 // 移除重复绑定事件的调用,避免多次触发
this.bindNotificationEvents(); // this.bindNotificationEvents();
} }
return unreadCount; return unreadCount;
@ -4822,14 +4839,9 @@
notificationModal.style.zIndex = '1000'; // 确保弹窗在最上层 notificationModal.style.zIndex = '1000'; // 确保弹窗在最上层
try { try {
// 先尝试从缓存获取数据 // 每次都从API获取最新数据,确保弹窗显示最新通知
let allCustomers = []; // 修复URL:移除重复的/supply路径
if (this.allPublicSeaCustomers) { const url = appendAuthParams(`${API_BASE_URL}/pool/all-customers`);
allCustomers = this.allPublicSeaCustomers;
console.log('📋 从缓存获取客户数据:', allCustomers.length, '条');
} else {
// 如果缓存中没有数据,直接从API获取
const url = appendAuthParams(`${API_BASE_URL}/supply/pool/all-customers`);
console.log('🌐 请求API地址:', url); console.log('🌐 请求API地址:', url);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`); if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);
@ -4840,12 +4852,11 @@
if (!result.success) throw new Error(result.message); if (!result.success) throw new Error(result.message);
const data = result.data || {}; const data = result.data || {};
allCustomers = Array.isArray(data) ? data : Object.values(data); const allCustomers = Array.isArray(data) ? data : Object.values(data);
console.log('🔄 转换后客户数组:', allCustomers.length, '条'); console.log('🔄 转换后客户数组:', allCustomers.length, '条');
// 更新缓存 // 更新缓存
this.allPublicSeaCustomers = allCustomers; this.allPublicSeaCustomers = allCustomers;
}
// 获取notice为banold的客户 // 获取notice为banold的客户
const banoldCustomers = allCustomers.filter(customer => customer.notice === 'banold'); const banoldCustomers = allCustomers.filter(customer => customer.notice === 'banold');
@ -4861,7 +4872,7 @@
const isRead = localStorage.getItem(`notification_read_${customer.id}`) === 'true'; const isRead = localStorage.getItem(`notification_read_${customer.id}`) === 'true';
const customerName = customer.company || customer.companyName || '未知'; const customerName = customer.company || customer.companyName || '未知';
contentHTML += ` contentHTML += `
<div class="notification-item ${isRead ? '' : 'unread'}" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)"> <div class="notification-item ${isRead ? '' : 'unread'}" onclick="viewCustomerDetails('${customer.id}', '${customer.phoneNumber || ''}', null, true)" data-customer-id="${customer.id}">
<div class="notification-icon"> <div class="notification-icon">
<i class="fas fa-user-alt"></i> <i class="fas fa-user-alt"></i>
</div> </div>
@ -4925,14 +4936,16 @@
// 标记所有通知为已读 // 标记所有通知为已读
notificationItems.forEach(item => { notificationItems.forEach(item => {
// 从点击事件中获取customerId // 从data-customer-id属性中获取customerId
const customerId = item.getAttribute('onclick').match(/'([^']+)'/)[1]; const customerId = item.getAttribute('data-customer-id');
if (customerId) {
localStorage.setItem(`notification_read_${customerId}`, 'true'); localStorage.setItem(`notification_read_${customerId}`, 'true');
item.classList.remove('unread'); item.classList.remove('unread');
const readStatus = item.querySelector('.read-status'); const readStatus = item.querySelector('.read-status');
if (readStatus) { if (readStatus) {
readStatus.remove(); readStatus.remove();
} }
}
}); });
// 更新未读数量 // 更新未读数量
@ -4968,7 +4981,7 @@
to { transform: translateX(0); opacity: 1; } to { transform: translateX(0); opacity: 1; }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(notificationStyle);
document.body.appendChild(toast); document.body.appendChild(toast);
@ -5082,6 +5095,78 @@
} }
} }
// 更新公海池缓存
updatePublicSeaCache(data) {
try {
const loginInfo = getLoginInfo();
const levels = ['company-sea-pools', 'organization-sea-pools', 'department-sea-pools'];
// 对每个等级分别过滤和缓存
for (const level of levels) {
const filteredCustomers = this.filterPublicSeaCustomersByLoginInfo(
data, loginInfo, level
).sort((a, b) => {
const aTime = new Date(a.updated_at || a.created_at).getTime();
const bTime = new Date(b.updated_at || b.created_at).getTime();
return bTime - aTime;
});
this.set(level, filteredCustomers);
this.updateUIIfActive(level, filteredCustomers);
}
// 保存所有公海客户数据到缓存中,用于通知弹窗
this.allPublicSeaCustomers = data;
// 更新通知状态
this.updateNotificationStatus(data);
console.log('✅ 公海池缓存已更新');
} catch (error) {
console.error('❌ 更新公海池缓存失败:', error);
}
}
// 更新所有客户缓存
updateAllCustomersCache(data) {
try {
const loginInfo = getLoginInfo();
const levels = ['important', 'normal', 'low-value', 'logistics', 'unclassified', 'all'];
// 对每个等级分别过滤和缓存
for (const level of levels) {
let filteredCustomers = this.filterCustomersByLoginInfo(
data, loginInfo, level
);
// 如果是特定等级,进一步过滤
if (level !== 'all') {
filteredCustomers = filteredCustomers.filter(customer => {
const customerLevel = standardizeCustomerLevel(customer.level);
return customerLevel === level;
});
}
// 排序
filteredCustomers = filteredCustomers.sort((a, b) => {
const aTime = new Date(a.updated_at || a.created_at).getTime();
const bTime = new Date(b.updated_at || b.created_at).getTime();
return bTime - aTime;
});
this.set(level, filteredCustomers);
this.updateUIIfActive(level, filteredCustomers);
}
// 更新通知状态
this.updateNotificationStatus(data);
console.log('✅ 所有客户缓存已更新');
} catch (error) {
console.error('❌ 更新所有客户缓存失败:', error);
}
}
// 手动刷新缓存 // 手动刷新缓存
async refreshCache(level = null) { async refreshCache(level = null) {
console.log('🔄 手动刷新缓存'); console.log('🔄 手动刷新缓存');
@ -5448,11 +5533,260 @@
initUserInfoDropdown(); initUserInfoDropdown();
}); });
// 在页面加载完成后初始化用户信息下拉菜单 // WebSocket客户端实现
document.addEventListener('DOMContentLoaded', function () { class WebSocketClient {
constructor() {
this.socket = null;
this.isConnected = false;
this.reconnectInterval = 5000;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.callbacks = {};
}
// 初始化WebSocket连接
init() {
// 添加SockJS和STOMP依赖
const script1 = document.createElement('script');
script1.src = 'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js';
document.head.appendChild(script1);
const script2 = document.createElement('script');
script2.src = 'https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js';
script2.onload = () => this.connect();
document.head.appendChild(script2);
}
// 连接WebSocket服务器
connect() {
try {
const socket = new SockJS('/DL/ws');
this.stompClient = Stomp.over(socket);
this.stompClient.connect({},
(frame) => {
console.log('✅ WebSocket连接成功:', frame);
this.isConnected = true;
this.reconnectAttempts = 0;
// 订阅公海池数据更新
this.stompClient.subscribe('/topic/public-sea', (message) => {
const data = JSON.parse(message.body);
this.handlePublicSeaUpdate(data);
});
// 订阅所有客户数据更新
this.stompClient.subscribe('/topic/all-customers', (message) => {
const data = JSON.parse(message.body);
this.handleAllCustomersUpdate(data);
});
// 订阅通知
this.stompClient.subscribe('/topic/notifications', (message) => {
const notification = JSON.parse(message.body);
this.handleNotification(notification);
});
},
(error) => {
console.error('❌ WebSocket连接错误:', error);
this.isConnected = false;
this.attemptReconnect();
}
);
} catch (error) {
console.error('❌ WebSocket初始化失败:', error);
this.attemptReconnect();
}
}
// 尝试重连
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`⏱️ 尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => this.connect(), this.reconnectInterval);
} else {
console.error('❌ 达到最大重连次数,停止尝试');
// 回退到轮询模式
console.log('🔄 回退到轮询模式');
window.customerCache.startAutoRefresh();
}
}
// 处理公海池数据更新
handlePublicSeaUpdate(data) {
console.log('🔄 收到公海池数据更新');
// 清除现有缓存,强制更新
window.customerCache.clear();
// 更新缓存
window.customerCache.updatePublicSeaCache(data);
// 重新加载当前视图
this.reloadCurrentView();
}
// 处理所有客户数据更新
handleAllCustomersUpdate(data) {
console.log('🔄 收到所有客户数据更新');
// 清除现有缓存,强制更新
window.customerCache.clear();
// 更新缓存
window.customerCache.updateAllCustomersCache(data);
// 重新加载当前视图
this.reloadCurrentView();
}
// 重新加载当前视图
reloadCurrentView() {
const currentActiveLevel = document.querySelector('.level-tab.active')?.getAttribute('data-level');
if (currentActiveLevel) {
console.log(`🔄 重新加载当前视图: ${currentActiveLevel}`);
// 触发当前视图的数据加载 - 检查loadCustomerData函数是否存在
if (typeof loadCustomerData === 'function') {
loadCustomerData(currentActiveLevel);
} else {
console.warn('⚠️ loadCustomerData函数未定义,跳过视图重新加载');
}
}
// 无论是否有活动标签页,都更新通知铃铛状态
this.updateNotificationBellStatus();
}
// 处理通知
handleNotification(notification) {
console.log('📢 收到通知:', notification);
// 显示通知
this.showNotification(notification);
// 立即更新通知铃铛状态
this.updateNotificationBellStatus();
// 调用回调函数
if (this.callbacks['notification']) {
this.callbacks['notification'].forEach(callback => callback(notification));
}
}
// 更新通知铃铛状态
updateNotificationBellStatus() {
console.log('🔔 更新通知铃铛状态');
// 直接调用CustomerDataCache的updateNotificationStatus方法,它已经实现了完整的通知更新逻辑
if (window.customerCache && window.customerCache.updateNotificationStatus) {
// 从CustomerDataCache实例中获取完整的公海客户数据
if (window.customerCache.allPublicSeaCustomers) {
console.log('📋 从缓存获取完整公海客户数据');
window.customerCache.updateNotificationStatus(window.customerCache.allPublicSeaCustomers);
} else {
console.warn('⚠️ 缓存中没有完整的公海客户数据');
// 如果缓存中没有数据,触发一次数据刷新
if (window.customerCache.refreshLevelData) {
window.customerCache.refreshLevelData('company-sea-pools');
}
}
} else {
console.warn('⚠️ CustomerDataCache或updateNotificationStatus方法未找到');
}
}
// 显示通知
showNotification(notification) {
const notificationEl = document.createElement('div');
notificationEl.className = 'websocket-notification';
notificationEl.innerHTML = `
<div class="notification-header">
<h4>${notification.title}</h4>
<button class="close-btn">&times;</button>
</div>
<div class="notification-body">
<p>${notification.message}</p>
<small>${new Date(notification.timestamp).toLocaleString()}</small>
</div>
`;
// 添加样式
notificationEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 16px;
width: 320px;
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
// 添加关闭事件
const closeBtn = notificationEl.querySelector('.close-btn');
closeBtn.addEventListener('click', () => notificationEl.remove());
// 添加到页面
document.body.appendChild(notificationEl);
// 3秒后自动关闭
setTimeout(() => notificationEl.remove(), 3000);
}
// 订阅事件
on(event, callback) {
if (!this.callbacks[event]) {
this.callbacks[event] = [];
}
this.callbacks[event].push(callback);
}
// 取消订阅
off(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
}
}
// 断开连接
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect();
this.isConnected = false;
}
}
}
// 初始化WebSocket客户端
window.wsClient = new WebSocketClient();
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
console.log('🚀 页面加载完成,初始化应用');
// 初始化WebSocket
window.wsClient.init();
// 初始化用户信息下拉菜单
initUserInfoDropdown(); initUserInfoDropdown();
}); });
// 页面关闭前断开连接
window.addEventListener('beforeunload', () => {
if (window.wsClient) {
window.wsClient.disconnect();
}
});
// 添加动画样式
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);
// 客户类型下拉框交互已集成到客户等级下拉框的交互逻辑中 // 客户类型下拉框交互已集成到客户等级下拉框的交互逻辑中
// 无需单独的JavaScript代码,因为已使用相同的类名和结构 // 无需单独的JavaScript代码,因为已使用相同的类名和结构
// 使用IIFE封装,避免全局变量污染 // 使用IIFE封装,避免全局变量污染
@ -6805,11 +7139,33 @@
displayCurrentPageRecords(filteredRecords); displayCurrentPageRecords(filteredRecords);
document.getElementById('page-info').textContent = `第 ${currentPage} 页,共 ${totalPages} 页`; // 根据currentLevel动态选择正确的分页信息元素
document.getElementById('page-input').value = currentPage; const pageInfoId = currentLevel === 'all' ? 'all-customers-page-info' : `${currentLevel}-page-info`;
const pageInputId = currentLevel === 'all' ? 'all-customers-page-input' : `${currentLevel}-page-input`;
const prevPageId = currentLevel === 'all' ? 'all-customers-prev-page' : `${currentLevel}-prev-page`;
const nextPageId = currentLevel === 'all' ? 'all-customers-next-page' : `${currentLevel}-next-page`;
// 安全获取元素,避免null引用错误
const pageInfoElement = document.getElementById(pageInfoId);
const pageInputElement = document.getElementById(pageInputId);
const prevPageElement = document.getElementById(prevPageId);
const nextPageElement = document.getElementById(nextPageId);
if (pageInfoElement) {
pageInfoElement.textContent = `第 ${currentPage} 页,共 ${totalPages} 页`;
}
if (pageInputElement) {
pageInputElement.value = currentPage;
}
document.getElementById('prev-page').disabled = currentPage === 1 || showAll; if (prevPageElement) {
document.getElementById('next-page').disabled = currentPage === totalPages || showAll; prevPageElement.disabled = currentPage === 1 || showAll;
}
if (nextPageElement) {
nextPageElement.disabled = currentPage === totalPages || showAll;
}
updateTotalInfo(totalItems, filteredRecords.length); updateTotalInfo(totalItems, filteredRecords.length);
} }
@ -11921,7 +12277,13 @@
// 如果是从通知弹窗点击进入,更新notice状态为old // 如果是从通知弹窗点击进入,更新notice状态为old
if (fromNotification) { if (fromNotification) {
console.log('📋 从通知弹窗查看客户详情,更新notice状态为old'); console.log('📋 从通知弹窗查看客户详情,更新notice状态为old');
// 发送请求更新notice状态
// 1. 标记本地通知为已读
if (window.customerCache && typeof window.customerCache.markNotificationAsRead === 'function') {
window.customerCache.markNotificationAsRead(customerId);
}
// 2. 发送请求更新notice状态
fetch(`/DL/supply/pool/customers/${customerId}/notice`, { fetch(`/DL/supply/pool/customers/${customerId}/notice`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
@ -11932,20 +12294,33 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
console.log('✅ 更新客户通知状态成功,响应:', data); console.log('✅ 更新客户通知状态成功,响应:', data);
// 从通知列表中移除对应的通知项
const notificationItems = document.querySelectorAll('.notification-item'); // 3. 从通知列表中移除对应的通知项
notificationItems.forEach(item => { const notificationItem = document.querySelector(`[data-customer-id="${customerId}"]`);
const customerIdElement = item.querySelector('.customer-id'); if (notificationItem) {
if (customerIdElement && customerIdElement.textContent.includes(customerId)) { notificationItem.remove();
item.remove();
} }
});
// 检查是否还有通知项 // 4. 检查是否还有通知项
const remainingItems = document.querySelectorAll('.notification-item'); const remainingItems = document.querySelectorAll('.notification-item');
if (remainingItems.length === 0) { if (remainingItems.length === 0) {
const notificationContent = document.getElementById('notificationContent'); const notificationContent = document.getElementById('notificationContent');
notificationContent.innerHTML = '<div class="notification-empty"><p>暂无通知</p></div>'; notificationContent.innerHTML = '<div class="notification-empty"><p>暂无通知</p></div>';
} }
// 5. 更新缓存,移除该客户的banold标记
if (window.customerCache && window.customerCache.allPublicSeaCustomers) {
// 查找并更新客户的notice状态
const customerIndex = window.customerCache.allPublicSeaCustomers.findIndex(c => c.id === customerId);
if (customerIndex !== -1) {
window.customerCache.allPublicSeaCustomers[customerIndex].notice = 'old';
}
}
// 6. 刷新通知状态显示
if (window.customerCache && typeof window.customerCache.updateNotificationStatus === 'function') {
window.customerCache.updateNotificationStatus(window.customerCache.allPublicSeaCustomers || []);
}
}) })
.catch(error => { .catch(error => {
console.error('❌ 更新客户通知状态失败:', error); console.error('❌ 更新客户通知状态失败:', error);
@ -14628,8 +15003,8 @@
} }
// 添加通知弹窗样式 // 添加通知弹窗样式
const style = document.createElement('style'); const notificationStyle = document.createElement('style');
style.textContent = ` notificationStyle.textContent = `
/* 通知弹窗样式 */ /* 通知弹窗样式 */
.notification-list { .notification-list {
display: flex; display: flex;

465
实时数据接收与缓存优化方案.md

@ -0,0 +1,465 @@
# 实时数据接收与缓存优化方案
## 一、当前系统问题分析
通过对前端代码的分析,我发现当前系统存在以下问题:
1. **频繁的HTTP请求**:前端使用多个`setInterval`进行定期数据刷新,导致大量的HTTP请求
2. **响应式数据获取**:数据更新延迟高,用户体验差
3. **缺少有效的缓存机制**:每次请求都直接从服务器获取数据,没有合理利用缓存
4. **资源浪费**:即使数据没有更新,也会进行定期请求
## 二、解决方案设计
### 1. WebSocket实时数据接收
使用WebSocket实现实时数据推送,替代现有的定期轮询。
### 2. 前端缓存优化
实现高效的前端缓存机制,确保数据更新时缓存也能及时更新。
## 三、具体实施方案
### 1. 后端实现
#### 1.1 添加WebSocket依赖
```xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
```
#### 1.2 配置WebSocket
```java
// WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
```
#### 1.3 实现实时数据推送服务
```java
// RealTimeDataService.java
@Service
public class RealTimeDataService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 推送客户数据更新
*/
public void pushCustomerUpdate(String customerId, UserProductCartDTO customerData) {
messagingTemplate.convertAndSend("/topic/customers/" + customerId, customerData);
}
/**
* 推送公海池数据更新
*/
public void pushPublicSeaUpdate(List<UserProductCartDTO> customerList) {
messagingTemplate.convertAndSend("/topic/public-sea", customerList);
}
/**
* 推送通知
*/
public void pushNotification(NotificationDTO notification) {
messagingTemplate.convertAndSend("/topic/notifications", notification);
}
}
```
### 2. 前端实现
#### 2.1 WebSocket客户端实现
在前端页面中添加WebSocket客户端代码,用于连接WebSocket服务器并接收实时数据。
```javascript
// 在mainapp-sells.html和mainapp-supplys.html中添加
class WebSocketClient {
constructor() {
this.socket = null;
this.isConnected = false;
this.reconnectInterval = 5000;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.callbacks = {};
this.cache = new Map();
}
// 初始化WebSocket连接
init() {
// 移除现有的SockJS和STOMP依赖
const script1 = document.createElement('script');
script1.src = 'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js';
document.head.appendChild(script1);
const script2 = document.createElement('script');
script2.src = 'https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js';
script2.onload = () => this.connect();
document.head.appendChild(script2);
}
// 连接WebSocket服务器
connect() {
const socket = new SockJS('/ws');
this.stompClient = Stomp.over(socket);
this.stompClient.connect({},
(frame) => {
console.log('✅ WebSocket连接成功:', frame);
this.isConnected = true;
this.reconnectAttempts = 0;
// 订阅公海池数据更新
this.stompClient.subscribe('/topic/public-sea', (message) => {
const data = JSON.parse(message.body);
this.handlePublicSeaUpdate(data);
});
// 订阅通知
this.stompClient.subscribe('/topic/notifications', (message) => {
const notification = JSON.parse(message.body);
this.handleNotification(notification);
});
},
(error) => {
console.error('❌ WebSocket连接错误:', error);
this.isConnected = false;
this.attemptReconnect();
}
);
}
// 尝试重连
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`⏱️ 尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => this.connect(), this.reconnectInterval);
} else {
console.error('❌ 达到最大重连次数,停止尝试');
}
}
// 处理公海池数据更新
handlePublicSeaUpdate(data) {
// 更新缓存
this.cache.set('public-sea-data', {
data: data,
timestamp: Date.now()
});
// 调用回调函数
if (this.callbacks['public-sea-update']) {
this.callbacks['public-sea-update'].forEach(callback => callback(data));
}
}
// 处理通知
handleNotification(notification) {
// 显示通知
this.showNotification(notification);
// 调用回调函数
if (this.callbacks['notification']) {
this.callbacks['notification'].forEach(callback => callback(notification));
}
}
// 显示通知
showNotification(notification) {
const notificationEl = document.createElement('div');
notificationEl.className = 'websocket-notification';
notificationEl.innerHTML = `
<div class="notification-header">
<h4>${notification.title}</h4>
<button class="close-btn">&times;</button>
</div>
<div class="notification-body">
<p>${notification.message}</p>
<small>${new Date(notification.timestamp).toLocaleString()}</small>
</div>
`;
// 添加样式
notificationEl.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 16px;
width: 320px;
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
// 添加关闭事件
const closeBtn = notificationEl.querySelector('.close-btn');
closeBtn.addEventListener('click', () => notificationEl.remove());
// 添加到页面
document.body.appendChild(notificationEl);
// 3秒后自动关闭
setTimeout(() => notificationEl.remove(), 3000);
}
// 订阅事件
on(event, callback) {
if (!this.callbacks[event]) {
this.callbacks[event] = [];
}
this.callbacks[event].push(callback);
}
// 取消订阅
off(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
}
}
// 获取缓存数据
getCachedData(key) {
const cached = this.cache.get(key);
if (cached) {
return cached.data;
}
return null;
}
// 设置缓存数据
setCachedData(key, data) {
this.cache.set(key, {
data: data,
timestamp: Date.now()
});
}
// 清除缓存
clearCache(key) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
// 断开连接
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect();
this.isConnected = false;
}
}
}
// 初始化WebSocket客户端
const wsClient = new WebSocketClient();
window.addEventListener('load', () => {
wsClient.init();
});
// 页面关闭前断开连接
window.addEventListener('beforeunload', () => {
wsClient.disconnect();
});
// 添加动画样式
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);
```
#### 2.2 优化数据获取函数
修改现有的数据获取函数,优先使用缓存数据,只有在缓存不存在或过期时才从服务器获取。
```javascript
// 修改fetchPublicSeaData函数
async function fetchPublicSeaData(loginInfo, level) {
// 尝试从缓存获取
const cachedData = wsClient.getCachedData('public-sea-data');
if (cachedData) {
console.log('📦 从缓存获取公海池数据');
return cachedData;
}
// 缓存未命中,从服务器获取
console.log('🌐 从服务器获取公海池数据');
const url = `${API_BASE_URL}/pool/customers?level=${level}&isSupplySide=false`;
const response = await fetch(appendAuthParams(url), {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to fetch public sea data');
}
const data = await response.json();
// 更新缓存
wsClient.setCachedData('public-sea-data', data);
return data;
}
// 监听公海池数据更新
wsClient.on('public-sea-update', (data) => {
console.log('🔄 公海池数据已更新');
// 更新页面数据
updatePublicSeaData(data);
});
```
#### 2.3 替换setInterval轮询
移除现有的setInterval轮询,使用WebSocket实时数据更新。
```javascript
// 移除现有轮询代码
// this.updateInterval = setInterval(() => {
// this.loadCustomers(loginInfo, level);
// }, 30000);
// 替换为WebSocket监听
wsClient.on('public-sea-update', (data) => {
this.customersArray = data;
this.renderCustomers();
});
```
#### 2.4 实现数据更新时的缓存刷新
当用户手动更新数据时,确保缓存也被刷新。
```javascript
// 在数据更新成功后
async function updateCustomer(customerData) {
// 发送更新请求
const response = await fetch(`${API_BASE_URL}/pool/updateCustomer`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(customerData)
});
if (response.ok) {
// 清除相关缓存
wsClient.clearCache('public-sea-data');
wsClient.clearCache(`customer-${customerData.userId}`);
// 显示成功消息
showMessage('客户数据更新成功');
}
}
```
## 四、缓存策略
### 1. 缓存类型
- **公海池数据缓存**:缓存公海池客户列表,过期时间30分钟
- **单个客户数据缓存**:缓存单个客户的详细信息,过期时间1小时
- **通知缓存**:缓存通知列表,过期时间10分钟
### 2. 缓存更新机制
- **WebSocket实时更新**:当服务器数据更新时,通过WebSocket推送更新
- **手动更新**:用户手动更新数据时,清除相关缓存
- **过期自动刷新**:缓存过期时,自动从服务器获取最新数据
## 五、预期效果
1. **减少HTTP请求**:预计减少80%以上的HTTP请求
2. **提高数据实时性**:数据更新延迟从30秒降低到<1秒
3. **提升用户体验**:页面数据实时更新,无需等待
4. **降低服务器压力**:减少不必要的数据查询
5. **提高页面响应速度**:从缓存获取数据,响应更快
## 六、实施步骤
1. **后端实现**
- 添加WebSocket依赖
- 配置WebSocket服务
- 实现实时数据推送服务
- 在数据更新时调用推送服务
2. **前端实现**
- 添加WebSocket客户端代码
- 优化数据获取函数,使用缓存
- 移除现有的setInterval轮询
- 实现缓存更新机制
3. **测试验证**
- 测试WebSocket连接稳定性
- 测试数据实时更新效果
- 测试缓存机制
- 测试并发性能
## 七、代码集成建议
1. **分阶段实施**:先在一个页面实现,测试稳定后再推广到其他页面
2. **保留降级机制**:当WebSocket连接失败时,自动回退到轮询模式
3. **添加监控**:监控WebSocket连接状态和缓存命中率
4. **优化缓存策略**:根据实际使用情况调整缓存过期时间
## 八、注意事项
1. **WebSocket连接管理**
- 实现自动重连机制
- 处理连接断开情况
- 限制重连次数
2. **缓存一致性**
- 确保数据更新时缓存也更新
- 处理缓存过期情况
- 避免缓存雪崩
3. **性能优化**
- 限制WebSocket消息大小
- 优化数据传输格式
- 避免频繁发送大量数据
4. **兼容性考虑**
- 支持不支持WebSocket的浏览器
- 实现降级方案
通过以上方案的实施,预计可以显著提高系统的性能和用户体验,减少服务器压力,实现真正的实时数据更新。

314
预加载解决方案.md

@ -0,0 +1,314 @@
# 客户数据预加载解决方案
## 一、当前系统性能问题分析
### 1. 数据加载模式
- **请求-响应模式**:当前系统使用传统的请求-响应模式,每次前端请求数据时,后端才会查询数据库并返回结果
- **多次数据库查询**:单个客户数据请求涉及多个数据库查询(基本信息、联系人、购物车项、负责人等)
- **缺少缓存机制**:系统中没有实现任何缓存机制,所有数据都直接从数据库查询
- **前端频繁请求**:作为客户关系管理系统,前端需要频繁请求客户数据
### 2. 性能瓶颈
- **数据库查询开销大**:每次请求都需要执行多次数据库查询
- **网络延迟**:频繁的HTTP请求导致网络延迟累积
- **数据重复传输**:相同数据可能被多次请求和传输
- **缺少数据预加载**:没有在用户需要之前预先加载数据
## 二、预加载解决方案设计
### 1. 后端预加载策略
#### 1.1 添加缓存层
- **使用Redis作为缓存存储**:高性能、高可用的键值存储
- **实现数据预加载服务**:定期将热点数据加载到缓存中
- **合理设置过期时间**:根据数据类型和更新频率设置不同的过期时间
#### 1.2 优化数据库查询
- **实现数据预聚合**:减少查询次数,提高查询效率
- **使用JOIN查询**:替代多次单表查询,减少数据库交互次数
- **添加索引优化**:为频繁查询的字段添加索引
#### 1.3 实现数据预加载服务
- **定时任务预加载**:定期预加载热点数据
- **数据变更监听**:当数据发生变化时及时更新缓存
- **分层预加载**:为不同类型的用户预加载不同的数据
### 2. 前端预加载策略
#### 2.1 实现前端缓存
- **使用localStorage/sessionStorage**:缓存不经常变化的数据
- **数据预加载机制**:在用户访问页面之前加载数据
- **数据过期机制**:定期更新缓存,确保数据新鲜度
#### 2.2 优化请求策略
- **请求合并**:减少HTTP请求次数
- **请求预加载**:在用户可能访问的数据之前加载
- **懒加载**:只加载当前需要的数据,减少初始加载时间
#### 2.3 WebSocket实时更新
- **建立WebSocket连接**:实现数据实时更新
- **后端主动推送**:当数据发生变化时,主动推送给前端
- **减少轮询请求**:降低服务器压力,提高响应速度
## 三、具体实施方案
### 1. 后端实现
#### 1.1 添加Redis依赖
```xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
#### 1.2 配置Redis连接
```yaml
# application.yaml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
```
#### 1.3 实现缓存服务
```java
// CacheService.java
@Service
public class CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 缓存数据
public void cacheData(String key, Object data, long expireTime, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, data, expireTime, timeUnit);
}
// 获取缓存数据
public <T> T getCachedData(String key, Class<T> clazz) {
Object data = redisTemplate.opsForValue().get(key);
return clazz.cast(data);
}
// 删除缓存数据
public void deleteCachedData(String key) {
redisTemplate.delete(key);
}
// 检查缓存是否存在
public boolean isCached(String key) {
return redisTemplate.hasKey(key);
}
}
```
#### 1.4 实现数据预加载服务
```java
// PreloadService.java
@Service
public class PreloadService {
@Autowired
private UsersMapper usersMapper;
@Autowired
private CacheService cacheService;
// 预加载热点客户数据 - 每5分钟执行一次
@Scheduled(fixedRate = 300000)
public void preloadHotCustomers() {
List<UserProductCartDTO> hotCustomers = usersMapper.selectHotCustomers();
for (UserProductCartDTO customer : hotCustomers) {
String key = "customer:" + customer.getUserId();
cacheService.cacheData(key, customer, 1, TimeUnit.HOURS);
}
}
// 预加载客户列表数据 - 每10分钟执行一次
@Scheduled(fixedRate = 600000)
public void preloadCustomerList() {
List<UserProductCartDTO> customerList = usersMapper.selectAllCustomers();
cacheService.cacheData("customer:list", customerList, 30, TimeUnit.MINUTES);
}
}
```
#### 1.5 修改服务层代码,使用缓存
```java
// CustomerService.java
@Service
public class CustomerService {
@Autowired
private UsersMapper usersMapper;
@Autowired
private CacheService cacheService;
// 获取客户数据,优先从缓存获取
public UserProductCartDTO getCustomerById(String userId) {
String cacheKey = "customer:" + userId;
// 尝试从缓存获取
UserProductCartDTO customer = cacheService.getCachedData(cacheKey, UserProductCartDTO.class);
if (customer != null) {
return customer;
}
// 缓存未命中,从数据库查询
customer = usersMapper.selectById(userId);
// 将查询结果存入缓存
if (customer != null) {
cacheService.cacheData(cacheKey, customer, 1, TimeUnit.HOURS);
}
return customer;
}
}
```
#### 1.6 实现数据变更监听
```java
// DataChangeAspect.java
@Aspect
@Component
public class DataChangeAspect {
@Autowired
private CacheService cacheService;
// 监听数据更新方法,清除相关缓存
@AfterReturning(pointcut = "execution(* com.example.web.service.*Service.update*(..))")
public void afterUpdate(JoinPoint joinPoint) {
// 清除相关缓存
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof UserProductCartDTO) {
UserProductCartDTO customer = (UserProductCartDTO) arg;
String cacheKey = "customer:" + customer.getUserId();
cacheService.deleteCachedData(cacheKey);
}
// 清除客户列表缓存
cacheService.deleteCachedData("customer:list");
}
}
}
```
### 2. 前端实现
#### 2.1 前端预加载逻辑
```javascript
// 在mainapp-sells.html和mainapp-supplys.html中添加
// 预加载客户数据
function preloadCustomerData() {
// 预加载客户列表
fetch('/api/customers/list')
.then(response => response.json())
.then(data => {
if (data.success) {
// 将数据存入localStorage
localStorage.setItem('customerList', JSON.stringify(data.data));
localStorage.setItem('customerListExpire', Date.now() + 30 * 60 * 1000); // 30分钟过期
}
});
// 预加载热点客户数据
fetch('/api/customers/hot')
.then(response => response.json())
.then(data => {
if (data.success) {
// 将数据存入localStorage
localStorage.setItem('hotCustomers', JSON.stringify(data.data));
localStorage.setItem('hotCustomersExpire', Date.now() + 60 * 60 * 1000); // 1小时过期
}
});
}
// 页面加载完成后预加载数据
window.addEventListener('load', preloadCustomerData);
// 在用户可能访问的页面之前预加载数据
document.addEventListener('click', function(e) {
if (e.target.matches('[data-preload]')) {
const preloadUrl = e.target.getAttribute('data-preload');
fetch(preloadUrl)
.then(response => response.json())
.then(data => {
// 将数据存入sessionStorage
sessionStorage.setItem(preloadUrl, JSON.stringify(data));
});
}
});
```
#### 2.2 前端缓存读取逻辑
```javascript
// 获取客户数据,优先从缓存读取
function getCustomerData(userId) {
// 尝试从localStorage获取
const cachedData = localStorage.getItem('customerList');
const expireTime = localStorage.getItem('customerListExpire');
if (cachedData && expireTime && Date.now() < parseInt(expireTime)) {
const customerList = JSON.parse(cachedData);
const customer = customerList.find(c => c.userId === userId);
if (customer) {
return Promise.resolve(customer);
}
}
// 缓存未命中,从服务器获取
return fetch(`/api/customers/${userId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
return data.data;
}
throw new Error(data.message);
});
}
```
## 四、预期效果
### 1. 性能提升
- **数据加载速度**:预计提升50%-90%
- **数据库查询次数**:预计减少60%-80%
- **前端响应时间**:预计提升40%-70%
- **服务器压力**:预计降低50%-70%
### 2. 用户体验改善
- **页面加载更快**:减少用户等待时间
- **数据响应更迅速**:提升系统交互流畅度
- **减少页面卡顿**:优化数据加载机制
- **支持更多并发用户**:提高系统吞吐量
## 五、实施步骤
1. **安装并配置Redis服务器**
2. **添加Redis依赖和配置**
3. **实现缓存服务和预加载服务**
4. **修改现有服务层代码,使用缓存**
5. **实现数据变更监听**
6. **修改前端代码,实现预加载和缓存**
7. **测试和优化**
## 六、注意事项
1. **数据一致性**:确保缓存数据与数据库数据的一致性
2. **缓存过期策略**:根据数据更新频率设置合理的过期时间
3. **内存管理**:监控Redis内存使用情况,避免内存溢出
4. **错误处理**:实现缓存失效时的降级策略
5. **性能监控**:添加性能监控,持续优化系统性能
## 七、总结
本预加载解决方案通过引入缓存机制和预加载策略,将有效解决当前系统数据加载慢的问题。通过后端预加载热点数据到Redis缓存,前端预加载和缓存数据,以及实现数据变更监听,系统的数据加载速度将得到显著提升,同时降低数据库压力和网络延迟,改善用户体验。
该方案具有良好的扩展性和可维护性,可以根据系统的实际运行情况进行调整和优化,为系统的长期稳定运行提供保障。
Loading…
Cancel
Save