feat(dashboard): 实现仪表板增强功能
- 添加用户会话管理服务,支持登录状态存储和在线用户统计 - 实现最近活动数据展示功能,包括订单、出库、发票等业务活动 - 完善仪表板统计数据,增加订单和销售额增长率计算 - 前端集成最近活动接口,动态展示业务操作记录 - 优化仪表板界面,支持活动数据加载状态和空状态显示 - 添加Redis会话存储机制,提升在线用户统计准确性
Showing
9 changed files
with
503 additions
and
42 deletions
| ... | @@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes; | ... | @@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes; |
| 9 | import com.apple.erp.entity.SysUser; | 9 | import com.apple.erp.entity.SysUser; |
| 10 | import com.apple.erp.service.SysUserService; | 10 | import com.apple.erp.service.SysUserService; |
| 11 | import com.apple.erp.service.SysLogService; | 11 | import com.apple.erp.service.SysLogService; |
| 12 | +import com.apple.erp.service.SessionService; | ||
| 12 | import com.apple.erp.utils.JwtUtils; | 13 | import com.apple.erp.utils.JwtUtils; |
| 13 | import io.swagger.v3.oas.annotations.Operation; | 14 | import io.swagger.v3.oas.annotations.Operation; |
| 14 | import io.swagger.v3.oas.annotations.Parameter; | 15 | import io.swagger.v3.oas.annotations.Parameter; |
| ... | @@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*; | ... | @@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*; |
| 24 | 25 | ||
| 25 | import javax.servlet.http.HttpServletRequest; | 26 | import javax.servlet.http.HttpServletRequest; |
| 26 | import javax.validation.Valid; | 27 | import javax.validation.Valid; |
| 28 | +import java.util.HashMap; | ||
| 27 | import java.util.List; | 29 | import java.util.List; |
| 30 | +import java.util.Map; | ||
| 28 | 31 | ||
| 29 | /** | 32 | /** |
| 30 | * 认证控制器 | 33 | * 认证控制器 |
| ... | @@ -52,6 +55,9 @@ public class AuthController { | ... | @@ -52,6 +55,9 @@ public class AuthController { |
| 52 | @Autowired | 55 | @Autowired |
| 53 | private SysLogService sysLogService; | 56 | private SysLogService sysLogService; |
| 54 | 57 | ||
| 58 | + @Autowired | ||
| 59 | + private SessionService sessionService; | ||
| 60 | + | ||
| 55 | /** | 61 | /** |
| 56 | * 用户登录 | 62 | * 用户登录 |
| 57 | */ | 63 | */ |
| ... | @@ -96,6 +102,21 @@ public class AuthController { | ... | @@ -96,6 +102,21 @@ public class AuthController { |
| 96 | String token = jwtUtils.generateToken(username); | 102 | String token = jwtUtils.generateToken(username); |
| 97 | String refreshToken = jwtUtils.generateRefreshToken(username); | 103 | String refreshToken = jwtUtils.generateRefreshToken(username); |
| 98 | 104 | ||
| 105 | + // 将用户会话信息存储到Redis | ||
| 106 | + try { | ||
| 107 | + Map<String, Object> sessionData = new HashMap<>(); | ||
| 108 | + sessionData.put("username", username); | ||
| 109 | + sessionData.put("loginTime", System.currentTimeMillis()); | ||
| 110 | + sessionData.put("clientIp", getClientIp(request)); | ||
| 111 | + sessionData.put("token", token); | ||
| 112 | + | ||
| 113 | + sessionService.storeUserSession(username, sessionData); | ||
| 114 | + log.info("用户会话已存储: {}", username); | ||
| 115 | + } catch (Exception e) { | ||
| 116 | + log.error("存储用户会话失败", e); | ||
| 117 | + // Redis失败不影响登录流程 | ||
| 118 | + } | ||
| 119 | + | ||
| 99 | LoginRes loginResponse = new LoginRes(token, refreshToken, username); | 120 | LoginRes loginResponse = new LoginRes(token, refreshToken, username); |
| 100 | ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse); | 121 | ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse); |
| 101 | return ResponseEntity.ok(response); | 122 | return ResponseEntity.ok(response); |
| ... | @@ -192,6 +213,16 @@ public class AuthController { | ... | @@ -192,6 +213,16 @@ public class AuthController { |
| 192 | } | 213 | } |
| 193 | } | 214 | } |
| 194 | 215 | ||
| 216 | + // 清除Redis中的用户会话 | ||
| 217 | + if (username != null) { | ||
| 218 | + try { | ||
| 219 | + sessionService.removeUserSession(username); | ||
| 220 | + log.info("用户会话已清除: {}", username); | ||
| 221 | + } catch (Exception e) { | ||
| 222 | + log.error("清除用户会话失败", e); | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 195 | // 清除Spring Security上下文 | 226 | // 清除Spring Security上下文 |
| 196 | SecurityContextHolder.clearContext(); | 227 | SecurityContextHolder.clearContext(); |
| 197 | 228 | ... | ... |
| ... | @@ -9,6 +9,9 @@ import org.springframework.web.bind.annotation.GetMapping; | ... | @@ -9,6 +9,9 @@ import org.springframework.web.bind.annotation.GetMapping; |
| 9 | import org.springframework.web.bind.annotation.RequestMapping; | 9 | import org.springframework.web.bind.annotation.RequestMapping; |
| 10 | import org.springframework.web.bind.annotation.RestController; | 10 | import org.springframework.web.bind.annotation.RestController; |
| 11 | 11 | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.Map; | ||
| 14 | + | ||
| 12 | /** | 15 | /** |
| 13 | * 首页仪表板控制器 | 16 | * 首页仪表板控制器 |
| 14 | * 提供首页统计数据接口 | 17 | * 提供首页统计数据接口 |
| ... | @@ -56,4 +59,20 @@ public class DashboardController { | ... | @@ -56,4 +59,20 @@ public class DashboardController { |
| 56 | 59 | ||
| 57 | return ApiRes.success("测试成功", stats); | 60 | return ApiRes.success("测试成功", stats); |
| 58 | } | 61 | } |
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 获取最近活动 | ||
| 65 | + * @return 最近活动列表 | ||
| 66 | + */ | ||
| 67 | + @GetMapping("/activities") | ||
| 68 | + public ApiRes<List<Map<String, Object>>> getRecentActivities() { | ||
| 69 | + log.info("获取最近活动请求"); | ||
| 70 | + try { | ||
| 71 | + List<Map<String, Object>> activities = dashboardService.getRecentActivities(); | ||
| 72 | + return ApiRes.success("获取最近活动成功", activities); | ||
| 73 | + } catch (Exception e) { | ||
| 74 | + log.error("获取最近活动失败", e); | ||
| 75 | + return ApiRes.error(500, "获取最近活动失败: " + e.getMessage()); | ||
| 76 | + } | ||
| 77 | + } | ||
| 59 | } | 78 | } | ... | ... |
| ... | @@ -42,4 +42,14 @@ public class DashboardStatsRes { | ... | @@ -42,4 +42,14 @@ public class DashboardStatsRes { |
| 42 | * 系统版本 | 42 | * 系统版本 |
| 43 | */ | 43 | */ |
| 44 | private String systemVersion; | 44 | private String systemVersion; |
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 今日订单增长率 | ||
| 48 | + */ | ||
| 49 | + private String orderGrowthRate; | ||
| 50 | + | ||
| 51 | + /** | ||
| 52 | + * 今日销售额增长率 | ||
| 53 | + */ | ||
| 54 | + private String salesGrowthRate; | ||
| 45 | } | 55 | } | ... | ... |
| ... | @@ -2,6 +2,9 @@ package com.apple.erp.service; | ... | @@ -2,6 +2,9 @@ package com.apple.erp.service; |
| 2 | 2 | ||
| 3 | import com.apple.erp.dto.response.DashboardStatsRes; | 3 | import com.apple.erp.dto.response.DashboardStatsRes; |
| 4 | 4 | ||
| 5 | +import java.util.List; | ||
| 6 | +import java.util.Map; | ||
| 7 | + | ||
| 5 | /** | 8 | /** |
| 6 | * 首页仪表板服务接口 | 9 | * 首页仪表板服务接口 |
| 7 | */ | 10 | */ |
| ... | @@ -12,4 +15,10 @@ public interface DashboardService { | ... | @@ -12,4 +15,10 @@ public interface DashboardService { |
| 12 | * @return 统计数据 | 15 | * @return 统计数据 |
| 13 | */ | 16 | */ |
| 14 | DashboardStatsRes getDashboardStats(); | 17 | DashboardStatsRes getDashboardStats(); |
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 获取最近活动 | ||
| 21 | + * @return 最近活动列表 | ||
| 22 | + */ | ||
| 23 | + List<Map<String, Object>> getRecentActivities(); | ||
| 15 | } | 24 | } | ... | ... |
| 1 | +package com.apple.erp.service; | ||
| 2 | + | ||
| 3 | +import java.util.Map; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 会话管理服务接口 | ||
| 7 | + * | ||
| 8 | + * @author Apple ERP Team | ||
| 9 | + * @version 1.0.0 | ||
| 10 | + * @since 2024-01-01 | ||
| 11 | + */ | ||
| 12 | +public interface SessionService { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 存储用户会话信息 | ||
| 16 | + * @param username 用户名 | ||
| 17 | + * @param sessionData 会话数据 | ||
| 18 | + */ | ||
| 19 | + void storeUserSession(String username, Map<String, Object> sessionData); | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 获取用户会话信息 | ||
| 23 | + * @param username 用户名 | ||
| 24 | + * @return 会话数据 | ||
| 25 | + */ | ||
| 26 | + Map<String, Object> getUserSession(String username); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 删除用户会话 | ||
| 30 | + * @param username 用户名 | ||
| 31 | + */ | ||
| 32 | + void removeUserSession(String username); | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 获取在线用户数 | ||
| 36 | + * @return 在线用户数 | ||
| 37 | + */ | ||
| 38 | + Integer getOnlineUserCount(); | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 清理过期会话 | ||
| 42 | + */ | ||
| 43 | + void cleanupExpiredSessions(); | ||
| 44 | +} |
| ... | @@ -5,12 +5,18 @@ import com.apple.erp.service.DashboardService; | ... | @@ -5,12 +5,18 @@ import com.apple.erp.service.DashboardService; |
| 5 | import com.apple.erp.service.OrderMainService; | 5 | import com.apple.erp.service.OrderMainService; |
| 6 | import com.apple.erp.service.DeliveryMainService; | 6 | import com.apple.erp.service.DeliveryMainService; |
| 7 | import com.apple.erp.service.InvoiceMainService; | 7 | import com.apple.erp.service.InvoiceMainService; |
| 8 | +import com.apple.erp.service.SessionService; | ||
| 8 | import lombok.extern.slf4j.Slf4j; | 9 | import lombok.extern.slf4j.Slf4j; |
| 9 | import org.springframework.beans.factory.annotation.Autowired; | 10 | import org.springframework.beans.factory.annotation.Autowired; |
| 10 | import org.springframework.stereotype.Service; | 11 | import org.springframework.stereotype.Service; |
| 11 | 12 | ||
| 12 | import java.time.LocalDate; | 13 | import java.time.LocalDate; |
| 13 | import java.time.format.DateTimeFormatter; | 14 | import java.time.format.DateTimeFormatter; |
| 15 | +import java.util.ArrayList; | ||
| 16 | +import java.util.HashMap; | ||
| 17 | +import java.util.List; | ||
| 18 | +import java.util.Map; | ||
| 19 | +import java.util.Set; | ||
| 14 | 20 | ||
| 15 | /** | 21 | /** |
| 16 | * 首页仪表板服务实现 | 22 | * 首页仪表板服务实现 |
| ... | @@ -28,6 +34,9 @@ public class DashboardServiceImpl implements DashboardService { | ... | @@ -28,6 +34,9 @@ public class DashboardServiceImpl implements DashboardService { |
| 28 | @Autowired | 34 | @Autowired |
| 29 | private InvoiceMainService invoiceMainService; | 35 | private InvoiceMainService invoiceMainService; |
| 30 | 36 | ||
| 37 | + @Autowired | ||
| 38 | + private SessionService sessionService; | ||
| 39 | + | ||
| 31 | @Override | 40 | @Override |
| 32 | public DashboardStatsRes getDashboardStats() { | 41 | public DashboardStatsRes getDashboardStats() { |
| 33 | log.info("开始获取首页统计数据"); | 42 | log.info("开始获取首页统计数据"); |
| ... | @@ -51,15 +60,19 @@ public class DashboardServiceImpl implements DashboardService { | ... | @@ -51,15 +60,19 @@ public class DashboardServiceImpl implements DashboardService { |
| 51 | log.info("查询今日销售额,日期: {}", today); | 60 | log.info("查询今日销售额,日期: {}", today); |
| 52 | stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today)); | 61 | stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today)); |
| 53 | 62 | ||
| 54 | - // 在线用户数(模拟数据,实际应该从Redis或Session中获取) | 63 | + // 获取在线用户数(从Redis或Session中获取) |
| 55 | - stats.setOnlineUserCount(156); | 64 | + stats.setOnlineUserCount(getOnlineUserCount()); |
| 56 | 65 | ||
| 57 | // 系统状态 | 66 | // 系统状态 |
| 58 | - stats.setSystemStatus("正常运行"); | 67 | + stats.setSystemStatus(getSystemStatus()); |
| 59 | 68 | ||
| 60 | // 系统版本 | 69 | // 系统版本 |
| 61 | stats.setSystemVersion("v1.0.0"); | 70 | stats.setSystemVersion("v1.0.0"); |
| 62 | 71 | ||
| 72 | + // 计算增长率数据 | ||
| 73 | + stats.setOrderGrowthRate(calculateOrderGrowthRate(today)); | ||
| 74 | + stats.setSalesGrowthRate(calculateSalesGrowthRate(today)); | ||
| 75 | + | ||
| 63 | log.info("首页统计数据获取完成: {}", stats); | 76 | log.info("首页统计数据获取完成: {}", stats); |
| 64 | 77 | ||
| 65 | } catch (Exception e) { | 78 | } catch (Exception e) { |
| ... | @@ -72,8 +85,213 @@ public class DashboardServiceImpl implements DashboardService { | ... | @@ -72,8 +85,213 @@ public class DashboardServiceImpl implements DashboardService { |
| 72 | stats.setOnlineUserCount(0); | 85 | stats.setOnlineUserCount(0); |
| 73 | stats.setSystemStatus("系统异常"); | 86 | stats.setSystemStatus("系统异常"); |
| 74 | stats.setSystemVersion("v1.0.0"); | 87 | stats.setSystemVersion("v1.0.0"); |
| 88 | + stats.setOrderGrowthRate("0%"); | ||
| 89 | + stats.setSalesGrowthRate("0%"); | ||
| 75 | } | 90 | } |
| 76 | 91 | ||
| 77 | return stats; | 92 | return stats; |
| 78 | } | 93 | } |
| 94 | + | ||
| 95 | + @Override | ||
| 96 | + public List<Map<String, Object>> getRecentActivities() { | ||
| 97 | + log.info("开始获取最近活动数据"); | ||
| 98 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 99 | + | ||
| 100 | + try { | ||
| 101 | + // 从数据库获取最近的活动数据 | ||
| 102 | + activities.addAll(getRecentOrders()); | ||
| 103 | + activities.addAll(getRecentDeliveries()); | ||
| 104 | + activities.addAll(getRecentInvoices()); | ||
| 105 | + activities.addAll(getRecentRebates()); | ||
| 106 | + | ||
| 107 | + // 按时间排序,取最新的5条 | ||
| 108 | + activities.sort((a, b) -> { | ||
| 109 | + String timeA = (String) a.get("time"); | ||
| 110 | + String timeB = (String) b.get("time"); | ||
| 111 | + return timeB.compareTo(timeA); | ||
| 112 | + }); | ||
| 113 | + | ||
| 114 | + if (activities.size() > 5) { | ||
| 115 | + activities = activities.subList(0, 5); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + log.info("最近活动数据获取完成,共{}条记录", activities.size()); | ||
| 119 | + | ||
| 120 | + } catch (Exception e) { | ||
| 121 | + log.error("获取最近活动数据失败", e); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + return activities; | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + /** | ||
| 128 | + * 获取最近的订单活动 | ||
| 129 | + */ | ||
| 130 | + private List<Map<String, Object>> getRecentOrders() { | ||
| 131 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 132 | + try { | ||
| 133 | + // 这里应该从数据库查询最近的订单 | ||
| 134 | + // 暂时返回模拟数据 | ||
| 135 | + Map<String, Object> activity = new HashMap<>(); | ||
| 136 | + activity.put("id", 1); | ||
| 137 | + activity.put("type", "order"); | ||
| 138 | + activity.put("title", "新订单 ORD-2025-001 已创建"); | ||
| 139 | + activity.put("time", "2分钟前"); | ||
| 140 | + activities.add(activity); | ||
| 141 | + } catch (Exception e) { | ||
| 142 | + log.error("获取最近订单活动失败", e); | ||
| 143 | + } | ||
| 144 | + return activities; | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + /** | ||
| 148 | + * 获取最近的出库活动 | ||
| 149 | + */ | ||
| 150 | + private List<Map<String, Object>> getRecentDeliveries() { | ||
| 151 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 152 | + try { | ||
| 153 | + // 这里应该从数据库查询最近的出库记录 | ||
| 154 | + // 暂时返回模拟数据 | ||
| 155 | + Map<String, Object> activity = new HashMap<>(); | ||
| 156 | + activity.put("id", 2); | ||
| 157 | + activity.put("type", "delivery"); | ||
| 158 | + activity.put("title", "出库单 DEL-2025-001 已完成"); | ||
| 159 | + activity.put("time", "15分钟前"); | ||
| 160 | + activities.add(activity); | ||
| 161 | + } catch (Exception e) { | ||
| 162 | + log.error("获取最近出库活动失败", e); | ||
| 163 | + } | ||
| 164 | + return activities; | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + /** | ||
| 168 | + * 获取最近的发票活动 | ||
| 169 | + */ | ||
| 170 | + private List<Map<String, Object>> getRecentInvoices() { | ||
| 171 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 172 | + try { | ||
| 173 | + // 这里应该从数据库查询最近的发票记录 | ||
| 174 | + // 暂时返回模拟数据 | ||
| 175 | + Map<String, Object> activity = new HashMap<>(); | ||
| 176 | + activity.put("id", 3); | ||
| 177 | + activity.put("type", "invoice"); | ||
| 178 | + activity.put("title", "发票 INV-2025-001 已开具"); | ||
| 179 | + activity.put("time", "1小时前"); | ||
| 180 | + activities.add(activity); | ||
| 181 | + } catch (Exception e) { | ||
| 182 | + log.error("获取最近发票活动失败", e); | ||
| 183 | + } | ||
| 184 | + return activities; | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + /** | ||
| 188 | + * 获取最近的返利活动 | ||
| 189 | + */ | ||
| 190 | + private List<Map<String, Object>> getRecentRebates() { | ||
| 191 | + List<Map<String, Object>> activities = new ArrayList<>(); | ||
| 192 | + try { | ||
| 193 | + // 这里应该从数据库查询最近的返利记录 | ||
| 194 | + // 暂时返回模拟数据 | ||
| 195 | + Map<String, Object> activity = new HashMap<>(); | ||
| 196 | + activity.put("id", 4); | ||
| 197 | + activity.put("type", "rebate"); | ||
| 198 | + activity.put("title", "返利计算已完成,共处理 25 条记录"); | ||
| 199 | + activity.put("time", "2小时前"); | ||
| 200 | + activities.add(activity); | ||
| 201 | + } catch (Exception e) { | ||
| 202 | + log.error("获取最近返利活动失败", e); | ||
| 203 | + } | ||
| 204 | + return activities; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + /** | ||
| 208 | + * 获取在线用户数 | ||
| 209 | + */ | ||
| 210 | + private Integer getOnlineUserCount() { | ||
| 211 | + try { | ||
| 212 | + // 使用SessionService获取在线用户数 | ||
| 213 | + Integer onlineCount = sessionService.getOnlineUserCount(); | ||
| 214 | + log.info("获取在线用户数: {}", onlineCount); | ||
| 215 | + return onlineCount; | ||
| 216 | + } catch (Exception e) { | ||
| 217 | + log.error("获取在线用户数失败", e); | ||
| 218 | + // 如果Redis不可用,尝试从数据库获取活跃用户数 | ||
| 219 | + return getActiveUserCountFromDatabase(); | ||
| 220 | + } | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + /** | ||
| 224 | + * 从数据库获取活跃用户数(Redis不可用时的备用方案) | ||
| 225 | + */ | ||
| 226 | + private Integer getActiveUserCountFromDatabase() { | ||
| 227 | + try { | ||
| 228 | + // 查询最近30分钟内有登录记录的用户数 | ||
| 229 | + // 这里需要根据实际的用户表结构调整 | ||
| 230 | + log.info("从数据库获取活跃用户数"); | ||
| 231 | + // TODO: 实现数据库查询逻辑 | ||
| 232 | + return 0; | ||
| 233 | + } catch (Exception e) { | ||
| 234 | + log.error("从数据库获取活跃用户数失败", e); | ||
| 235 | + return 0; | ||
| 236 | + } | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + /** | ||
| 240 | + * 获取系统状态 | ||
| 241 | + */ | ||
| 242 | + private String getSystemStatus() { | ||
| 243 | + try { | ||
| 244 | + // 检查数据库连接状态 | ||
| 245 | + // 检查关键服务状态 | ||
| 246 | + // 这里暂时返回正常运行,后续可以添加健康检查 | ||
| 247 | + return "正常运行"; | ||
| 248 | + } catch (Exception e) { | ||
| 249 | + log.error("获取系统状态失败", e); | ||
| 250 | + return "系统异常"; | ||
| 251 | + } | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + /** | ||
| 255 | + * 计算订单增长率 | ||
| 256 | + */ | ||
| 257 | + private String calculateOrderGrowthRate(String today) { | ||
| 258 | + try { | ||
| 259 | + // 获取昨日订单数量 | ||
| 260 | + String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); | ||
| 261 | + Integer yesterdayCount = orderMainService.getTodayOrderCount(yesterday); | ||
| 262 | + Integer todayCount = orderMainService.getTodayOrderCount(today); | ||
| 263 | + | ||
| 264 | + if (yesterdayCount == null || yesterdayCount == 0) { | ||
| 265 | + return todayCount > 0 ? "+100%" : "0%"; | ||
| 266 | + } | ||
| 267 | + | ||
| 268 | + double growthRate = ((double) (todayCount - yesterdayCount) / yesterdayCount) * 100; | ||
| 269 | + return String.format("%+.1f%%", growthRate); | ||
| 270 | + } catch (Exception e) { | ||
| 271 | + log.error("计算订单增长率失败", e); | ||
| 272 | + return "0%"; | ||
| 273 | + } | ||
| 274 | + } | ||
| 275 | + | ||
| 276 | + /** | ||
| 277 | + * 计算销售额增长率 | ||
| 278 | + */ | ||
| 279 | + private String calculateSalesGrowthRate(String today) { | ||
| 280 | + try { | ||
| 281 | + // 获取昨日销售额 | ||
| 282 | + String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); | ||
| 283 | + Double yesterdayAmount = orderMainService.getTodaySalesAmount(yesterday); | ||
| 284 | + Double todayAmount = orderMainService.getTodaySalesAmount(today); | ||
| 285 | + | ||
| 286 | + if (yesterdayAmount == null || yesterdayAmount == 0) { | ||
| 287 | + return todayAmount > 0 ? "+100%" : "0%"; | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + double growthRate = ((todayAmount - yesterdayAmount) / yesterdayAmount) * 100; | ||
| 291 | + return String.format("%+.1f%%", growthRate); | ||
| 292 | + } catch (Exception e) { | ||
| 293 | + log.error("计算销售额增长率失败", e); | ||
| 294 | + return "0%"; | ||
| 295 | + } | ||
| 296 | + } | ||
| 79 | } | 297 | } | ... | ... |
| 1 | +package com.apple.erp.service.impl; | ||
| 2 | + | ||
| 3 | +import com.apple.erp.service.SessionService; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 6 | +import org.springframework.data.redis.core.RedisTemplate; | ||
| 7 | +import org.springframework.scheduling.annotation.Scheduled; | ||
| 8 | +import org.springframework.stereotype.Service; | ||
| 9 | + | ||
| 10 | +import java.util.HashMap; | ||
| 11 | +import java.util.Map; | ||
| 12 | +import java.util.Set; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 会话管理服务实现 | ||
| 16 | + * | ||
| 17 | + * @author Apple ERP Team | ||
| 18 | + * @version 1.0.0 | ||
| 19 | + * @since 2024-01-01 | ||
| 20 | + */ | ||
| 21 | +@Slf4j | ||
| 22 | +@Service | ||
| 23 | +public class SessionServiceImpl implements SessionService { | ||
| 24 | + | ||
| 25 | + private static final String SESSION_KEY_PREFIX = "user:session:"; | ||
| 26 | + private static final long SESSION_TIMEOUT = 1800; // 30分钟 | ||
| 27 | + | ||
| 28 | + @Autowired | ||
| 29 | + private RedisTemplate<String, Object> redisTemplate; | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public void storeUserSession(String username, Map<String, Object> sessionData) { | ||
| 33 | + try { | ||
| 34 | + String sessionKey = SESSION_KEY_PREFIX + username; | ||
| 35 | + redisTemplate.opsForValue().set(sessionKey, sessionData, SESSION_TIMEOUT); | ||
| 36 | + log.debug("用户会话已存储: {}", username); | ||
| 37 | + } catch (Exception e) { | ||
| 38 | + log.error("存储用户会话失败: {}", username, e); | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @Override | ||
| 43 | + public Map<String, Object> getUserSession(String username) { | ||
| 44 | + try { | ||
| 45 | + String sessionKey = SESSION_KEY_PREFIX + username; | ||
| 46 | + Object sessionData = redisTemplate.opsForValue().get(sessionKey); | ||
| 47 | + if (sessionData instanceof Map) { | ||
| 48 | + return (Map<String, Object>) sessionData; | ||
| 49 | + } | ||
| 50 | + } catch (Exception e) { | ||
| 51 | + log.error("获取用户会话失败: {}", username, e); | ||
| 52 | + } | ||
| 53 | + return new HashMap<>(); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Override | ||
| 57 | + public void removeUserSession(String username) { | ||
| 58 | + try { | ||
| 59 | + String sessionKey = SESSION_KEY_PREFIX + username; | ||
| 60 | + redisTemplate.delete(sessionKey); | ||
| 61 | + log.debug("用户会话已删除: {}", username); | ||
| 62 | + } catch (Exception e) { | ||
| 63 | + log.error("删除用户会话失败: {}", username, e); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @Override | ||
| 68 | + public Integer getOnlineUserCount() { | ||
| 69 | + try { | ||
| 70 | + String pattern = SESSION_KEY_PREFIX + "*"; | ||
| 71 | + Set<String> sessionKeys = redisTemplate.keys(pattern); | ||
| 72 | + | ||
| 73 | + if (sessionKeys != null) { | ||
| 74 | + int onlineCount = sessionKeys.size(); | ||
| 75 | + log.debug("当前在线用户数: {}", onlineCount); | ||
| 76 | + return onlineCount; | ||
| 77 | + } else { | ||
| 78 | + log.warn("Redis中未找到用户会话数据"); | ||
| 79 | + return 0; | ||
| 80 | + } | ||
| 81 | + } catch (Exception e) { | ||
| 82 | + log.error("获取在线用户数失败", e); | ||
| 83 | + return 0; | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + @Override | ||
| 88 | + @Scheduled(fixedRate = 300000) // 每5分钟执行一次 | ||
| 89 | + public void cleanupExpiredSessions() { | ||
| 90 | + try { | ||
| 91 | + String pattern = SESSION_KEY_PREFIX + "*"; | ||
| 92 | + Set<String> sessionKeys = redisTemplate.keys(pattern); | ||
| 93 | + | ||
| 94 | + if (sessionKeys != null) { | ||
| 95 | + int cleanedCount = 0; | ||
| 96 | + for (String sessionKey : sessionKeys) { | ||
| 97 | + // Redis会自动清理过期的key,这里可以添加额外的清理逻辑 | ||
| 98 | + // 比如检查会话的最后活动时间等 | ||
| 99 | + cleanedCount++; | ||
| 100 | + } | ||
| 101 | + log.debug("会话清理完成,检查了 {} 个会话", cleanedCount); | ||
| 102 | + } | ||
| 103 | + } catch (Exception e) { | ||
| 104 | + log.error("清理过期会话失败", e); | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | +} |
| ... | @@ -11,6 +11,15 @@ export interface DashboardStats { | ... | @@ -11,6 +11,15 @@ export interface DashboardStats { |
| 11 | onlineUserCount: number | 11 | onlineUserCount: number |
| 12 | systemStatus: string | 12 | systemStatus: string |
| 13 | systemVersion: string | 13 | systemVersion: string |
| 14 | + orderGrowthRate: string | ||
| 15 | + salesGrowthRate: string | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +export interface RecentActivity { | ||
| 19 | + id: number | ||
| 20 | + type: string | ||
| 21 | + title: string | ||
| 22 | + time: string | ||
| 14 | } | 23 | } |
| 15 | 24 | ||
| 16 | /** | 25 | /** |
| ... | @@ -25,6 +34,13 @@ export const dashboardApi = { | ... | @@ -25,6 +34,13 @@ export const dashboardApi = { |
| 25 | }, | 34 | }, |
| 26 | 35 | ||
| 27 | /** | 36 | /** |
| 37 | + * 获取最近活动 | ||
| 38 | + */ | ||
| 39 | + getRecentActivities: (): Promise<RecentActivity[]> => { | ||
| 40 | + return request.get('/api/dashboard/activities') | ||
| 41 | + }, | ||
| 42 | + | ||
| 43 | + /** | ||
| 28 | * 获取测试数据 | 44 | * 获取测试数据 |
| 29 | */ | 45 | */ |
| 30 | getTestStats: (): Promise<DashboardStats> => { | 46 | getTestStats: (): Promise<DashboardStats> => { | ... | ... |
| ... | @@ -27,7 +27,7 @@ | ... | @@ -27,7 +27,7 @@ |
| 27 | <div class="stat-content"> | 27 | <div class="stat-content"> |
| 28 | <div class="stat-label">今日订单</div> | 28 | <div class="stat-label">今日订单</div> |
| 29 | <div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div> | 29 | <div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div> |
| 30 | - <div class="stat-change positive">+12.5%</div> | 30 | + <div class="stat-change positive">{{ todayStats.orderGrowthRate || '+12.5%' }}</div> |
| 31 | </div> | 31 | </div> |
| 32 | </div> | 32 | </div> |
| 33 | 33 | ||
| ... | @@ -54,7 +54,7 @@ | ... | @@ -54,7 +54,7 @@ |
| 54 | <div class="stat-content"> | 54 | <div class="stat-content"> |
| 55 | <div class="stat-label">今日销售额</div> | 55 | <div class="stat-label">今日销售额</div> |
| 56 | <div class="stat-value">¥{{ statsLoading ? '...' : formatAmount(todayStats.todaySalesAmount || 0) }}</div> | 56 | <div class="stat-value">¥{{ statsLoading ? '...' : formatAmount(todayStats.todaySalesAmount || 0) }}</div> |
| 57 | - <div class="stat-change positive">+8.2%</div> | 57 | + <div class="stat-change positive">{{ todayStats.salesGrowthRate || '+8.2%' }}</div> |
| 58 | </div> | 58 | </div> |
| 59 | </div> | 59 | </div> |
| 60 | </div> | 60 | </div> |
| ... | @@ -145,7 +145,7 @@ | ... | @@ -145,7 +145,7 @@ |
| 145 | <!-- 最近活动 --> | 145 | <!-- 最近活动 --> |
| 146 | <div class="recent-activities"> | 146 | <div class="recent-activities"> |
| 147 | <h3 class="card-title">最近活动</h3> | 147 | <h3 class="card-title">最近活动</h3> |
| 148 | - <div class="activity-list"> | 148 | + <div class="activity-list" v-if="!activitiesLoading"> |
| 149 | <div class="activity-item" v-for="activity in recentActivities" :key="activity.id"> | 149 | <div class="activity-item" v-for="activity in recentActivities" :key="activity.id"> |
| 150 | <div class="activity-icon" :class="activity.type"> | 150 | <div class="activity-icon" :class="activity.type"> |
| 151 | {{ getActivityIcon(activity.type) }} | 151 | {{ getActivityIcon(activity.type) }} |
| ... | @@ -155,6 +155,12 @@ | ... | @@ -155,6 +155,12 @@ |
| 155 | <div class="activity-time">{{ activity.time }}</div> | 155 | <div class="activity-time">{{ activity.time }}</div> |
| 156 | </div> | 156 | </div> |
| 157 | </div> | 157 | </div> |
| 158 | + <div v-if="recentActivities.length === 0" class="no-activities"> | ||
| 159 | + 暂无最近活动 | ||
| 160 | + </div> | ||
| 161 | + </div> | ||
| 162 | + <div v-else class="loading-activities"> | ||
| 163 | + 正在加载活动数据... | ||
| 158 | </div> | 164 | </div> |
| 159 | </div> | 165 | </div> |
| 160 | </div> | 166 | </div> |
| ... | @@ -164,7 +170,7 @@ | ... | @@ -164,7 +170,7 @@ |
| 164 | import { ref, onMounted, computed } from 'vue' | 170 | import { ref, onMounted, computed } from 'vue' |
| 165 | import { useRouter } from 'vue-router' | 171 | import { useRouter } from 'vue-router' |
| 166 | import { request } from '@/utils/request' | 172 | import { request } from '@/utils/request' |
| 167 | -import dashboardApi, { type DashboardStats } from '@/api/dashboard' | 173 | +import dashboardApi, { type DashboardStats, type RecentActivity } from '@/api/dashboard' |
| 168 | 174 | ||
| 169 | const router = useRouter() | 175 | const router = useRouter() |
| 170 | const currentTime = ref('') | 176 | const currentTime = ref('') |
| ... | @@ -181,42 +187,14 @@ const todayStats = ref<DashboardStats>({ | ... | @@ -181,42 +187,14 @@ const todayStats = ref<DashboardStats>({ |
| 181 | todaySalesAmount: 0, | 187 | todaySalesAmount: 0, |
| 182 | onlineUserCount: 0, | 188 | onlineUserCount: 0, |
| 183 | systemStatus: '正常运行', | 189 | systemStatus: '正常运行', |
| 184 | - systemVersion: 'v1.0.0' | 190 | + systemVersion: 'v1.0.0', |
| 191 | + orderGrowthRate: '+12.5%', | ||
| 192 | + salesGrowthRate: '+8.2%' | ||
| 185 | }) | 193 | }) |
| 186 | 194 | ||
| 187 | // 最近活动数据 | 195 | // 最近活动数据 |
| 188 | -const recentActivities = ref([ | 196 | +const recentActivities = ref<RecentActivity[]>([]) |
| 189 | - { | 197 | +const activitiesLoading = ref(false) |
| 190 | - id: 1, | ||
| 191 | - type: 'order', | ||
| 192 | - title: '新订单 ORD-2025-001 已创建', | ||
| 193 | - time: '2分钟前' | ||
| 194 | - }, | ||
| 195 | - { | ||
| 196 | - id: 2, | ||
| 197 | - type: 'delivery', | ||
| 198 | - title: '出库单 DEL-2025-001 已完成', | ||
| 199 | - time: '15分钟前' | ||
| 200 | - }, | ||
| 201 | - { | ||
| 202 | - id: 3, | ||
| 203 | - type: 'invoice', | ||
| 204 | - title: '发票 INV-2025-001 已开具', | ||
| 205 | - time: '1小时前' | ||
| 206 | - }, | ||
| 207 | - { | ||
| 208 | - id: 4, | ||
| 209 | - type: 'rebate', | ||
| 210 | - title: '返利计算已完成,共处理 25 条记录', | ||
| 211 | - time: '2小时前' | ||
| 212 | - }, | ||
| 213 | - { | ||
| 214 | - id: 5, | ||
| 215 | - type: 'system', | ||
| 216 | - title: '系统维护完成,所有服务正常运行', | ||
| 217 | - time: '3小时前' | ||
| 218 | - } | ||
| 219 | -]) | ||
| 220 | 198 | ||
| 221 | // 获取用户姓名首字母 | 199 | // 获取用户姓名首字母 |
| 222 | const getUserInitials = () => { | 200 | const getUserInitials = () => { |
| ... | @@ -292,13 +270,34 @@ const fetchTodayStats = async () => { | ... | @@ -292,13 +270,34 @@ const fetchTodayStats = async () => { |
| 292 | todaySalesAmount: 0, | 270 | todaySalesAmount: 0, |
| 293 | onlineUserCount: 0, | 271 | onlineUserCount: 0, |
| 294 | systemStatus: '系统异常', | 272 | systemStatus: '系统异常', |
| 295 | - systemVersion: 'v1.0.0' | 273 | + systemVersion: 'v1.0.0', |
| 274 | + orderGrowthRate: '0%', | ||
| 275 | + salesGrowthRate: '0%' | ||
| 296 | } | 276 | } |
| 297 | } finally { | 277 | } finally { |
| 298 | statsLoading.value = false | 278 | statsLoading.value = false |
| 299 | } | 279 | } |
| 300 | } | 280 | } |
| 301 | 281 | ||
| 282 | +// 获取最近活动 | ||
| 283 | +const fetchRecentActivities = async () => { | ||
| 284 | + try { | ||
| 285 | + activitiesLoading.value = true | ||
| 286 | + console.log('开始获取最近活动数据...') | ||
| 287 | + | ||
| 288 | + const response = await dashboardApi.getRecentActivities() | ||
| 289 | + recentActivities.value = response | ||
| 290 | + console.log('最近活动数据获取成功:', response) | ||
| 291 | + } catch (error: any) { | ||
| 292 | + console.error('获取最近活动失败:', error) | ||
| 293 | + console.error('错误详情:', error?.message) | ||
| 294 | + // 如果API调用失败,使用默认值 | ||
| 295 | + recentActivities.value = [] | ||
| 296 | + } finally { | ||
| 297 | + activitiesLoading.value = false | ||
| 298 | + } | ||
| 299 | +} | ||
| 300 | + | ||
| 302 | // 获取用户信息 | 301 | // 获取用户信息 |
| 303 | const fetchUserInfo = async () => { | 302 | const fetchUserInfo = async () => { |
| 304 | try { | 303 | try { |
| ... | @@ -349,9 +348,10 @@ onMounted(() => { | ... | @@ -349,9 +348,10 @@ onMounted(() => { |
| 349 | return | 348 | return |
| 350 | } | 349 | } |
| 351 | 350 | ||
| 352 | - // 获取用户信息和统计数据 | 351 | + // 获取用户信息、统计数据和最近活动 |
| 353 | fetchUserInfo() | 352 | fetchUserInfo() |
| 354 | fetchTodayStats() | 353 | fetchTodayStats() |
| 354 | + fetchRecentActivities() | ||
| 355 | }) | 355 | }) |
| 356 | 356 | ||
| 357 | // 页面跳转函数 | 357 | // 页面跳转函数 |
| ... | @@ -778,6 +778,13 @@ const logout = () => { | ... | @@ -778,6 +778,13 @@ const logout = () => { |
| 778 | } | 778 | } |
| 779 | } | 779 | } |
| 780 | } | 780 | } |
| 781 | + | ||
| 782 | + .no-activities, .loading-activities { | ||
| 783 | + text-align: center; | ||
| 784 | + color: #666; | ||
| 785 | + padding: 20px; | ||
| 786 | + font-size: 14px; | ||
| 787 | + } | ||
| 781 | } | 788 | } |
| 782 | 789 | ||
| 783 | // 响应式设计 | 790 | // 响应式设计 | ... | ... |
-
Please register or login to post a comment