后台完成修复,初始化项目
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.org.flashsalesystem;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FlashSaleSystemApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FlashSaleSystemApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
295
src/main/java/com/org/flashsalesystem/config/RedissonConfig.java
Normal file
295
src/main/java/com/org/flashsalesystem/config/RedissonConfig.java
Normal file
@@ -0,0 +1,295 @@
|
||||
package com.org.flashsalesystem.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.codec.JsonJacksonCodec;
|
||||
import org.redisson.config.ClusterServersConfig;
|
||||
import org.redisson.config.Config;
|
||||
import org.redisson.config.SentinelServersConfig;
|
||||
import org.redisson.config.SingleServerConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
|
||||
/**
|
||||
* Redisson配置类
|
||||
* 配置Redisson客户端和相关组件
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class RedissonConfig {
|
||||
|
||||
@Value("${spring.redis.cluster.nodes:}")
|
||||
private String[] clusterNodes;
|
||||
|
||||
@Value("${spring.redis.sentinel.nodes:}")
|
||||
private String[] sentinelNodes;
|
||||
|
||||
@Value("${spring.redis.sentinel.master:}")
|
||||
private String sentinelMaster;
|
||||
|
||||
@Value("${spring.redis.host:localhost}")
|
||||
private String host;
|
||||
|
||||
@Value("${spring.redis.port:6379}")
|
||||
private int port;
|
||||
|
||||
@Value("${spring.redis.password:}")
|
||||
private String password;
|
||||
|
||||
@Value("${spring.redis.database:0}")
|
||||
private int database;
|
||||
|
||||
@Value("${spring.redis.timeout:2000}")
|
||||
private int timeout;
|
||||
|
||||
/**
|
||||
* 配置Jackson ObjectMapper
|
||||
*/
|
||||
private ObjectMapper createObjectMapper() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
// 注册JSR310模块以支持Java 8时间类型
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redisson客户端配置
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public RedissonClient redissonClient() {
|
||||
log.info("初始化Redisson客户端...");
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
// 配置JSON编解码器
|
||||
JsonJacksonCodec codec = new JsonJacksonCodec(createObjectMapper());
|
||||
config.setCodec(codec);
|
||||
|
||||
// 根据配置选择连接模式
|
||||
if (clusterNodes != null && clusterNodes.length > 0) {
|
||||
// 集群模式
|
||||
log.info("配置Redis集群模式");
|
||||
ClusterServersConfig clusterConfig = config.useClusterServers();
|
||||
|
||||
for (String node : clusterNodes) {
|
||||
clusterConfig.addNodeAddress("redis://" + node);
|
||||
log.info("添加集群节点: {}", node);
|
||||
}
|
||||
|
||||
if (password != null && !password.isEmpty()) {
|
||||
clusterConfig.setPassword(password);
|
||||
}
|
||||
|
||||
clusterConfig
|
||||
.setConnectTimeout(timeout)
|
||||
.setTimeout(timeout)
|
||||
.setRetryAttempts(3)
|
||||
.setRetryInterval(1500)
|
||||
.setMasterConnectionMinimumIdleSize(10)
|
||||
.setMasterConnectionPoolSize(64)
|
||||
.setSlaveConnectionMinimumIdleSize(10)
|
||||
.setSlaveConnectionPoolSize(64);
|
||||
|
||||
} else if (sentinelNodes != null && sentinelNodes.length > 0) {
|
||||
// 哨兵模式
|
||||
log.info("配置Redis哨兵模式");
|
||||
SentinelServersConfig sentinelConfig = config.useSentinelServers();
|
||||
|
||||
sentinelConfig.setMasterName(sentinelMaster);
|
||||
for (String node : sentinelNodes) {
|
||||
sentinelConfig.addSentinelAddress("redis://" + node);
|
||||
log.info("添加哨兵节点: {}", node);
|
||||
}
|
||||
|
||||
if (password != null && !password.isEmpty()) {
|
||||
sentinelConfig.setPassword(password);
|
||||
}
|
||||
|
||||
sentinelConfig
|
||||
.setDatabase(database)
|
||||
.setConnectTimeout(timeout)
|
||||
.setTimeout(timeout)
|
||||
.setRetryAttempts(3)
|
||||
.setRetryInterval(1500)
|
||||
.setMasterConnectionMinimumIdleSize(10)
|
||||
.setMasterConnectionPoolSize(64)
|
||||
.setSlaveConnectionMinimumIdleSize(10)
|
||||
.setSlaveConnectionPoolSize(64);
|
||||
|
||||
} else {
|
||||
// 单机模式
|
||||
log.info("配置Redis单机模式: {}:{}", host, port);
|
||||
SingleServerConfig singleConfig = config.useSingleServer();
|
||||
|
||||
singleConfig.setAddress("redis://" + host + ":" + port);
|
||||
|
||||
if (password != null && !password.isEmpty()) {
|
||||
singleConfig.setPassword(password);
|
||||
}
|
||||
|
||||
singleConfig
|
||||
.setDatabase(database)
|
||||
.setConnectTimeout(timeout)
|
||||
.setTimeout(timeout)
|
||||
.setRetryAttempts(3)
|
||||
.setRetryInterval(1500)
|
||||
.setConnectionMinimumIdleSize(10)
|
||||
.setConnectionPoolSize(64);
|
||||
}
|
||||
|
||||
RedissonClient redissonClient = Redisson.create(config);
|
||||
log.info("Redisson客户端初始化完成");
|
||||
|
||||
return redissonClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* RedisTemplate配置(保持兼容性)
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
log.info("配置RedisTemplate");
|
||||
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// JSON序列化配置
|
||||
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(
|
||||
Object.class);
|
||||
jackson2JsonRedisSerializer.setObjectMapper(createObjectMapper());
|
||||
|
||||
// String序列化配置
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// 设置序列化器
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
template.setValueSerializer(jackson2JsonRedisSerializer);
|
||||
template.setHashValueSerializer(jackson2JsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
|
||||
log.info("RedisTemplate配置完成");
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义StringRedisTemplate配置(用于字符串操作)
|
||||
*/
|
||||
@Bean("customStringRedisTemplate")
|
||||
public RedisTemplate<String, String> customStringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
log.info("配置自定义StringRedisTemplate");
|
||||
|
||||
RedisTemplate<String, String> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// String序列化配置
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
|
||||
// 设置所有序列化器为String
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
template.setValueSerializer(stringRedisSerializer);
|
||||
template.setHashValueSerializer(stringRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
|
||||
log.info("自定义StringRedisTemplate配置完成");
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis消息监听容器
|
||||
*/
|
||||
@Bean
|
||||
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
|
||||
log.info("配置Redis消息监听容器");
|
||||
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> flashSaleScript() {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/flashsale.lua")));
|
||||
script.setResultType(Long.class);
|
||||
log.info("加载秒杀Lua脚本");
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分布式锁Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<String> lockScript() {
|
||||
DefaultRedisScript<String> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/distributed_lock.lua")));
|
||||
script.setResultType(String.class);
|
||||
log.info("加载分布式锁Lua脚本");
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> unlockScript() {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/unlock.lua")));
|
||||
script.setResultType(Long.class);
|
||||
log.info("加载释放锁Lua脚本");
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> rateLimitScript() {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/rate_limit.lua")));
|
||||
script.setResultType(Long.class);
|
||||
log.info("加载限流Lua脚本");
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车操作Lua脚本
|
||||
*/
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> cartOperationScript() {
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/cart_operation.lua")));
|
||||
script.setResultType(Long.class);
|
||||
log.info("加载购物车操作Lua脚本");
|
||||
return script;
|
||||
}
|
||||
}
|
||||
113
src/main/java/com/org/flashsalesystem/config/SwaggerConfig.java
Normal file
113
src/main/java/com/org/flashsalesystem/config/SwaggerConfig.java
Normal file
@@ -0,0 +1,113 @@
|
||||
package com.org.flashsalesystem.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import org.springdoc.core.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger配置类
|
||||
* 配置Knife4j接口文档
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
/**
|
||||
* 创建OpenAPI基本信息
|
||||
*/
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("秒杀系统API文档")
|
||||
.description("FlashSaleSystem - 高并发秒杀系统接口文档")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("开发团队")
|
||||
.email("dev@flashsale.com")
|
||||
.url("https://github.com/flashsale"))
|
||||
.license(new License()
|
||||
.name("MIT License")
|
||||
.url("https://opensource.org/licenses/MIT")));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户管理API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi userApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("用户管理")
|
||||
.pathsToMatch("/api/user/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品管理API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi productApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("商品管理")
|
||||
.pathsToMatch("/api/product/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀管理API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi flashSaleApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("秒杀管理")
|
||||
.pathsToMatch("/api/flashsale/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单管理API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi orderApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("订单管理")
|
||||
.pathsToMatch("/api/order/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车管理API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi cartApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("购物车管理")
|
||||
.pathsToMatch("/api/cart/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统管理API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi systemApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("系统管理")
|
||||
.pathsToMatch("/test/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整API分组
|
||||
*/
|
||||
@Bean
|
||||
public GroupedOpenApi allApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("完整接口")
|
||||
.pathsToMatch("/**")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/org/flashsalesystem/config/WebConfig.java
Normal file
68
src/main/java/com/org/flashsalesystem/config/WebConfig.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.org.flashsalesystem.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
||||
import org.springframework.web.servlet.view.JstlView;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
* 配置JSP视图解析器和静态资源
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* JSP视图解析器
|
||||
*/
|
||||
@Bean
|
||||
public ViewResolver viewResolver() {
|
||||
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
|
||||
resolver.setPrefix("/WEB-INF/views/");
|
||||
resolver.setSuffix(".jsp");
|
||||
resolver.setViewClass(JstlView.class);
|
||||
resolver.setOrder(1);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态资源处理
|
||||
*/
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 确保不覆盖默认的静态资源处理
|
||||
registry.addResourceHandler("/static/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
|
||||
registry.addResourceHandler("/css/**")
|
||||
.addResourceLocations("classpath:/static/css/");
|
||||
|
||||
registry.addResourceHandler("/js/**")
|
||||
.addResourceLocations("classpath:/static/js/");
|
||||
|
||||
registry.addResourceHandler("/images/**")
|
||||
.addResourceLocations("classpath:/static/images/");
|
||||
|
||||
registry.addResourceHandler("/webjars/**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||
|
||||
// 添加Knife4j相关的静态资源映射
|
||||
registry.addResourceHandler("/doc.html**")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
|
||||
registry.addResourceHandler("/favicon.ico")
|
||||
.addResourceLocations("classpath:/META-INF/resources/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认视图控制器
|
||||
*/
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/").setViewName("index");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.service.AdminService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理后台API控制器
|
||||
*/
|
||||
@Tag(name = "管理后台API", description = "管理后台相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
@Slf4j
|
||||
public class AdminController {
|
||||
|
||||
@Autowired
|
||||
private AdminService adminService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
@Operation(summary = "获取仪表盘统计数据")
|
||||
@GetMapping("/dashboard/stats")
|
||||
public ResponseEntity<Map<String, Object>> getDashboardStats() {
|
||||
try {
|
||||
Map<String, Object> stats = adminService.getDashboardStats();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取统计数据成功");
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取仪表盘统计数据失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取统计数据失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
*/
|
||||
@Operation(summary = "获取用户统计数据")
|
||||
@GetMapping("/users/stats")
|
||||
public ResponseEntity<Map<String, Object>> getUserStats() {
|
||||
try {
|
||||
Map<String, Object> stats = adminService.getUserStats();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取用户统计数据成功");
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户统计数据失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取用户统计数据失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计数据
|
||||
*/
|
||||
@Operation(summary = "获取订单统计数据")
|
||||
@GetMapping("/orders/stats")
|
||||
public ResponseEntity<Map<String, Object>> getOrderStats() {
|
||||
try {
|
||||
Map<String, Object> stats = adminService.getOrderStats();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取订单统计数据成功");
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单统计数据失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取订单统计数据失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品统计数据
|
||||
*/
|
||||
@Operation(summary = "获取商品统计数据")
|
||||
@GetMapping("/products/stats")
|
||||
public ResponseEntity<Map<String, Object>> getProductStats() {
|
||||
try {
|
||||
Map<String, Object> stats = adminService.getProductStats();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取商品统计数据成功");
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品统计数据失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取商品统计数据失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀统计数据
|
||||
*/
|
||||
@Operation(summary = "获取秒杀统计数据")
|
||||
@GetMapping("/flashsales/stats")
|
||||
public ResponseEntity<Map<String, Object>> getFlashSaleStats() {
|
||||
try {
|
||||
Map<String, Object> stats = adminService.getFlashSaleStats();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取秒杀统计数据成功");
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀统计数据失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取秒杀统计数据失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近订单列表
|
||||
*/
|
||||
@Operation(summary = "获取最近订单列表")
|
||||
@GetMapping("/orders/recent")
|
||||
public ResponseEntity<Map<String, Object>> getRecentOrders(@RequestParam(defaultValue = "10") int limit) {
|
||||
try {
|
||||
Object orders = adminService.getRecentOrders(limit);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取最近订单成功");
|
||||
response.put("data", orders);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近订单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取最近订单失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品列表
|
||||
*/
|
||||
@Operation(summary = "获取热门商品列表")
|
||||
@GetMapping("/products/hot")
|
||||
public ResponseEntity<Map<String, Object>> getHotProducts(@RequestParam(defaultValue = "5") int limit) {
|
||||
try {
|
||||
Object products = adminService.getHotProducts(limit);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取热门商品成功");
|
||||
response.put("data", products);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取热门商品失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
@Operation(summary = "获取用户列表")
|
||||
@GetMapping("/users")
|
||||
public ResponseEntity<Map<String, Object>> getUsers(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
try {
|
||||
Object users = adminService.getUsers(page, size, keyword, status);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取用户列表成功");
|
||||
response.put("data", users);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取用户列表失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
@Operation(summary = "获取订单列表")
|
||||
@GetMapping("/orders")
|
||||
public ResponseEntity<Map<String, Object>> getOrders(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status) {
|
||||
try {
|
||||
Object orders = adminService.getOrders(page, size, keyword, status);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取订单列表成功");
|
||||
response.put("data", orders);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取订单列表失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
*/
|
||||
@Operation(summary = "获取商品列表")
|
||||
@GetMapping("/products")
|
||||
public ResponseEntity<Map<String, Object>> getProducts(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
try {
|
||||
Object products = adminService.getProducts(page, size, keyword, status);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取商品列表成功");
|
||||
response.put("data", products);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取商品列表失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统状态
|
||||
*/
|
||||
@Operation(summary = "获取系统状态")
|
||||
@GetMapping("/monitor/system")
|
||||
public ResponseEntity<Map<String, Object>> getSystemStatus() {
|
||||
try {
|
||||
Object systemStatus = adminService.getSystemStatus();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取系统状态成功");
|
||||
response.put("data", systemStatus);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取系统状态失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取系统状态失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis状态
|
||||
*/
|
||||
@Operation(summary = "获取Redis状态")
|
||||
@GetMapping("/monitor/redis")
|
||||
public ResponseEntity<Map<String, Object>> getRedisStatus() {
|
||||
try {
|
||||
Object redisStatus = adminService.getRedisStatus();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取Redis状态成功");
|
||||
response.put("data", redisStatus);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取Redis状态失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取Redis状态失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个商品详情
|
||||
*/
|
||||
@Operation(summary = "获取商品详情")
|
||||
@GetMapping("/products/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getProduct(@PathVariable Long id) {
|
||||
try {
|
||||
Object product = adminService.getProduct(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取商品详情成功");
|
||||
response.put("data", product);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品详情失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "获取商品详情失败");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品
|
||||
*/
|
||||
@Operation(summary = "更新商品")
|
||||
@PutMapping("/products/{id}")
|
||||
public ResponseEntity<Map<String, Object>> updateProduct(@PathVariable Long id,
|
||||
@RequestBody Map<String, Object> productData) {
|
||||
try {
|
||||
adminService.updateProduct(id, productData);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品更新成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "更新商品失败: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
@Operation(summary = "删除商品")
|
||||
@DeleteMapping("/products/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteProduct(@PathVariable Long id) {
|
||||
try {
|
||||
adminService.deleteProduct(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品删除成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("删除商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "删除商品失败: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加商品
|
||||
*/
|
||||
@Operation(summary = "添加商品")
|
||||
@PostMapping("/products")
|
||||
public ResponseEntity<Map<String, Object>> addProduct(@RequestBody Map<String, Object> productData) {
|
||||
try {
|
||||
Object product = adminService.addProduct(productData);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品添加成功");
|
||||
response.put("data", product);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("添加商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "添加商品失败: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.CartDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.CartService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 购物车控制器
|
||||
* 处理购物车相关的HTTP请求
|
||||
*/
|
||||
@Tag(name = "购物车管理", description = "购物车商品添加、删除、清空等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/cart")
|
||||
@Slf4j
|
||||
public class CartController {
|
||||
|
||||
@Autowired
|
||||
private CartService cartService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 添加商品到购物车
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public ResponseEntity<Map<String, Object>> addToCart(@Validated @RequestBody CartDTO.AddItemDTO addItemDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
CartDTO cart = cartService.addToCart(userId, addItemDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品添加到购物车成功");
|
||||
response.put("data", cart);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("添加商品到购物车失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新购物车商品数量
|
||||
*/
|
||||
@PutMapping("/update-quantity")
|
||||
public ResponseEntity<Map<String, Object>> updateQuantity(@Validated @RequestBody CartDTO.UpdateQuantityDTO updateDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
CartDTO cart = cartService.updateQuantity(userId, updateDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "购物车商品数量更新成功");
|
||||
response.put("data", cart);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新购物车商品数量失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从购物车移除商品
|
||||
*/
|
||||
@DeleteMapping("/remove")
|
||||
public ResponseEntity<Map<String, Object>> removeFromCart(@Validated @RequestBody CartDTO.RemoveItemDTO removeDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
CartDTO cart = cartService.removeFromCart(userId, removeDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品从购物车移除成功");
|
||||
response.put("data", cart);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("从购物车移除商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作购物车
|
||||
*/
|
||||
@PostMapping("/batch-operation")
|
||||
public ResponseEntity<Map<String, Object>> batchOperation(@Validated @RequestBody CartDTO.BatchOperationDTO batchDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
CartDTO cart;
|
||||
String message;
|
||||
|
||||
if ("remove".equals(batchDTO.getOperation())) {
|
||||
cart = cartService.batchRemove(userId, batchDTO.getProductIds());
|
||||
message = "批量移除商品成功";
|
||||
} else if ("clear".equals(batchDTO.getOperation())) {
|
||||
cartService.clearCart(userId);
|
||||
cart = cartService.getCart(userId);
|
||||
message = "购物车清空成功";
|
||||
} else {
|
||||
throw new RuntimeException("不支持的操作类型");
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", message);
|
||||
response.put("data", cart);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("批量操作购物车失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
@DeleteMapping("/clear")
|
||||
public ResponseEntity<Map<String, Object>> clearCart(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
cartService.clearCart(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "购物车清空成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("清空购物车失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车信息
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getCart(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
CartDTO cart = cartService.getCart(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", cart);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取购物车信息失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车商品数量
|
||||
*/
|
||||
@GetMapping("/count")
|
||||
public ResponseEntity<Map<String, Object>> getCartItemCount(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
int count = cartService.getCartItemCount(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("count", count);
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取购物车商品数量失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购物车库存状态
|
||||
*/
|
||||
@GetMapping("/check-stock")
|
||||
public ResponseEntity<Map<String, Object>> checkCartStock(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
Map<String, Object> stockCheck = cartService.checkCartStock(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stockCheck);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("检查购物车库存状态失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新购物车过期时间
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<Map<String, Object>> refreshCart(HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
cartService.refreshCartExpireTime(userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "购物车过期时间刷新成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("刷新购物车过期时间失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建未授权响应
|
||||
*/
|
||||
private ResponseEntity<Map<String, Object>> createUnauthorizedResponse() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.FlashSaleDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.FlashSaleService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 秒杀控制器
|
||||
* 处理秒杀相关的HTTP请求
|
||||
*/
|
||||
@Tag(name = "秒杀管理", description = "秒杀活动创建、参与、状态管理等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/flashsale")
|
||||
@Slf4j
|
||||
public class FlashSaleController {
|
||||
|
||||
@Autowired
|
||||
private FlashSaleService flashSaleService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 创建秒杀活动
|
||||
*/
|
||||
@Operation(summary = "创建秒杀活动", description = "创建新的秒杀活动")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "秒杀活动创建成功"),
|
||||
@ApiResponse(responseCode = "400", description = "创建失败,参数验证错误")
|
||||
})
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<Map<String, Object>> createFlashSale(@Validated @RequestBody FlashSaleDTO.CreateDTO createDTO) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.createFlashSale(createDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动创建成功");
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("创建秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 参与秒杀
|
||||
*/
|
||||
@PostMapping("/participate")
|
||||
public ResponseEntity<Map<String, Object>> participateFlashSale(@Validated @RequestBody FlashSaleDTO.ParticipateDTO participateDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
FlashSaleDTO.ResultDTO result = flashSaleService.participateFlashSale(userId, participateDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", result.getSuccess());
|
||||
response.put("message", result.getMessage());
|
||||
response.put("data", result);
|
||||
|
||||
if (result.getSuccess()) {
|
||||
return ResponseEntity.ok(response);
|
||||
} else {
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("参与秒杀失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "系统异常,请稍后重试");
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getFlashSaleList(@RequestBody FlashSaleDTO.QueryDTO queryDTO) {
|
||||
try {
|
||||
Map<String, Object> result = flashSaleService.getFlashSaleList(queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀活动列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在进行的秒杀活动
|
||||
*/
|
||||
@GetMapping("/active")
|
||||
public ResponseEntity<Map<String, Object>> getActiveFlashSales() {
|
||||
try {
|
||||
List<FlashSaleDTO> activeFlashSales = flashSaleService.getActiveFlashSales();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", activeFlashSales);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取正在进行的秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动详情
|
||||
*/
|
||||
@Operation(summary = "获取秒杀活动详情", description = "根据ID获取秒杀活动的详细信息")
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.getFlashSaleDTOById(id);
|
||||
|
||||
if (flashSale == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "秒杀活动不存在");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀活动详情失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动
|
||||
*/
|
||||
@Operation(summary = "更新秒杀活动", description = "更新秒杀活动信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "秒杀活动更新成功"),
|
||||
@ApiResponse(responseCode = "400", description = "更新失败,参数验证错误"),
|
||||
@ApiResponse(responseCode = "404", description = "秒杀活动不存在")
|
||||
})
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> updateFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id,
|
||||
@Validated @RequestBody FlashSaleDTO.UpdateDTO updateDTO) {
|
||||
try {
|
||||
FlashSaleDTO flashSale = flashSaleService.updateFlashSale(id, updateDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动更新成功");
|
||||
response.put("data", flashSale);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除秒杀活动
|
||||
*/
|
||||
@Operation(summary = "删除秒杀活动", description = "删除指定的秒杀活动")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "秒杀活动删除成功"),
|
||||
@ApiResponse(responseCode = "400", description = "删除失败"),
|
||||
@ApiResponse(responseCode = "404", description = "秒杀活动不存在")
|
||||
})
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteFlashSale(@Parameter(description = "秒杀活动ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
boolean success = flashSaleService.deleteFlashSale(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", success);
|
||||
response.put("message", success ? "秒杀活动删除成功" : "秒杀活动删除失败");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("删除秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动剩余库存
|
||||
*/
|
||||
@GetMapping("/{id}/stock")
|
||||
public ResponseEntity<Map<String, Object>> getFlashSaleStock(@PathVariable Long id) {
|
||||
try {
|
||||
Integer stock = flashSaleService.getFlashSaleStock(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("flashSaleId", id);
|
||||
data.put("remainingStock", stock);
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀活动库存失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热秒杀活动
|
||||
*/
|
||||
@PostMapping("/{id}/preload")
|
||||
public ResponseEntity<Map<String, Object>> preloadFlashSale(@PathVariable Long id) {
|
||||
try {
|
||||
flashSaleService.preloadFlashSale(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动预热成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("预热秒杀活动失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态(定时任务调用)
|
||||
*/
|
||||
@PostMapping("/update-status")
|
||||
public ResponseEntity<Map<String, Object>> updateFlashSaleStatus() {
|
||||
try {
|
||||
flashSaleService.updateFlashSaleStatus();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "秒杀活动状态更新成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新秒杀活动状态失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀压力测试接口
|
||||
*/
|
||||
@PostMapping("/stress-test")
|
||||
public ResponseEntity<Map<String, Object>> stressTest(@RequestParam Long flashSaleId,
|
||||
@RequestParam(defaultValue = "100") int concurrency,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
// 这里可以实现压力测试逻辑
|
||||
// 模拟多个用户同时参与秒杀
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "压力测试启动成功");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("flashSaleId", flashSaleId);
|
||||
data.put("concurrency", concurrency);
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("秒杀压力测试失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建未授权响应
|
||||
*/
|
||||
private ResponseEntity<Map<String, Object>> createUnauthorizedResponse() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.OrderDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.OrderService;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 订单控制器
|
||||
* 处理订单相关的HTTP请求
|
||||
*/
|
||||
@Tag(name = "订单管理", description = "订单创建、查询、状态管理等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/order")
|
||||
@Slf4j
|
||||
public class OrderController {
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<Map<String, Object>> createOrder(@Validated @RequestBody OrderDTO.CreateDTO createDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderDTO order = orderService.createOrder(userId, createDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单创建成功");
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("创建订单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getOrder(@PathVariable Long id, HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderDTO order = orderService.getOrderById(id);
|
||||
|
||||
if (order == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "订单不存在");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// 验证用户权限(普通用户只能查看自己的订单)
|
||||
if (!order.getUserId().equals(userId)) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "无权限查看此订单");
|
||||
return ResponseEntity.status(403).body(response);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单详情失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
@PostMapping("/my-orders")
|
||||
public ResponseEntity<Map<String, Object>> getMyOrders(@RequestBody OrderDTO.QueryDTO queryDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
queryDTO.setUserId(userId); // 确保只查询当前用户的订单
|
||||
Map<String, Object> result = orderService.getUserOrders(userId, queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户订单列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有订单列表(管理员)
|
||||
*/
|
||||
@PostMapping("/all")
|
||||
public ResponseEntity<Map<String, Object>> getAllOrders(@RequestBody OrderDTO.QueryDTO queryDTO) {
|
||||
try {
|
||||
Map<String, Object> result = orderService.getAllOrders(queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取所有订单列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
@PutMapping("/status")
|
||||
public ResponseEntity<Map<String, Object>> updateOrderStatus(@Validated @RequestBody OrderDTO.StatusUpdateDTO statusUpdateDTO) {
|
||||
try {
|
||||
OrderDTO order = orderService.updateOrderStatus(
|
||||
statusUpdateDTO.getOrderId(),
|
||||
statusUpdateDTO.getStatus(),
|
||||
statusUpdateDTO.getRemark()
|
||||
);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单状态更新成功");
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新订单状态失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
@PostMapping("/{id}/cancel")
|
||||
public ResponseEntity<Map<String, Object>> cancelOrder(@PathVariable Long id, HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderDTO order = orderService.cancelOrder(id, userId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单取消成功");
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("取消订单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作订单
|
||||
*/
|
||||
@PostMapping("/batch-operation")
|
||||
public ResponseEntity<Map<String, Object>> batchOperateOrders(@Validated @RequestBody OrderDTO.BatchOperationDTO batchDTO) {
|
||||
try {
|
||||
List<OrderDTO> results = orderService.batchOperateOrders(batchDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "批量操作完成");
|
||||
response.put("data", results);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("批量操作订单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计信息
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getOrderStatistics() {
|
||||
try {
|
||||
OrderDTO.StatisticsDTO statistics = orderService.getOrderStatistics();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", statistics);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单统计信息失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付订单(模拟)
|
||||
*/
|
||||
@PostMapping("/{id}/pay")
|
||||
public ResponseEntity<Map<String, Object>> payOrder(@PathVariable Long id, HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
// 这里可以集成真实的支付接口
|
||||
// 目前只是简单地更新订单状态为已支付
|
||||
OrderDTO order = orderService.updateOrderStatus(id, 2, "用户支付");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付成功");
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("支付订单失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认收货
|
||||
*/
|
||||
@PostMapping("/{id}/confirm")
|
||||
public ResponseEntity<Map<String, Object>> confirmOrder(@PathVariable Long id, HttpServletRequest request) {
|
||||
try {
|
||||
Long userId = getCurrentUserId(request);
|
||||
if (userId == null) {
|
||||
return createUnauthorizedResponse();
|
||||
}
|
||||
|
||||
OrderDTO order = orderService.updateOrderStatus(id, 4, "用户确认收货");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "确认收货成功");
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("确认收货失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*/
|
||||
private Long getCurrentUserId(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建未授权响应
|
||||
*/
|
||||
private ResponseEntity<Map<String, Object>> createUnauthorizedResponse() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录或登录已过期");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
/**
|
||||
* 页面控制器
|
||||
* 处理JSP页面的路由和渲染
|
||||
*/
|
||||
@Controller
|
||||
@Slf4j
|
||||
public class PageController {
|
||||
|
||||
/**
|
||||
* 首页
|
||||
*/
|
||||
@GetMapping("/")
|
||||
public String index(Model model) {
|
||||
model.addAttribute("pageTitle", "首页");
|
||||
return "index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录页面
|
||||
*/
|
||||
@GetMapping("/login")
|
||||
public String login(Model model, HttpServletRequest request,
|
||||
@RequestParam(required = false) String returnUrl) {
|
||||
// 如果用户已登录,重定向到首页
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null && session.getAttribute("user") != null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "用户登录");
|
||||
if (returnUrl != null) {
|
||||
model.addAttribute("returnUrl", returnUrl);
|
||||
}
|
||||
return "login";
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册页面
|
||||
*/
|
||||
@GetMapping("/register")
|
||||
public String register(Model model, HttpServletRequest request) {
|
||||
// 如果用户已登录,重定向到首页
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null && session.getAttribute("user") != null) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "用户注册");
|
||||
return "register";
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品列表页面
|
||||
*/
|
||||
@GetMapping("/products")
|
||||
public String products(Model model) {
|
||||
model.addAttribute("pageTitle", "商品列表");
|
||||
return "products";
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品详情页面
|
||||
*/
|
||||
@GetMapping("/product/{id}")
|
||||
public String productDetail(@PathVariable Long id, Model model) {
|
||||
model.addAttribute("pageTitle", "商品详情");
|
||||
model.addAttribute("productId", id);
|
||||
return "product-detail";
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀活动列表页面
|
||||
*/
|
||||
@GetMapping("/flashsales")
|
||||
public String flashSales(Model model) {
|
||||
model.addAttribute("pageTitle", "秒杀活动");
|
||||
return "flashsales";
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀活动详情页面
|
||||
*/
|
||||
@GetMapping("/flashsale/{id}")
|
||||
public String flashSaleDetail(@PathVariable Long id, Model model) {
|
||||
model.addAttribute("pageTitle", "秒杀详情");
|
||||
model.addAttribute("flashSaleId", id);
|
||||
return "flashsale-detail";
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车页面
|
||||
*/
|
||||
@GetMapping("/cart")
|
||||
public String cart(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/cart";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "购物车");
|
||||
return "cart";
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单列表页面
|
||||
*/
|
||||
@GetMapping("/orders")
|
||||
public String orders(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/orders";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "我的订单");
|
||||
return "orders";
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单详情页面
|
||||
*/
|
||||
@GetMapping("/order/{id}")
|
||||
public String orderDetail(@PathVariable Long id, Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/order/" + id;
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "订单详情");
|
||||
model.addAttribute("orderId", id);
|
||||
return "order-detail";
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人设置页面
|
||||
*/
|
||||
@GetMapping("/profile")
|
||||
public String profile(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/profile";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "个人设置");
|
||||
return "profile";
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理后台首页
|
||||
*/
|
||||
@GetMapping("/admin")
|
||||
public String admin(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "管理后台");
|
||||
return "admin/index";
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品管理页面
|
||||
*/
|
||||
@GetMapping("/admin/products")
|
||||
public String adminProducts(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/products";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "商品管理");
|
||||
return "admin/products";
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀活动管理页面
|
||||
*/
|
||||
@GetMapping("/admin/flashsales")
|
||||
public String adminFlashSales(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/flashsales";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "秒杀管理");
|
||||
return "admin/flashsales";
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单管理页面
|
||||
*/
|
||||
@GetMapping("/admin/orders")
|
||||
public String adminOrders(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/orders";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "订单管理");
|
||||
return "admin/orders";
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户管理页面
|
||||
*/
|
||||
@GetMapping("/admin/users")
|
||||
public String adminUsers(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/users";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "用户管理");
|
||||
return "admin/users";
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统监控页面
|
||||
*/
|
||||
@GetMapping("/admin/monitor")
|
||||
public String adminMonitor(Model model, HttpServletRequest request) {
|
||||
if (!isUserLoggedIn(request)) {
|
||||
return "redirect:/login?returnUrl=/admin/monitor";
|
||||
}
|
||||
|
||||
model.addAttribute("pageTitle", "系统监控");
|
||||
return "admin/monitor";
|
||||
}
|
||||
|
||||
/**
|
||||
* 帮助页面
|
||||
*/
|
||||
@GetMapping("/help")
|
||||
public String help(Model model) {
|
||||
model.addAttribute("pageTitle", "帮助中心");
|
||||
return "help";
|
||||
}
|
||||
|
||||
/**
|
||||
* 关于页面
|
||||
*/
|
||||
@GetMapping("/about")
|
||||
public String about(Model model) {
|
||||
model.addAttribute("pageTitle", "关于我们");
|
||||
return "about";
|
||||
}
|
||||
|
||||
/**
|
||||
* 404错误页面
|
||||
*/
|
||||
@GetMapping("/404")
|
||||
public String notFound(Model model) {
|
||||
model.addAttribute("pageTitle", "页面未找到");
|
||||
return "error/404";
|
||||
}
|
||||
|
||||
/**
|
||||
* 500错误页面
|
||||
*/
|
||||
@GetMapping("/500")
|
||||
public String serverError(Model model) {
|
||||
model.addAttribute("pageTitle", "服务器错误");
|
||||
return "error/500";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录
|
||||
*/
|
||||
private boolean isUserLoggedIn(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
return session != null && session.getAttribute("user") != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.service.ProductService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商品控制器
|
||||
* 处理商品相关的HTTP请求
|
||||
*/
|
||||
@Tag(name = "商品管理", description = "商品CRUD、库存管理、销量统计等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/product")
|
||||
@Slf4j
|
||||
public class ProductController {
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
/**
|
||||
* 创建商品
|
||||
*/
|
||||
@Operation(summary = "创建商品", description = "新增商品信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "商品创建成功"),
|
||||
@ApiResponse(responseCode = "400", description = "创建失败,参数验证错误")
|
||||
})
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<Map<String, Object>> createProduct(@Validated @RequestBody ProductDTO.CreateDTO createDTO) {
|
||||
try {
|
||||
ProductDTO product = productService.createProduct(createDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品创建成功");
|
||||
response.put("data", product);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("创建商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品详情
|
||||
*/
|
||||
@Operation(summary = "获取商品详情", description = "根据商品ID获取商品详细信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "获取成功"),
|
||||
@ApiResponse(responseCode = "404", description = "商品不存在")
|
||||
})
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getProduct(@Parameter(description = "商品ID", required = true) @PathVariable Long id) {
|
||||
try {
|
||||
ProductDTO product = productService.getProductById(id);
|
||||
|
||||
if (product == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "商品不存在");
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", product);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品详情失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
public ResponseEntity<Map<String, Object>> getProductList(@RequestBody ProductDTO.QueryDTO queryDTO) {
|
||||
try {
|
||||
Map<String, Object> result = productService.getProductList(queryDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", result);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品列表失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品
|
||||
*/
|
||||
@GetMapping("/hot")
|
||||
public ResponseEntity<Map<String, Object>> getHotProducts(@RequestParam(defaultValue = "10") int limit) {
|
||||
try {
|
||||
List<ProductDTO> hotProducts = productService.getHotProducts(limit);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", hotProducts);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品信息
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> updateProduct(@PathVariable Long id,
|
||||
@Validated @RequestBody ProductDTO.UpdateDTO updateDTO) {
|
||||
try {
|
||||
ProductDTO product = productService.updateProduct(id, updateDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "商品更新成功");
|
||||
response.put("data", product);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteProduct(@PathVariable Long id) {
|
||||
try {
|
||||
boolean success = productService.deleteProduct(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", success);
|
||||
response.put("message", success ? "商品删除成功" : "商品删除失败");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("删除商品失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品库存
|
||||
*/
|
||||
@PostMapping("/stock/update")
|
||||
public ResponseEntity<Map<String, Object>> updateStock(@Validated @RequestBody ProductDTO.StockUpdateDTO stockUpdateDTO) {
|
||||
try {
|
||||
boolean success = productService.updateStock(
|
||||
stockUpdateDTO.getProductId(),
|
||||
stockUpdateDTO.getQuantity(),
|
||||
stockUpdateDTO.getOperation()
|
||||
);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", success);
|
||||
response.put("message", success ? "库存更新成功" : "库存更新失败");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品库存失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品库存
|
||||
*/
|
||||
@GetMapping("/{id}/stock")
|
||||
public ResponseEntity<Map<String, Object>> getProductStock(@PathVariable Long id) {
|
||||
try {
|
||||
Integer stock = productService.getProductStock(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("productId", id);
|
||||
data.put("stock", stock);
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品库存失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热商品库存
|
||||
*/
|
||||
@PostMapping("/{id}/stock/preload")
|
||||
public ResponseEntity<Map<String, Object>> preloadStock(@PathVariable Long id) {
|
||||
try {
|
||||
productService.preloadProductStock(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "库存预热成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("预热商品库存失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量预热商品库存
|
||||
*/
|
||||
@PostMapping("/stock/batch-preload")
|
||||
public ResponseEntity<Map<String, Object>> batchPreloadStock(@RequestBody List<Long> productIds) {
|
||||
try {
|
||||
productService.batchPreloadProductStock(productIds);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "批量库存预热成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("批量预热商品库存失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品销量排行榜
|
||||
*/
|
||||
@GetMapping("/sales-rank")
|
||||
public ResponseEntity<Map<String, Object>> getSalesRank(@RequestParam(defaultValue = "10") int limit) {
|
||||
try {
|
||||
List<Map<String, Object>> salesRank = productService.getSalesRank(limit);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", salesRank);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品销量排行榜失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 测试控制器
|
||||
* 用于验证应用程序是否正常启动
|
||||
*/
|
||||
@Tag(name = "系统测试", description = "系统健康检查和测试接口")
|
||||
@RestController
|
||||
@RequestMapping("/test")
|
||||
public class TestController {
|
||||
|
||||
@Operation(summary = "系统问候", description = "测试系统是否正常运行")
|
||||
@GetMapping("/hello")
|
||||
public Map<String, Object> hello() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("message", "Hello! 秒杀系统运行正常");
|
||||
response.put("status", "success");
|
||||
response.put("timestamp", System.currentTimeMillis());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Operation(summary = "健康检查", description = "检查系统健康状态")
|
||||
@GetMapping("/health")
|
||||
public Map<String, Object> health() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("status", "UP");
|
||||
response.put("application", "FlashSaleSystem");
|
||||
response.put("version", "1.0.0");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package com.org.flashsalesystem.controller;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
* 处理用户相关的HTTP请求
|
||||
*/
|
||||
@Tag(name = "用户管理", description = "用户注册、登录、个人信息管理等接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@Slf4j
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@Operation(summary = "用户注册", description = "新用户注册接口")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "注册成功"),
|
||||
@ApiResponse(responseCode = "400", description = "注册失败,参数验证错误或用户名已存在")
|
||||
})
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<Map<String, Object>> register(@Validated @RequestBody UserDTO.RegisterDTO registerDTO) {
|
||||
try {
|
||||
UserDTO user = userService.register(registerDTO);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "注册成功");
|
||||
response.put("data", user);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("用户注册失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@Operation(summary = "用户登录", description = "用户登录验证接口,成功后返回token并设置session")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "登录成功"),
|
||||
@ApiResponse(responseCode = "400", description = "登录失败,用户名或密码错误")
|
||||
})
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<Map<String, Object>> login(@Validated @RequestBody UserDTO.LoginDTO loginDTO,
|
||||
@Parameter(hidden = true) HttpServletRequest request) {
|
||||
try {
|
||||
Map<String, Object> loginResult = userService.login(loginDTO);
|
||||
|
||||
// 将token存储到session中
|
||||
HttpSession session = request.getSession();
|
||||
session.setAttribute("token", loginResult.get("token"));
|
||||
session.setAttribute("user", loginResult.get("user"));
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "登录成功");
|
||||
response.put("data", loginResult);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("用户登录失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@Operation(summary = "用户登出", description = "用户登出接口,清除session信息")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "登出成功")
|
||||
})
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<Map<String, Object>> logout(@Parameter(hidden = true) HttpServletRequest request) {
|
||||
try {
|
||||
HttpSession session = request.getSession(false);
|
||||
String token = null;
|
||||
|
||||
if (session != null) {
|
||||
token = (String) session.getAttribute("token");
|
||||
session.invalidate();
|
||||
}
|
||||
|
||||
userService.logout(token);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "登出成功");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("用户登出失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/current")
|
||||
public ResponseEntity<Map<String, Object>> getCurrentUser(HttpServletRequest request) {
|
||||
try {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||
}
|
||||
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO user = userService.getUserByToken(token);
|
||||
|
||||
if (user == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户信息已过期,请重新登录");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", user);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取当前用户信息失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
public ResponseEntity<Map<String, Object>> updateUser(@Validated @RequestBody UserDTO.UpdateDTO updateDTO,
|
||||
HttpServletRequest request) {
|
||||
try {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未登录");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||
}
|
||||
|
||||
String token = (String) session.getAttribute("token");
|
||||
UserDTO currentUser = userService.getUserByToken(token);
|
||||
|
||||
if (currentUser == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户信息已过期,请重新登录");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||
}
|
||||
|
||||
UserDTO updatedUser = userService.updateUser(currentUser.getId(), updateDTO);
|
||||
|
||||
// 更新session中的用户信息
|
||||
session.setAttribute("user", updatedUser);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "用户信息更新成功");
|
||||
response.put("data", updatedUser);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("更新用户信息失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户统计
|
||||
*/
|
||||
@GetMapping("/online-stats")
|
||||
public ResponseEntity<Map<String, Object>> getOnlineStats() {
|
||||
try {
|
||||
long onlineUserCount = userService.getOnlineUserCount();
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("onlineUserCount", onlineUserCount);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("获取在线用户统计失败", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", e.getMessage());
|
||||
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/main/java/com/org/flashsalesystem/dto/CartDTO.java
Normal file
95
src/main/java/com/org/flashsalesystem/dto/CartDTO.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 购物车数据传输对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CartDTO {
|
||||
|
||||
private Long userId;
|
||||
private List<CartItemDTO> items;
|
||||
private BigDecimal totalPrice;
|
||||
private Integer totalQuantity;
|
||||
|
||||
/**
|
||||
* 购物车商品项DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CartItemDTO {
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private BigDecimal productPrice;
|
||||
private String productImageUrl;
|
||||
private Integer quantity;
|
||||
private BigDecimal subtotal;
|
||||
private Integer stock; // 商品库存
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加商品到购物车DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AddItemDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Min(value = 1, message = "商品数量必须大于0")
|
||||
private Integer quantity = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新购物车商品数量DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateQuantityDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Min(value = 1, message = "商品数量必须大于0")
|
||||
private Integer quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除购物车商品DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RemoveItemDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class BatchOperationDTO {
|
||||
@NotNull(message = "商品ID列表不能为空")
|
||||
private List<Long> productIds;
|
||||
|
||||
/**
|
||||
* 操作类型:remove-移除,clear-清空
|
||||
*/
|
||||
private String operation;
|
||||
}
|
||||
}
|
||||
164
src/main/java/com/org/flashsalesystem/dto/FlashSaleDTO.java
Normal file
164
src/main/java/com/org/flashsalesystem/dto/FlashSaleDTO.java
Normal file
@@ -0,0 +1,164 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 秒杀活动数据传输对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FlashSaleDTO {
|
||||
|
||||
private Long id;
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private BigDecimal originalPrice;
|
||||
private BigDecimal flashPrice;
|
||||
private Integer flashStock;
|
||||
private Integer remainingStock;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// 活动状态描述
|
||||
private String statusDescription;
|
||||
// 是否可以参与秒杀
|
||||
private Boolean canParticipate;
|
||||
// 距离开始时间(毫秒)
|
||||
private Long timeToStart;
|
||||
// 距离结束时间(毫秒)
|
||||
private Long timeToEnd;
|
||||
|
||||
/**
|
||||
* 创建秒杀活动DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CreateDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "秒杀价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "秒杀价格必须大于0")
|
||||
private BigDecimal flashPrice;
|
||||
|
||||
@Min(value = 1, message = "秒杀库存必须大于0")
|
||||
private Integer flashStock;
|
||||
|
||||
@NotNull(message = "开始时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@NotNull(message = "结束时间不能为空")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateDTO {
|
||||
@DecimalMin(value = "0.01", message = "秒杀价格必须大于0")
|
||||
private BigDecimal flashPrice;
|
||||
|
||||
@Min(value = 1, message = "秒杀库存必须大于0")
|
||||
private Integer flashStock;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Min(value = 1, message = "状态值无效")
|
||||
@Max(value = 3, message = "状态值无效")
|
||||
private Integer status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀参与DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ParticipateDTO {
|
||||
@NotNull(message = "秒杀活动ID不能为空")
|
||||
private Long flashSaleId;
|
||||
|
||||
@Min(value = 1, message = "购买数量必须大于0")
|
||||
private Integer quantity = 1;
|
||||
|
||||
// 用户地址信息(可选)
|
||||
private String address;
|
||||
private String phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀查询DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class QueryDTO {
|
||||
private Integer status; // 活动状态
|
||||
private Long productId; // 商品ID
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
private Integer page = 0;
|
||||
private Integer size = 10;
|
||||
private String sortBy = "startTime";
|
||||
private String sortDirection = "asc";
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀结果DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ResultDTO {
|
||||
private Boolean success;
|
||||
private String message;
|
||||
private Long orderId;
|
||||
private Long flashSaleId;
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private Integer quantity;
|
||||
private BigDecimal totalPrice;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime orderTime;
|
||||
|
||||
// 排队信息(如果需要排队)
|
||||
private Integer queuePosition;
|
||||
private Long estimatedWaitTime;
|
||||
}
|
||||
}
|
||||
137
src/main/java/com/org/flashsalesystem/dto/OrderDTO.java
Normal file
137
src/main/java/com/org/flashsalesystem/dto/OrderDTO.java
Normal file
@@ -0,0 +1,137 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 订单数据传输对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OrderDTO {
|
||||
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private Long productId;
|
||||
private String productName;
|
||||
private String productImageUrl;
|
||||
private Integer quantity;
|
||||
private BigDecimal totalPrice;
|
||||
private Integer status;
|
||||
private String statusDescription;
|
||||
private Integer orderType;
|
||||
private String orderTypeDescription;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 创建订单DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CreateDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@Min(value = 1, message = "商品数量必须大于0")
|
||||
private Integer quantity;
|
||||
|
||||
// 收货地址信息
|
||||
private String receiverName;
|
||||
private String receiverPhone;
|
||||
private String receiverAddress;
|
||||
|
||||
// 备注
|
||||
private String remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单查询DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class QueryDTO {
|
||||
private Long userId;
|
||||
private Integer status;
|
||||
private Integer orderType;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
private Integer page = 0;
|
||||
private Integer size = 10;
|
||||
private String sortBy = "createdAt";
|
||||
private String sortDirection = "desc";
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态更新DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class StatusUpdateDTO {
|
||||
@NotNull(message = "订单ID不能为空")
|
||||
private Long orderId;
|
||||
|
||||
@NotNull(message = "订单状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量订单操作DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class BatchOperationDTO {
|
||||
@NotNull(message = "订单ID列表不能为空")
|
||||
private java.util.List<Long> orderIds;
|
||||
|
||||
/**
|
||||
* 操作类型:cancel-取消,pay-支付,ship-发货,complete-完成
|
||||
*/
|
||||
@NotNull(message = "操作类型不能为空")
|
||||
private String operation;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单统计DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class StatisticsDTO {
|
||||
private Long totalOrders;
|
||||
private Long pendingPaymentOrders;
|
||||
private Long paidOrders;
|
||||
private Long shippedOrders;
|
||||
private Long completedOrders;
|
||||
private Long cancelledOrders;
|
||||
private BigDecimal totalAmount;
|
||||
private BigDecimal todayAmount;
|
||||
private Long flashSaleOrders;
|
||||
private Long normalOrders;
|
||||
}
|
||||
}
|
||||
127
src/main/java/com/org/flashsalesystem/dto/ProductDTO.java
Normal file
127
src/main/java/com/org/flashsalesystem/dto/ProductDTO.java
Normal file
@@ -0,0 +1,127 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 商品数据传输对象
|
||||
*/
|
||||
@Schema(description = "商品信息")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProductDTO {
|
||||
|
||||
@Schema(description = "商品ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商品名称", example = "iPhone 15 Pro", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "商品名称不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "商品描述", example = "最新款iPhone,搭载A17 Pro芯片")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "商品价格", example = "8999.00", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "商品价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "商品价格必须大于0")
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "库存数量", example = "100")
|
||||
@Min(value = 0, message = "库存不能为负数")
|
||||
private Integer stock;
|
||||
|
||||
private String imageUrl;
|
||||
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 商品创建DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CreateDTO {
|
||||
@NotBlank(message = "商品名称不能为空")
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "商品价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "商品价格必须大于0")
|
||||
private BigDecimal price;
|
||||
|
||||
@Min(value = 0, message = "库存不能为负数")
|
||||
private Integer stock = 0;
|
||||
|
||||
private String imageUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品更新DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateDTO {
|
||||
private String name;
|
||||
private String description;
|
||||
private BigDecimal price;
|
||||
private Integer stock;
|
||||
private String imageUrl;
|
||||
private Integer status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品查询DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class QueryDTO {
|
||||
private String name;
|
||||
private BigDecimal minPrice;
|
||||
private BigDecimal maxPrice;
|
||||
private Integer status;
|
||||
private Integer page = 0;
|
||||
private Integer size = 10;
|
||||
private String sortBy = "id";
|
||||
private String sortDirection = "desc";
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品库存更新DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class StockUpdateDTO {
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "库存变化量不能为空")
|
||||
private Integer quantity;
|
||||
|
||||
/**
|
||||
* 操作类型:increase-增加,decrease-减少
|
||||
*/
|
||||
@NotBlank(message = "操作类型不能为空")
|
||||
private String operation;
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/org/flashsalesystem/dto/UserDTO.java
Normal file
117
src/main/java/com/org/flashsalesystem/dto/UserDTO.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.org.flashsalesystem.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户数据传输对象
|
||||
*/
|
||||
@Schema(description = "用户信息")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDTO {
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户名", example = "admin", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "密码", example = "123456", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "邮箱地址", example = "admin@example.com")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号码", example = "13888888888")
|
||||
@Size(max = 20, message = "手机号长度不能超过20个字符")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "用户状态", example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否在线", example = "true")
|
||||
private Boolean isOnline;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "最后登录时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 用户登录DTO
|
||||
*/
|
||||
@Schema(description = "用户登录信息")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class LoginDTO {
|
||||
@Schema(description = "用户名", example = "admin", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "密码", example = "123456", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RegisterDTO {
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@Size(max = 20, message = "手机号长度不能超过20个字符")
|
||||
private String phone;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息更新DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UpdateDTO {
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@Size(max = 20, message = "手机号长度不能超过20个字符")
|
||||
private String phone;
|
||||
}
|
||||
}
|
||||
89
src/main/java/com/org/flashsalesystem/entity/FlashSale.java
Normal file
89
src/main/java/com/org/flashsalesystem/entity/FlashSale.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 秒杀活动实体类
|
||||
* 对应数据库flash_sales表
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "flash_sales")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FlashSale {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@NotNull(message = "秒杀价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "秒杀价格必须大于0")
|
||||
@Column(name = "flash_price", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal flashPrice;
|
||||
|
||||
@Min(value = 1, message = "秒杀库存必须大于0")
|
||||
@Column(name = "flash_stock", nullable = false)
|
||||
private Integer flashStock;
|
||||
|
||||
@NotNull(message = "开始时间不能为空")
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@NotNull(message = "结束时间不能为空")
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 活动状态:1-未开始,2-进行中,3-已结束
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "product_id", insertable = false, updatable = false)
|
||||
private Product product;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查活动是否已开始
|
||||
*/
|
||||
public boolean isStarted() {
|
||||
return LocalDateTime.now().isAfter(startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查活动是否已结束
|
||||
*/
|
||||
public boolean isEnded() {
|
||||
return LocalDateTime.now().isAfter(endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查活动是否正在进行中
|
||||
*/
|
||||
public boolean isActive() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return now.isAfter(startTime) && now.isBefore(endTime) && status == 2;
|
||||
}
|
||||
}
|
||||
133
src/main/java/com/org/flashsalesystem/entity/Order.java
Normal file
133
src/main/java/com/org/flashsalesystem/entity/Order.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 订单实体类
|
||||
* 对应数据库orders表
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Order {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@NotNull(message = "商品ID不能为空")
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Min(value = 1, message = "商品数量必须大于0")
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
|
||||
@NotNull(message = "订单总价不能为空")
|
||||
@DecimalMin(value = "0.01", message = "订单总价必须大于0")
|
||||
@Column(name = "total_price", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal totalPrice;
|
||||
|
||||
/**
|
||||
* 订单状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 订单类型:1-普通订单,2-秒杀订单
|
||||
*/
|
||||
@Column(name = "order_type", nullable = false)
|
||||
private Integer orderType = 1;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", insertable = false, updatable = false)
|
||||
private User user;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "product_id", insertable = false, updatable = false)
|
||||
private Product product;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态枚举
|
||||
*/
|
||||
public enum OrderStatus {
|
||||
PENDING_PAYMENT(1, "待支付"),
|
||||
PAID(2, "已支付"),
|
||||
SHIPPED(3, "已发货"),
|
||||
COMPLETED(4, "已完成"),
|
||||
CANCELLED(5, "已取消");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
OrderStatus(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单类型枚举
|
||||
*/
|
||||
public enum OrderType {
|
||||
NORMAL(1, "普通订单"),
|
||||
FLASH_SALE(2, "秒杀订单");
|
||||
|
||||
private final int code;
|
||||
private final String description;
|
||||
|
||||
OrderType(int code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/org/flashsalesystem/entity/Product.java
Normal file
71
src/main/java/com/org/flashsalesystem/entity/Product.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 商品实体类
|
||||
* 对应数据库products表
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Product {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "商品名称不能为空")
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "商品价格不能为空")
|
||||
@DecimalMin(value = "0.01", message = "商品价格必须大于0")
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal price;
|
||||
|
||||
@Min(value = 0, message = "库存不能为负数")
|
||||
@Column(nullable = false)
|
||||
private Integer stock = 0;
|
||||
|
||||
@Column(name = "image_url", length = 500)
|
||||
private String imageUrl;
|
||||
|
||||
/**
|
||||
* 商品状态:1-上架,0-下架
|
||||
*/
|
||||
@Column(nullable = false)
|
||||
private Integer status = 1;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/org/flashsalesystem/entity/User.java
Normal file
68
src/main/java/com/org/flashsalesystem/entity/User.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.org.flashsalesystem.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.persistence.*;
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
* 对应数据库users表
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
|
||||
@Column(unique = true, nullable = false, length = 50)
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")
|
||||
@Column(nullable = false, length = 100)
|
||||
private String password;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Column(length = 100)
|
||||
private String email;
|
||||
|
||||
@Size(max = 20, message = "手机号长度不能超过20个字符")
|
||||
@Column(length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(name = "status", nullable = false)
|
||||
private Integer status = 1; // 1-正常, 0-禁用
|
||||
|
||||
@Column(name = "last_login")
|
||||
private LocalDateTime lastLogin;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.FlashSale;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 秒杀活动数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface FlashSaleRepository extends JpaRepository<FlashSale, Long> {
|
||||
|
||||
/**
|
||||
* 根据商品ID查找秒杀活动
|
||||
*/
|
||||
Optional<FlashSale> findByProductId(Long productId);
|
||||
|
||||
/**
|
||||
* 分页查找指定商品的秒杀活动
|
||||
*/
|
||||
Page<FlashSale> findByProductId(Long productId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据商品ID和状态查找秒杀活动
|
||||
*/
|
||||
Page<FlashSale> findByProductIdAndStatus(Long productId, Integer status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找正在进行的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.startTime <= :now AND f.endTime > :now AND f.status = 2")
|
||||
List<FlashSale> findActiveFlashSales(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 分页查找正在进行的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.startTime <= :now AND f.endTime > :now AND f.status = 2")
|
||||
Page<FlashSale> findActiveFlashSales(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找即将开始的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.startTime > :now AND f.status = 1")
|
||||
List<FlashSale> findUpcomingFlashSales(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 分页查找即将开始的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.startTime > :now AND f.status = 1")
|
||||
Page<FlashSale> findUpcomingFlashSales(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找已结束的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.endTime <= :now OR f.status = 3")
|
||||
List<FlashSale> findEndedFlashSales(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 分页查找已结束的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.endTime <= :now OR f.status = 3")
|
||||
Page<FlashSale> findEndedFlashSales(@Param("now") LocalDateTime now, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 更新秒杀库存
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE FlashSale f SET f.flashStock = f.flashStock - :quantity WHERE f.id = :flashSaleId AND f.flashStock" +
|
||||
" >= :quantity")
|
||||
int updateFlashStock(@Param("flashSaleId") Long flashSaleId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE FlashSale f SET f.status = :status WHERE f.id = :flashSaleId")
|
||||
int updateStatus(@Param("flashSaleId") Long flashSaleId, @Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 查找指定时间范围内的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.startTime >= :startTime AND f.endTime <= :endTime")
|
||||
List<FlashSale> findByTimeRange(@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 查找有库存的正在进行的秒杀活动
|
||||
*/
|
||||
@Query("SELECT f FROM FlashSale f WHERE f.startTime <= :now AND f.endTime > :now AND f.status = 2 AND f" +
|
||||
".flashStock > 0")
|
||||
List<FlashSale> findActiveFlashSalesWithStock(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内正在进行的秒杀活动数量
|
||||
*/
|
||||
long countByStartTimeLessThanEqualAndEndTimeGreaterThanEqual(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的秒杀活动数量
|
||||
*/
|
||||
long countByStartTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 统计已结束的秒杀活动数量
|
||||
*/
|
||||
long countByEndTimeLessThan(LocalDateTime endTime);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
|
||||
/**
|
||||
* 根据用户ID查找订单
|
||||
*/
|
||||
List<Order> findByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查找用户订单
|
||||
*/
|
||||
Page<Order> findByUserId(Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户ID和订单状态查找订单
|
||||
*/
|
||||
List<Order> findByUserIdAndStatus(Long userId, Integer status);
|
||||
|
||||
/**
|
||||
* 分页查找用户指定状态的订单
|
||||
*/
|
||||
Page<Order> findByUserIdAndStatus(Long userId, Integer status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据订单类型查找订单
|
||||
*/
|
||||
List<Order> findByOrderType(Integer orderType);
|
||||
|
||||
/**
|
||||
* 分页查找指定类型的订单
|
||||
*/
|
||||
Page<Order> findByOrderType(Integer orderType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找秒杀订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.orderType = 2")
|
||||
List<Order> findFlashSaleOrders();
|
||||
|
||||
/**
|
||||
* 查找指定时间范围内的订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.createdAt >= :startTime AND o.createdAt <= :endTime")
|
||||
List<Order> findByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 分页查找指定时间范围内的订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.createdAt >= :startTime AND o.createdAt <= :endTime")
|
||||
Page<Order> findByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计用户订单数量
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.userId = :userId")
|
||||
Long countByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 统计指定状态的订单数量
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.status = :status")
|
||||
Long countByStatus(@Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 根据订单状态查找订单
|
||||
*/
|
||||
List<Order> findByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 根据订单状态查找订单(分页)
|
||||
*/
|
||||
Page<Order> findByStatus(Integer status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计指定类型的订单数量
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.orderType = :orderType")
|
||||
Long countByOrderType(@Param("orderType") Integer orderType);
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Order o SET o.status = :status WHERE o.id = :orderId")
|
||||
int updateStatus(@Param("orderId") Long orderId, @Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 查找用户在指定商品上的订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.userId = :userId AND o.productId = :productId")
|
||||
List<Order> findByUserIdAndProductId(@Param("userId") Long userId, @Param("productId") Long productId);
|
||||
|
||||
/**
|
||||
* 查找用户的秒杀订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.userId = :userId AND o.orderType = 2")
|
||||
List<Order> findFlashSaleOrdersByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 检查用户是否已经购买过指定商品的秒杀
|
||||
*/
|
||||
@Query("SELECT COUNT(o) > 0 FROM Order o WHERE o.userId = :userId AND o.productId = :productId AND o.orderType = 2")
|
||||
boolean existsFlashSaleOrder(@Param("userId") Long userId, @Param("productId") Long productId);
|
||||
|
||||
/**
|
||||
* 根据创建时间范围统计订单数量
|
||||
*/
|
||||
long countByCreatedAtBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 根据订单状态计算总金额
|
||||
*/
|
||||
@Query("SELECT SUM(o.totalPrice) FROM Order o WHERE o.status = :status")
|
||||
BigDecimal sumTotalPriceByStatus(@Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 根据订单ID或用户名搜索订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o JOIN User u ON o.userId = u.id WHERE CAST(o.id AS string) LIKE %:keyword% OR u" +
|
||||
".username LIKE %:keyword%")
|
||||
Page<Order> findByIdContainingOrUserUsernameContaining(@Param("keyword") String keyword1,
|
||||
@Param("keyword") String keyword2, Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 商品数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
|
||||
/**
|
||||
* 查找所有上架的商品
|
||||
*/
|
||||
List<Product> findByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 分页查找上架的商品
|
||||
*/
|
||||
Page<Product> findByStatus(Integer status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据商品名称模糊查询
|
||||
*/
|
||||
@Query("SELECT p FROM Product p WHERE p.name LIKE %:name% AND p.status = 1")
|
||||
List<Product> findByNameContaining(@Param("name") String name);
|
||||
|
||||
/**
|
||||
* 根据商品名称模糊查询(分页)
|
||||
*/
|
||||
@Query("SELECT p FROM Product p WHERE p.name LIKE %:name%")
|
||||
Page<Product> findByNameContaining(@Param("name") String name, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据商品名称和状态查询(分页)
|
||||
*/
|
||||
@Query("SELECT p FROM Product p WHERE p.name LIKE %:name% AND p.status = :status")
|
||||
Page<Product> findByNameContainingAndStatus(@Param("name") String name, @Param("status") Integer status,
|
||||
Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找库存大于指定数量的商品
|
||||
*/
|
||||
@Query("SELECT p FROM Product p WHERE p.stock > :stock AND p.status = 1")
|
||||
List<Product> findByStockGreaterThan(@Param("stock") Integer stock);
|
||||
|
||||
/**
|
||||
* 更新商品库存
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :productId AND p.stock >= :quantity")
|
||||
int updateStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 增加商品库存
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Product p SET p.stock = p.stock + :quantity WHERE p.id = :productId")
|
||||
int increaseStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
|
||||
|
||||
/**
|
||||
* 查找热门商品(可以根据销量等指标排序)
|
||||
*/
|
||||
@Query("SELECT p FROM Product p WHERE p.status = 1 ORDER BY p.id DESC")
|
||||
List<Product> findHotProducts(Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计指定状态的商品数量
|
||||
*/
|
||||
long countByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 统计库存小于指定数量的商品数量
|
||||
*/
|
||||
long countByStockLessThan(Integer stock);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.org.flashsalesystem.repository;
|
||||
|
||||
import com.org.flashsalesystem.entity.User;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 用户数据访问层
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
/**
|
||||
* 根据用户名查找用户
|
||||
*/
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
*/
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
/**
|
||||
* 根据手机号查找用户
|
||||
*/
|
||||
Optional<User> findByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 检查用户名是否存在
|
||||
*/
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
*/
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
/**
|
||||
* 检查手机号是否存在
|
||||
*/
|
||||
boolean existsByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 根据用户名和密码查找用户(用于登录验证)
|
||||
*/
|
||||
@Query("SELECT u FROM User u WHERE u.username = :username AND u.password = :password")
|
||||
Optional<User> findByUsernameAndPassword(@Param("username") String username, @Param("password") String password);
|
||||
|
||||
/**
|
||||
* 统计最近登录的用户数量
|
||||
*/
|
||||
long countByLastLoginAfter(LocalDateTime lastLogin);
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内创建的用户数量
|
||||
*/
|
||||
long countByCreatedAtBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 根据用户名或邮箱搜索用户
|
||||
*/
|
||||
Page<User> findByUsernameContainingOrEmailContaining(String username, String email, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据状态查找用户
|
||||
*/
|
||||
Page<User> findByStatus(Integer status, Pageable pageable);
|
||||
}
|
||||
671
src/main/java/com/org/flashsalesystem/service/AdminService.java
Normal file
671
src/main/java/com/org/flashsalesystem/service/AdminService.java
Normal file
@@ -0,0 +1,671 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.entity.User;
|
||||
import com.org.flashsalesystem.repository.FlashSaleRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import com.org.flashsalesystem.repository.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 管理后台服务类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AdminService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private FlashSaleRepository flashSaleRepository;
|
||||
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
public Map<String, Object> getDashboardStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总用户数
|
||||
long totalUsers = userRepository.count();
|
||||
stats.put("totalUsers", totalUsers);
|
||||
|
||||
// 总商品数
|
||||
long totalProducts = productRepository.count();
|
||||
stats.put("totalProducts", totalProducts);
|
||||
|
||||
// 活跃秒杀数
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long activeFlashSales = flashSaleRepository.countByStartTimeLessThanEqualAndEndTimeGreaterThanEqual(now,
|
||||
now);
|
||||
stats.put("activeFlashSales", activeFlashSales);
|
||||
|
||||
// 今日订单数
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||
long todayOrders = orderRepository.countByCreatedAtBetween(startOfDay, endOfDay);
|
||||
stats.put("todayOrders", todayOrders);
|
||||
|
||||
// 总订单数
|
||||
long totalOrders = orderRepository.count();
|
||||
stats.put("totalOrders", totalOrders);
|
||||
|
||||
// 已支付订单数
|
||||
Long paidOrdersCount = orderRepository.countByStatus(2); // 2-已支付
|
||||
long paidOrders = paidOrdersCount != null ? paidOrdersCount : 0L;
|
||||
stats.put("paidOrders", paidOrders);
|
||||
|
||||
// 待处理订单数
|
||||
Long pendingOrdersCount = orderRepository.countByStatus(1); // 1-待支付
|
||||
long pendingOrders = pendingOrdersCount != null ? pendingOrdersCount : 0L;
|
||||
stats.put("pendingOrders", pendingOrders);
|
||||
|
||||
// 总交易额
|
||||
BigDecimal totalAmount = orderRepository.sumTotalPriceByStatus(2); // 2-已支付
|
||||
if (totalAmount == null) {
|
||||
totalAmount = BigDecimal.ZERO;
|
||||
}
|
||||
stats.put("totalAmount", totalAmount);
|
||||
|
||||
log.info("获取仪表盘统计数据成功: {}", stats);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取仪表盘统计数据失败", e);
|
||||
// 返回默认值
|
||||
stats.put("totalUsers", 0L);
|
||||
stats.put("totalProducts", 0L);
|
||||
stats.put("activeFlashSales", 0L);
|
||||
stats.put("todayOrders", 0L);
|
||||
stats.put("totalOrders", 0L);
|
||||
stats.put("paidOrders", 0L);
|
||||
stats.put("pendingOrders", 0L);
|
||||
stats.put("totalAmount", BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
*/
|
||||
public Map<String, Object> getUserStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总用户数
|
||||
long totalUsers = userRepository.count();
|
||||
stats.put("totalUsers", totalUsers);
|
||||
|
||||
// 活跃用户数(最近7天登录)
|
||||
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
|
||||
long activeUsers = userRepository.countByLastLoginAfter(sevenDaysAgo);
|
||||
stats.put("activeUsers", activeUsers);
|
||||
|
||||
// 新用户数(今天注册)
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||
long newUsers = userRepository.countByCreatedAtBetween(startOfDay, endOfDay);
|
||||
stats.put("newUsers", newUsers);
|
||||
|
||||
// 在线用户数(从Redis获取)
|
||||
String onlineUsersKey = "online_users";
|
||||
long onlineUsers = redisService.sCard(onlineUsersKey);
|
||||
stats.put("onlineUsers", onlineUsers);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户统计数据失败", e);
|
||||
stats.put("totalUsers", 0L);
|
||||
stats.put("activeUsers", 0L);
|
||||
stats.put("newUsers", 0L);
|
||||
stats.put("onlineUsers", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计数据
|
||||
*/
|
||||
public Map<String, Object> getOrderStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总订单数
|
||||
long totalOrders = orderRepository.count();
|
||||
stats.put("totalOrders", totalOrders);
|
||||
|
||||
// 已支付订单数
|
||||
Long paidOrdersCount = orderRepository.countByStatus(2); // 2-已支付
|
||||
long paidOrders = paidOrdersCount != null ? paidOrdersCount : 0L;
|
||||
stats.put("paidOrders", paidOrders);
|
||||
|
||||
// 待处理订单数
|
||||
Long pendingOrdersCount = orderRepository.countByStatus(1); // 1-待支付
|
||||
long pendingOrders = pendingOrdersCount != null ? pendingOrdersCount : 0L;
|
||||
stats.put("pendingOrders", pendingOrders);
|
||||
|
||||
// 已取消订单数
|
||||
Long cancelledOrdersCount = orderRepository.countByStatus(5); // 5-已取消
|
||||
long cancelledOrders = cancelledOrdersCount != null ? cancelledOrdersCount : 0L;
|
||||
stats.put("cancelledOrders", cancelledOrders);
|
||||
|
||||
// 总交易额
|
||||
BigDecimal totalAmount = orderRepository.sumTotalPriceByStatus(2); // 2-已支付
|
||||
if (totalAmount == null) {
|
||||
totalAmount = BigDecimal.ZERO;
|
||||
}
|
||||
stats.put("totalAmount", totalAmount);
|
||||
|
||||
// 今日订单数
|
||||
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
|
||||
long todayOrders = orderRepository.countByCreatedAtBetween(startOfDay, endOfDay);
|
||||
stats.put("todayOrders", todayOrders);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单统计数据失败", e);
|
||||
stats.put("totalOrders", 0L);
|
||||
stats.put("paidOrders", 0L);
|
||||
stats.put("pendingOrders", 0L);
|
||||
stats.put("cancelledOrders", 0L);
|
||||
stats.put("totalAmount", BigDecimal.ZERO);
|
||||
stats.put("todayOrders", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品统计数据
|
||||
*/
|
||||
public Map<String, Object> getProductStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 总商品数
|
||||
long totalProducts = productRepository.count();
|
||||
stats.put("totalProducts", totalProducts);
|
||||
|
||||
// 上架商品数
|
||||
long activeProducts = productRepository.countByStatus(1);
|
||||
stats.put("activeProducts", activeProducts);
|
||||
|
||||
// 下架商品数
|
||||
long inactiveProducts = productRepository.countByStatus(0);
|
||||
stats.put("inactiveProducts", inactiveProducts);
|
||||
|
||||
// 库存不足商品数(库存小于10)
|
||||
long lowStockProducts = productRepository.countByStockLessThan(10);
|
||||
stats.put("lowStockProducts", lowStockProducts);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品统计数据失败", e);
|
||||
stats.put("totalProducts", 0L);
|
||||
stats.put("activeProducts", 0L);
|
||||
stats.put("inactiveProducts", 0L);
|
||||
stats.put("lowStockProducts", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀统计数据
|
||||
*/
|
||||
public Map<String, Object> getFlashSaleStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 总秒杀活动数
|
||||
long totalFlashSales = flashSaleRepository.count();
|
||||
stats.put("totalFlashSales", totalFlashSales);
|
||||
|
||||
// 活跃秒杀数
|
||||
long activeFlashSales = flashSaleRepository.countByStartTimeLessThanEqualAndEndTimeGreaterThanEqual(now,
|
||||
now);
|
||||
stats.put("activeFlashSales", activeFlashSales);
|
||||
|
||||
// 即将开始的秒杀数
|
||||
LocalDateTime oneHourLater = now.plusHours(1);
|
||||
long upcomingFlashSales = flashSaleRepository.countByStartTimeBetween(now, oneHourLater);
|
||||
stats.put("upcomingFlashSales", upcomingFlashSales);
|
||||
|
||||
// 已结束的秒杀数
|
||||
long endedFlashSales = flashSaleRepository.countByEndTimeLessThan(now);
|
||||
stats.put("endedFlashSales", endedFlashSales);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取秒杀统计数据失败", e);
|
||||
stats.put("totalFlashSales", 0L);
|
||||
stats.put("activeFlashSales", 0L);
|
||||
stats.put("upcomingFlashSales", 0L);
|
||||
stats.put("endedFlashSales", 0L);
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近订单列表
|
||||
*/
|
||||
public List<Map<String, Object>> getRecentOrders(int limit) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Order> orders = orderRepository.findAll(pageable);
|
||||
|
||||
return orders.getContent().stream().map(order -> {
|
||||
Map<String, Object> orderMap = new HashMap<>();
|
||||
orderMap.put("id", order.getId());
|
||||
orderMap.put("username", order.getUser().getUsername());
|
||||
orderMap.put("productName", order.getProduct().getName());
|
||||
orderMap.put("quantity", order.getQuantity());
|
||||
orderMap.put("totalAmount", order.getTotalPrice());
|
||||
orderMap.put("status", order.getStatus());
|
||||
orderMap.put("createdAt", order.getCreatedAt());
|
||||
orderMap.put("isFlashSale", order.getOrderType() == 2); // 2表示秒杀订单
|
||||
return orderMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取最近订单失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品列表
|
||||
*/
|
||||
public List<Map<String, Object>> getHotProducts(int limit) {
|
||||
try {
|
||||
// 这里可以根据销量排序,暂时按创建时间排序
|
||||
Pageable pageable = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Product> products = productRepository.findAll(pageable);
|
||||
|
||||
return products.getContent().stream().map(product -> {
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("id", product.getId());
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("price", product.getPrice());
|
||||
productMap.put("stock", product.getStock());
|
||||
productMap.put("sales", 0); // 暂时设为0,后续可以添加销量统计
|
||||
return productMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取热门商品失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public Map<String, Object> getUsers(int page, int size, String keyword, Integer status) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<User> userPage;
|
||||
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
userPage = userRepository.findByUsernameContainingOrEmailContaining(keyword, keyword, pageable);
|
||||
} else if (status != null) {
|
||||
userPage = userRepository.findByStatus(status, pageable);
|
||||
} else {
|
||||
userPage = userRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
List<UserDTO> userDTOs = userPage.getContent().stream().map(user -> {
|
||||
UserDTO dto = new UserDTO();
|
||||
BeanUtils.copyProperties(user, dto);
|
||||
dto.setPassword(null); // 不返回密码
|
||||
dto.setIsOnline(redisService.sIsMember("online_users", user.getId().toString()));
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("users", userDTOs);
|
||||
result.put("total", userPage.getTotalElements());
|
||||
result.put("totalPages", userPage.getTotalPages());
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户列表失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("users", new ArrayList<>());
|
||||
result.put("total", 0L);
|
||||
result.put("totalPages", 0);
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
public Map<String, Object> getOrders(int page, int size, String keyword, String status) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Order> orderPage;
|
||||
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
orderPage = orderRepository.findByIdContainingOrUserUsernameContaining(keyword, keyword, pageable);
|
||||
} else if (status != null && !status.trim().isEmpty()) {
|
||||
Integer statusInt = Integer.parseInt(status);
|
||||
orderPage = orderRepository.findByStatus(statusInt, pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> orders = orderPage.getContent().stream().map(order -> {
|
||||
Map<String, Object> orderMap = new HashMap<>();
|
||||
orderMap.put("id", order.getId());
|
||||
orderMap.put("username", order.getUser().getUsername());
|
||||
orderMap.put("productName", order.getProduct().getName());
|
||||
orderMap.put("quantity", order.getQuantity());
|
||||
orderMap.put("totalAmount", order.getTotalPrice());
|
||||
orderMap.put("status", order.getStatus());
|
||||
orderMap.put("createdAt", order.getCreatedAt());
|
||||
orderMap.put("isFlashSale", order.getOrderType() == 2); // 2表示秒杀订单
|
||||
return orderMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orders", orders);
|
||||
result.put("total", orderPage.getTotalElements());
|
||||
result.put("totalPages", orderPage.getTotalPages());
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取订单列表失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("orders", new ArrayList<>());
|
||||
result.put("total", 0L);
|
||||
result.put("totalPages", 0);
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表
|
||||
*/
|
||||
public Object getProducts(int page, int size, String keyword, Integer status) {
|
||||
try {
|
||||
Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<Product> productPage;
|
||||
|
||||
if (keyword != null && !keyword.trim().isEmpty() && status != null) {
|
||||
// 同时按关键词和状态筛选
|
||||
productPage = productRepository.findByNameContainingAndStatus(keyword, status, pageable);
|
||||
} else if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
productPage = productRepository.findByNameContaining(keyword, pageable);
|
||||
} else if (status != null) {
|
||||
productPage = productRepository.findByStatus(status, pageable);
|
||||
} else {
|
||||
productPage = productRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<Map<String, Object>> productList = productPage.getContent().stream().map(product -> {
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("id", product.getId());
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("price", product.getPrice());
|
||||
productMap.put("stock", product.getStock());
|
||||
productMap.put("status", product.getStatus());
|
||||
productMap.put("description", product.getDescription());
|
||||
productMap.put("imageUrl", product.getImageUrl());
|
||||
productMap.put("createdAt", product.getCreatedAt());
|
||||
return productMap;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("products", productList);
|
||||
result.put("total", productPage.getTotalElements());
|
||||
result.put("totalPages", productPage.getTotalPages());
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品列表失败", e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("products", new ArrayList<>());
|
||||
result.put("total", 0L);
|
||||
result.put("totalPages", 0);
|
||||
result.put("currentPage", page);
|
||||
result.put("size", size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统状态
|
||||
*/
|
||||
public Object getSystemStatus() {
|
||||
try {
|
||||
Map<String, Object> systemStatus = new HashMap<>();
|
||||
|
||||
// 获取JVM内存使用情况
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long totalMemory = runtime.totalMemory();
|
||||
long freeMemory = runtime.freeMemory();
|
||||
long usedMemory = totalMemory - freeMemory;
|
||||
double memoryUsage = (double) usedMemory / totalMemory * 100;
|
||||
|
||||
// 获取可用处理器数量(模拟CPU使用率)
|
||||
int availableProcessors = runtime.availableProcessors();
|
||||
double cpuUsage = Math.random() * 30 + 20; // 模拟20-50%的CPU使用率
|
||||
|
||||
// 模拟磁盘使用率
|
||||
double diskUsage = Math.random() * 40 + 10; // 模拟10-50%的磁盘使用率
|
||||
|
||||
systemStatus.put("status", "正常");
|
||||
systemStatus.put("cpuUsage", Math.round(cpuUsage));
|
||||
systemStatus.put("memoryUsage", Math.round(memoryUsage));
|
||||
systemStatus.put("diskUsage", Math.round(diskUsage));
|
||||
systemStatus.put("availableProcessors", availableProcessors);
|
||||
systemStatus.put("totalMemory", totalMemory / 1024 / 1024 + "MB");
|
||||
systemStatus.put("usedMemory", usedMemory / 1024 / 1024 + "MB");
|
||||
|
||||
return systemStatus;
|
||||
} catch (Exception e) {
|
||||
log.error("获取系统状态失败", e);
|
||||
Map<String, Object> errorStatus = new HashMap<>();
|
||||
errorStatus.put("status", "异常");
|
||||
errorStatus.put("cpuUsage", 0);
|
||||
errorStatus.put("memoryUsage", 0);
|
||||
errorStatus.put("diskUsage", 0);
|
||||
return errorStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis状态
|
||||
*/
|
||||
public Object getRedisStatus() {
|
||||
try {
|
||||
List<Map<String, Object>> redisNodes = new ArrayList<>();
|
||||
|
||||
// 模拟Redis集群节点状态
|
||||
String[] nodes = {
|
||||
"42.192.62.91:7000", "42.192.62.91:7001", "42.192.62.91:7002",
|
||||
"42.192.62.91:7003", "42.192.62.91:7004", "42.192.62.91:7005"
|
||||
};
|
||||
|
||||
for (String node : nodes) {
|
||||
Map<String, Object> nodeStatus = new HashMap<>();
|
||||
nodeStatus.put("node", node);
|
||||
nodeStatus.put("status", "正常");
|
||||
nodeStatus.put("memory", (200 + (int) (Math.random() * 100)) + "MB");
|
||||
nodeStatus.put("connections", 30 + (int) (Math.random() * 30));
|
||||
redisNodes.add(nodeStatus);
|
||||
}
|
||||
|
||||
return redisNodes;
|
||||
} catch (Exception e) {
|
||||
log.error("获取Redis状态失败", e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个商品详情
|
||||
*/
|
||||
public Object getProduct(Long id) {
|
||||
try {
|
||||
Optional<Product> productOpt = productRepository.findById(id);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("id", product.getId());
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("price", product.getPrice());
|
||||
productMap.put("stock", product.getStock());
|
||||
productMap.put("status", product.getStatus());
|
||||
productMap.put("description", product.getDescription());
|
||||
productMap.put("imageUrl", product.getImageUrl());
|
||||
productMap.put("createdAt", product.getCreatedAt());
|
||||
productMap.put("updatedAt", product.getUpdatedAt());
|
||||
|
||||
// 添加统计信息(模拟数据,实际应该从统计表获取)
|
||||
productMap.put("totalSales", 0); // 总销量
|
||||
productMap.put("totalRevenue", 0.0); // 总收入
|
||||
productMap.put("viewCount", 0); // 浏览次数
|
||||
productMap.put("rating", 0.0); // 平均评分
|
||||
|
||||
return productMap;
|
||||
} else {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取商品详情失败", e);
|
||||
throw new RuntimeException("获取商品详情失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品
|
||||
*/
|
||||
public void updateProduct(Long id, Map<String, Object> productData) {
|
||||
try {
|
||||
Optional<Product> productOpt = productRepository.findById(id);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
|
||||
if (productData.containsKey("name")) {
|
||||
product.setName((String) productData.get("name"));
|
||||
}
|
||||
if (productData.containsKey("price")) {
|
||||
product.setPrice(new BigDecimal(productData.get("price").toString()));
|
||||
}
|
||||
if (productData.containsKey("stock")) {
|
||||
product.setStock(Integer.parseInt(productData.get("stock").toString()));
|
||||
}
|
||||
if (productData.containsKey("status")) {
|
||||
product.setStatus(Integer.parseInt(productData.get("status").toString()));
|
||||
}
|
||||
if (productData.containsKey("description")) {
|
||||
product.setDescription((String) productData.get("description"));
|
||||
}
|
||||
if (productData.containsKey("imageUrl")) {
|
||||
product.setImageUrl((String) productData.get("imageUrl"));
|
||||
}
|
||||
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
productRepository.save(product);
|
||||
} else {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品失败", e);
|
||||
throw new RuntimeException("更新商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
public void deleteProduct(Long id) {
|
||||
try {
|
||||
if (productRepository.existsById(id)) {
|
||||
productRepository.deleteById(id);
|
||||
} else {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("删除商品失败", e);
|
||||
throw new RuntimeException("删除商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加商品
|
||||
*/
|
||||
public Object addProduct(Map<String, Object> productData) {
|
||||
try {
|
||||
Product product = new Product();
|
||||
product.setName((String) productData.get("name"));
|
||||
product.setPrice(new BigDecimal(productData.get("price").toString()));
|
||||
product.setStock(Integer.parseInt(productData.get("stock").toString()));
|
||||
product.setStatus(Integer.parseInt(productData.get("status").toString()));
|
||||
product.setDescription((String) productData.get("description"));
|
||||
product.setImageUrl((String) productData.get("imageUrl"));
|
||||
product.setCreatedAt(LocalDateTime.now());
|
||||
product.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
Product savedProduct = productRepository.save(product);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", savedProduct.getId());
|
||||
result.put("name", savedProduct.getName());
|
||||
result.put("price", savedProduct.getPrice());
|
||||
result.put("stock", savedProduct.getStock());
|
||||
result.put("status", savedProduct.getStatus());
|
||||
result.put("description", savedProduct.getDescription());
|
||||
result.put("imageUrl", savedProduct.getImageUrl());
|
||||
result.put("createdAt", savedProduct.getCreatedAt());
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("添加商品失败", e);
|
||||
throw new RuntimeException("添加商品失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
314
src/main/java/com/org/flashsalesystem/service/CartService.java
Normal file
314
src/main/java/com/org/flashsalesystem/service/CartService.java
Normal file
@@ -0,0 +1,314 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.CartDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 购物车服务类
|
||||
* 使用Redis Hash结构存储购物车数据
|
||||
* 实现购物车的增删改查、持久化等功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class CartService {
|
||||
|
||||
private static final String CART_PREFIX = "user:";
|
||||
private static final String CART_SUFFIX = ":cart";
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Value("${flashsale.cart.expire-days:7}")
|
||||
private int cartExpireDays;
|
||||
@Value("${flashsale.cart.max-items:20}")
|
||||
private int maxCartItems;
|
||||
|
||||
/**
|
||||
* 添加商品到购物车
|
||||
*/
|
||||
public CartDTO addToCart(Long userId, CartDTO.AddItemDTO addItemDTO) {
|
||||
log.info("添加商品到购物车: 用户ID={}, 商品ID={}, 数量={}",
|
||||
userId, addItemDTO.getProductId(), addItemDTO.getQuantity());
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 检查商品是否存在
|
||||
ProductDTO product = productService.getProductById(addItemDTO.getProductId());
|
||||
if (product == null) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
// 检查商品是否上架
|
||||
if (product.getStatus() != 1) {
|
||||
throw new RuntimeException("商品已下架");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
Integer availableStock = productService.getProductStock(addItemDTO.getProductId());
|
||||
if (availableStock < addItemDTO.getQuantity()) {
|
||||
throw new RuntimeException("商品库存不足");
|
||||
}
|
||||
|
||||
// 检查购物车商品种类数量限制
|
||||
Long cartSize = redisService.hLen(cartKey);
|
||||
if (cartSize != null && cartSize >= maxCartItems &&
|
||||
!redisService.hExists(cartKey, addItemDTO.getProductId().toString())) {
|
||||
throw new RuntimeException("购物车商品种类已达上限");
|
||||
}
|
||||
|
||||
// 获取当前购物车中该商品的数量
|
||||
String currentQuantityStr = (String) redisService.hGet(cartKey, addItemDTO.getProductId().toString());
|
||||
int currentQuantity = currentQuantityStr != null ? Integer.parseInt(currentQuantityStr) : 0;
|
||||
|
||||
// 计算新的数量
|
||||
int newQuantity = currentQuantity + addItemDTO.getQuantity();
|
||||
|
||||
// 再次检查库存
|
||||
if (availableStock < newQuantity) {
|
||||
throw new RuntimeException("商品库存不足,当前库存:" + availableStock);
|
||||
}
|
||||
|
||||
// 更新购物车
|
||||
redisService.hSet(cartKey, addItemDTO.getProductId().toString(), String.valueOf(newQuantity));
|
||||
|
||||
// 设置过期时间
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
|
||||
log.info("商品添加到购物车成功: 用户ID={}, 商品ID={}, 新数量={}",
|
||||
userId, addItemDTO.getProductId(), newQuantity);
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新购物车商品数量
|
||||
*/
|
||||
public CartDTO updateQuantity(Long userId, CartDTO.UpdateQuantityDTO updateDTO) {
|
||||
log.info("更新购物车商品数量: 用户ID={}, 商品ID={}, 数量={}",
|
||||
userId, updateDTO.getProductId(), updateDTO.getQuantity());
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 检查商品是否在购物车中
|
||||
if (!redisService.hExists(cartKey, updateDTO.getProductId().toString())) {
|
||||
throw new RuntimeException("商品不在购物车中");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
Integer availableStock = productService.getProductStock(updateDTO.getProductId());
|
||||
if (availableStock < updateDTO.getQuantity()) {
|
||||
throw new RuntimeException("商品库存不足,当前库存:" + availableStock);
|
||||
}
|
||||
|
||||
// 更新数量
|
||||
redisService.hSet(cartKey, updateDTO.getProductId().toString(), String.valueOf(updateDTO.getQuantity()));
|
||||
|
||||
// 刷新过期时间
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
|
||||
log.info("购物车商品数量更新成功: 用户ID={}, 商品ID={}, 新数量={}",
|
||||
userId, updateDTO.getProductId(), updateDTO.getQuantity());
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从购物车移除商品
|
||||
*/
|
||||
public CartDTO removeFromCart(Long userId, CartDTO.RemoveItemDTO removeDTO) {
|
||||
log.info("从购物车移除商品: 用户ID={}, 商品ID={}", userId, removeDTO.getProductId());
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 移除商品
|
||||
redisService.hDel(cartKey, removeDTO.getProductId().toString());
|
||||
|
||||
log.info("商品从购物车移除成功: 用户ID={}, 商品ID={}", userId, removeDTO.getProductId());
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移除购物车商品
|
||||
*/
|
||||
public CartDTO batchRemove(Long userId, List<Long> productIds) {
|
||||
log.info("批量移除购物车商品: 用户ID={}, 商品IDs={}", userId, productIds);
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
|
||||
// 批量移除
|
||||
String[] fields = productIds.stream()
|
||||
.map(String::valueOf)
|
||||
.toArray(String[]::new);
|
||||
redisService.hDel(cartKey, (Object[]) fields);
|
||||
|
||||
log.info("批量移除购物车商品成功: 用户ID={}, 移除数量={}", userId, productIds.size());
|
||||
|
||||
return getCart(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
public void clearCart(Long userId) {
|
||||
log.info("清空购物车: 用户ID={}", userId);
|
||||
|
||||
String cartKey = buildCartKey(userId);
|
||||
redisService.delete(cartKey);
|
||||
|
||||
log.info("购物车清空成功: 用户ID={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车信息
|
||||
*/
|
||||
public CartDTO getCart(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
Map<Object, Object> cartData = redisService.hGetAll(cartKey);
|
||||
|
||||
if (cartData.isEmpty()) {
|
||||
return new CartDTO(userId, new ArrayList<>(), BigDecimal.ZERO, 0);
|
||||
}
|
||||
|
||||
List<CartDTO.CartItemDTO> items = new ArrayList<>();
|
||||
BigDecimal totalPrice = BigDecimal.ZERO;
|
||||
int totalQuantity = 0;
|
||||
|
||||
for (Map.Entry<Object, Object> entry : cartData.entrySet()) {
|
||||
Long productId = Long.valueOf(entry.getKey().toString());
|
||||
Integer quantity = Integer.valueOf(entry.getValue().toString());
|
||||
|
||||
// 获取商品信息
|
||||
ProductDTO product = productService.getProductById(productId);
|
||||
if (product == null || product.getStatus() != 1) {
|
||||
// 商品不存在或已下架,从购物车中移除
|
||||
redisService.hDel(cartKey, entry.getKey().toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取当前库存
|
||||
Integer currentStock = productService.getProductStock(productId);
|
||||
|
||||
// 如果购物车中的数量超过库存,调整为库存数量
|
||||
if (quantity > currentStock) {
|
||||
quantity = currentStock;
|
||||
if (quantity > 0) {
|
||||
redisService.hSet(cartKey, productId.toString(), String.valueOf(quantity));
|
||||
} else {
|
||||
redisService.hDel(cartKey, productId.toString());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算小计
|
||||
BigDecimal subtotal = product.getPrice().multiply(BigDecimal.valueOf(quantity));
|
||||
|
||||
CartDTO.CartItemDTO item = new CartDTO.CartItemDTO();
|
||||
item.setProductId(productId);
|
||||
item.setProductName(product.getName());
|
||||
item.setProductPrice(product.getPrice());
|
||||
item.setProductImageUrl(product.getImageUrl());
|
||||
item.setQuantity(quantity);
|
||||
item.setSubtotal(subtotal);
|
||||
item.setStock(currentStock);
|
||||
|
||||
items.add(item);
|
||||
totalPrice = totalPrice.add(subtotal);
|
||||
totalQuantity += quantity;
|
||||
}
|
||||
|
||||
return new CartDTO(userId, items, totalPrice, totalQuantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车商品数量
|
||||
*/
|
||||
public int getCartItemCount(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
Map<Object, Object> cartData = redisService.hGetAll(cartKey);
|
||||
|
||||
return cartData.values().stream()
|
||||
.mapToInt(value -> Integer.parseInt(value.toString()))
|
||||
.sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购物车中商品的库存状态
|
||||
*/
|
||||
public Map<String, Object> checkCartStock(Long userId) {
|
||||
CartDTO cart = getCart(userId);
|
||||
|
||||
List<Map<String, Object>> stockIssues = new ArrayList<>();
|
||||
boolean hasStockIssue = false;
|
||||
|
||||
for (CartDTO.CartItemDTO item : cart.getItems()) {
|
||||
if (item.getQuantity() > item.getStock()) {
|
||||
hasStockIssue = true;
|
||||
Map<String, Object> issue = new HashMap<>();
|
||||
issue.put("productId", item.getProductId());
|
||||
issue.put("productName", item.getProductName());
|
||||
issue.put("requestedQuantity", item.getQuantity());
|
||||
issue.put("availableStock", item.getStock());
|
||||
stockIssues.add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("hasStockIssue", hasStockIssue);
|
||||
result.put("stockIssues", stockIssues);
|
||||
result.put("cart", cart);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步购物车到数据库(可选功能)
|
||||
*/
|
||||
public void syncCartToDatabase(Long userId) {
|
||||
// 这里可以实现将购物车数据同步到数据库的逻辑
|
||||
// 用于数据持久化和恢复
|
||||
log.info("同步购物车到数据库: 用户ID={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库恢复购物车(可选功能)
|
||||
*/
|
||||
public void restoreCartFromDatabase(Long userId) {
|
||||
// 这里可以实现从数据库恢复购物车数据的逻辑
|
||||
log.info("从数据库恢复购物车: 用户ID={}", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建购物车Redis键
|
||||
*/
|
||||
private String buildCartKey(Long userId) {
|
||||
return CART_PREFIX + userId + CART_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车过期时间
|
||||
*/
|
||||
public Long getCartExpireTime(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
return redisService.getExpire(cartKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新购物车过期时间
|
||||
*/
|
||||
public void refreshCartExpireTime(Long userId) {
|
||||
String cartKey = buildCartKey(userId);
|
||||
redisService.expire(cartKey, cartExpireDays, TimeUnit.DAYS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 分布式锁服务
|
||||
* 基于Redis实现分布式锁,防止并发问题
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DistributedLockService {
|
||||
|
||||
private static final String LOCK_PREFIX = "lock:";
|
||||
private static final int DEFAULT_EXPIRE_SECONDS = 30;
|
||||
private static final int DEFAULT_RETRY_TIMES = 3;
|
||||
private static final long DEFAULT_RETRY_INTERVAL_MS = 100;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 锁的值(用于释放锁时验证)
|
||||
*/
|
||||
public String acquireLock(String lockKey) {
|
||||
return acquireLock(lockKey, DEFAULT_EXPIRE_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
*
|
||||
* @return 锁的值(用于释放锁时验证)
|
||||
*/
|
||||
public String acquireLock(String lockKey, int expireSeconds) {
|
||||
return acquireLock(lockKey, expireSeconds, DEFAULT_RETRY_TIMES, DEFAULT_RETRY_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分布式锁(带重试机制)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
* @param retryTimes 重试次数
|
||||
* @param retryIntervalMs 重试间隔(毫秒)
|
||||
*
|
||||
* @return 锁的值(用于释放锁时验证),获取失败返回null
|
||||
*/
|
||||
public String acquireLock(String lockKey, int expireSeconds, int retryTimes, long retryIntervalMs) {
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
String lockValue = UUID.randomUUID().toString();
|
||||
|
||||
for (int i = 0; i <= retryTimes; i++) {
|
||||
try {
|
||||
// 使用Lua脚本原子性地设置锁和过期时间
|
||||
String result = redisService.executeLockScript(fullLockKey, lockValue, expireSeconds);
|
||||
if ("OK".equals(result)) {
|
||||
log.debug("成功获取分布式锁: {}, 值: {}", fullLockKey, lockValue);
|
||||
return lockValue;
|
||||
}
|
||||
|
||||
if (i < retryTimes) {
|
||||
Thread.sleep(retryIntervalMs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取分布式锁异常: {}", fullLockKey, e);
|
||||
if (i < retryTimes) {
|
||||
try {
|
||||
Thread.sleep(retryIntervalMs);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("获取分布式锁失败: {}", fullLockKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param lockValue 锁的值
|
||||
*
|
||||
* @return 是否释放成功
|
||||
*/
|
||||
public boolean releaseLock(String lockKey, String lockValue) {
|
||||
if (lockValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
|
||||
try {
|
||||
// 使用Lua脚本原子性地检查锁的值并删除
|
||||
Long result = redisService.executeUnlockScript(fullLockKey, lockValue);
|
||||
boolean success = result != null && result > 0;
|
||||
|
||||
if (success) {
|
||||
log.debug("成功释放分布式锁: {}, 值: {}", fullLockKey, lockValue);
|
||||
} else {
|
||||
log.warn("释放分布式锁失败,锁可能已过期或被其他线程释放: {}, 值: {}", fullLockKey, lockValue);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (Exception e) {
|
||||
log.error("释放分布式锁异常: {}, 值: {}", fullLockKey, lockValue, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, Runnable task) {
|
||||
return executeWithLock(lockKey, DEFAULT_EXPIRE_SECONDS, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, int expireSeconds, Runnable task) {
|
||||
String lockValue = acquireLock(lockKey, expireSeconds);
|
||||
if (lockValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
task.run();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("执行锁定任务异常: {}", lockKey, e);
|
||||
throw e;
|
||||
} finally {
|
||||
releaseLock(lockKey, lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行有返回值的业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param supplier 要执行的任务
|
||||
* @param <T> 返回值类型
|
||||
*
|
||||
* @return 执行结果,获取锁失败返回null
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, java.util.function.Supplier<T> supplier) {
|
||||
return executeWithLock(lockKey, DEFAULT_EXPIRE_SECONDS, supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁并执行有返回值的业务逻辑
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param expireSeconds 锁的过期时间(秒)
|
||||
* @param supplier 要执行的任务
|
||||
* @param <T> 返回值类型
|
||||
*
|
||||
* @return 执行结果,获取锁失败返回null
|
||||
*/
|
||||
public <T> T executeWithLock(String lockKey, int expireSeconds, java.util.function.Supplier<T> supplier) {
|
||||
String lockValue = acquireLock(lockKey, expireSeconds);
|
||||
if (lockValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception e) {
|
||||
log.error("执行锁定任务异常: {}", lockKey, e);
|
||||
throw e;
|
||||
} finally {
|
||||
releaseLock(lockKey, lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否存在
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 锁是否存在
|
||||
*/
|
||||
public boolean isLocked(String lockKey) {
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
return redisService.exists(fullLockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放锁(谨慎使用)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否释放成功
|
||||
*/
|
||||
public boolean forceReleaseLock(String lockKey) {
|
||||
String fullLockKey = LOCK_PREFIX + lockKey;
|
||||
try {
|
||||
Boolean result = redisService.delete(fullLockKey);
|
||||
log.warn("强制释放分布式锁: {}, 结果: {}", fullLockKey, result);
|
||||
return result != null && result;
|
||||
} catch (Exception e) {
|
||||
log.error("强制释放分布式锁异常: {}", fullLockKey, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.FlashSaleDTO;
|
||||
import com.org.flashsalesystem.entity.FlashSale;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.repository.FlashSaleRepository;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 秒杀服务类
|
||||
* 实现秒杀活动管理、库存控制、分布式锁等核心功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class FlashSaleService {
|
||||
|
||||
private static final String FLASH_SALE_CACHE_PREFIX = "flashsale:";
|
||||
private static final String FLASH_SALE_STOCK_PREFIX = "flashsale_stock:";
|
||||
private static final String FLASH_SALE_LOCK_PREFIX = "flashsale_lock:";
|
||||
private static final String FLASH_SALE_SUCCESS_USERS_PREFIX = "flashsale_success:";
|
||||
private static final String ACTIVE_FLASH_SALES = "active_flashsales";
|
||||
@Autowired
|
||||
private FlashSaleRepository flashSaleRepository;
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private DistributedLockService lockService;
|
||||
@Autowired
|
||||
private RedissonLockService redissonLockService;
|
||||
@Autowired
|
||||
private RateLimitService rateLimitService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Value("${flashsale.cache.flashsale-expire-minutes:10}")
|
||||
private int flashSaleCacheExpireMinutes;
|
||||
@Value("${flashsale.seckill.max-quantity-per-user:1}")
|
||||
private int maxQuantityPerUser;
|
||||
|
||||
/**
|
||||
* 创建秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO createFlashSale(FlashSaleDTO.CreateDTO createDTO) {
|
||||
log.info("创建秒杀活动: 商品ID={}, 秒杀价格={}, 库存={}",
|
||||
createDTO.getProductId(), createDTO.getFlashPrice(), createDTO.getFlashStock());
|
||||
|
||||
// 验证商品是否存在
|
||||
Optional<Product> productOpt = productRepository.findById(createDTO.getProductId());
|
||||
if (!productOpt.isPresent()) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
Product product = productOpt.get();
|
||||
if (product.getStatus() != 1) {
|
||||
throw new RuntimeException("商品未上架");
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
if (createDTO.getStartTime().isAfter(createDTO.getEndTime())) {
|
||||
throw new RuntimeException("开始时间不能晚于结束时间");
|
||||
}
|
||||
|
||||
if (createDTO.getStartTime().isBefore(LocalDateTime.now())) {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
|
||||
// 检查是否已有该商品的秒杀活动
|
||||
Optional<FlashSale> existingFlashSale = flashSaleRepository.findByProductId(createDTO.getProductId());
|
||||
if (existingFlashSale.isPresent()) {
|
||||
throw new RuntimeException("该商品已有秒杀活动");
|
||||
}
|
||||
|
||||
// 创建秒杀活动
|
||||
FlashSale flashSale = new FlashSale();
|
||||
BeanUtils.copyProperties(createDTO, flashSale);
|
||||
flashSale.setStatus(1); // 未开始
|
||||
|
||||
flashSale = flashSaleRepository.save(flashSale);
|
||||
|
||||
// 缓存秒杀活动信息
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 预热库存到Redis
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("秒杀活动创建成功: ID={}", flashSale.getId());
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参与秒杀
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO.ResultDTO participateFlashSale(Long userId, FlashSaleDTO.ParticipateDTO participateDTO) {
|
||||
log.info("用户参与秒杀: 用户ID={}, 秒杀ID={}, 数量={}",
|
||||
userId, participateDTO.getFlashSaleId(), participateDTO.getQuantity());
|
||||
|
||||
// 限流检查
|
||||
if (!rateLimitService.checkFlashSaleRateLimit(userId)) {
|
||||
return createFailResult("请求过于频繁,请稍后再试");
|
||||
}
|
||||
|
||||
// 获取秒杀活动信息
|
||||
FlashSale flashSale = getFlashSaleById(participateDTO.getFlashSaleId());
|
||||
if (flashSale == null) {
|
||||
return createFailResult("秒杀活动不存在");
|
||||
}
|
||||
|
||||
// 检查活动状态
|
||||
if (!flashSale.isActive()) {
|
||||
return createFailResult("秒杀活动未开始或已结束");
|
||||
}
|
||||
|
||||
// 检查用户是否已经参与过
|
||||
String successUsersKey = FLASH_SALE_SUCCESS_USERS_PREFIX + flashSale.getId();
|
||||
if (redisService.sIsMember(successUsersKey, userId)) {
|
||||
return createFailResult("您已经参与过该秒杀活动");
|
||||
}
|
||||
|
||||
// 检查数据库中是否已有订单
|
||||
if (orderRepository.existsFlashSaleOrder(userId, flashSale.getProductId())) {
|
||||
return createFailResult("您已经购买过该商品");
|
||||
}
|
||||
|
||||
// 检查购买数量限制
|
||||
if (participateDTO.getQuantity() > maxQuantityPerUser) {
|
||||
return createFailResult("超过单用户最大购买数量限制");
|
||||
}
|
||||
|
||||
// 使用Redisson分布式锁防止超卖
|
||||
String lockKey = FLASH_SALE_LOCK_PREFIX + flashSale.getId();
|
||||
|
||||
if (!redissonLockService.tryLock(lockKey, 3, 10)) { // 等待3秒,持有10秒
|
||||
return createFailResult("系统繁忙,请稍后再试");
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用Lua脚本原子性扣减库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
Long remainingStock = redisService.executeFlashSaleScript(stockKey, participateDTO.getQuantity());
|
||||
|
||||
if (remainingStock < 0) {
|
||||
if (remainingStock == -1) {
|
||||
return createFailResult("秒杀活动库存信息异常");
|
||||
} else {
|
||||
return createFailResult("商品已售罄");
|
||||
}
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
Order order = createFlashSaleOrder(userId, flashSale, participateDTO);
|
||||
|
||||
// 添加到成功用户集合
|
||||
redisService.sAdd(successUsersKey, userId);
|
||||
redisService.expire(successUsersKey, 24, TimeUnit.HOURS);
|
||||
|
||||
// 更新数据库库存
|
||||
flashSaleRepository.updateFlashStock(flashSale.getId(), participateDTO.getQuantity());
|
||||
|
||||
// 发布秒杀成功消息
|
||||
publishFlashSaleResult(userId, flashSale, order, true);
|
||||
|
||||
// 更新商品销量排行榜
|
||||
productService.increaseSalesRank(flashSale.getProductId(), participateDTO.getQuantity());
|
||||
|
||||
log.info("秒杀成功: 用户ID={}, 订单ID={}, 剩余库存={}", userId, order.getId(), remainingStock);
|
||||
|
||||
return createSuccessResult(order, flashSale);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("秒杀处理异常: 用户ID={}, 秒杀ID={}", userId, participateDTO.getFlashSaleId(), e);
|
||||
return createFailResult("秒杀失败,请重试");
|
||||
} finally {
|
||||
redissonLockService.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动列表
|
||||
*/
|
||||
public Map<String, Object> getFlashSaleList(FlashSaleDTO.QueryDTO queryDTO) {
|
||||
// 验证排序字段
|
||||
String sortBy = validateSortField(queryDTO.getSortBy());
|
||||
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), sortBy);
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<FlashSale> flashSalePage;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 如果指定了商品ID,按商品ID查询
|
||||
if (queryDTO.getProductId() != null) {
|
||||
if (queryDTO.getStatus() != null) {
|
||||
switch (queryDTO.getStatus()) {
|
||||
case 1: // 未开始
|
||||
flashSalePage = flashSaleRepository.findByProductIdAndStatus(queryDTO.getProductId(), 1,
|
||||
pageable);
|
||||
break;
|
||||
case 2: // 进行中
|
||||
flashSalePage = flashSaleRepository.findByProductIdAndStatus(queryDTO.getProductId(), 2,
|
||||
pageable);
|
||||
break;
|
||||
case 3: // 已结束
|
||||
flashSalePage = flashSaleRepository.findByProductIdAndStatus(queryDTO.getProductId(), 3,
|
||||
pageable);
|
||||
break;
|
||||
default:
|
||||
flashSalePage = flashSaleRepository.findByProductId(queryDTO.getProductId(), pageable);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
flashSalePage = flashSaleRepository.findByProductId(queryDTO.getProductId(), pageable);
|
||||
}
|
||||
} else {
|
||||
// 根据状态查询
|
||||
if (queryDTO.getStatus() != null) {
|
||||
switch (queryDTO.getStatus()) {
|
||||
case 1: // 未开始
|
||||
flashSalePage = flashSaleRepository.findUpcomingFlashSales(now, pageable);
|
||||
break;
|
||||
case 2: // 进行中
|
||||
flashSalePage = flashSaleRepository.findActiveFlashSales(now, pageable);
|
||||
break;
|
||||
case 3: // 已结束
|
||||
flashSalePage = flashSaleRepository.findEndedFlashSales(now, pageable);
|
||||
break;
|
||||
default:
|
||||
flashSalePage = flashSaleRepository.findAll(pageable);
|
||||
}
|
||||
} else {
|
||||
flashSalePage = flashSaleRepository.findAll(pageable);
|
||||
}
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<FlashSaleDTO> flashSaleDTOs = flashSalePage.getContent().stream()
|
||||
.map(flashSale -> {
|
||||
Product product = productRepository.findById(
|
||||
flashSale.getProductId()).orElse(null);
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", flashSaleDTOs);
|
||||
result.put("totalElements", flashSalePage.getTotalElements());
|
||||
result.put("totalPages", flashSalePage.getTotalPages());
|
||||
result.put("currentPage", flashSalePage.getNumber());
|
||||
result.put("size", flashSalePage.getSize());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在进行的秒杀活动
|
||||
*/
|
||||
public List<FlashSaleDTO> getActiveFlashSales() {
|
||||
// 尝试从缓存获取
|
||||
List<Object> cachedFlashSales = redisService.lRange(ACTIVE_FLASH_SALES, 0, -1);
|
||||
if (!cachedFlashSales.isEmpty()) {
|
||||
return cachedFlashSales.stream()
|
||||
.map(obj -> (FlashSaleDTO) obj)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<FlashSale> activeFlashSales = flashSaleRepository.findActiveFlashSalesWithStock(now);
|
||||
|
||||
List<FlashSaleDTO> flashSaleDTOs = activeFlashSales.stream()
|
||||
.map(flashSale -> {
|
||||
Product product = productRepository.findById(
|
||||
flashSale.getProductId()).orElse(null);
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存结果
|
||||
if (!flashSaleDTOs.isEmpty()) {
|
||||
redisService.delete(ACTIVE_FLASH_SALES);
|
||||
redisService.rPush(ACTIVE_FLASH_SALES, flashSaleDTOs.toArray());
|
||||
redisService.expire(ACTIVE_FLASH_SALES, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
return flashSaleDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取秒杀活动
|
||||
*/
|
||||
public FlashSaleDTO getFlashSaleDTOById(Long flashSaleId) {
|
||||
FlashSale flashSale = getFlashSaleById(flashSaleId);
|
||||
if (flashSale == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热秒杀活动
|
||||
*/
|
||||
public void preloadFlashSale(Long flashSaleId) {
|
||||
log.info("预热秒杀活动: {}", flashSaleId);
|
||||
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
log.warn("秒杀活动不存在: {}", flashSaleId);
|
||||
return;
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
|
||||
// 缓存秒杀活动信息
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 预热库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("秒杀活动预热完成: {}", flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒杀活动剩余库存
|
||||
*/
|
||||
public Integer getFlashSaleStock(Long flashSaleId) {
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
Object stock = redisService.get(stockKey);
|
||||
return stock != null ? Integer.valueOf(stock.toString()) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public FlashSaleDTO updateFlashSale(Long flashSaleId, FlashSaleDTO.UpdateDTO updateDTO) {
|
||||
log.info("更新秒杀活动: ID={}", flashSaleId);
|
||||
|
||||
// 获取现有秒杀活动
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
throw new RuntimeException("秒杀活动不存在");
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
|
||||
// 检查活动状态,只有未开始的活动才能修改
|
||||
if (flashSale.getStatus() != 1) {
|
||||
throw new RuntimeException("只有未开始的秒杀活动才能修改");
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if (updateDTO.getFlashPrice() != null) {
|
||||
flashSale.setFlashPrice(updateDTO.getFlashPrice());
|
||||
}
|
||||
if (updateDTO.getFlashStock() != null) {
|
||||
flashSale.setFlashStock(updateDTO.getFlashStock());
|
||||
}
|
||||
if (updateDTO.getStartTime() != null) {
|
||||
if (updateDTO.getStartTime().isBefore(LocalDateTime.now())) {
|
||||
throw new RuntimeException("开始时间不能早于当前时间");
|
||||
}
|
||||
flashSale.setStartTime(updateDTO.getStartTime());
|
||||
}
|
||||
if (updateDTO.getEndTime() != null) {
|
||||
flashSale.setEndTime(updateDTO.getEndTime());
|
||||
}
|
||||
if (updateDTO.getStatus() != null) {
|
||||
flashSale.setStatus(updateDTO.getStatus());
|
||||
}
|
||||
|
||||
// 验证时间
|
||||
if (flashSale.getStartTime().isAfter(flashSale.getEndTime())) {
|
||||
throw new RuntimeException("开始时间不能晚于结束时间");
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
flashSale = flashSaleRepository.save(flashSale);
|
||||
|
||||
// 更新缓存
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
|
||||
// 更新Redis库存
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||
redisService.set(stockKey, flashSale.getFlashStock());
|
||||
|
||||
log.info("秒杀活动更新成功: ID={}", flashSale.getId());
|
||||
|
||||
return buildFlashSaleDTO(flashSale, product);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除秒杀活动
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteFlashSale(Long flashSaleId) {
|
||||
log.info("删除秒杀活动: ID={}", flashSaleId);
|
||||
|
||||
// 获取现有秒杀活动
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (!flashSaleOpt.isPresent()) {
|
||||
throw new RuntimeException("秒杀活动不存在");
|
||||
}
|
||||
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
|
||||
// 检查活动状态,只有未开始的活动才能删除
|
||||
if (flashSale.getStatus() != 1) {
|
||||
throw new RuntimeException("只有未开始的秒杀活动才能删除");
|
||||
}
|
||||
|
||||
// 检查是否有相关订单
|
||||
if (orderRepository.existsFlashSaleOrder(null, flashSale.getProductId())) {
|
||||
throw new RuntimeException("该秒杀活动已有订单,无法删除");
|
||||
}
|
||||
|
||||
// 删除秒杀活动
|
||||
flashSaleRepository.deleteById(flashSaleId);
|
||||
|
||||
// 清除相关缓存
|
||||
clearFlashSaleCache(flashSaleId);
|
||||
|
||||
log.info("秒杀活动删除成功: ID={}", flashSaleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新秒杀活动状态
|
||||
*/
|
||||
@Transactional
|
||||
public void updateFlashSaleStatus() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 更新未开始的活动为进行中
|
||||
List<FlashSale> upcomingFlashSales = flashSaleRepository.findUpcomingFlashSales(now);
|
||||
for (FlashSale flashSale : upcomingFlashSales) {
|
||||
if (flashSale.isStarted() && !flashSale.isEnded()) {
|
||||
flashSaleRepository.updateStatus(flashSale.getId(), 2);
|
||||
log.info("秒杀活动开始: {}", flashSale.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进行中的活动为已结束
|
||||
List<FlashSale> activeFlashSales = flashSaleRepository.findActiveFlashSales(now);
|
||||
for (FlashSale flashSale : activeFlashSales) {
|
||||
if (flashSale.isEnded()) {
|
||||
flashSaleRepository.updateStatus(flashSale.getId(), 3);
|
||||
log.info("秒杀活动结束: {}", flashSale.getId());
|
||||
|
||||
// 清除相关缓存
|
||||
clearFlashSaleCache(flashSale.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取秒杀活动实体
|
||||
*/
|
||||
private FlashSale getFlashSaleById(Long flashSaleId) {
|
||||
// 先从缓存获取
|
||||
String cacheKey = FLASH_SALE_CACHE_PREFIX + flashSaleId;
|
||||
Map<Object, Object> flashSaleMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!flashSaleMap.isEmpty()) {
|
||||
FlashSale flashSale = new FlashSale();
|
||||
flashSale.setId(flashSaleId);
|
||||
flashSale.setProductId(Long.valueOf((String) flashSaleMap.get("productId")));
|
||||
flashSale.setFlashPrice(new BigDecimal((String) flashSaleMap.get("flashPrice")));
|
||||
flashSale.setFlashStock(Integer.valueOf((String) flashSaleMap.get("flashStock")));
|
||||
flashSale.setStartTime(LocalDateTime.parse((String) flashSaleMap.get("startTime")));
|
||||
flashSale.setEndTime(LocalDateTime.parse((String) flashSaleMap.get("endTime")));
|
||||
flashSale.setStatus(Integer.valueOf((String) flashSaleMap.get("status")));
|
||||
return flashSale;
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<FlashSale> flashSaleOpt = flashSaleRepository.findById(flashSaleId);
|
||||
if (flashSaleOpt.isPresent()) {
|
||||
FlashSale flashSale = flashSaleOpt.get();
|
||||
Product product = productRepository.findById(flashSale.getProductId()).orElse(null);
|
||||
cacheFlashSaleInfo(flashSale, product);
|
||||
return flashSale;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存秒杀活动信息
|
||||
*/
|
||||
private void cacheFlashSaleInfo(FlashSale flashSale, Product product) {
|
||||
String cacheKey = FLASH_SALE_CACHE_PREFIX + flashSale.getId();
|
||||
Map<String, Object> flashSaleMap = new HashMap<>();
|
||||
flashSaleMap.put("productId", flashSale.getProductId().toString());
|
||||
flashSaleMap.put("flashPrice", flashSale.getFlashPrice().toString());
|
||||
flashSaleMap.put("flashStock", flashSale.getFlashStock().toString());
|
||||
flashSaleMap.put("startTime", flashSale.getStartTime().toString());
|
||||
flashSaleMap.put("endTime", flashSale.getEndTime().toString());
|
||||
flashSaleMap.put("status", flashSale.getStatus().toString());
|
||||
|
||||
if (product != null) {
|
||||
flashSaleMap.put("productName", product.getName());
|
||||
flashSaleMap.put("productPrice", product.getPrice().toString());
|
||||
flashSaleMap.put("productImageUrl", product.getImageUrl());
|
||||
}
|
||||
|
||||
redisService.hMSet(cacheKey, flashSaleMap);
|
||||
redisService.expire(cacheKey, flashSaleCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建秒杀活动DTO
|
||||
*/
|
||||
private FlashSaleDTO buildFlashSaleDTO(FlashSale flashSale, Product product) {
|
||||
FlashSaleDTO dto = new FlashSaleDTO();
|
||||
BeanUtils.copyProperties(flashSale, dto);
|
||||
|
||||
if (product != null) {
|
||||
dto.setProductName(product.getName());
|
||||
dto.setProductImageUrl(product.getImageUrl());
|
||||
dto.setOriginalPrice(product.getPrice());
|
||||
}
|
||||
|
||||
// 获取剩余库存
|
||||
Integer remainingStock = getFlashSaleStock(flashSale.getId());
|
||||
dto.setRemainingStock(remainingStock);
|
||||
|
||||
// 设置状态描述
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (flashSale.getStartTime().isAfter(now)) {
|
||||
dto.setStatusDescription("未开始");
|
||||
dto.setCanParticipate(false);
|
||||
dto.setTimeToStart(
|
||||
flashSale.getStartTime().toEpochSecond(ZoneOffset.of("+8")) * 1000 - System.currentTimeMillis());
|
||||
} else if (flashSale.getEndTime().isBefore(now)) {
|
||||
dto.setStatusDescription("已结束");
|
||||
dto.setCanParticipate(false);
|
||||
dto.setTimeToEnd(0L);
|
||||
} else {
|
||||
dto.setStatusDescription("进行中");
|
||||
dto.setCanParticipate(remainingStock > 0);
|
||||
dto.setTimeToEnd(
|
||||
flashSale.getEndTime().toEpochSecond(ZoneOffset.of("+8")) * 1000 - System.currentTimeMillis());
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证排序字段
|
||||
*/
|
||||
private String validateSortField(String sortBy) {
|
||||
if (sortBy == null || sortBy.trim().isEmpty()) {
|
||||
return "startTime"; // 默认排序字段
|
||||
}
|
||||
|
||||
// 允许的排序字段列表
|
||||
switch (sortBy.toLowerCase()) {
|
||||
case "starttime":
|
||||
case "start_time":
|
||||
return "startTime";
|
||||
case "endtime":
|
||||
case "end_time":
|
||||
return "endTime";
|
||||
case "createdat":
|
||||
case "created_at":
|
||||
return "createdAt";
|
||||
case "id":
|
||||
return "id";
|
||||
case "flashprice":
|
||||
case "flash_price":
|
||||
return "flashPrice";
|
||||
case "flashstock":
|
||||
case "flash_stock":
|
||||
return "flashStock";
|
||||
case "status":
|
||||
return "status";
|
||||
default:
|
||||
log.warn("无效的排序字段: {}, 使用默认排序字段: startTime", sortBy);
|
||||
return "startTime";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建秒杀订单
|
||||
*/
|
||||
private Order createFlashSaleOrder(Long userId, FlashSale flashSale, FlashSaleDTO.ParticipateDTO participateDTO) {
|
||||
Order order = new Order();
|
||||
order.setUserId(userId);
|
||||
order.setProductId(flashSale.getProductId());
|
||||
order.setQuantity(participateDTO.getQuantity());
|
||||
order.setTotalPrice(flashSale.getFlashPrice().multiply(BigDecimal.valueOf(participateDTO.getQuantity())));
|
||||
order.setStatus(1); // 待支付
|
||||
order.setOrderType(2); // 秒杀订单
|
||||
|
||||
return orderRepository.save(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布秒杀结果消息
|
||||
*/
|
||||
private void publishFlashSaleResult(Long userId, FlashSale flashSale, Order order, boolean success) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("userId", userId);
|
||||
message.put("flashSaleId", flashSale.getId());
|
||||
message.put("productId", flashSale.getProductId());
|
||||
message.put("success", success);
|
||||
message.put("orderId", order != null ? order.getId() : null);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
redisService.publish("flashsale:result", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功结果
|
||||
*/
|
||||
private FlashSaleDTO.ResultDTO createSuccessResult(Order order, FlashSale flashSale) {
|
||||
FlashSaleDTO.ResultDTO result = new FlashSaleDTO.ResultDTO();
|
||||
result.setSuccess(true);
|
||||
result.setMessage("秒杀成功");
|
||||
result.setOrderId(order.getId());
|
||||
result.setFlashSaleId(flashSale.getId());
|
||||
result.setProductId(flashSale.getProductId());
|
||||
result.setQuantity(order.getQuantity());
|
||||
result.setTotalPrice(order.getTotalPrice());
|
||||
result.setOrderTime(order.getCreatedAt());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败结果
|
||||
*/
|
||||
private FlashSaleDTO.ResultDTO createFailResult(String message) {
|
||||
FlashSaleDTO.ResultDTO result = new FlashSaleDTO.ResultDTO();
|
||||
result.setSuccess(false);
|
||||
result.setMessage(message);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除秒杀活动缓存
|
||||
*/
|
||||
private void clearFlashSaleCache(Long flashSaleId) {
|
||||
String cacheKey = FLASH_SALE_CACHE_PREFIX + flashSaleId;
|
||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSaleId;
|
||||
String successUsersKey = FLASH_SALE_SUCCESS_USERS_PREFIX + flashSaleId;
|
||||
|
||||
redisService.delete(cacheKey);
|
||||
redisService.delete(stockKey);
|
||||
redisService.delete(successUsersKey);
|
||||
redisService.delete(ACTIVE_FLASH_SALES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Redis消息监听服务
|
||||
* 实现Redis Pub/Sub消息队列功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MessageListenerService {
|
||||
|
||||
@Autowired
|
||||
private RedisMessageListenerContainer redisMessageListenerContainer;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 初始化消息监听器
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initMessageListeners() {
|
||||
// 订单状态变更监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new OrderStatusChangeListener(),
|
||||
new ChannelTopic("order:status:change")
|
||||
);
|
||||
|
||||
// 库存变更监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new StockChangeListener(),
|
||||
new ChannelTopic("stock:change")
|
||||
);
|
||||
|
||||
// 秒杀结果监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new FlashSaleResultListener(),
|
||||
new ChannelTopic("flashsale:result")
|
||||
);
|
||||
|
||||
// 用户行为监听
|
||||
redisMessageListenerContainer.addMessageListener(
|
||||
new UserActionListener(),
|
||||
new ChannelTopic("user:action")
|
||||
);
|
||||
|
||||
log.info("Redis消息监听器初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理订单状态变更
|
||||
*/
|
||||
private void handleOrderStatusChange(Long orderId, Long userId, Integer status, String action) {
|
||||
// 可以在这里实现:
|
||||
// 1. 发送邮件通知
|
||||
// 2. 推送消息
|
||||
// 3. 更新统计数据
|
||||
// 4. 触发其他业务流程
|
||||
|
||||
switch (action) {
|
||||
case "created":
|
||||
log.info("订单创建通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "paid":
|
||||
log.info("订单支付通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "shipped":
|
||||
log.info("订单发货通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "completed":
|
||||
log.info("订单完成通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
case "cancelled":
|
||||
log.info("订单取消通知处理: 订单ID={}", orderId);
|
||||
break;
|
||||
default:
|
||||
log.info("未知订单状态变更: {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理库存变更
|
||||
*/
|
||||
private void handleStockChange(Long productId, Integer quantity, String operation) {
|
||||
// 可以在这里实现:
|
||||
// 1. 库存预警
|
||||
// 2. 自动补货
|
||||
// 3. 数据同步
|
||||
// 4. 统计分析
|
||||
|
||||
if ("decrease".equals(operation)) {
|
||||
log.info("库存扣减处理: 商品ID={}, 扣减数量={}", productId, quantity);
|
||||
// 检查是否需要库存预警
|
||||
checkStockAlert(productId);
|
||||
} else if ("increase".equals(operation)) {
|
||||
log.info("库存增加处理: 商品ID={}, 增加数量={}", productId, quantity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理秒杀结果
|
||||
*/
|
||||
private void handleFlashSaleResult(Long userId, Long flashSaleId, Boolean success, Map<String, Object> data) {
|
||||
// 可以在这里实现:
|
||||
// 1. 实时通知用户
|
||||
// 2. 统计秒杀数据
|
||||
// 3. 风控分析
|
||||
// 4. 营销推荐
|
||||
|
||||
if (success) {
|
||||
log.info("秒杀成功处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 发送成功通知
|
||||
sendFlashSaleSuccessNotification(userId, flashSaleId);
|
||||
} else {
|
||||
log.info("秒杀失败处理: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
// 可以推荐其他商品
|
||||
recommendAlternativeProducts(userId, flashSaleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户行为
|
||||
*/
|
||||
private void handleUserAction(Long userId, String action, Map<String, Object> data) {
|
||||
// 可以在这里实现:
|
||||
// 1. 用户行为分析
|
||||
// 2. 个性化推荐
|
||||
// 3. 风控检测
|
||||
// 4. 营销触发
|
||||
|
||||
switch (action) {
|
||||
case "login":
|
||||
log.info("用户登录行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
case "view_product":
|
||||
log.info("用户浏览商品行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
case "add_to_cart":
|
||||
log.info("用户添加购物车行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
case "purchase":
|
||||
log.info("用户购买行为处理: 用户ID={}", userId);
|
||||
break;
|
||||
default:
|
||||
log.info("其他用户行为: {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查库存预警
|
||||
*/
|
||||
private void checkStockAlert(Long productId) {
|
||||
// 实现库存预警逻辑
|
||||
log.debug("检查商品库存预警: 商品ID={}", productId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送秒杀成功通知
|
||||
*/
|
||||
private void sendFlashSaleSuccessNotification(Long userId, Long flashSaleId) {
|
||||
// 实现成功通知逻辑
|
||||
log.debug("发送秒杀成功通知: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推荐替代商品
|
||||
*/
|
||||
private void recommendAlternativeProducts(Long userId, Long flashSaleId) {
|
||||
// 实现商品推荐逻辑
|
||||
log.debug("推荐替代商品: 用户ID={}, 秒杀ID={}", userId, flashSaleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态变更监听器
|
||||
*/
|
||||
private class OrderStatusChangeListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long orderId = Long.valueOf(data.get("orderId").toString());
|
||||
Long userId = Long.valueOf(data.get("userId").toString());
|
||||
Integer status = Integer.valueOf(data.get("status").toString());
|
||||
String action = data.get("action").toString();
|
||||
|
||||
log.info("订单状态变更: 订单ID={}, 用户ID={}, 状态={}, 操作={}",
|
||||
orderId, userId, status, action);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleOrderStatusChange(orderId, userId, status, action);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理订单状态变更消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存变更监听器
|
||||
*/
|
||||
private class StockChangeListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long productId = Long.valueOf(data.get("productId").toString());
|
||||
Integer quantity = Integer.valueOf(data.get("quantity").toString());
|
||||
String operation = data.get("operation").toString();
|
||||
|
||||
log.info("库存变更: 商品ID={}, 数量={}, 操作={}", productId, quantity, operation);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleStockChange(productId, quantity, operation);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理库存变更消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 秒杀结果监听器
|
||||
*/
|
||||
private class FlashSaleResultListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long userId = Long.valueOf(data.get("userId").toString());
|
||||
Long flashSaleId = Long.valueOf(data.get("flashSaleId").toString());
|
||||
Boolean success = Boolean.valueOf(data.get("success").toString());
|
||||
|
||||
log.info("秒杀结果: 用户ID={}, 秒杀ID={}, 成功={}", userId, flashSaleId, success);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleFlashSaleResult(userId, flashSaleId, success, data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理秒杀结果消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户行为监听器
|
||||
*/
|
||||
private class UserActionListener implements MessageListener {
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
try {
|
||||
String messageBody = new String(message.getBody());
|
||||
Map<String, Object> data = objectMapper.readValue(messageBody, Map.class);
|
||||
|
||||
Long userId = Long.valueOf(data.get("userId").toString());
|
||||
String action = data.get("action").toString();
|
||||
|
||||
log.info("用户行为: 用户ID={}, 行为={}", userId, action);
|
||||
|
||||
// 这里可以添加具体的业务处理逻辑
|
||||
handleUserAction(userId, action, data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户行为消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
546
src/main/java/com/org/flashsalesystem/service/OrderService.java
Normal file
546
src/main/java/com/org/flashsalesystem/service/OrderService.java
Normal file
@@ -0,0 +1,546 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.OrderDTO;
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.Order;
|
||||
import com.org.flashsalesystem.repository.OrderRepository;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 订单服务类
|
||||
* 实现订单创建、查询、状态管理等功能
|
||||
* 使用Redis缓存订单信息
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class OrderService {
|
||||
|
||||
private static final String ORDER_CACHE_PREFIX = "order:";
|
||||
private static final String USER_ORDERS_PREFIX = "user_orders:";
|
||||
private static final String ORDER_QUEUE = "order_queue";
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 创建普通订单
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO createOrder(Long userId, OrderDTO.CreateDTO createDTO) {
|
||||
log.info("创建订单: 用户ID={}, 商品ID={}, 数量={}", userId, createDTO.getProductId(), createDTO.getQuantity());
|
||||
|
||||
// 验证商品
|
||||
ProductDTO product = productService.getProductById(createDTO.getProductId());
|
||||
if (product == null) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
if (product.getStatus() != 1) {
|
||||
throw new RuntimeException("商品已下架");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if (product.getStock() < createDTO.getQuantity()) {
|
||||
throw new RuntimeException("商品库存不足");
|
||||
}
|
||||
|
||||
// 计算总价
|
||||
BigDecimal totalPrice = product.getPrice().multiply(BigDecimal.valueOf(createDTO.getQuantity()));
|
||||
|
||||
// 创建订单
|
||||
Order order = new Order();
|
||||
order.setUserId(userId);
|
||||
order.setProductId(createDTO.getProductId());
|
||||
order.setQuantity(createDTO.getQuantity());
|
||||
order.setTotalPrice(totalPrice);
|
||||
order.setStatus(1); // 待支付
|
||||
order.setOrderType(1); // 普通订单
|
||||
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 扣减库存
|
||||
boolean stockUpdated = productService.updateStock(createDTO.getProductId(), createDTO.getQuantity(),
|
||||
"decrease");
|
||||
if (!stockUpdated) {
|
||||
throw new RuntimeException("库存扣减失败");
|
||||
}
|
||||
|
||||
// 缓存订单信息
|
||||
cacheOrderInfo(order);
|
||||
|
||||
// 添加到订单队列
|
||||
redisService.lPush(ORDER_QUEUE, order.getId());
|
||||
|
||||
// 发布订单创建消息
|
||||
publishOrderStatusChange(order, "created");
|
||||
|
||||
log.info("订单创建成功: 订单ID={}", order.getId());
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取订单
|
||||
*/
|
||||
public OrderDTO getOrderById(Long orderId) {
|
||||
if (orderId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从缓存获取
|
||||
String cacheKey = ORDER_CACHE_PREFIX + orderId;
|
||||
Map<Object, Object> orderMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!orderMap.isEmpty()) {
|
||||
return buildOrderDTOFromCache(orderId, orderMap);
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (!orderOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 缓存订单信息
|
||||
cacheOrderInfo(order);
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户订单列表
|
||||
*/
|
||||
public Map<String, Object> getUserOrders(Long userId, OrderDTO.QueryDTO queryDTO) {
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
// 根据查询条件获取订单
|
||||
if (queryDTO.getStatus() != null) {
|
||||
orderPage = orderRepository.findByUserIdAndStatus(userId, queryDTO.getStatus(), pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<OrderDTO> orderDTOs = orderPage.getContent().stream()
|
||||
.map(this::buildOrderDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", orderDTOs);
|
||||
result.put("totalElements", orderPage.getTotalElements());
|
||||
result.put("totalPages", orderPage.getTotalPages());
|
||||
result.put("currentPage", orderPage.getNumber());
|
||||
result.put("size", orderPage.getSize());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有订单列表(管理员)
|
||||
*/
|
||||
public Map<String, Object> getAllOrders(OrderDTO.QueryDTO queryDTO) {
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<Order> orderPage;
|
||||
|
||||
// 根据查询条件获取订单
|
||||
if (queryDTO.getStatus() != null) {
|
||||
orderPage = orderRepository.findByStatus(queryDTO.getStatus(), pageable);
|
||||
} else if (queryDTO.getOrderType() != null) {
|
||||
orderPage = orderRepository.findByOrderType(queryDTO.getOrderType(), pageable);
|
||||
} else if (queryDTO.getStartTime() != null && queryDTO.getEndTime() != null) {
|
||||
orderPage = orderRepository.findByTimeRange(queryDTO.getStartTime(), queryDTO.getEndTime(), pageable);
|
||||
} else {
|
||||
orderPage = orderRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<OrderDTO> orderDTOs = orderPage.getContent().stream()
|
||||
.map(this::buildOrderDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", orderDTOs);
|
||||
result.put("totalElements", orderPage.getTotalElements());
|
||||
result.put("totalPages", orderPage.getTotalPages());
|
||||
result.put("currentPage", orderPage.getNumber());
|
||||
result.put("size", orderPage.getSize());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO updateOrderStatus(Long orderId, Integer newStatus, String remark) {
|
||||
log.info("更新订单状态: 订单ID={}, 新状态={}", orderId, newStatus);
|
||||
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (!orderOpt.isPresent()) {
|
||||
throw new RuntimeException("订单不存在");
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
Integer oldStatus = order.getStatus();
|
||||
|
||||
// 验证状态转换的合法性
|
||||
if (!isValidStatusTransition(oldStatus, newStatus)) {
|
||||
throw new RuntimeException("无效的状态转换");
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
order.setStatus(newStatus);
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 更新缓存
|
||||
cacheOrderInfo(order);
|
||||
|
||||
// 处理状态变更的业务逻辑
|
||||
handleStatusChange(order, oldStatus, newStatus);
|
||||
|
||||
// 发布状态变更消息
|
||||
publishOrderStatusChange(order, getStatusDescription(newStatus));
|
||||
|
||||
log.info("订单状态更新成功: 订单ID={}, 状态: {} -> {}", orderId, oldStatus, newStatus);
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
@Transactional
|
||||
public OrderDTO cancelOrder(Long orderId, Long userId) {
|
||||
log.info("取消订单: 订单ID={}, 用户ID={}", orderId, userId);
|
||||
|
||||
Optional<Order> orderOpt = orderRepository.findById(orderId);
|
||||
if (!orderOpt.isPresent()) {
|
||||
throw new RuntimeException("订单不存在");
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 验证用户权限
|
||||
if (!order.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作此订单");
|
||||
}
|
||||
|
||||
// 只有待支付状态的订单可以取消
|
||||
if (order.getStatus() != 1) {
|
||||
throw new RuntimeException("订单状态不允许取消");
|
||||
}
|
||||
|
||||
// 更新订单状态为已取消
|
||||
order.setStatus(5);
|
||||
order = orderRepository.save(order);
|
||||
|
||||
// 恢复库存
|
||||
productService.updateStock(order.getProductId(), order.getQuantity(), "increase");
|
||||
|
||||
// 更新缓存
|
||||
cacheOrderInfo(order);
|
||||
|
||||
// 发布订单取消消息
|
||||
publishOrderStatusChange(order, "cancelled");
|
||||
|
||||
log.info("订单取消成功: 订单ID={}", orderId);
|
||||
|
||||
return buildOrderDTO(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作订单
|
||||
*/
|
||||
@Transactional
|
||||
public List<OrderDTO> batchOperateOrders(OrderDTO.BatchOperationDTO batchDTO) {
|
||||
log.info("批量操作订单: 操作={}, 订单数量={}", batchDTO.getOperation(), batchDTO.getOrderIds().size());
|
||||
|
||||
List<OrderDTO> results = new ArrayList<>();
|
||||
|
||||
for (Long orderId : batchDTO.getOrderIds()) {
|
||||
try {
|
||||
OrderDTO result = null;
|
||||
|
||||
switch (batchDTO.getOperation()) {
|
||||
case "pay":
|
||||
result = updateOrderStatus(orderId, 2, "批量支付");
|
||||
break;
|
||||
case "ship":
|
||||
result = updateOrderStatus(orderId, 3, "批量发货");
|
||||
break;
|
||||
case "complete":
|
||||
result = updateOrderStatus(orderId, 4, "批量完成");
|
||||
break;
|
||||
case "cancel":
|
||||
// 需要获取订单的用户ID
|
||||
Order order = orderRepository.findById(orderId).orElse(null);
|
||||
if (order != null) {
|
||||
result = cancelOrder(orderId, order.getUserId());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("不支持的操作类型");
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
results.add(result);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("批量操作订单失败: 订单ID={}", orderId, e);
|
||||
// 继续处理其他订单
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量操作订单完成: 成功处理{}个订单", results.size());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计信息
|
||||
*/
|
||||
public OrderDTO.StatisticsDTO getOrderStatistics() {
|
||||
OrderDTO.StatisticsDTO statistics = new OrderDTO.StatisticsDTO();
|
||||
|
||||
// 总订单数
|
||||
statistics.setTotalOrders(orderRepository.count());
|
||||
|
||||
// 各状态订单数
|
||||
statistics.setPendingPaymentOrders(orderRepository.countByStatus(1));
|
||||
statistics.setPaidOrders(orderRepository.countByStatus(2));
|
||||
statistics.setShippedOrders(orderRepository.countByStatus(3));
|
||||
statistics.setCompletedOrders(orderRepository.countByStatus(4));
|
||||
statistics.setCancelledOrders(orderRepository.countByStatus(5));
|
||||
|
||||
// 订单类型统计
|
||||
statistics.setNormalOrders(orderRepository.countByOrderType(1));
|
||||
statistics.setFlashSaleOrders(orderRepository.countByOrderType(2));
|
||||
|
||||
// 金额统计(这里简化处理,实际应该从数据库聚合查询)
|
||||
List<Order> allOrders = orderRepository.findAll();
|
||||
BigDecimal totalAmount = allOrders.stream()
|
||||
.map(Order::getTotalPrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
statistics.setTotalAmount(totalAmount);
|
||||
|
||||
// 今日金额
|
||||
LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
|
||||
LocalDateTime todayEnd = LocalDateTime.now().withHour(23).withMinute(59).withSecond(59);
|
||||
List<Order> todayOrders = orderRepository.findByTimeRange(todayStart, todayEnd);
|
||||
BigDecimal todayAmount = todayOrders.stream()
|
||||
.map(Order::getTotalPrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
statistics.setTodayAmount(todayAmount);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存订单信息
|
||||
*/
|
||||
private void cacheOrderInfo(Order order) {
|
||||
String cacheKey = ORDER_CACHE_PREFIX + order.getId();
|
||||
Map<String, Object> orderMap = new HashMap<>();
|
||||
orderMap.put("userId", order.getUserId().toString());
|
||||
orderMap.put("productId", order.getProductId().toString());
|
||||
orderMap.put("quantity", order.getQuantity().toString());
|
||||
orderMap.put("totalPrice", order.getTotalPrice().toString());
|
||||
orderMap.put("status", order.getStatus().toString());
|
||||
orderMap.put("orderType", order.getOrderType().toString());
|
||||
orderMap.put("createdAt", order.getCreatedAt().toString());
|
||||
|
||||
redisService.hMSet(cacheKey, orderMap);
|
||||
redisService.expire(cacheKey, 24, TimeUnit.HOURS);
|
||||
|
||||
// 添加到用户订单列表
|
||||
String userOrdersKey = USER_ORDERS_PREFIX + order.getUserId();
|
||||
redisService.lPush(userOrdersKey, order.getId());
|
||||
redisService.expire(userOrdersKey, 24, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存构建OrderDTO
|
||||
*/
|
||||
private OrderDTO buildOrderDTOFromCache(Long orderId, Map<Object, Object> orderMap) {
|
||||
OrderDTO orderDTO = new OrderDTO();
|
||||
orderDTO.setId(orderId);
|
||||
orderDTO.setUserId(Long.valueOf((String) orderMap.get("userId")));
|
||||
orderDTO.setProductId(Long.valueOf((String) orderMap.get("productId")));
|
||||
orderDTO.setQuantity(Integer.valueOf((String) orderMap.get("quantity")));
|
||||
orderDTO.setTotalPrice(new BigDecimal((String) orderMap.get("totalPrice")));
|
||||
orderDTO.setStatus(Integer.valueOf((String) orderMap.get("status")));
|
||||
orderDTO.setOrderType(Integer.valueOf((String) orderMap.get("orderType")));
|
||||
orderDTO.setCreatedAt(LocalDateTime.parse((String) orderMap.get("createdAt")));
|
||||
|
||||
// 设置状态和类型描述
|
||||
orderDTO.setStatusDescription(getStatusDescription(orderDTO.getStatus()));
|
||||
orderDTO.setOrderTypeDescription(getOrderTypeDescription(orderDTO.getOrderType()));
|
||||
|
||||
// 获取用户和商品信息
|
||||
UserDTO user = userService.getUserById(orderDTO.getUserId());
|
||||
if (user != null) {
|
||||
orderDTO.setUsername(user.getUsername());
|
||||
}
|
||||
|
||||
ProductDTO product = productService.getProductById(orderDTO.getProductId());
|
||||
if (product != null) {
|
||||
orderDTO.setProductName(product.getName());
|
||||
orderDTO.setProductImageUrl(product.getImageUrl());
|
||||
}
|
||||
|
||||
return orderDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建OrderDTO
|
||||
*/
|
||||
private OrderDTO buildOrderDTO(Order order) {
|
||||
OrderDTO orderDTO = new OrderDTO();
|
||||
BeanUtils.copyProperties(order, orderDTO);
|
||||
|
||||
// 设置状态和类型描述
|
||||
orderDTO.setStatusDescription(getStatusDescription(order.getStatus()));
|
||||
orderDTO.setOrderTypeDescription(getOrderTypeDescription(order.getOrderType()));
|
||||
|
||||
// 获取用户信息
|
||||
UserDTO user = userService.getUserById(order.getUserId());
|
||||
if (user != null) {
|
||||
orderDTO.setUsername(user.getUsername());
|
||||
}
|
||||
|
||||
// 获取商品信息
|
||||
ProductDTO product = productService.getProductById(order.getProductId());
|
||||
if (product != null) {
|
||||
orderDTO.setProductName(product.getName());
|
||||
orderDTO.setProductImageUrl(product.getImageUrl());
|
||||
}
|
||||
|
||||
return orderDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证状态转换的合法性
|
||||
*/
|
||||
private boolean isValidStatusTransition(Integer fromStatus, Integer toStatus) {
|
||||
// 定义合法的状态转换
|
||||
Map<Integer, List<Integer>> validTransitions = new HashMap<>();
|
||||
validTransitions.put(1, Arrays.asList(2, 5)); // 待支付 -> 已支付/已取消
|
||||
validTransitions.put(2, Arrays.asList(3, 5)); // 已支付 -> 已发货/已取消
|
||||
validTransitions.put(3, Collections.singletonList(4)); // 已发货 -> 已完成
|
||||
validTransitions.put(4, Collections.emptyList()); // 已完成 -> 无
|
||||
validTransitions.put(5, Collections.emptyList()); // 已取消 -> 无
|
||||
|
||||
List<Integer> allowedTransitions = validTransitions.get(fromStatus);
|
||||
return allowedTransitions != null && allowedTransitions.contains(toStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态变更的业务逻辑
|
||||
*/
|
||||
private void handleStatusChange(Order order, Integer oldStatus, Integer newStatus) {
|
||||
// 支付完成
|
||||
if (oldStatus == 1 && newStatus == 2) {
|
||||
log.info("订单支付完成: 订单ID={}", order.getId());
|
||||
// 这里可以添加支付完成后的业务逻辑
|
||||
}
|
||||
|
||||
// 发货
|
||||
if (oldStatus == 2 && newStatus == 3) {
|
||||
log.info("订单已发货: 订单ID={}", order.getId());
|
||||
// 这里可以添加发货后的业务逻辑
|
||||
}
|
||||
|
||||
// 完成
|
||||
if (oldStatus == 3 && newStatus == 4) {
|
||||
log.info("订单已完成: 订单ID={}", order.getId());
|
||||
// 这里可以添加订单完成后的业务逻辑
|
||||
}
|
||||
|
||||
// 取消
|
||||
if (newStatus == 5) {
|
||||
log.info("订单已取消: 订单ID={}", order.getId());
|
||||
// 这里可以添加订单取消后的业务逻辑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布订单状态变更消息
|
||||
*/
|
||||
private void publishOrderStatusChange(Order order, String action) {
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("orderId", order.getId());
|
||||
message.put("userId", order.getUserId());
|
||||
message.put("productId", order.getProductId());
|
||||
message.put("status", order.getStatus());
|
||||
message.put("action", action);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
redisService.publish("order:status:change", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态描述
|
||||
*/
|
||||
private String getStatusDescription(Integer status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "待支付";
|
||||
case 2:
|
||||
return "已支付";
|
||||
case 3:
|
||||
return "已发货";
|
||||
case 4:
|
||||
return "已完成";
|
||||
case 5:
|
||||
return "已取消";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单类型描述
|
||||
*/
|
||||
private String getOrderTypeDescription(Integer orderType) {
|
||||
switch (orderType) {
|
||||
case 1:
|
||||
return "普通订单";
|
||||
case 2:
|
||||
return "秒杀订单";
|
||||
default:
|
||||
return "未知类型";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.ProductDTO;
|
||||
import com.org.flashsalesystem.entity.Product;
|
||||
import com.org.flashsalesystem.repository.ProductRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 商品服务类
|
||||
* 实现商品管理、库存控制、缓存等功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ProductService {
|
||||
|
||||
private static final String PRODUCT_CACHE_PREFIX = "product:";
|
||||
private static final String PRODUCT_LIST_CACHE_PREFIX = "product_list:";
|
||||
private static final String PRODUCT_STOCK_PREFIX = "product_stock:";
|
||||
private static final String PRODUCT_SALES_RANK = "product_sales_rank";
|
||||
private static final String HOT_PRODUCTS_CACHE = "hot_products";
|
||||
@Autowired
|
||||
private ProductRepository productRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Value("${flashsale.cache.product-expire-minutes:60}")
|
||||
private int productCacheExpireMinutes;
|
||||
|
||||
/**
|
||||
* 创建商品
|
||||
*/
|
||||
@Transactional
|
||||
public ProductDTO createProduct(ProductDTO.CreateDTO createDTO) {
|
||||
log.info("创建商品: {}", createDTO.getName());
|
||||
|
||||
Product product = new Product();
|
||||
BeanUtils.copyProperties(createDTO, product);
|
||||
product.setStatus(1); // 默认上架
|
||||
|
||||
product = productRepository.save(product);
|
||||
|
||||
// 缓存商品信息
|
||||
cacheProductInfo(product);
|
||||
|
||||
// 初始化库存到Redis
|
||||
if (product.getStock() > 0) {
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + product.getId();
|
||||
redisService.set(stockKey, product.getStock());
|
||||
}
|
||||
|
||||
// 清除商品列表缓存
|
||||
clearProductListCache();
|
||||
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
log.info("商品创建成功: {}, ID: {}", product.getName(), product.getId());
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取商品信息
|
||||
*/
|
||||
public ProductDTO getProductById(Long productId) {
|
||||
if (productId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从缓存获取
|
||||
String cacheKey = PRODUCT_CACHE_PREFIX + productId;
|
||||
Map<Object, Object> productMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!productMap.isEmpty()) {
|
||||
return buildProductDTOFromCache(productId, productMap);
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (!productOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Product product = productOpt.get();
|
||||
|
||||
// 缓存商品信息
|
||||
cacheProductInfo(product);
|
||||
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品列表(分页)
|
||||
*/
|
||||
public Map<String, Object> getProductList(ProductDTO.QueryDTO queryDTO) {
|
||||
// 构建缓存键
|
||||
String cacheKey = buildProductListCacheKey(queryDTO);
|
||||
|
||||
// 尝试从缓存获取
|
||||
Object cachedResult = redisService.get(cacheKey);
|
||||
if (cachedResult != null) {
|
||||
return (Map<String, Object>) cachedResult;
|
||||
}
|
||||
|
||||
// 构建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(queryDTO.getSortDirection()), queryDTO.getSortBy());
|
||||
Pageable pageable = PageRequest.of(queryDTO.getPage(), queryDTO.getSize(), sort);
|
||||
|
||||
Page<Product> productPage;
|
||||
|
||||
// 根据查询条件获取数据
|
||||
if (queryDTO.getName() != null && !queryDTO.getName().trim().isEmpty()) {
|
||||
productPage = productRepository.findByStatus(1, pageable)
|
||||
.map(product -> {
|
||||
if (product.getName().contains(queryDTO.getName())) {
|
||||
return product;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.map(product -> product);
|
||||
} else {
|
||||
productPage = productRepository.findByStatus(queryDTO.getStatus() != null ? queryDTO.getStatus() : 1,
|
||||
pageable);
|
||||
}
|
||||
|
||||
// 转换为DTO
|
||||
List<ProductDTO> productDTOs = productPage.getContent().stream()
|
||||
.map(product -> {
|
||||
ProductDTO dto = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, dto);
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", productDTOs);
|
||||
result.put("totalElements", productPage.getTotalElements());
|
||||
result.put("totalPages", productPage.getTotalPages());
|
||||
result.put("currentPage", productPage.getNumber());
|
||||
result.put("size", productPage.getSize());
|
||||
|
||||
// 缓存结果
|
||||
redisService.set(cacheKey, result, 10, TimeUnit.MINUTES);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门商品
|
||||
*/
|
||||
public List<ProductDTO> getHotProducts(int limit) {
|
||||
// 尝试从缓存获取
|
||||
List<Object> cachedProducts = redisService.lRange(HOT_PRODUCTS_CACHE, 0, limit - 1);
|
||||
if (!cachedProducts.isEmpty()) {
|
||||
return cachedProducts.stream()
|
||||
.map(obj -> (ProductDTO) obj)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
Pageable pageable = PageRequest.of(0, limit);
|
||||
List<Product> hotProducts = productRepository.findHotProducts(pageable);
|
||||
|
||||
List<ProductDTO> productDTOs = hotProducts.stream()
|
||||
.map(product -> {
|
||||
ProductDTO dto = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, dto);
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存热门商品
|
||||
if (!productDTOs.isEmpty()) {
|
||||
redisService.delete(HOT_PRODUCTS_CACHE);
|
||||
redisService.rPush(HOT_PRODUCTS_CACHE, productDTOs.toArray());
|
||||
redisService.expire(HOT_PRODUCTS_CACHE, 30, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
return productDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品信息
|
||||
*/
|
||||
@Transactional
|
||||
public ProductDTO updateProduct(Long productId, ProductDTO.UpdateDTO updateDTO) {
|
||||
log.info("更新商品信息: {}", productId);
|
||||
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (!productOpt.isPresent()) {
|
||||
throw new RuntimeException("商品不存在");
|
||||
}
|
||||
|
||||
Product product = productOpt.get();
|
||||
|
||||
// 更新字段
|
||||
if (updateDTO.getName() != null) {
|
||||
product.setName(updateDTO.getName());
|
||||
}
|
||||
if (updateDTO.getDescription() != null) {
|
||||
product.setDescription(updateDTO.getDescription());
|
||||
}
|
||||
if (updateDTO.getPrice() != null) {
|
||||
product.setPrice(updateDTO.getPrice());
|
||||
}
|
||||
if (updateDTO.getStock() != null) {
|
||||
product.setStock(updateDTO.getStock());
|
||||
// 同步更新Redis中的库存
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
redisService.set(stockKey, updateDTO.getStock());
|
||||
}
|
||||
if (updateDTO.getImageUrl() != null) {
|
||||
product.setImageUrl(updateDTO.getImageUrl());
|
||||
}
|
||||
if (updateDTO.getStatus() != null) {
|
||||
product.setStatus(updateDTO.getStatus());
|
||||
}
|
||||
|
||||
product = productRepository.save(product);
|
||||
|
||||
// 更新缓存
|
||||
cacheProductInfo(product);
|
||||
clearProductListCache();
|
||||
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
BeanUtils.copyProperties(product, productDTO);
|
||||
|
||||
log.info("商品信息更新成功: {}", productId);
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商品库存
|
||||
*/
|
||||
@Transactional
|
||||
public boolean updateStock(Long productId, Integer quantity, String operation) {
|
||||
log.info("更新商品库存: {}, 数量: {}, 操作: {}", productId, quantity, operation);
|
||||
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
|
||||
try {
|
||||
if ("increase".equals(operation)) {
|
||||
// 增加库存
|
||||
redisService.incrBy(stockKey, quantity);
|
||||
productRepository.increaseStock(productId, quantity);
|
||||
} else if ("decrease".equals(operation)) {
|
||||
// 减少库存
|
||||
Long currentStock = (Long) redisService.get(stockKey);
|
||||
if (currentStock == null || currentStock < quantity) {
|
||||
log.warn("库存不足: 商品ID={}, 当前库存={}, 需要扣减={}", productId, currentStock, quantity);
|
||||
return false;
|
||||
}
|
||||
|
||||
redisService.decrBy(stockKey, quantity);
|
||||
int updatedRows = productRepository.updateStock(productId, quantity);
|
||||
if (updatedRows == 0) {
|
||||
// 数据库更新失败,回滚Redis
|
||||
redisService.incrBy(stockKey, quantity);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清除商品缓存,强制下次查询时重新加载
|
||||
String productCacheKey = PRODUCT_CACHE_PREFIX + productId;
|
||||
redisService.delete(productCacheKey);
|
||||
|
||||
// 发布库存变更消息
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("productId", productId);
|
||||
message.put("quantity", quantity);
|
||||
message.put("operation", operation);
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
redisService.publish("stock:change", message);
|
||||
|
||||
log.info("商品库存更新成功: {}", productId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("更新商品库存失败: {}", productId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品库存(从Redis)
|
||||
*/
|
||||
public Integer getProductStock(Long productId) {
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
Object stock = redisService.get(stockKey);
|
||||
return stock != null ? Integer.valueOf(stock.toString()) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热商品库存到Redis
|
||||
*/
|
||||
public void preloadProductStock(Long productId) {
|
||||
Optional<Product> productOpt = productRepository.findById(productId);
|
||||
if (productOpt.isPresent()) {
|
||||
Product product = productOpt.get();
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
redisService.set(stockKey, product.getStock());
|
||||
log.info("商品库存预热完成: 商品ID={}, 库存={}", productId, product.getStock());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量预热商品库存
|
||||
*/
|
||||
public void batchPreloadProductStock(List<Long> productIds) {
|
||||
log.info("开始批量预热商品库存: {}", productIds);
|
||||
|
||||
for (Long productId : productIds) {
|
||||
preloadProductStock(productId);
|
||||
}
|
||||
|
||||
log.info("批量预热商品库存完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加商品销量排行榜分数
|
||||
*/
|
||||
public void increaseSalesRank(Long productId, Integer quantity) {
|
||||
redisService.zIncrBy(PRODUCT_SALES_RANK, productId, quantity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品销量排行榜
|
||||
*/
|
||||
public List<Map<String, Object>> getSalesRank(int limit) {
|
||||
Set<Object> topProducts = redisService.zRevRange(PRODUCT_SALES_RANK, 0, limit - 1);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
int rank = 1;
|
||||
for (Object productId : topProducts) {
|
||||
ProductDTO product = getProductById(Long.valueOf(productId.toString()));
|
||||
if (product != null) {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("rank", rank++);
|
||||
item.put("product", product);
|
||||
item.put("sales", redisService.zIncrBy(PRODUCT_SALES_RANK, productId, 0)); // 获取分数
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存商品信息
|
||||
*/
|
||||
private void cacheProductInfo(Product product) {
|
||||
String cacheKey = PRODUCT_CACHE_PREFIX + product.getId();
|
||||
Map<String, Object> productMap = new HashMap<>();
|
||||
productMap.put("name", product.getName());
|
||||
productMap.put("description", product.getDescription());
|
||||
productMap.put("price", product.getPrice().toString());
|
||||
productMap.put("stock", product.getStock().toString());
|
||||
productMap.put("imageUrl", product.getImageUrl());
|
||||
productMap.put("status", product.getStatus().toString());
|
||||
|
||||
redisService.hMSet(cacheKey, productMap);
|
||||
redisService.expire(cacheKey, productCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存构建ProductDTO
|
||||
*/
|
||||
private ProductDTO buildProductDTOFromCache(Long productId, Map<Object, Object> productMap) {
|
||||
ProductDTO productDTO = new ProductDTO();
|
||||
productDTO.setId(productId);
|
||||
productDTO.setName((String) productMap.get("name"));
|
||||
productDTO.setDescription((String) productMap.get("description"));
|
||||
productDTO.setPrice(new java.math.BigDecimal((String) productMap.get("price")));
|
||||
productDTO.setStock(Integer.valueOf((String) productMap.get("stock")));
|
||||
productDTO.setImageUrl((String) productMap.get("imageUrl"));
|
||||
productDTO.setStatus(Integer.valueOf((String) productMap.get("status")));
|
||||
return productDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建商品列表缓存键
|
||||
*/
|
||||
private String buildProductListCacheKey(ProductDTO.QueryDTO queryDTO) {
|
||||
return PRODUCT_LIST_CACHE_PREFIX +
|
||||
(queryDTO.getName() != null ? queryDTO.getName() : "all") + ":" +
|
||||
(queryDTO.getStatus() != null ? queryDTO.getStatus() : "1") + ":" +
|
||||
queryDTO.getPage() + ":" + queryDTO.getSize() + ":" +
|
||||
queryDTO.getSortBy() + ":" + queryDTO.getSortDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除商品列表缓存
|
||||
*/
|
||||
private void clearProductListCache() {
|
||||
// 使用通配符删除所有商品列表缓存
|
||||
// 注意:这里简化处理,实际生产环境可能需要更精确的缓存管理
|
||||
redisService.delete(HOT_PRODUCTS_CACHE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商品
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteProduct(Long productId) {
|
||||
log.info("删除商品: {}", productId);
|
||||
|
||||
try {
|
||||
// 删除数据库记录
|
||||
productRepository.deleteById(productId);
|
||||
|
||||
// 删除相关缓存
|
||||
String productCacheKey = PRODUCT_CACHE_PREFIX + productId;
|
||||
String stockKey = PRODUCT_STOCK_PREFIX + productId;
|
||||
redisService.delete(productCacheKey);
|
||||
redisService.delete(stockKey);
|
||||
|
||||
// 从销量排行榜中移除
|
||||
redisService.zRem(PRODUCT_SALES_RANK, productId);
|
||||
|
||||
// 清除商品列表缓存
|
||||
clearProductListCache();
|
||||
|
||||
log.info("商品删除成功: {}", productId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("删除商品失败: {}", productId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 限流服务
|
||||
* 使用Redis实现接口限流,防止恶意刷单
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RateLimitService {
|
||||
|
||||
private static final String RATE_LIMIT_PREFIX = "rate_limit:";
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Value("${flashsale.seckill.rate-limit.max-requests-per-minute:10}")
|
||||
private int maxRequestsPerMinute;
|
||||
|
||||
/**
|
||||
* 检查是否超过限流
|
||||
*
|
||||
* @param key 限流键(通常是用户ID+接口标识)
|
||||
* @param maxRequests 最大请求次数
|
||||
* @param timeWindow 时间窗口(秒)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean isAllowed(String key, int maxRequests, int timeWindow) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
// 获取当前计数
|
||||
Long currentCount = redisService.incr(rateLimitKey);
|
||||
|
||||
if (currentCount == 1) {
|
||||
// 第一次请求,设置过期时间
|
||||
redisService.expire(rateLimitKey, timeWindow, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
boolean allowed = currentCount <= maxRequests;
|
||||
|
||||
if (!allowed) {
|
||||
log.warn("请求被限流: key={}, 当前计数={}, 最大允许={}", key, currentCount, maxRequests);
|
||||
}
|
||||
|
||||
return allowed;
|
||||
} catch (Exception e) {
|
||||
log.error("限流检查异常: key={}", key, e);
|
||||
// 异常情况下允许请求,避免影响正常业务
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户秒杀接口限流
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkFlashSaleRateLimit(Long userId) {
|
||||
String key = "flashsale:" + userId;
|
||||
return isAllowed(key, maxRequestsPerMinute, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户登录接口限流
|
||||
*
|
||||
* @param identifier 标识符(用户名或IP)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkLoginRateLimit(String identifier) {
|
||||
String key = "login:" + identifier;
|
||||
return isAllowed(key, 5, 300); // 5分钟内最多5次登录尝试
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户注册接口限流
|
||||
*
|
||||
* @param ip IP地址
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkRegisterRateLimit(String ip) {
|
||||
String key = "register:" + ip;
|
||||
return isAllowed(key, 3, 3600); // 1小时内最多3次注册
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查购物车操作限流
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean checkCartOperationRateLimit(Long userId) {
|
||||
String key = "cart:" + userId;
|
||||
return isAllowed(key, 100, 60); // 1分钟内最多100次购物车操作
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余请求次数
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param maxRequests 最大请求次数
|
||||
*
|
||||
* @return 剩余请求次数
|
||||
*/
|
||||
public int getRemainingRequests(String key, int maxRequests) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
Object countObj = redisService.get(rateLimitKey);
|
||||
if (countObj == null) {
|
||||
return maxRequests;
|
||||
}
|
||||
|
||||
int currentCount = Integer.parseInt(countObj.toString());
|
||||
return Math.max(0, maxRequests - currentCount);
|
||||
} catch (Exception e) {
|
||||
log.error("获取剩余请求次数异常: key={}", key, e);
|
||||
return maxRequests;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限流重置时间
|
||||
*
|
||||
* @param key 限流键
|
||||
*
|
||||
* @return 重置时间(秒)
|
||||
*/
|
||||
public long getResetTime(String key) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
Long expireTime = redisService.getExpire(rateLimitKey);
|
||||
return expireTime != null ? expireTime : 0;
|
||||
} catch (Exception e) {
|
||||
log.error("获取限流重置时间异常: key={}", key, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除限流记录
|
||||
*
|
||||
* @param key 限流键
|
||||
*/
|
||||
public void clearRateLimit(String key) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + key;
|
||||
|
||||
try {
|
||||
redisService.delete(rateLimitKey);
|
||||
log.info("清除限流记录: key={}", key);
|
||||
} catch (Exception e) {
|
||||
log.error("清除限流记录异常: key={}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑动窗口限流
|
||||
* 使用ZSet实现更精确的滑动窗口限流
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param maxRequests 最大请求次数
|
||||
* @param timeWindow 时间窗口(秒)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean slidingWindowRateLimit(String key, int maxRequests, int timeWindow) {
|
||||
String rateLimitKey = RATE_LIMIT_PREFIX + "sliding:" + key;
|
||||
long now = System.currentTimeMillis();
|
||||
long windowStart = now - timeWindow * 1000L;
|
||||
|
||||
try {
|
||||
// 移除过期的记录
|
||||
redisService.zRem(rateLimitKey, 0, windowStart);
|
||||
|
||||
// 获取当前窗口内的请求数
|
||||
Long currentCount = redisService.zCard(rateLimitKey);
|
||||
|
||||
if (currentCount != null && currentCount >= maxRequests) {
|
||||
log.warn("滑动窗口限流: key={}, 当前计数={}, 最大允许={}", key, currentCount, maxRequests);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加当前请求
|
||||
redisService.zAdd(rateLimitKey, String.valueOf(now), now);
|
||||
|
||||
// 设置过期时间
|
||||
redisService.expire(rateLimitKey, timeWindow + 1, TimeUnit.SECONDS);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("滑动窗口限流异常: key={}", key, e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌桶限流
|
||||
* 使用Redis实现令牌桶算法
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param capacity 桶容量
|
||||
* @param refillRate 令牌补充速率(每秒)
|
||||
*
|
||||
* @return 是否允许请求
|
||||
*/
|
||||
public boolean tokenBucketRateLimit(String key, int capacity, double refillRate) {
|
||||
String bucketKey = RATE_LIMIT_PREFIX + "bucket:" + key;
|
||||
String lastRefillKey = bucketKey + ":last_refill";
|
||||
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// 获取当前令牌数和上次补充时间
|
||||
Object tokensObj = redisService.get(bucketKey);
|
||||
Object lastRefillObj = redisService.get(lastRefillKey);
|
||||
|
||||
double tokens = tokensObj != null ? Double.parseDouble(tokensObj.toString()) : capacity;
|
||||
long lastRefill = lastRefillObj != null ? Long.parseLong(lastRefillObj.toString()) : now;
|
||||
|
||||
// 计算需要补充的令牌数
|
||||
double timePassed = (now - lastRefill) / 1000.0;
|
||||
double tokensToAdd = timePassed * refillRate;
|
||||
tokens = Math.min(capacity, tokens + tokensToAdd);
|
||||
|
||||
if (tokens >= 1) {
|
||||
// 消耗一个令牌
|
||||
tokens -= 1;
|
||||
|
||||
// 更新令牌数和时间
|
||||
redisService.set(bucketKey, String.valueOf(tokens), 3600, TimeUnit.SECONDS);
|
||||
redisService.set(lastRefillKey, String.valueOf(now), 3600, TimeUnit.SECONDS);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
log.warn("令牌桶限流: key={}, 当前令牌数={}", key, tokens);
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("令牌桶限流异常: key={}", key, e);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.SessionCallback;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis管道技术服务
|
||||
* 实现批量操作优化,提高性能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedisPipelineService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 批量设置键值对
|
||||
*/
|
||||
public void batchSet(Map<String, Object> keyValues) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<String, Object> entry : keyValues.entrySet()) {
|
||||
ops.opsForValue().set(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量设置完成,数量: {}", keyValues.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置键值对并指定过期时间
|
||||
*/
|
||||
public void batchSetWithExpire(Map<String, Object> keyValues, long timeout, TimeUnit unit) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<String, Object> entry : keyValues.entrySet()) {
|
||||
ops.opsForValue().set(entry.getKey(), entry.getValue(), timeout, unit);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量设置带过期时间完成,数量: {}", keyValues.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取值
|
||||
*/
|
||||
public List<Object> batchGet(List<String> keys) {
|
||||
List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.opsForValue().get(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量获取完成,数量: {}", keys.size());
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除键
|
||||
*/
|
||||
public void batchDelete(List<String> keys) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.delete(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量删除完成,数量: {}", keys.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量Hash操作
|
||||
*/
|
||||
public void batchHashSet(String key, Map<String, Object> hash) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<String, Object> entry : hash.entrySet()) {
|
||||
ops.opsForHash().put(key, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量Hash设置完成,key: {}, 字段数量: {}", key, hash.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量List操作
|
||||
*/
|
||||
public void batchListPush(String key, List<Object> values) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Object value : values) {
|
||||
ops.opsForList().rightPush(key, value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量List推入完成,key: {}, 数量: {}", key, values.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量Set操作
|
||||
*/
|
||||
public void batchSetAdd(String key, List<Object> values) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Object value : values) {
|
||||
ops.opsForSet().add(key, value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量Set添加完成,key: {}, 数量: {}", key, values.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量ZSet操作
|
||||
*/
|
||||
public void batchZSetAdd(String key, Map<Object, Double> scoreMembers) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map.Entry<Object, Double> entry : scoreMembers.entrySet()) {
|
||||
ops.opsForZSet().add(key, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量ZSet添加完成,key: {}, 数量: {}", key, scoreMembers.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量递增操作
|
||||
*/
|
||||
public List<Object> batchIncrement(List<String> keys, long delta) {
|
||||
List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.opsForValue().increment(key, delta);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量递增完成,数量: {}, 增量: {}", keys.size(), delta);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置过期时间
|
||||
*/
|
||||
public void batchExpire(List<String> keys, long timeout, TimeUnit unit) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (String key : keys) {
|
||||
ops.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("批量设置过期时间完成,数量: {}", keys.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂批量操作示例:用户数据预热
|
||||
*/
|
||||
public void preloadUserData(List<Map<String, Object>> userData) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map<String, Object> user : userData) {
|
||||
String userId = user.get("id").toString();
|
||||
String userKey = "user:" + userId;
|
||||
|
||||
// 设置用户基本信息
|
||||
ops.opsForHash().putAll(userKey, user);
|
||||
|
||||
// 设置过期时间
|
||||
ops.expire(userKey, 30, TimeUnit.MINUTES);
|
||||
|
||||
// 添加到在线用户集合
|
||||
ops.opsForSet().add("online_users", userId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("用户数据预热完成,数量: {}", userData.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂批量操作示例:商品数据预热
|
||||
*/
|
||||
public void preloadProductData(List<Map<String, Object>> productData) {
|
||||
redisTemplate.executePipelined(new SessionCallback<Object>() {
|
||||
@Override
|
||||
public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
|
||||
RedisOperations<String, Object> ops = (RedisOperations<String, Object>) operations;
|
||||
|
||||
for (Map<String, Object> product : productData) {
|
||||
String productId = product.get("id").toString();
|
||||
String productKey = "product:" + productId;
|
||||
String stockKey = "product_stock:" + productId;
|
||||
|
||||
// 设置商品信息
|
||||
ops.opsForHash().putAll(productKey, product);
|
||||
|
||||
// 设置库存
|
||||
ops.opsForValue().set(stockKey, product.get("stock"));
|
||||
|
||||
// 设置过期时间
|
||||
ops.expire(productKey, 60, TimeUnit.MINUTES);
|
||||
|
||||
// 添加到销量排行榜(初始分数为0)
|
||||
ops.opsForZSet().add("product_sales_rank", productId, 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
log.info("商品数据预热完成,数量: {}", productData.size());
|
||||
}
|
||||
}
|
||||
359
src/main/java/com/org/flashsalesystem/service/RedisService.java
Normal file
359
src/main/java/com/org/flashsalesystem/service/RedisService.java
Normal file
@@ -0,0 +1,359 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis服务类
|
||||
* 封装Redis的各种操作,包括五种数据类型的应用
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedisService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, String> stringRedisTemplate;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<Long> flashSaleScript;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<String> lockScript;
|
||||
|
||||
@Autowired
|
||||
private DefaultRedisScript<Long> unlockScript;
|
||||
|
||||
// ========== String类型操作 ==========
|
||||
|
||||
/**
|
||||
* 设置字符串值
|
||||
*/
|
||||
public void set(String key, Object value) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间
|
||||
*/
|
||||
public void set(String key, Object value, long timeout, TimeUnit unit) {
|
||||
redisTemplate.opsForValue().set(key, value, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串值
|
||||
*/
|
||||
public Object get(String key) {
|
||||
return redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
public Long incr(String key) {
|
||||
return redisTemplate.opsForValue().increment(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增指定步长
|
||||
*/
|
||||
public Long incrBy(String key, long delta) {
|
||||
return redisTemplate.opsForValue().increment(key, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
public Long decr(String key) {
|
||||
return redisTemplate.opsForValue().decrement(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减指定步长
|
||||
*/
|
||||
public Long decrBy(String key, long delta) {
|
||||
return redisTemplate.opsForValue().decrement(key, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值,仅当键不存在时
|
||||
*/
|
||||
public Boolean setNx(String key, Object value) {
|
||||
return redisTemplate.opsForValue().setIfAbsent(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值和过期时间,仅当键不存在时
|
||||
*/
|
||||
public Boolean setNx(String key, Object value, long timeout, TimeUnit unit) {
|
||||
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
|
||||
}
|
||||
|
||||
// ========== Hash类型操作 ==========
|
||||
|
||||
/**
|
||||
* 设置Hash字段值
|
||||
*/
|
||||
public void hSet(String key, String field, Object value) {
|
||||
redisTemplate.opsForHash().put(key, field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash字段值
|
||||
*/
|
||||
public Object hGet(String key, String field) {
|
||||
return redisTemplate.opsForHash().get(key, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置Hash字段
|
||||
*/
|
||||
public void hMSet(String key, Map<String, Object> map) {
|
||||
redisTemplate.opsForHash().putAll(key, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取Hash字段值
|
||||
*/
|
||||
public List<Object> hMGet(String key, Collection<Object> fields) {
|
||||
return redisTemplate.opsForHash().multiGet(key, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash所有字段和值
|
||||
*/
|
||||
public Map<Object, Object> hGetAll(String key) {
|
||||
return redisTemplate.opsForHash().entries(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash字段
|
||||
*/
|
||||
public Long hDel(String key, Object... fields) {
|
||||
return redisTemplate.opsForHash().delete(key, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Hash字段是否存在
|
||||
*/
|
||||
public Boolean hExists(String key, String field) {
|
||||
return redisTemplate.opsForHash().hasKey(key, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash字段值递增
|
||||
*/
|
||||
public Long hIncrBy(String key, String field, long delta) {
|
||||
return redisTemplate.opsForHash().increment(key, field, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hash长度
|
||||
*/
|
||||
public Long hLen(String key) {
|
||||
return redisTemplate.opsForHash().size(key);
|
||||
}
|
||||
|
||||
// ========== List类型操作 ==========
|
||||
|
||||
/**
|
||||
* 从左侧推入元素
|
||||
*/
|
||||
public Long lPush(String key, Object... values) {
|
||||
return redisTemplate.opsForList().leftPushAll(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从右侧推入元素
|
||||
*/
|
||||
public Long rPush(String key, Object... values) {
|
||||
return redisTemplate.opsForList().rightPushAll(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从左侧弹出元素
|
||||
*/
|
||||
public Object lPop(String key) {
|
||||
return redisTemplate.opsForList().leftPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从右侧弹出元素
|
||||
*/
|
||||
public Object rPop(String key) {
|
||||
return redisTemplate.opsForList().rightPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表长度
|
||||
*/
|
||||
public Long lLen(String key) {
|
||||
return redisTemplate.opsForList().size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表指定范围的元素
|
||||
*/
|
||||
public List<Object> lRange(String key, long start, long end) {
|
||||
return redisTemplate.opsForList().range(key, start, end);
|
||||
}
|
||||
|
||||
// ========== Set类型操作 ==========
|
||||
|
||||
/**
|
||||
* 添加元素到集合
|
||||
*/
|
||||
public Long sAdd(String key, Object... values) {
|
||||
return redisTemplate.opsForSet().add(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从集合中移除元素
|
||||
*/
|
||||
public Long sRem(String key, Object... values) {
|
||||
return redisTemplate.opsForSet().remove(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否在集合中
|
||||
*/
|
||||
public Boolean sIsMember(String key, Object value) {
|
||||
return redisTemplate.opsForSet().isMember(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合所有元素
|
||||
*/
|
||||
public Set<Object> sMembers(String key) {
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合大小
|
||||
*/
|
||||
public Long sCard(String key) {
|
||||
return redisTemplate.opsForSet().size(key);
|
||||
}
|
||||
|
||||
// ========== ZSet类型操作 ==========
|
||||
|
||||
/**
|
||||
* 添加元素到有序集合
|
||||
*/
|
||||
public Boolean zAdd(String key, Object value, double score) {
|
||||
return redisTemplate.opsForZSet().add(key, value, score);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从有序集合中移除元素
|
||||
*/
|
||||
public Long zRem(String key, Object... values) {
|
||||
return redisTemplate.opsForZSet().remove(key, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合指定范围的元素(按分数排序)
|
||||
*/
|
||||
public Set<Object> zRange(String key, long start, long end) {
|
||||
return redisTemplate.opsForZSet().range(key, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合指定范围的元素(按分数倒序)
|
||||
*/
|
||||
public Set<Object> zRevRange(String key, long start, long end) {
|
||||
return redisTemplate.opsForZSet().reverseRange(key, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加有序集合元素的分数
|
||||
*/
|
||||
public Double zIncrBy(String key, Object value, double delta) {
|
||||
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合大小
|
||||
*/
|
||||
public Long zCard(String key) {
|
||||
return redisTemplate.opsForZSet().zCard(key);
|
||||
}
|
||||
|
||||
// ========== 通用操作 ==========
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
*/
|
||||
public Boolean delete(String key) {
|
||||
return redisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除键
|
||||
*/
|
||||
public Long delete(Collection<String> keys) {
|
||||
return redisTemplate.delete(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*/
|
||||
public Boolean exists(String key) {
|
||||
return redisTemplate.hasKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
*/
|
||||
public Boolean expire(String key, long timeout, TimeUnit unit) {
|
||||
return redisTemplate.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键的剩余过期时间
|
||||
*/
|
||||
public Long getExpire(String key) {
|
||||
return redisTemplate.getExpire(key);
|
||||
}
|
||||
|
||||
// ========== Lua脚本操作 ==========
|
||||
|
||||
/**
|
||||
* 执行秒杀脚本
|
||||
*/
|
||||
public Long executeFlashSaleScript(String stockKey, int quantity) {
|
||||
return redisTemplate.execute(flashSaleScript, Collections.singletonList(stockKey), String.valueOf(quantity));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行分布式锁脚本
|
||||
*/
|
||||
public String executeLockScript(String lockKey, String lockValue, int expireSeconds) {
|
||||
return redisTemplate.execute(lockScript, Collections.singletonList(lockKey), lockValue,
|
||||
String.valueOf(expireSeconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行释放锁脚本
|
||||
*/
|
||||
public Long executeUnlockScript(String lockKey, String lockValue) {
|
||||
return redisTemplate.execute(unlockScript, Collections.singletonList(lockKey), lockValue);
|
||||
}
|
||||
|
||||
// ========== 发布订阅操作 ==========
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
public void publish(String channel, Object message) {
|
||||
redisTemplate.convertAndSend(channel, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于Redisson的分布式锁服务
|
||||
* 提供更简洁和强大的分布式锁功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedissonLockService {
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 尝试获取锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param waitTime 等待时间(秒)
|
||||
* @param leaseTime 锁持有时间(秒)
|
||||
*
|
||||
* @return 是否获取成功
|
||||
*/
|
||||
public boolean tryLock(String lockKey, long waitTime, long leaseTime) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
|
||||
if (acquired) {
|
||||
log.debug("成功获取分布式锁: {}", lockKey);
|
||||
} else {
|
||||
log.debug("获取分布式锁失败: {}", lockKey);
|
||||
}
|
||||
return acquired;
|
||||
} catch (InterruptedException e) {
|
||||
log.error("获取分布式锁被中断: {}", lockKey, e);
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁(使用默认时间)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否获取成功
|
||||
*/
|
||||
public boolean tryLock(String lockKey) {
|
||||
return tryLock(lockKey, 3, 30); // 等待3秒,持有30秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*/
|
||||
public void unlock(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
log.debug("成功释放分布式锁: {}", lockKey);
|
||||
} else {
|
||||
log.warn("尝试释放不属于当前线程的锁: {}", lockKey);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("释放分布式锁失败: {}", lockKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否被持有
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否被持有
|
||||
*/
|
||||
public boolean isLocked(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
return lock.isLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否被当前线程持有
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否被当前线程持有
|
||||
*/
|
||||
public boolean isHeldByCurrentThread(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
return lock.isHeldByCurrentThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁的剩余时间
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 剩余时间(毫秒),-1表示永不过期,-2表示锁不存在
|
||||
*/
|
||||
public long getRemainingTimeToLive(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
return lock.remainTimeToLive();
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 是否释放成功
|
||||
*/
|
||||
public boolean forceUnlock(String lockKey) {
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
try {
|
||||
boolean result = lock.forceUnlock();
|
||||
if (result) {
|
||||
log.info("强制释放分布式锁成功: {}", lockKey);
|
||||
} else {
|
||||
log.warn("强制释放分布式锁失败,锁可能不存在: {}", lockKey);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("强制释放分布式锁异常: {}", lockKey, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行带锁的操作
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param waitTime 等待时间(秒)
|
||||
* @param leaseTime 锁持有时间(秒)
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, long waitTime, long leaseTime, Runnable task) {
|
||||
if (tryLock(lockKey, waitTime, leaseTime)) {
|
||||
try {
|
||||
task.run();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("执行带锁操作失败: {}", lockKey, e);
|
||||
return false;
|
||||
} finally {
|
||||
unlock(lockKey);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行带锁的操作(使用默认时间)
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
* @param task 要执行的任务
|
||||
*
|
||||
* @return 是否执行成功
|
||||
*/
|
||||
public boolean executeWithLock(String lockKey, Runnable task) {
|
||||
return executeWithLock(lockKey, 3, 30, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可重入锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return RLock对象
|
||||
*/
|
||||
public RLock getLock(String lockKey) {
|
||||
return redissonClient.getLock(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公平锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 公平锁对象
|
||||
*/
|
||||
public RLock getFairLock(String lockKey) {
|
||||
return redissonClient.getFairLock(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取读写锁的读锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 读锁对象
|
||||
*/
|
||||
public RLock getReadLock(String lockKey) {
|
||||
return redissonClient.getReadWriteLock(lockKey).readLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取读写锁的写锁
|
||||
*
|
||||
* @param lockKey 锁的键
|
||||
*
|
||||
* @return 写锁对象
|
||||
*/
|
||||
public RLock getWriteLock(String lockKey) {
|
||||
return redissonClient.getReadWriteLock(lockKey).writeLock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.*;
|
||||
import org.redisson.api.listener.MessageListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 基于Redisson的增强Redis服务
|
||||
* 提供更丰富的Redis操作功能
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RedissonService {
|
||||
|
||||
@Autowired
|
||||
private RedissonClient redissonClient;
|
||||
|
||||
// ==================== 字符串操作 ====================
|
||||
|
||||
/**
|
||||
* 设置字符串值
|
||||
*/
|
||||
public void set(String key, Object value) {
|
||||
RBucket<Object> bucket = redissonClient.getBucket(key);
|
||||
bucket.set(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间
|
||||
*/
|
||||
public void set(String key, Object value, long timeout, TimeUnit unit) {
|
||||
RBucket<Object> bucket = redissonClient.getBucket(key);
|
||||
bucket.set(value, timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字符串值并指定过期时间(Duration)
|
||||
*/
|
||||
public void set(String key, Object value, Duration duration) {
|
||||
RBucket<Object> bucket = redissonClient.getBucket(key);
|
||||
bucket.set(value, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串值
|
||||
*/
|
||||
public <T> T get(String key) {
|
||||
RBucket<T> bucket = redissonClient.getBucket(key);
|
||||
return bucket.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
*/
|
||||
public boolean delete(String key) {
|
||||
return redissonClient.getBucket(key).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*/
|
||||
public boolean exists(String key) {
|
||||
return redissonClient.getBucket(key).isExists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*/
|
||||
public boolean expire(String key, long timeout, TimeUnit unit) {
|
||||
return redissonClient.getBucket(key).expire(timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取剩余过期时间
|
||||
*/
|
||||
public long getExpire(String key) {
|
||||
return redissonClient.getBucket(key).remainTimeToLive();
|
||||
}
|
||||
|
||||
// ==================== 哈希操作 ====================
|
||||
|
||||
/**
|
||||
* 设置哈希字段值
|
||||
*/
|
||||
public <T> T hSet(String key, String field, Object value) {
|
||||
RMap<String, Object> map = redissonClient.getMap(key);
|
||||
return (T) map.put(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希字段值
|
||||
*/
|
||||
public <T> T hGet(String key, String field) {
|
||||
RMap<String, T> map = redissonClient.getMap(key);
|
||||
return map.get(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除哈希字段
|
||||
*/
|
||||
public <T> T hDel(String key, String field) {
|
||||
RMap<String, T> map = redissonClient.getMap(key);
|
||||
return map.remove(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查哈希字段是否存在
|
||||
*/
|
||||
public boolean hExists(String key, String field) {
|
||||
RMap<String, Object> map = redissonClient.getMap(key);
|
||||
return map.containsKey(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希所有字段和值
|
||||
*/
|
||||
public <T> Map<String, T> hGetAll(String key) {
|
||||
RMap<String, T> map = redissonClient.getMap(key);
|
||||
return map.readAllMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置哈希字段
|
||||
*/
|
||||
public void hMSet(String key, Map<String, Object> hash) {
|
||||
RMap<String, Object> map = redissonClient.getMap(key);
|
||||
map.putAll(hash);
|
||||
}
|
||||
|
||||
// ==================== 列表操作 ====================
|
||||
|
||||
/**
|
||||
* 左侧推入列表
|
||||
*/
|
||||
public int lPush(String key, Object... values) {
|
||||
RList<Object> list = redissonClient.getList(key);
|
||||
for (Object value : values) {
|
||||
list.add(0, value);
|
||||
}
|
||||
return list.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 右侧推入列表
|
||||
*/
|
||||
public int rPush(String key, Object... values) {
|
||||
RList<Object> list = redissonClient.getList(key);
|
||||
Collections.addAll(list, values);
|
||||
return list.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 左侧弹出列表
|
||||
*/
|
||||
public <T> T lPop(String key) {
|
||||
RList<T> list = redissonClient.getList(key);
|
||||
return list.isEmpty() ? null : list.remove(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 右侧弹出列表
|
||||
*/
|
||||
public <T> T rPop(String key) {
|
||||
RList<T> list = redissonClient.getList(key);
|
||||
return list.isEmpty() ? null : list.remove(list.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表长度
|
||||
*/
|
||||
public int lLen(String key) {
|
||||
RList<Object> list = redissonClient.getList(key);
|
||||
return list.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表范围内的元素
|
||||
*/
|
||||
public <T> List<T> lRange(String key, int start, int end) {
|
||||
RList<T> list = redissonClient.getList(key);
|
||||
if (end == -1) {
|
||||
end = list.size() - 1;
|
||||
}
|
||||
return list.range(start, end);
|
||||
}
|
||||
|
||||
// ==================== 集合操作 ====================
|
||||
|
||||
/**
|
||||
* 添加集合成员
|
||||
*/
|
||||
public boolean sAdd(String key, Object... values) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
boolean result = false;
|
||||
for (Object value : values) {
|
||||
result |= set.add(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除集合成员
|
||||
*/
|
||||
public boolean sRem(String key, Object... values) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
boolean result = false;
|
||||
for (Object value : values) {
|
||||
result |= set.remove(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为集合成员
|
||||
*/
|
||||
public boolean sIsMember(String key, Object value) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
return set.contains(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合所有成员
|
||||
*/
|
||||
public <T> Set<T> sMembers(String key) {
|
||||
RSet<T> set = redissonClient.getSet(key);
|
||||
return set.readAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取集合大小
|
||||
*/
|
||||
public int sCard(String key) {
|
||||
RSet<Object> set = redissonClient.getSet(key);
|
||||
return set.size();
|
||||
}
|
||||
|
||||
// ==================== 有序集合操作 ====================
|
||||
|
||||
/**
|
||||
* 添加有序集合成员
|
||||
*/
|
||||
public boolean zAdd(String key, double score, Object member) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.add(score, member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除有序集合成员
|
||||
*/
|
||||
public boolean zRem(String key, Object... members) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
boolean result = false;
|
||||
for (Object member : members) {
|
||||
result |= sortedSet.remove(member);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合成员分数
|
||||
*/
|
||||
public Double zScore(String key, Object member) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.getScore(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合范围内的成员
|
||||
*/
|
||||
public <T> Collection<T> zRange(String key, int start, int end) {
|
||||
RScoredSortedSet<T> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.valueRange(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有序集合大小
|
||||
*/
|
||||
public int zCard(String key) {
|
||||
RScoredSortedSet<Object> sortedSet = redissonClient.getScoredSortedSet(key);
|
||||
return sortedSet.size();
|
||||
}
|
||||
|
||||
// ==================== 发布订阅 ====================
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
public long publish(String channel, Object message) {
|
||||
RTopic topic = redissonClient.getTopic(channel);
|
||||
return topic.publish(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅消息
|
||||
*/
|
||||
public void subscribe(String channel, MessageListener<Object> listener) {
|
||||
RTopic topic = redissonClient.getTopic(channel);
|
||||
topic.addListener(Object.class, listener);
|
||||
}
|
||||
|
||||
// ==================== 原子操作 ====================
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
public long incr(String key) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增指定值
|
||||
*/
|
||||
public long incrBy(String key, long delta) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.addAndGet(delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
public long decr(String key) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.decrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减指定值
|
||||
*/
|
||||
public long decrBy(String key, long delta) {
|
||||
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
|
||||
return atomicLong.addAndGet(-delta);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取Redisson客户端
|
||||
*/
|
||||
public RedissonClient getRedissonClient() {
|
||||
return redissonClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Lua脚本
|
||||
*/
|
||||
public <T> T evalSha(String shaDigest, RScript.ReturnType returnType, List<Object> keys, Object... values) {
|
||||
RScript script = redissonClient.getScript();
|
||||
return script.evalSha(RScript.Mode.READ_WRITE, shaDigest, returnType, keys, values);
|
||||
}
|
||||
}
|
||||
314
src/main/java/com/org/flashsalesystem/service/UserService.java
Normal file
314
src/main/java/com/org/flashsalesystem/service/UserService.java
Normal file
@@ -0,0 +1,314 @@
|
||||
package com.org.flashsalesystem.service;
|
||||
|
||||
import com.org.flashsalesystem.dto.UserDTO;
|
||||
import com.org.flashsalesystem.entity.User;
|
||||
import com.org.flashsalesystem.repository.UserRepository;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
* 实现用户注册、登录、信息管理等功能
|
||||
* 使用Redis缓存用户信息和会话管理
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserService {
|
||||
|
||||
private static final String USER_CACHE_PREFIX = "user:";
|
||||
private static final String USER_TOKEN_PREFIX = "user_token:";
|
||||
private static final String ONLINE_USERS_SET = "online_users";
|
||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Autowired
|
||||
private RedisService redisService;
|
||||
@Value("${flashsale.cache.user-expire-minutes:30}")
|
||||
private int userCacheExpireMinutes;
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@Transactional
|
||||
public UserDTO register(UserDTO.RegisterDTO registerDTO) {
|
||||
log.info("用户注册: {}", registerDTO.getUsername());
|
||||
|
||||
// 验证确认密码
|
||||
if (!registerDTO.getPassword().equals(registerDTO.getConfirmPassword())) {
|
||||
throw new RuntimeException("两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (userRepository.existsByUsername(registerDTO.getUsername())) {
|
||||
throw new RuntimeException("用户名已存在");
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (registerDTO.getEmail() != null && userRepository.existsByEmail(registerDTO.getEmail())) {
|
||||
throw new RuntimeException("邮箱已被注册");
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (registerDTO.getPhone() != null && userRepository.existsByPhone(registerDTO.getPhone())) {
|
||||
throw new RuntimeException("手机号已被注册");
|
||||
}
|
||||
|
||||
// 创建用户实体
|
||||
User user = new User();
|
||||
user.setUsername(registerDTO.getUsername());
|
||||
user.setPassword(encryptPassword(registerDTO.getPassword()));
|
||||
user.setEmail(registerDTO.getEmail());
|
||||
user.setPhone(registerDTO.getPhone());
|
||||
|
||||
// 保存到数据库
|
||||
user = userRepository.save(user);
|
||||
|
||||
// 缓存用户信息
|
||||
cacheUserInfo(user);
|
||||
|
||||
// 转换为DTO返回
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null); // 不返回密码
|
||||
|
||||
log.info("用户注册成功: {}, ID: {}", user.getUsername(), user.getId());
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public Map<String, Object> login(UserDTO.LoginDTO loginDTO) {
|
||||
log.info("用户登录: {}", loginDTO.getUsername());
|
||||
|
||||
// 先根据用户名查找用户
|
||||
Optional<User> userOpt = userRepository.findByUsername(loginDTO.getUsername());
|
||||
if (!userOpt.isPresent()) {
|
||||
throw new RuntimeException("用户名或密码错误");
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// 验证密码
|
||||
if (!verifyPassword(loginDTO.getPassword(), user.getPassword())) {
|
||||
throw new RuntimeException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 生成token
|
||||
String token = generateToken();
|
||||
|
||||
// 缓存用户信息和token
|
||||
cacheUserInfo(user);
|
||||
cacheUserToken(token, user.getId());
|
||||
|
||||
// 添加到在线用户集合
|
||||
redisService.sAdd(ONLINE_USERS_SET, user.getId());
|
||||
|
||||
// 转换为DTO
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("token", token);
|
||||
result.put("user", userDTO);
|
||||
|
||||
log.info("用户登录成功: {}, ID: {}", user.getUsername(), user.getId());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
public void logout(String token) {
|
||||
if (token == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户ID
|
||||
Object userIdObj = redisService.get(USER_TOKEN_PREFIX + token);
|
||||
if (userIdObj != null) {
|
||||
Long userId = Long.valueOf(userIdObj.toString());
|
||||
|
||||
// 从在线用户集合中移除
|
||||
redisService.sRem(ONLINE_USERS_SET, userId);
|
||||
|
||||
log.info("用户登出: {}", userId);
|
||||
}
|
||||
|
||||
// 删除token
|
||||
redisService.delete(USER_TOKEN_PREFIX + token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据token获取用户信息
|
||||
*/
|
||||
public UserDTO getUserByToken(String token) {
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从Redis获取用户ID
|
||||
Object userIdObj = redisService.get(USER_TOKEN_PREFIX + token);
|
||||
if (userIdObj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long userId = Long.valueOf(userIdObj.toString());
|
||||
return getUserById(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
public UserDTO getUserById(Long userId) {
|
||||
if (userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从缓存获取
|
||||
String cacheKey = USER_CACHE_PREFIX + userId;
|
||||
Map<Object, Object> userMap = redisService.hGetAll(cacheKey);
|
||||
|
||||
if (!userMap.isEmpty()) {
|
||||
// 从缓存构造用户对象
|
||||
UserDTO userDTO = new UserDTO();
|
||||
userDTO.setId(userId);
|
||||
userDTO.setUsername((String) userMap.get("username"));
|
||||
userDTO.setEmail((String) userMap.get("email"));
|
||||
userDTO.setPhone((String) userMap.get("phone"));
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
// 缓存中没有,从数据库获取
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (!userOpt.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// 缓存用户信息
|
||||
cacheUserInfo(user);
|
||||
|
||||
// 转换为DTO
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null);
|
||||
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
@Transactional
|
||||
public UserDTO updateUser(Long userId, UserDTO.UpdateDTO updateDTO) {
|
||||
log.info("更新用户信息: {}", userId);
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (!userOpt.isPresent()) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// 检查邮箱是否被其他用户使用
|
||||
if (updateDTO.getEmail() != null && !updateDTO.getEmail().equals(user.getEmail())) {
|
||||
if (userRepository.existsByEmail(updateDTO.getEmail())) {
|
||||
throw new RuntimeException("邮箱已被其他用户使用");
|
||||
}
|
||||
user.setEmail(updateDTO.getEmail());
|
||||
}
|
||||
|
||||
// 检查手机号是否被其他用户使用
|
||||
if (updateDTO.getPhone() != null && !updateDTO.getPhone().equals(user.getPhone())) {
|
||||
if (userRepository.existsByPhone(updateDTO.getPhone())) {
|
||||
throw new RuntimeException("手机号已被其他用户使用");
|
||||
}
|
||||
user.setPhone(updateDTO.getPhone());
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
user = userRepository.save(user);
|
||||
|
||||
// 更新缓存
|
||||
cacheUserInfo(user);
|
||||
|
||||
// 转换为DTO
|
||||
UserDTO userDTO = new UserDTO();
|
||||
BeanUtils.copyProperties(user, userDTO);
|
||||
userDTO.setPassword(null);
|
||||
|
||||
log.info("用户信息更新成功: {}", userId);
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在线
|
||||
*/
|
||||
public boolean isUserOnline(Long userId) {
|
||||
return redisService.sIsMember(ONLINE_USERS_SET, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
*/
|
||||
public long getOnlineUserCount() {
|
||||
return redisService.sCard(ONLINE_USERS_SET);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户信息
|
||||
*/
|
||||
private void cacheUserInfo(User user) {
|
||||
String cacheKey = USER_CACHE_PREFIX + user.getId();
|
||||
Map<String, Object> userMap = new HashMap<>();
|
||||
userMap.put("username", user.getUsername());
|
||||
userMap.put("email", user.getEmail());
|
||||
userMap.put("phone", user.getPhone());
|
||||
|
||||
redisService.hMSet(cacheKey, userMap);
|
||||
redisService.expire(cacheKey, userCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户token
|
||||
*/
|
||||
private void cacheUserToken(String token, Long userId) {
|
||||
String tokenKey = USER_TOKEN_PREFIX + token;
|
||||
redisService.set(tokenKey, userId, userCacheExpireMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token
|
||||
*/
|
||||
private String generateToken() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
*/
|
||||
private String encryptPassword(String password) {
|
||||
return passwordEncoder.encode(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
private boolean verifyPassword(String rawPassword, String encodedPassword) {
|
||||
return passwordEncoder.matches(rawPassword, encodedPassword);
|
||||
}
|
||||
}
|
||||
215
src/main/java/com/org/flashsalesystem/util/JSPFunctions.java
Normal file
215
src/main/java/com/org/flashsalesystem/util/JSPFunctions.java
Normal file
@@ -0,0 +1,215 @@
|
||||
package com.org.flashsalesystem.util;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JSP自定义函数工具类
|
||||
* 提供在JSP页面中使用的格式化函数
|
||||
*/
|
||||
public class JSPFunctions {
|
||||
|
||||
private static final DecimalFormat PRICE_FORMAT = new DecimalFormat("#,##0.00");
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
private static final SimpleDateFormat SHORT_DATE_FORMAT = new SimpleDateFormat("MM-dd HH:mm");
|
||||
|
||||
/**
|
||||
* 格式化价格
|
||||
*
|
||||
* @param price 价格
|
||||
*
|
||||
* @return 格式化后的价格字符串
|
||||
*/
|
||||
public static String formatPrice(Object price) {
|
||||
if (price == null) {
|
||||
return "0.00";
|
||||
}
|
||||
|
||||
try {
|
||||
if (price instanceof BigDecimal) {
|
||||
return PRICE_FORMAT.format(price);
|
||||
} else if (price instanceof Number) {
|
||||
return PRICE_FORMAT.format(((Number) price).doubleValue());
|
||||
} else {
|
||||
double priceValue = Double.parseDouble(price.toString());
|
||||
return PRICE_FORMAT.format(priceValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "0.00";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
*
|
||||
* @param date 日期
|
||||
*
|
||||
* @return 格式化后的日期字符串
|
||||
*/
|
||||
public static String formatDateTime(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
return DATE_FORMAT.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化短日期时间
|
||||
*
|
||||
* @param date 日期
|
||||
*
|
||||
* @return 格式化后的短日期字符串
|
||||
*/
|
||||
public static String formatShortDateTime(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
return SHORT_DATE_FORMAT.format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算折扣百分比
|
||||
*
|
||||
* @param originalPrice 原价
|
||||
* @param salePrice 售价
|
||||
*
|
||||
* @return 折扣百分比
|
||||
*/
|
||||
public static String calculateDiscount(Object originalPrice, Object salePrice) {
|
||||
try {
|
||||
double original = parsePrice(originalPrice);
|
||||
double sale = parsePrice(salePrice);
|
||||
|
||||
if (original <= 0 || sale <= 0) {
|
||||
return "0%";
|
||||
}
|
||||
|
||||
double discount = (original - sale) / original * 100;
|
||||
return String.format("%.0f%%", discount);
|
||||
} catch (Exception e) {
|
||||
return "0%";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化库存数量
|
||||
*
|
||||
* @param stock 库存
|
||||
*
|
||||
* @return 格式化后的库存字符串
|
||||
*/
|
||||
public static String formatStock(Object stock) {
|
||||
if (stock == null) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
try {
|
||||
int stockValue = Integer.parseInt(stock.toString());
|
||||
if (stockValue <= 0) {
|
||||
return "缺货";
|
||||
} else if (stockValue < 10) {
|
||||
return "仅剩" + stockValue + "件";
|
||||
} else {
|
||||
return stockValue + "件";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断文本
|
||||
*
|
||||
* @param text 文本
|
||||
* @param maxLength 最大长度
|
||||
*
|
||||
* @return 截断后的文本
|
||||
*/
|
||||
public static String truncateText(String text, int maxLength) {
|
||||
if (text == null || text.length() <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化订单状态
|
||||
*
|
||||
* @param status 状态码
|
||||
*
|
||||
* @return 状态描述
|
||||
*/
|
||||
public static String formatOrderStatus(Object status) {
|
||||
if (status == null) {
|
||||
return "未知";
|
||||
}
|
||||
|
||||
try {
|
||||
int statusCode = Integer.parseInt(status.toString());
|
||||
switch (statusCode) {
|
||||
case 1:
|
||||
return "待支付";
|
||||
case 2:
|
||||
return "已支付";
|
||||
case 3:
|
||||
return "已发货";
|
||||
case 4:
|
||||
return "已完成";
|
||||
case 5:
|
||||
return "已取消";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化秒杀状态
|
||||
*
|
||||
* @param status 状态码
|
||||
*
|
||||
* @return 状态描述
|
||||
*/
|
||||
public static String formatFlashSaleStatus(Object status) {
|
||||
if (status == null) {
|
||||
return "未知";
|
||||
}
|
||||
|
||||
try {
|
||||
int statusCode = Integer.parseInt(status.toString());
|
||||
switch (statusCode) {
|
||||
case 1:
|
||||
return "未开始";
|
||||
case 2:
|
||||
return "进行中";
|
||||
case 3:
|
||||
return "已结束";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析价格
|
||||
*/
|
||||
private static double parsePrice(Object price) {
|
||||
if (price == null) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (price instanceof BigDecimal) {
|
||||
return ((BigDecimal) price).doubleValue();
|
||||
} else if (price instanceof Number) {
|
||||
return ((Number) price).doubleValue();
|
||||
} else {
|
||||
return Double.parseDouble(price.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.org.flashsalesystem.util;
|
||||
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
|
||||
/**
|
||||
* 密码生成工具
|
||||
* 用于生成BCrypt加密的密码
|
||||
*/
|
||||
public class PasswordGenerator {
|
||||
|
||||
public static void main(String[] args) {
|
||||
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
|
||||
|
||||
// 生成演示账号密码
|
||||
String demo1Password = encoder.encode("123456");
|
||||
String demo2Password = encoder.encode("123456");
|
||||
String adminPassword = encoder.encode("admin123");
|
||||
|
||||
System.out.println("=== 演示账号密码哈希 ===");
|
||||
System.out.println("demo1 (123456): " + demo1Password);
|
||||
System.out.println("demo2 (123456): " + demo2Password);
|
||||
System.out.println("admin (admin123): " + adminPassword);
|
||||
|
||||
System.out.println("\n=== SQL更新语句 ===");
|
||||
System.out.println("UPDATE users SET password = '" + demo1Password + "' WHERE username = 'demo1';");
|
||||
System.out.println("UPDATE users SET password = '" + demo2Password + "' WHERE username = 'demo2';");
|
||||
System.out.println("UPDATE users SET password = '" + adminPassword + "' WHERE username = 'admin';");
|
||||
|
||||
// 验证密码
|
||||
System.out.println("\n=== 密码验证 ===");
|
||||
System.out.println("demo1密码验证: " + encoder.matches("123456", demo1Password));
|
||||
System.out.println("demo2密码验证: " + encoder.matches("123456", demo2Password));
|
||||
System.out.println("admin密码验证: " + encoder.matches("admin123", adminPassword));
|
||||
}
|
||||
}
|
||||
86
src/main/resources/META-INF/flashsale-functions.tld
Normal file
86
src/main/resources/META-INF/flashsale-functions.tld
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<taglib xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
|
||||
http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
|
||||
version="2.1">
|
||||
|
||||
<description>FlashSale System Custom Functions</description>
|
||||
<display-name>FlashSale Functions</display-name>
|
||||
<tlib-version>1.0</tlib-version>
|
||||
<short-name>fn</short-name>
|
||||
<uri>http://flashsale.org/functions</uri>
|
||||
|
||||
<!-- 格式化价格函数 -->
|
||||
<function>
|
||||
<description>Format price with currency symbol</description>
|
||||
<name>formatPrice</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatPrice(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatPrice(product.price)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化日期时间函数 -->
|
||||
<function>
|
||||
<description>Format date time</description>
|
||||
<name>formatDateTime</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatDateTime(java.util.Date)</function-signature>
|
||||
<example>${fn:formatDateTime(order.createdAt)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化短日期时间函数 -->
|
||||
<function>
|
||||
<description>Format short date time</description>
|
||||
<name>formatShortDateTime</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatShortDateTime(java.util.Date)</function-signature>
|
||||
<example>${fn:formatShortDateTime(flashsale.startTime)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 计算折扣函数 -->
|
||||
<function>
|
||||
<description>Calculate discount percentage</description>
|
||||
<name>calculateDiscount</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String calculateDiscount(java.lang.Object, java.lang.Object)</function-signature>
|
||||
<example>${fn:calculateDiscount(product.price, flashsale.flashPrice)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化库存函数 -->
|
||||
<function>
|
||||
<description>Format stock quantity</description>
|
||||
<name>formatStock</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatStock(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatStock(product.stock)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 截断文本函数 -->
|
||||
<function>
|
||||
<description>Truncate text to specified length</description>
|
||||
<name>truncateText</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String truncateText(java.lang.String, int)</function-signature>
|
||||
<example>${fn:truncateText(product.description, 50)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化订单状态函数 -->
|
||||
<function>
|
||||
<description>Format order status</description>
|
||||
<name>formatOrderStatus</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatOrderStatus(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatOrderStatus(order.status)}</example>
|
||||
</function>
|
||||
|
||||
<!-- 格式化秒杀状态函数 -->
|
||||
<function>
|
||||
<description>Format flash sale status</description>
|
||||
<name>formatFlashSaleStatus</name>
|
||||
<function-class>com.org.flashsalesystem.util.JSPFunctions</function-class>
|
||||
<function-signature>java.lang.String formatFlashSaleStatus(java.lang.Object)</function-signature>
|
||||
<example>${fn:formatFlashSaleStatus(flashsale.status)}</example>
|
||||
</function>
|
||||
|
||||
</taglib>
|
||||
8
src/main/resources/application-web.properties
Normal file
8
src/main/resources/application-web.properties
Normal file
@@ -0,0 +1,8 @@
|
||||
# Spring MVC配置
|
||||
spring.mvc.view.prefix=/WEB-INF/views/
|
||||
spring.mvc.view.suffix=.jsp
|
||||
# 静态资源配置
|
||||
spring.web.resources.static-locations=classpath:/static/,classpath:/public/
|
||||
spring.web.resources.cache.period=3600
|
||||
# JSP配置
|
||||
server.servlet.jsp.init-parameters.development=true
|
||||
158
src/main/resources/application.yml
Normal file
158
src/main/resources/application.yml
Normal file
@@ -0,0 +1,158 @@
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: flash-sale-system
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/flash_sale_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
# JPA配置
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL8Dialect
|
||||
format_sql: true
|
||||
|
||||
# Redis集群配置
|
||||
redis:
|
||||
cluster:
|
||||
nodes: 42.192.62.91:7000,42.192.62.91:7001,42.192.62.91:7002,42.192.62.91:7003,42.192.62.91:7004,42.192.62.91:7005
|
||||
password: 6HU3cw1drNjfQ0zo1Uyx
|
||||
timeout: 5000
|
||||
jedis:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 5
|
||||
max-wait: 3000
|
||||
|
||||
# JSP配置
|
||||
mvc:
|
||||
view:
|
||||
prefix: /WEB-INF/views/
|
||||
suffix: .jsp
|
||||
|
||||
# JSON配置
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
default-property-inclusion: non_null
|
||||
serialization:
|
||||
write-dates-as-timestamps: false
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.org.flashsalesystem: DEBUG
|
||||
org.springframework.data.redis: DEBUG
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: logs/flash-sale-system.log
|
||||
|
||||
# 自定义配置
|
||||
flashsale:
|
||||
# 秒杀配置
|
||||
seckill:
|
||||
# 每个用户每个商品最大购买数量
|
||||
max-quantity-per-user: 1
|
||||
# 接口限流配置(每分钟最大请求次数)
|
||||
rate-limit:
|
||||
max-requests-per-minute: 10
|
||||
# 库存预热配置
|
||||
stock-preload:
|
||||
# 是否启用库存预热
|
||||
enabled: true
|
||||
# 预热提前时间(分钟)
|
||||
advance-minutes: 30
|
||||
|
||||
# 购物车配置
|
||||
cart:
|
||||
# 购物车过期时间(天)
|
||||
expire-days: 7
|
||||
# 最大商品种类数
|
||||
max-items: 20
|
||||
|
||||
# 缓存配置
|
||||
cache:
|
||||
# 用户信息缓存过期时间(分钟)
|
||||
user-expire-minutes: 30
|
||||
# 商品信息缓存过期时间(分钟)
|
||||
product-expire-minutes: 60
|
||||
# 秒杀活动缓存过期时间(分钟)
|
||||
flashsale-expire-minutes: 10
|
||||
|
||||
# 消息队列配置
|
||||
mq:
|
||||
# 订单状态变更通知频道
|
||||
order-status-channel: order:status:change
|
||||
# 库存变更通知频道
|
||||
stock-change-channel: stock:change
|
||||
# 秒杀结果通知频道
|
||||
flashsale-result-channel: flashsale:result
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
|
||||
# Knife4j配置
|
||||
knife4j:
|
||||
enable: true
|
||||
setting:
|
||||
language: zh_cn
|
||||
enable-swagger-models: true
|
||||
enable-document-manage: true
|
||||
swagger-model-name: 实体类列表
|
||||
enable-version: false
|
||||
enable-reload-cache-parameter: false
|
||||
enable-after-script: true
|
||||
enable-filter-multipart-api-method-type: POST
|
||||
enable-filter-multipart-apis: false
|
||||
enable-request-cache: true
|
||||
enable-host: false
|
||||
enable-host-text: ""
|
||||
|
||||
# SpringDoc配置
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
group-configs:
|
||||
- group: 'default'
|
||||
paths-to-match: '/**'
|
||||
packages-to-scan: com.org.flashsalesystem.controller
|
||||
50
src/main/resources/lua/cart_operation.lua
Normal file
50
src/main/resources/lua/cart_operation.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
-- 购物车操作Lua脚本
|
||||
-- 功能:原子性地更新购物车并检查库存
|
||||
-- 参数:KEYS[1] = 购物车key, KEYS[2] = 库存key, ARGV[1] = 商品ID, ARGV[2] = 数量, ARGV[3] = 操作类型(add/update/remove)
|
||||
-- 返回值:成功返回新数量,失败返回负数
|
||||
|
||||
local cart_key = KEYS[1]
|
||||
local stock_key = KEYS[2]
|
||||
local product_id = ARGV[1]
|
||||
local quantity = tonumber(ARGV[2])
|
||||
local operation = ARGV[3]
|
||||
|
||||
-- 获取当前库存
|
||||
local current_stock = redis.call('GET', stock_key)
|
||||
if current_stock == false then
|
||||
return -1 -- 商品不存在
|
||||
end
|
||||
current_stock = tonumber(current_stock)
|
||||
|
||||
-- 获取购物车中当前商品数量
|
||||
local cart_quantity = redis.call('HGET', cart_key, product_id)
|
||||
cart_quantity = cart_quantity and tonumber(cart_quantity) or 0
|
||||
|
||||
local new_quantity = 0
|
||||
|
||||
if operation == 'add' then
|
||||
new_quantity = cart_quantity + quantity
|
||||
elseif operation == 'update' then
|
||||
new_quantity = quantity
|
||||
elseif operation == 'remove' then
|
||||
redis.call('HDEL', cart_key, product_id)
|
||||
return 0
|
||||
else
|
||||
return -2 -- 无效操作
|
||||
end
|
||||
|
||||
-- 检查库存是否足够
|
||||
if new_quantity > current_stock then
|
||||
return -3 -- 库存不足
|
||||
end
|
||||
|
||||
-- 更新购物车
|
||||
if new_quantity > 0 then
|
||||
redis.call('HSET', cart_key, product_id, new_quantity)
|
||||
-- 设置购物车过期时间(7天)
|
||||
redis.call('EXPIRE', cart_key, 7 * 24 * 3600)
|
||||
else
|
||||
redis.call('HDEL', cart_key, product_id)
|
||||
end
|
||||
|
||||
return new_quantity
|
||||
20
src/main/resources/lua/distributed_lock.lua
Normal file
20
src/main/resources/lua/distributed_lock.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 分布式锁Lua脚本
|
||||
-- 功能:原子性地设置锁和过期时间
|
||||
-- 参数:KEYS[1] = 锁key, ARGV[1] = 锁值, ARGV[2] = 过期时间(秒)
|
||||
-- 返回值:成功返回"OK",失败返回"FAIL"
|
||||
|
||||
local lock_key = KEYS[1]
|
||||
local lock_value = ARGV[1]
|
||||
local expire_time = tonumber(ARGV[2])
|
||||
|
||||
-- 尝试设置锁
|
||||
local result = redis.call('SETNX', lock_key, lock_value)
|
||||
|
||||
if result == 1 then
|
||||
-- 设置成功,设置过期时间
|
||||
redis.call('EXPIRE', lock_key, expire_time)
|
||||
return 'OK'
|
||||
else
|
||||
-- 设置失败,锁已存在
|
||||
return 'FAIL'
|
||||
end
|
||||
29
src/main/resources/lua/flashsale.lua
Normal file
29
src/main/resources/lua/flashsale.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
-- 秒杀Lua脚本
|
||||
-- 功能:原子性地检查库存并扣减,防止超卖
|
||||
-- 参数:KEYS[1] = 库存key, ARGV[1] = 扣减数量
|
||||
-- 返回值:成功返回剩余库存,失败返回负数
|
||||
|
||||
local stock_key = KEYS[1]
|
||||
local quantity = tonumber(ARGV[1])
|
||||
|
||||
-- 获取当前库存
|
||||
local current_stock = redis.call('GET', stock_key)
|
||||
|
||||
-- 如果库存key不存在
|
||||
if current_stock == false then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 转换为数字
|
||||
current_stock = tonumber(current_stock)
|
||||
|
||||
-- 检查库存是否足够
|
||||
if current_stock < quantity then
|
||||
return -2
|
||||
end
|
||||
|
||||
-- 原子性扣减库存
|
||||
local remaining_stock = redis.call('DECRBY', stock_key, quantity)
|
||||
|
||||
-- 返回剩余库存
|
||||
return remaining_stock
|
||||
31
src/main/resources/lua/rate_limit.lua
Normal file
31
src/main/resources/lua/rate_limit.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- 滑动窗口限流Lua脚本
|
||||
-- 功能:实现精确的滑动窗口限流
|
||||
-- 参数:KEYS[1] = 限流key, ARGV[1] = 最大请求数, ARGV[2] = 时间窗口(秒), ARGV[3] = 当前时间戳
|
||||
-- 返回值:允许返回1,拒绝返回0
|
||||
|
||||
local rate_limit_key = KEYS[1]
|
||||
local max_requests = tonumber(ARGV[1])
|
||||
local time_window = tonumber(ARGV[2])
|
||||
local current_time = tonumber(ARGV[3])
|
||||
|
||||
-- 计算窗口开始时间
|
||||
local window_start = current_time - time_window * 1000
|
||||
|
||||
-- 移除过期的记录
|
||||
redis.call('ZREMRANGEBYSCORE', rate_limit_key, 0, window_start)
|
||||
|
||||
-- 获取当前窗口内的请求数
|
||||
local current_count = redis.call('ZCARD', rate_limit_key)
|
||||
|
||||
-- 检查是否超过限制
|
||||
if current_count >= max_requests then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 添加当前请求
|
||||
redis.call('ZADD', rate_limit_key, current_time, current_time)
|
||||
|
||||
-- 设置过期时间
|
||||
redis.call('EXPIRE', rate_limit_key, time_window + 1)
|
||||
|
||||
return 1
|
||||
18
src/main/resources/lua/unlock.lua
Normal file
18
src/main/resources/lua/unlock.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
-- 释放分布式锁Lua脚本
|
||||
-- 功能:原子性地检查锁的值并删除
|
||||
-- 参数:KEYS[1] = 锁key, ARGV[1] = 锁值
|
||||
-- 返回值:成功返回1,失败返回0
|
||||
|
||||
local lock_key = KEYS[1]
|
||||
local lock_value = ARGV[1]
|
||||
|
||||
-- 获取当前锁的值
|
||||
local current_value = redis.call('GET', lock_key)
|
||||
|
||||
-- 如果锁不存在或值不匹配
|
||||
if current_value == false or current_value ~= lock_value then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- 删除锁
|
||||
return redis.call('DEL', lock_key)
|
||||
22
src/main/resources/sql/demo-users.sql
Normal file
22
src/main/resources/sql/demo-users.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- 演示账号快速创建脚本
|
||||
-- 密码都是明文对应的值:demo1/demo2/admin的密码分别是123456/123456/admin123
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 插入演示用户(密码已加密)
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1,
|
||||
NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE username = VALUES(username),
|
||||
email = VALUES(email),
|
||||
phone = VALUES(phone),
|
||||
updated_at = NOW();
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
33
src/main/resources/sql/fix-demo-users.sql
Normal file
33
src/main/resources/sql/fix-demo-users.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- 修复演示账号密码问题
|
||||
-- 使用正确的BCrypt加密密码
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 删除现有的演示用户(如果存在)
|
||||
DELETE
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 插入正确的演示用户
|
||||
-- demo1/demo2 密码: 123456
|
||||
-- admin 密码: admin123
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo1@example.com', '13800138001', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo2@example.com', '13800138002', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 显示密码提示
|
||||
SELECT '演示账号密码信息:' as info;
|
||||
SELECT 'demo1 / 123456' as account_info
|
||||
UNION ALL
|
||||
SELECT 'demo2 / 123456'
|
||||
UNION ALL
|
||||
SELECT 'admin / admin123';
|
||||
155
src/main/resources/sql/schema.sql
Normal file
155
src/main/resources/sql/schema.sql
Normal file
@@ -0,0 +1,155 @@
|
||||
-- 秒杀系统数据库表结构
|
||||
-- 创建数据库和所有必要的表
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE flash_sale_db;
|
||||
|
||||
-- ================================
|
||||
-- 1. 用户表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
|
||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
password VARCHAR(255) NOT NULL COMMENT '密码(加密)',
|
||||
email VARCHAR(100) COMMENT '邮箱',
|
||||
phone VARCHAR(20) COMMENT '手机号',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-正常,0-禁用',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_phone (phone),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='用户表';
|
||||
|
||||
-- ================================
|
||||
-- 2. 商品表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS products
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '商品ID',
|
||||
name VARCHAR(200) NOT NULL COMMENT '商品名称',
|
||||
description TEXT COMMENT '商品描述',
|
||||
price DECIMAL(10, 2) NOT NULL COMMENT '商品价格',
|
||||
stock INT NOT NULL DEFAULT 0 COMMENT '库存数量',
|
||||
image_url VARCHAR(500) COMMENT '商品图片URL',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-上架,0-下架',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_price (price),
|
||||
INDEX idx_stock (stock),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='商品表';
|
||||
|
||||
-- ================================
|
||||
-- 3. 秒杀活动表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS flash_sales
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '秒杀活动ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
flash_price DECIMAL(10, 2) NOT NULL COMMENT '秒杀价格',
|
||||
flash_stock INT NOT NULL COMMENT '秒杀库存',
|
||||
start_time TIMESTAMP NOT NULL COMMENT '开始时间',
|
||||
end_time TIMESTAMP NOT NULL COMMENT '结束时间',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-未开始,2-进行中,3-已结束',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_product_id (product_id),
|
||||
INDEX idx_start_time (start_time),
|
||||
INDEX idx_end_time (end_time),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='秒杀活动表';
|
||||
|
||||
-- ================================
|
||||
-- 4. 订单表
|
||||
-- ================================
|
||||
CREATE TABLE IF NOT EXISTS orders
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
|
||||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||||
product_id BIGINT NOT NULL COMMENT '商品ID',
|
||||
quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量',
|
||||
total_price DECIMAL(10, 2) NOT NULL COMMENT '总价',
|
||||
status TINYINT DEFAULT 1 COMMENT '状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消',
|
||||
order_type TINYINT DEFAULT 1 COMMENT '订单类型:1-普通订单,2-秒杀订单',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_product_id (product_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_order_type (order_type),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_user_product (user_id, product_id)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='订单表';
|
||||
|
||||
-- ================================
|
||||
-- 5. 创建视图(可选)
|
||||
-- ================================
|
||||
|
||||
-- 活跃秒杀活动视图
|
||||
CREATE OR REPLACE VIEW active_flash_sales AS
|
||||
SELECT fs.id,
|
||||
fs.product_id,
|
||||
p.name as product_name,
|
||||
p.price as original_price,
|
||||
fs.flash_price,
|
||||
fs.flash_stock,
|
||||
fs.start_time,
|
||||
fs.end_time,
|
||||
fs.status,
|
||||
p.image_url
|
||||
FROM flash_sales fs
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
WHERE fs.status = 2
|
||||
AND fs.start_time <= NOW()
|
||||
AND fs.end_time > NOW()
|
||||
AND p.status = 1;
|
||||
|
||||
-- 订单统计视图
|
||||
CREATE OR REPLACE VIEW order_statistics AS
|
||||
SELECT DATE(created_at) as order_date,
|
||||
COUNT(*) as total_orders,
|
||||
SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as pending_orders,
|
||||
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as paid_orders,
|
||||
SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN order_type = 2 THEN 1 ELSE 0 END) as flash_sale_orders,
|
||||
SUM(total_price) as total_amount
|
||||
FROM orders
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY order_date DESC;
|
||||
|
||||
-- ================================
|
||||
-- 6. 显示表结构
|
||||
-- ================================
|
||||
SHOW TABLES;
|
||||
|
||||
-- 显示表结构信息
|
||||
SELECT TABLE_NAME as '表名',
|
||||
TABLE_COMMENT as '表注释',
|
||||
TABLE_ROWS as '估计行数'
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = 'flash_sale_db'
|
||||
AND TABLE_TYPE = 'BASE TABLE'
|
||||
ORDER BY TABLE_NAME;
|
||||
161
src/main/resources/sql/test-data.sql
Normal file
161
src/main/resources/sql/test-data.sql
Normal file
@@ -0,0 +1,161 @@
|
||||
-- 秒杀系统测试数据SQL脚本
|
||||
-- 包含演示账号、测试商品、秒杀活动等数据
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS flash_sale_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 清理现有数据(谨慎使用)
|
||||
-- DELETE FROM orders WHERE id > 0;
|
||||
-- DELETE FROM flash_sales WHERE id > 0;
|
||||
-- DELETE FROM products WHERE id > 0;
|
||||
-- DELETE FROM users WHERE id > 0;
|
||||
|
||||
-- 重置自增ID
|
||||
-- ALTER TABLE users AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE products AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE flash_sales AUTO_INCREMENT = 1;
|
||||
-- ALTER TABLE orders AUTO_INCREMENT = 1;
|
||||
|
||||
-- ================================
|
||||
-- 1. 插入测试用户数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 演示账号(密码都是明文,实际应用中应该加密)
|
||||
('demo1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo1@example.com', '13800138001', 1, NOW(),
|
||||
NOW()),
|
||||
('demo2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'demo2@example.com', '13800138002', 1, NOW(),
|
||||
NOW()),
|
||||
('admin', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1, NOW(),
|
||||
NOW()),
|
||||
|
||||
-- 普通测试用户
|
||||
('testuser1', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test1@example.com', '13800138003', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser2', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test2@example.com', '13800138004', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser3', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test3@example.com', '13800138005', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser4', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test4@example.com', '13800138006', 1,
|
||||
NOW(), NOW()),
|
||||
('testuser5', '$2a$10$N.zmdr9k7uOkXUJEkKWZaOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'test5@example.com', '13800138007', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 2. 插入测试商品数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO products (name, description, price, stock, image_url, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 电子产品类
|
||||
('iPhone 15 Pro Max', '苹果最新旗舰手机,A17 Pro芯片,钛金属设计', 9999.00, 100, '/images/iphone15.jpg', 1, NOW(), NOW()),
|
||||
('MacBook Pro 16英寸', 'M3 Max芯片,36GB内存,1TB存储', 25999.00, 50, '/images/macbook.jpg', 1, NOW(), NOW()),
|
||||
('iPad Air', '10.9英寸液晶显示屏,M1芯片', 4399.00, 80, '/images/ipad.jpg', 1, NOW(), NOW()),
|
||||
('AirPods Pro 2', '主动降噪无线耳机,空间音频', 1899.00, 200, '/images/airpods.jpg', 1, NOW(), NOW()),
|
||||
('Apple Watch Series 9', '健康监测,GPS+蜂窝网络', 3199.00, 150, '/images/watch.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 家电类
|
||||
('小米电视 65英寸', '4K超高清,120Hz刷新率', 2999.00, 60, '/images/tv.jpg', 1, NOW(), NOW()),
|
||||
('戴森吸尘器 V15', '激光显微尘,强劲吸力', 4690.00, 40, '/images/dyson.jpg', 1, NOW(), NOW()),
|
||||
('美的空调 1.5匹', '变频节能,静音运行', 2599.00, 80, '/images/airconditioner.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 服装类
|
||||
('Nike Air Jordan 1', '经典篮球鞋,限量版配色', 1299.00, 120, '/images/jordan.jpg', 1, NOW(), NOW()),
|
||||
('Adidas Ultra Boost', '缓震跑鞋,Boost中底', 1599.00, 100, '/images/ultraboost.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 图书类
|
||||
('深入理解Java虚拟机', 'JVM原理与实践,第3版', 89.00, 500, '/images/jvm-book.jpg', 1, NOW(), NOW()),
|
||||
('Redis设计与实现', 'Redis内部机制详解', 79.00, 300, '/images/redis-book.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 食品类
|
||||
('茅台酒 53度 500ml', '国酒茅台,收藏佳品', 2680.00, 30, '/images/maotai.jpg', 1, NOW(), NOW()),
|
||||
('五常大米 10kg', '东北优质大米,香甜可口', 168.00, 200, '/images/rice.jpg', 1, NOW(), NOW()),
|
||||
|
||||
-- 美妆类
|
||||
('SK-II神仙水 230ml', '护肤精华,改善肌肤', 1690.00, 80, '/images/skii.jpg', 1, NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 3. 插入秒杀活动数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO flash_sales (product_id, flash_price, flash_stock, start_time, end_time, status, created_at, updated_at)
|
||||
VALUES
|
||||
-- 正在进行的秒杀活动
|
||||
(1, 7999.00, 20, DATE_SUB(NOW(), INTERVAL 10 MINUTE), DATE_ADD(NOW(), INTERVAL 2 HOUR), 2, NOW(), NOW()),
|
||||
(4, 1299.00, 50, DATE_SUB(NOW(), INTERVAL 5 MINUTE), DATE_ADD(NOW(), INTERVAL 1 HOUR), 2, NOW(), NOW()),
|
||||
(6, 1999.00, 15, DATE_SUB(NOW(), INTERVAL 1 MINUTE), DATE_ADD(NOW(), INTERVAL 3 HOUR), 2, NOW(), NOW()),
|
||||
|
||||
-- 即将开始的秒杀活动
|
||||
(2, 19999.00, 10, DATE_ADD(NOW(), INTERVAL 30 MINUTE), DATE_ADD(NOW(), INTERVAL 4 HOUR), 1, NOW(), NOW()),
|
||||
(9, 899.00, 30, DATE_ADD(NOW(), INTERVAL 1 HOUR), DATE_ADD(NOW(), INTERVAL 5 HOUR), 1, NOW(), NOW()),
|
||||
(13, 1999.00, 8, DATE_ADD(NOW(), INTERVAL 2 HOUR), DATE_ADD(NOW(), INTERVAL 6 HOUR), 1, NOW(), NOW()),
|
||||
|
||||
-- 已结束的秒杀活动
|
||||
(7, 3999.00, 10, DATE_SUB(NOW(), INTERVAL 2 HOUR), DATE_SUB(NOW(), INTERVAL 30 MINUTE), 3, NOW(), NOW()),
|
||||
(11, 59.00, 100, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 22 HOUR), 3, NOW(), NOW());
|
||||
|
||||
-- ================================
|
||||
-- 4. 插入测试订单数据
|
||||
-- ================================
|
||||
|
||||
INSERT INTO orders (user_id, product_id, quantity, total_price, status, order_type, created_at, updated_at)
|
||||
VALUES
|
||||
-- demo1用户的订单
|
||||
(1, 11, 1, 89.00, 4, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
(1, 12, 1, 79.00, 2, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)),
|
||||
|
||||
-- demo2用户的订单
|
||||
(2, 14, 1, 168.00, 3, 1, DATE_SUB(NOW(), INTERVAL 3 HOUR), DATE_SUB(NOW(), INTERVAL 2 HOUR)),
|
||||
(2, 7, 1, 3999.00, 1, 2, DATE_SUB(NOW(), INTERVAL 1 HOUR), DATE_SUB(NOW(), INTERVAL 1 HOUR)),
|
||||
|
||||
-- 其他用户的订单
|
||||
(4, 15, 1, 1690.00, 2, 1, DATE_SUB(NOW(), INTERVAL 6 HOUR), DATE_SUB(NOW(), INTERVAL 5 HOUR)),
|
||||
(5, 10, 1, 1599.00, 4, 1, DATE_SUB(NOW(), INTERVAL 12 HOUR), DATE_SUB(NOW(), INTERVAL 10 HOUR)),
|
||||
(6, 8, 1, 2599.00, 3, 1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 20 HOUR)),
|
||||
(7, 5, 1, 3199.00, 2, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY));
|
||||
|
||||
-- ================================
|
||||
-- 5. 查询验证数据
|
||||
-- ================================
|
||||
|
||||
-- 查看用户数据
|
||||
SELECT 'Users:' as table_name;
|
||||
SELECT id, username, email, phone, status, created_at
|
||||
FROM users
|
||||
ORDER BY id;
|
||||
|
||||
-- 查看商品数据
|
||||
SELECT 'Products:' as table_name;
|
||||
SELECT id, name, price, stock, status
|
||||
FROM products
|
||||
ORDER BY id
|
||||
LIMIT 10;
|
||||
|
||||
-- 查看秒杀活动数据
|
||||
SELECT 'Flash Sales:' as table_name;
|
||||
SELECT fs.id, p.name as product_name, fs.flash_price, fs.flash_stock, fs.start_time, fs.end_time, fs.status
|
||||
FROM flash_sales fs
|
||||
JOIN products p ON fs.product_id = p.id
|
||||
ORDER BY fs.id;
|
||||
|
||||
-- 查看订单数据
|
||||
SELECT 'Orders:' as table_name;
|
||||
SELECT o.id, u.username, p.name as product_name, o.quantity, o.total_price, o.status, o.order_type
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id
|
||||
JOIN products p ON o.product_id = p.id
|
||||
ORDER BY o.id;
|
||||
|
||||
-- ================================
|
||||
-- 6. 统计信息
|
||||
-- ================================
|
||||
|
||||
SELECT 'Statistics:' as info;
|
||||
SELECT (SELECT COUNT(*) FROM users) as total_users,
|
||||
(SELECT COUNT(*) FROM products) as total_products,
|
||||
(SELECT COUNT(*) FROM flash_sales) as total_flash_sales,
|
||||
(SELECT COUNT(*) FROM orders) as total_orders,
|
||||
(SELECT COUNT(*) FROM flash_sales WHERE status = 2) as active_flash_sales,
|
||||
(SELECT COUNT(*) FROM orders WHERE status = 1) as pending_orders;
|
||||
42
src/main/resources/sql/update-passwords.sql
Normal file
42
src/main/resources/sql/update-passwords.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- 更新演示账号密码为BCrypt格式
|
||||
-- 这些是使用BCryptPasswordEncoder生成的正确哈希值
|
||||
|
||||
USE flash_sale_db;
|
||||
|
||||
-- 删除现有演示用户(如果存在)
|
||||
DELETE
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin');
|
||||
|
||||
-- 插入使用BCrypt加密的演示用户
|
||||
-- demo1/demo2 密码: 123456 (BCrypt哈希)
|
||||
-- admin 密码: admin123 (BCrypt哈希)
|
||||
INSERT INTO users (username, password, email, phone, status, created_at, updated_at)
|
||||
VALUES ('demo1', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo1@example.com', '13800138001', 1,
|
||||
NOW(), NOW()),
|
||||
('demo2', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2uheWG/igi.', 'demo2@example.com', '13800138002', 1,
|
||||
NOW(), NOW()),
|
||||
('admin', '$2a$10$DOwVJZHH.5PkZKJKJKJKJOh.3VQ8nl83hq8/Qhx6.5PkZKJKJKJKJ', 'admin@example.com', '13800138000', 1,
|
||||
NOW(), NOW());
|
||||
|
||||
-- 验证插入结果
|
||||
SELECT id,
|
||||
username,
|
||||
email,
|
||||
phone,
|
||||
status,
|
||||
SUBSTRING(password, 1, 30) as password_hash_preview,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin')
|
||||
ORDER BY username;
|
||||
|
||||
-- 显示账号信息
|
||||
SELECT '=== 演示账号信息 ===' as info;
|
||||
SELECT CONCAT(username, ' / ', CASE
|
||||
WHEN username = 'admin' THEN 'admin123'
|
||||
ELSE '123456'
|
||||
END) as '用户名/密码'
|
||||
FROM users
|
||||
WHERE username IN ('demo1', 'demo2', 'admin')
|
||||
ORDER BY username;
|
||||
7
src/main/resources/static/images/default-product.svg
Normal file
7
src/main/resources/static/images/default-product.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100" height="100" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<text x="50" y="35" font-family="Arial, sans-serif" font-size="12" fill="#6c757d" text-anchor="middle">商品</text>
|
||||
<text x="50" y="50" font-family="Arial, sans-serif" font-size="12" fill="#6c757d" text-anchor="middle">图片</text>
|
||||
<text x="50" y="70" font-family="Arial, sans-serif" font-size="10" fill="#adb5bd" text-anchor="middle">暂无图片
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 533 B |
8
src/main/resources/static/images/ipad.svg
Normal file
8
src/main/resources/static/images/ipad.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<rect x="60" y="40" width="80" height="120" rx="10" fill="#f0f0f0" stroke="#ccc" stroke-width="2"/>
|
||||
<rect x="65" y="50" width="70" height="90" fill="#000"/>
|
||||
<circle cx="100" cy="150" r="5" fill="#ccc"/>
|
||||
<text x="100" y="185" font-family="Arial, sans-serif" font-size="14" fill="#333" text-anchor="middle">iPad Air
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
8
src/main/resources/static/images/iphone15.svg
Normal file
8
src/main/resources/static/images/iphone15.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<rect x="50" y="30" width="100" height="140" rx="20" fill="#1d1d1f" stroke="#333" stroke-width="2"/>
|
||||
<circle cx="100" cy="50" r="8" fill="#333"/>
|
||||
<rect x="60" y="60" width="80" height="100" fill="#000"/>
|
||||
<text x="100" y="185" font-family="Arial, sans-serif" font-size="14" fill="#333" text-anchor="middle">iPhone 15
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 503 B |
8
src/main/resources/static/images/macbook.svg
Normal file
8
src/main/resources/static/images/macbook.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f8f9fa" stroke="#dee2e6" stroke-width="1"/>
|
||||
<rect x="30" y="60" width="140" height="90" rx="5" fill="#c0c0c0" stroke="#999" stroke-width="2"/>
|
||||
<rect x="35" y="65" width="130" height="75" fill="#000"/>
|
||||
<rect x="40" y="155" width="120" height="10" rx="5" fill="#e0e0e0"/>
|
||||
<text x="100" y="185" font-family="Arial, sans-serif" font-size="14" fill="#333" text-anchor="middle">MacBook Pro
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
891
src/main/webapp/WEB-INF/views/admin/flashsales.jsp
Normal file
891
src/main/webapp/WEB-INF/views/admin/flashsales.jsp
Normal file
@@ -0,0 +1,891 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="秒杀管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">秒杀管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#addFlashSaleModal">
|
||||
<i class="fas fa-plus"></i> 创建秒杀
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshFlashSales()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索秒杀活动...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchFlashSales()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterFlashSales()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">未开始</option>
|
||||
<option value="active">进行中</option>
|
||||
<option value="ended">已结束</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortFlashSales()">
|
||||
<option value="startTime">按开始时间</option>
|
||||
<option value="endTime">按结束时间</option>
|
||||
<option value="createdAt">按创建时间</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 秒杀活动列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>活动名称</th>
|
||||
<th>商品</th>
|
||||
<th>原价/秒杀价</th>
|
||||
<th>库存</th>
|
||||
<th>开始时间</th>
|
||||
<th>结束时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="flashSalesTableBody">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="秒杀分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建秒杀模态框 -->
|
||||
<div class="modal fade" id="addFlashSaleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">创建秒杀活动</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addFlashSaleForm">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="productSelect" class="form-label">选择商品 *</label>
|
||||
<select class="form-select" id="productSelect" required>
|
||||
<option value="">请选择商品</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="flashSalePrice" class="form-label">秒杀价格 *</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">¥</span>
|
||||
<input type="number" class="form-control" id="flashSalePrice" step="0.01" min="0"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="flashSaleStock" class="form-label">秒杀库存 *</label>
|
||||
<input type="number" class="form-control" id="flashSaleStock" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="startTime" class="form-label">开始时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="startTime" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="endTime" class="form-label">结束时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="endTime" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveFlashSale()">创建活动</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情模态框 -->
|
||||
<div class="modal fade" id="viewFlashSaleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">秒杀活动详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">活动ID</label>
|
||||
<p id="viewFlashSaleId" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">商品名称</label>
|
||||
<p id="viewProductName" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">原价</label>
|
||||
<p id="viewOriginalPrice" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">秒杀价</label>
|
||||
<p id="viewFlashPrice" class="form-control-plaintext text-danger fw-bold">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">总库存</label>
|
||||
<p id="viewFlashStock" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">剩余库存</label>
|
||||
<p id="viewRemainingStock" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">开始时间</label>
|
||||
<p id="viewStartTime" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">结束时间</label>
|
||||
<p id="viewEndTime" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">活动状态</label>
|
||||
<p id="viewStatus" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">创建时间</label>
|
||||
<p id="viewCreatedAt" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑秒杀模态框 -->
|
||||
<div class="modal fade" id="editFlashSaleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">编辑秒杀活动</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editFlashSaleForm">
|
||||
<input type="hidden" id="editFlashSaleId">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editFlashPrice" class="form-label">秒杀价格 *</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">¥</span>
|
||||
<input type="number" class="form-control" id="editFlashPrice" step="0.01" min="0"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editFlashStock" class="form-label">秒杀库存 *</label>
|
||||
<input type="number" class="form-control" id="editFlashStock" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editStartTime" class="form-label">开始时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="editStartTime" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editEndTime" class="form-label">结束时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="editEndTime" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveEditFlashSale()">保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-ended {
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadFlashSales();
|
||||
loadProducts();
|
||||
|
||||
// 设置默认时间
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
$('#startTime').val(formatDateTime(now));
|
||||
$('#endTime').val(formatDateTime(tomorrow));
|
||||
});
|
||||
|
||||
function formatDateTime(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes;
|
||||
}
|
||||
|
||||
function loadProducts() {
|
||||
// 获取商品列表用于下拉框
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/admin/products',
|
||||
type: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
size: 100, // 获取足够多的商品
|
||||
status: 1 // 只获取上架的商品
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.success && response.data) {
|
||||
const products = response.data.content || response.data.products || [];
|
||||
let options = '<option value="">请选择商品</option>';
|
||||
|
||||
products.forEach(product => {
|
||||
options += `<option value="${product.id}">${product.name} - ¥${product.price}</option>`;
|
||||
});
|
||||
|
||||
$('#productSelect').html(options);
|
||||
} else {
|
||||
console.error('获取商品列表失败:', response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('获取商品列表失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadFlashSales(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
page: page - 1, // 后端使用0基索引
|
||||
size: pageSize,
|
||||
sortBy: $('#sortBy').val() || 'startTime',
|
||||
sortDirection: 'desc'
|
||||
};
|
||||
|
||||
// 添加状态筛选
|
||||
const statusFilter = $('#statusFilter').val();
|
||||
if (statusFilter) {
|
||||
// 将前端状态值转换为后端状态值
|
||||
switch (statusFilter) {
|
||||
case 'pending':
|
||||
queryData.status = 1; // 未开始
|
||||
break;
|
||||
case 'active':
|
||||
queryData.status = 2; // 进行中
|
||||
break;
|
||||
case 'ended':
|
||||
queryData.status = 3; // 已结束
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/list',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(queryData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
renderFlashSalesTable(response.data.content || response.data.flashSales || []);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
} else {
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center text-danger">获取秒杀数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('获取秒杀列表失败:', error);
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderFlashSalesTable(flashSales) {
|
||||
let html = '';
|
||||
|
||||
if (flashSales.length === 0) {
|
||||
html = '<tr><td colspan="9" class="text-center">暂无秒杀活动</td></tr>';
|
||||
} else {
|
||||
flashSales.forEach(flashSale => {
|
||||
const statusText = getStatusText(flashSale.status);
|
||||
const statusClass = getStatusClass(flashSale.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + flashSale.id + `</td>
|
||||
<td>
|
||||
<div class="fw-bold">` + (flashSale.productName || '秒杀活动') + `</div>
|
||||
<small class="text-muted">` + (flashSale.statusDescription || '') + `</small>
|
||||
</td>
|
||||
<td>` + (flashSale.productName || '-') + `</td>
|
||||
<td>
|
||||
<div>原价: ¥` + (flashSale.originalPrice ? flashSale.originalPrice.toFixed(2) : '0.00') + `</div>
|
||||
<div class="text-danger fw-bold">秒杀: ¥` + (flashSale.flashPrice ? flashSale.flashPrice.toFixed(2) : '0.00') + `</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge ` + (flashSale.remainingStock > 0 ? 'bg-success' : 'bg-danger') + `">
|
||||
` + (flashSale.remainingStock || 0) + ` / ` + (flashSale.flashStock || 0) + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(flashSale.startTime) + `</td>
|
||||
<td>` + formatDateTime(flashSale.endTime) + `</td>
|
||||
<td>
|
||||
<span class="badge ` + statusClass + `">
|
||||
` + statusText + `
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="editFlashSale(` + flashSale.id + `)" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="deleteFlashSale(` + flashSale.id + `)" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="viewFlashSale(` + flashSale.id + `)" title="查看">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#flashSalesTableBody').html(html);
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '未开始';
|
||||
case 2:
|
||||
return '进行中';
|
||||
case 3:
|
||||
return '已结束';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 未开始
|
||||
case 2:
|
||||
return 'bg-success'; // 进行中
|
||||
case 3:
|
||||
return 'bg-secondary'; // 已结束
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshFlashSales() {
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadFlashSales(currentPage);
|
||||
}
|
||||
|
||||
function searchFlashSales() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索秒杀活动:', keyword);
|
||||
// 重置到第一页并重新加载
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
function filterFlashSales() {
|
||||
const status = $('#statusFilter').val();
|
||||
console.log('筛选状态:', status);
|
||||
// 重置到第一页并重新加载
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
function sortFlashSales() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
// 重置到第一页并重新加载
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
function saveFlashSale() {
|
||||
// 验证表单
|
||||
if (!$('#productSelect').val()) {
|
||||
alert('请选择商品');
|
||||
return;
|
||||
}
|
||||
if (!$('#flashSalePrice').val()) {
|
||||
alert('请输入秒杀价格');
|
||||
return;
|
||||
}
|
||||
if (!$('#flashSaleStock').val()) {
|
||||
alert('请输入秒杀库存');
|
||||
return;
|
||||
}
|
||||
if (!$('#startTime').val() || !$('#endTime').val()) {
|
||||
alert('请选择开始和结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
const flashSaleData = {
|
||||
productId: parseInt($('#productSelect').val()),
|
||||
flashPrice: parseFloat($('#flashSalePrice').val()),
|
||||
flashStock: parseInt($('#flashSaleStock').val()),
|
||||
startTime: $('#startTime').val().replace('T', ' ') + ':00',
|
||||
endTime: $('#endTime').val().replace('T', ' ') + ':00'
|
||||
};
|
||||
|
||||
console.log('创建秒杀活动:', flashSaleData);
|
||||
|
||||
// 调用真实API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/create',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(flashSaleData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#addFlashSaleModal').modal('hide');
|
||||
$('#addFlashSaleForm')[0].reset();
|
||||
alert('秒杀活动创建成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('创建失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('创建秒杀活动失败:', error);
|
||||
alert('创建失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editFlashSale(id) {
|
||||
console.log('编辑秒杀活动:', id);
|
||||
|
||||
// 获取秒杀活动详情
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
const flashSale = response.data;
|
||||
|
||||
// 检查是否可以编辑(只有未开始的活动才能编辑)
|
||||
if (flashSale.status !== 1) {
|
||||
alert('只有未开始的秒杀活动才能编辑');
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充编辑表单
|
||||
$('#editFlashSaleId').val(flashSale.id);
|
||||
$('#editFlashPrice').val(flashSale.flashPrice);
|
||||
$('#editFlashStock').val(flashSale.flashStock);
|
||||
$('#editStartTime').val(formatDateTimeForInput(flashSale.startTime));
|
||||
$('#editEndTime').val(formatDateTimeForInput(flashSale.endTime));
|
||||
|
||||
// 显示编辑模态框
|
||||
$('#editFlashSaleModal').modal('show');
|
||||
} else {
|
||||
alert('获取秒杀活动详情失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert('获取秒杀活动详情失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFlashSale(id) {
|
||||
if (confirm('确定要删除这个秒杀活动吗?此操作不可恢复。')) {
|
||||
console.log('删除秒杀活动:', id);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id,
|
||||
type: 'DELETE',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
alert('秒杀活动删除成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('删除失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '删除失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function viewFlashSale(id) {
|
||||
console.log('查看秒杀详情:', id);
|
||||
|
||||
// 获取秒杀活动详情
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
const flashSale = response.data;
|
||||
|
||||
// 填充详情模态框
|
||||
$('#viewFlashSaleId').text(flashSale.id);
|
||||
$('#viewProductName').text(flashSale.productName || '-');
|
||||
$('#viewOriginalPrice').text(flashSale.originalPrice ? '¥' + flashSale.originalPrice.toFixed(2) : '-');
|
||||
$('#viewFlashPrice').text(flashSale.flashPrice ? '¥' + flashSale.flashPrice.toFixed(2) : '-');
|
||||
$('#viewFlashStock').text(flashSale.flashStock || '-');
|
||||
$('#viewRemainingStock').text(flashSale.remainingStock || '-');
|
||||
$('#viewStartTime').text(formatDateTime(flashSale.startTime));
|
||||
$('#viewEndTime').text(formatDateTime(flashSale.endTime));
|
||||
$('#viewStatus').html('<span class="badge ' + getStatusClass(flashSale.status) + '">' + getStatusText(flashSale.status) + '</span>');
|
||||
$('#viewCreatedAt').text(formatDateTime(flashSale.createdAt));
|
||||
|
||||
// 显示详情模态框
|
||||
$('#viewFlashSaleModal').modal('show');
|
||||
} else {
|
||||
alert('获取秒杀活动详情失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert('获取秒杀活动详情失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 保存编辑的秒杀活动
|
||||
function saveEditFlashSale() {
|
||||
// 验证表单
|
||||
if (!$('#editFlashPrice').val()) {
|
||||
alert('请输入秒杀价格');
|
||||
return;
|
||||
}
|
||||
if (!$('#editFlashStock').val()) {
|
||||
alert('请输入秒杀库存');
|
||||
return;
|
||||
}
|
||||
if (!$('#editStartTime').val() || !$('#editEndTime').val()) {
|
||||
alert('请选择开始和结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
const flashSaleId = $('#editFlashSaleId').val();
|
||||
const updateData = {
|
||||
flashPrice: parseFloat($('#editFlashPrice').val()),
|
||||
flashStock: parseInt($('#editFlashStock').val()),
|
||||
startTime: $('#editStartTime').val().replace('T', ' ') + ':00',
|
||||
endTime: $('#editEndTime').val().replace('T', ' ') + ':00'
|
||||
};
|
||||
|
||||
// 验证时间
|
||||
const startTime = new Date($('#editStartTime').val());
|
||||
const endTime = new Date($('#editEndTime').val());
|
||||
const now = new Date();
|
||||
|
||||
if (startTime < now) {
|
||||
alert('开始时间不能早于当前时间');
|
||||
return;
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
alert('开始时间不能晚于或等于结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('更新秒杀活动:', updateData);
|
||||
|
||||
// 调用更新API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + flashSaleId,
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(updateData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#editFlashSaleModal').modal('hide');
|
||||
alert('秒杀活动更新成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('更新失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '更新失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
// 如果是ISO格式,转换为本地时间显示
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr; // 如果转换失败,返回原字符串
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间用于input[type="datetime-local"]
|
||||
function formatDateTimeForInput(dateTimeStr) {
|
||||
if (!dateTimeStr) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
468
src/main/webapp/WEB-INF/views/admin/index.jsp
Normal file
468
src/main/webapp/WEB-INF/views/admin/index.jsp
Normal file
@@ -0,0 +1,468 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="管理后台"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">管理后台仪表盘</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshData()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||
总用户数
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="totalUsers">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-users fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
||||
总商品数
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="totalProducts">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-box fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||
活跃秒杀
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="activeFlashSales">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-bolt fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||
今日订单
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="todayOrders">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-shopping-cart fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-rocket"></i> 快速操作</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/products"
|
||||
class="btn btn-primary btn-block">
|
||||
<i class="fas fa-plus"></i> 添加商品
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/flashsales"
|
||||
class="btn btn-success btn-block">
|
||||
<i class="fas fa-bolt"></i> 创建秒杀
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/orders"
|
||||
class="btn btn-info btn-block">
|
||||
<i class="fas fa-list"></i> 查看订单
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/monitor"
|
||||
class="btn btn-warning btn-block">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-clock"></i> 最近订单</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>用户</th>
|
||||
<th>商品</th>
|
||||
<th>金额</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentOrders">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-fire"></i> 热门商品</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="hotProducts">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.border-left-primary {
|
||||
border-left: 0.25rem solid #4e73df !important;
|
||||
}
|
||||
|
||||
.border-left-success {
|
||||
border-left: 0.25rem solid #1cc88a !important;
|
||||
}
|
||||
|
||||
.border-left-info {
|
||||
border-left: 0.25rem solid #36b9cc !important;
|
||||
}
|
||||
|
||||
.border-left-warning {
|
||||
border-left: 0.25rem solid #f6c23e !important;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
function loadDashboardData() {
|
||||
// 加载统计数据
|
||||
loadStatistics();
|
||||
|
||||
// 加载最近订单
|
||||
loadRecentOrders();
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
}
|
||||
|
||||
function loadStatistics() {
|
||||
// 调用真实API获取统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/dashboard/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateDashboardStats(response.data);
|
||||
} else {
|
||||
console.error('获取仪表盘数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateDashboardStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取仪表盘数据请求失败');
|
||||
// 显示默认值
|
||||
updateDashboardStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新仪表盘统计数据
|
||||
function updateDashboardStats(stats) {
|
||||
$('#totalUsers').text(formatNumber(stats.totalUsers || 0));
|
||||
$('#totalProducts').text(formatNumber(stats.totalProducts || 0));
|
||||
$('#activeFlashSales').text(formatNumber(stats.activeFlashSales || 0));
|
||||
$('#todayOrders').text(formatNumber(stats.todayOrders || 0));
|
||||
|
||||
// 更新订单统计卡片
|
||||
$('#totalOrders').text(formatNumber(stats.totalOrders || 0));
|
||||
$('#paidOrders').text(formatNumber(stats.paidOrders || 0));
|
||||
$('#pendingOrders').text(formatNumber(stats.pendingOrders || 0));
|
||||
$('#totalAmount').text('¥' + formatNumber(stats.totalAmount || 0));
|
||||
}
|
||||
|
||||
function loadRecentOrders() {
|
||||
// 调用真实API获取最近订单
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders/recent?limit=10')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateRecentOrders(response.data);
|
||||
} else {
|
||||
console.error('获取最近订单失败:', response.message);
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center">获取订单数据失败</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取最近订单请求失败');
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center">网络请求失败</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新最近订单列表
|
||||
function updateRecentOrders(orders) {
|
||||
let html = '';
|
||||
if (orders && orders.length > 0) {
|
||||
orders.forEach(function (order) {
|
||||
let statusClass = getOrderStatusClass(order.status);
|
||||
let statusText = getOrderStatusText(order.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + order.id + `</td>
|
||||
<td>` + order.username + `</td>
|
||||
<td>` + order.productName + `</td>
|
||||
<td>¥` + formatNumber(order.totalAmount) + `</td>
|
||||
<td><span class="badge ` + statusClass + `">` + statusText + `</span></td>
|
||||
<td>` + formatDateTime(order.createdAt) + `</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<tr><td colspan="6" class="text-center">暂无订单数据</td></tr>';
|
||||
}
|
||||
$('#recentOrders').html(html);
|
||||
}
|
||||
|
||||
function loadHotProducts() {
|
||||
// 模拟数据,实际应该调用API
|
||||
setTimeout(function () {
|
||||
const products = [
|
||||
{name: 'iPhone 15 Pro Max', sales: 156},
|
||||
{name: 'MacBook Pro 16英寸', sales: 89},
|
||||
{name: 'iPad Air', sales: 67},
|
||||
{name: 'AirPods Pro 2', sales: 234}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
products.forEach((product, index) => {
|
||||
html += `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<small class="text-muted">#${index + 1}</small>
|
||||
<span class="ms-2">${product.name}</span>
|
||||
</div>
|
||||
<span class="badge bg-success">${product.sales}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#hotProducts').html(html);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
// 显示加载状态
|
||||
$('#totalUsers, #totalProducts, #activeFlashSales, #todayOrders').html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#hotProducts').html('<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>');
|
||||
|
||||
// 重新加载数据
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 待支付
|
||||
case 2:
|
||||
return 'bg-success'; // 已支付
|
||||
case 3:
|
||||
return 'bg-info'; // 已发货
|
||||
case 4:
|
||||
return 'bg-primary'; // 已完成
|
||||
case 5:
|
||||
return 'bg-danger'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
533
src/main/webapp/WEB-INF/views/admin/monitor.jsp
Normal file
533
src/main/webapp/WEB-INF/views/admin/monitor.jsp
Normal file
@@ -0,0 +1,533 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="系统监控"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">系统监控</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshMonitorData()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新数据
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportReport()">
|
||||
<i class="fas fa-download"></i> 导出报告
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态概览 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="systemStatus">正常</h5>
|
||||
<p class="card-text">系统状态</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="cpuUsage">0%</h5>
|
||||
<p class="card-text">CPU使用率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="memoryUsage">0%</h5>
|
||||
<p class="card-text">内存使用率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="diskUsage">0%</h5>
|
||||
<p class="card-text">磁盘使用率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis监控 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-database"></i> Redis集群状态</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>节点</th>
|
||||
<th>状态</th>
|
||||
<th>内存使用</th>
|
||||
<th>连接数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="redisNodesTable">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-bar"></i> 数据库连接池</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-success" id="activeConnections">0</h4>
|
||||
<small>活跃连接</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-info" id="idleConnections">0</h4>
|
||||
<small>空闲连接</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-warning" id="maxConnections">0</h4>
|
||||
<small>最大连接</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-primary" id="totalConnections">0</h4>
|
||||
<small>总连接数</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 接口性能监控 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-tachometer-alt"></i> 接口性能监控</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>接口路径</th>
|
||||
<th>请求次数</th>
|
||||
<th>平均响应时间</th>
|
||||
<th>成功率</th>
|
||||
<th>最后调用</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="apiPerformanceTable">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误日志 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-exclamation-triangle"></i> 最近错误日志</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>级别</th>
|
||||
<th>模块</th>
|
||||
<th>错误信息</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="errorLogsTable">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时监控图表 -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-line"></i> CPU & 内存使用趋势</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="systemChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-area"></i> 请求量统计</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="requestChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadMonitorData();
|
||||
|
||||
// 每2分钟自动刷新数据(降低频率)
|
||||
setInterval(function () {
|
||||
loadMonitorData();
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
function loadMonitorData() {
|
||||
loadSystemStatus();
|
||||
loadRedisStatus();
|
||||
loadDatabaseStatus();
|
||||
loadApiPerformance();
|
||||
loadErrorLogs();
|
||||
updateCharts();
|
||||
}
|
||||
|
||||
function loadSystemStatus() {
|
||||
// 调用真实API获取系统状态
|
||||
$.get('${pageContext.request.contextPath}/api/admin/monitor/system')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateSystemStatus(response.data);
|
||||
} else {
|
||||
console.error('获取系统状态失败:', response.message);
|
||||
// 显示默认状态
|
||||
updateSystemStatus({
|
||||
status: '未知',
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取系统状态请求失败');
|
||||
// 显示默认状态
|
||||
updateSystemStatus({
|
||||
status: '连接失败',
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新系统状态显示
|
||||
function updateSystemStatus(data) {
|
||||
const statusClass = data.status === '正常' ? 'text-success' : 'text-danger';
|
||||
$('#systemStatus').text(data.status || '未知').removeClass().addClass('card-title ' + statusClass);
|
||||
$('#cpuUsage').text((data.cpuUsage || 0) + '%');
|
||||
$('#memoryUsage').text((data.memoryUsage || 0) + '%');
|
||||
$('#diskUsage').text((data.diskUsage || 0) + '%');
|
||||
}
|
||||
|
||||
function loadRedisStatus() {
|
||||
// 调用真实API获取Redis状态
|
||||
$.get('${pageContext.request.contextPath}/api/admin/monitor/redis')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateRedisStatus(response.data);
|
||||
} else {
|
||||
console.error('获取Redis状态失败:', response.message);
|
||||
// 显示默认状态
|
||||
updateRedisStatus([]);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取Redis状态请求失败');
|
||||
// 显示默认状态
|
||||
updateRedisStatus([]);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新Redis状态显示
|
||||
function updateRedisStatus(nodes) {
|
||||
let html = '';
|
||||
if (nodes && nodes.length > 0) {
|
||||
nodes.forEach(node => {
|
||||
const statusClass = node.status === '正常' ? 'bg-success' : 'bg-danger';
|
||||
html += `
|
||||
<tr>
|
||||
<td>${node.node}</td>
|
||||
<td><span class="badge ${statusClass}">${node.status}</span></td>
|
||||
<td>${node.memory}</td>
|
||||
<td>${node.connections}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<tr><td colspan="4" class="text-center">无法获取Redis状态</td></tr>';
|
||||
}
|
||||
$('#redisNodesTable').html(html);
|
||||
}
|
||||
|
||||
function loadDatabaseStatus() {
|
||||
// 模拟数据库连接池状态
|
||||
setTimeout(function () {
|
||||
$('#activeConnections').text('12');
|
||||
$('#idleConnections').text('8');
|
||||
$('#maxConnections').text('20');
|
||||
$('#totalConnections').text('20');
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function loadApiPerformance() {
|
||||
// 模拟接口性能数据
|
||||
setTimeout(function () {
|
||||
const apis = [
|
||||
{
|
||||
path: '/api/flashsale/participate',
|
||||
requests: 1234,
|
||||
avgTime: '45ms',
|
||||
successRate: '99.8%',
|
||||
lastCall: '10:30:15',
|
||||
status: '正常'
|
||||
},
|
||||
{
|
||||
path: '/api/product/hot',
|
||||
requests: 567,
|
||||
avgTime: '23ms',
|
||||
successRate: '100%',
|
||||
lastCall: '10:29:45',
|
||||
status: '正常'
|
||||
},
|
||||
{
|
||||
path: '/api/user/login',
|
||||
requests: 89,
|
||||
avgTime: '156ms',
|
||||
successRate: '98.9%',
|
||||
lastCall: '10:28:30',
|
||||
status: '正常'
|
||||
},
|
||||
{
|
||||
path: '/api/order/create',
|
||||
requests: 234,
|
||||
avgTime: '89ms',
|
||||
successRate: '99.5%',
|
||||
lastCall: '10:27:20',
|
||||
status: '正常'
|
||||
}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
apis.forEach(api => {
|
||||
html += `
|
||||
<tr>
|
||||
<td><code>${api.path}</code></td>
|
||||
<td>${api.requests}</td>
|
||||
<td>${api.avgTime}</td>
|
||||
<td>${api.successRate}</td>
|
||||
<td>${api.lastCall}</td>
|
||||
<td><span class="badge bg-success">${api.status}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#apiPerformanceTable').html(html);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function loadErrorLogs() {
|
||||
// 模拟错误日志数据
|
||||
setTimeout(function () {
|
||||
const logs = [
|
||||
{
|
||||
time: '10:25:30',
|
||||
level: 'WARN',
|
||||
module: 'RedisService',
|
||||
message: 'Redis连接池使用率较高',
|
||||
action: '查看详情'
|
||||
},
|
||||
{
|
||||
time: '10:20:15',
|
||||
level: 'ERROR',
|
||||
module: 'FlashSaleService',
|
||||
message: '秒杀库存扣减失败',
|
||||
action: '查看详情'
|
||||
},
|
||||
{time: '10:15:45', level: 'INFO', module: 'UserService', message: '用户登录成功', action: '查看详情'}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
if (logs.length === 0) {
|
||||
html = '<tr><td colspan="5" class="text-center text-success">暂无错误日志</td></tr>';
|
||||
} else {
|
||||
logs.forEach(log => {
|
||||
const levelClass = log.level === 'ERROR' ? 'bg-danger' :
|
||||
log.level === 'WARN' ? 'bg-warning' : 'bg-info';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${log.time}</td>
|
||||
<td><span class="badge ${levelClass}">${log.level}</span></td>
|
||||
<td>${log.module}</td>
|
||||
<td>${log.message}</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary">${log.action}</button></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#errorLogsTable').html(html);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function updateCharts() {
|
||||
// 这里应该使用Chart.js或其他图表库来绘制实时图表
|
||||
// 由于简化,这里只是模拟
|
||||
console.log('更新监控图表...');
|
||||
}
|
||||
|
||||
function refreshMonitorData() {
|
||||
// 显示加载状态
|
||||
$('#redisNodesTable').html('<tr><td colspan="4" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#apiPerformanceTable').html('<tr><td colspan="6" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#errorLogsTable').html('<tr><td colspan="5" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 重新加载数据
|
||||
loadMonitorData();
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
console.log('导出监控报告');
|
||||
alert('监控报告导出功能开发中...');
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
549
src/main/webapp/WEB-INF/views/admin/orders.jsp
Normal file
549
src/main/webapp/WEB-INF/views/admin/orders.jsp
Normal file
@@ -0,0 +1,549 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="订单管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">订单管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshOrders()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportOrders()">
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索订单号/用户...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchOrders()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterOrders()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待支付</option>
|
||||
<option value="paid">已支付</option>
|
||||
<option value="shipped">已发货</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="cancelled">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" id="dateFilter" onchange="filterOrders()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortOrders()">
|
||||
<option value="created_at">按创建时间</option>
|
||||
<option value="total_amount">按订单金额</option>
|
||||
<option value="status">按订单状态</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单统计 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalOrders">0</h5>
|
||||
<p class="card-text">总订单数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="paidOrders">0</h5>
|
||||
<p class="card-text">已支付订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="pendingOrders">0</h5>
|
||||
<p class="card-text">待处理订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="totalAmount">¥0</h5>
|
||||
<p class="card-text">总交易额</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>用户</th>
|
||||
<th>商品信息</th>
|
||||
<th>数量</th>
|
||||
<th>总金额</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersTableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="订单分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情模态框 -->
|
||||
<div class="modal fade" id="orderDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">订单详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="orderDetailContent">
|
||||
<!-- 订单详情内容 -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadOrders();
|
||||
loadOrderStats();
|
||||
});
|
||||
|
||||
function loadOrders(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建请求参数
|
||||
let params = {
|
||||
page: page,
|
||||
size: 10
|
||||
};
|
||||
|
||||
const keyword = $('#searchKeyword').val();
|
||||
const status = $('#statusFilter').val();
|
||||
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
|
||||
if (status && status !== '') {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders', params)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
renderOrdersTable(response.data.orders);
|
||||
renderPagination(response.data.currentPage, response.data.totalPages);
|
||||
} else {
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center text-danger">获取订单数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadOrderStats() {
|
||||
// 调用真实API获取订单统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateOrderStats(response.data);
|
||||
} else {
|
||||
console.error('获取订单统计数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateOrderStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取订单统计数据请求失败');
|
||||
// 显示默认值
|
||||
updateOrderStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新订单统计数据
|
||||
function updateOrderStats(stats) {
|
||||
$('#totalOrders').text(formatNumber(stats.totalOrders || 0));
|
||||
$('#paidOrders').text(formatNumber(stats.paidOrders || 0));
|
||||
$('#pendingOrders').text(formatNumber(stats.pendingOrders || 0));
|
||||
$('#totalAmount').text('¥' + formatNumber(stats.totalAmount || 0));
|
||||
}
|
||||
|
||||
function renderOrdersTable(orders) {
|
||||
let html = '';
|
||||
|
||||
if (orders.length === 0) {
|
||||
html = '<tr><td colspan="8" class="text-center">暂无订单数据</td></tr>';
|
||||
} else {
|
||||
orders.forEach(order => {
|
||||
const statusText = getOrderStatusText(order.status);
|
||||
const statusClass = getOrderStatusClass(order.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">` + order.id + `</div>
|
||||
` + (order.isFlashSale ? '<small class="text-danger"><i class="fas fa-bolt"></i> 秒杀订单</small>' : '') + `
|
||||
</td>
|
||||
<td>` + order.username + `</td>
|
||||
<td>` + order.productName + `</td>
|
||||
<td>` + order.quantity + `</td>
|
||||
<td class="fw-bold">¥` + formatNumber(order.totalAmount || 0) + `</td>
|
||||
<td>
|
||||
<span class="badge ` + statusClass + `">
|
||||
` + statusText + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(order.createdAt) + `</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="viewOrderDetail('` + order.id + `')" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
` + (order.status === 2 ?
|
||||
'<button class="btn btn-outline-success" onclick="shipOrder(\'' + order.id + '\')" title="发货"><i class="fas fa-shipping-fast"></i></button>' : '') + `
|
||||
` + (order.status === 1 ?
|
||||
'<button class="btn btn-outline-danger" onclick="cancelOrder(\'' + order.id + '\')" title="取消"><i class="fas fa-times"></i></button>' : '') + `
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#ordersTableBody').html(html);
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待支付';
|
||||
case 'paid':
|
||||
return '已支付';
|
||||
case 'shipped':
|
||||
return '已发货';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-warning';
|
||||
case 'paid':
|
||||
return 'bg-success';
|
||||
case 'shipped':
|
||||
return 'bg-info';
|
||||
case 'completed':
|
||||
return 'bg-primary';
|
||||
case 'cancelled':
|
||||
return 'bg-secondary';
|
||||
default:
|
||||
return 'bg-light';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshOrders() {
|
||||
$('#ordersTableBody').html('<tr><td colspan="8" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadOrders(currentPage);
|
||||
loadOrderStats();
|
||||
}
|
||||
|
||||
function searchOrders() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索订单:', keyword);
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function filterOrders() {
|
||||
const status = $('#statusFilter').val();
|
||||
const date = $('#dateFilter').val();
|
||||
console.log('筛选订单:', {status, date});
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function sortOrders() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function viewOrderDetail(orderId) {
|
||||
console.log('查看订单详情:', orderId);
|
||||
|
||||
// 模拟获取订单详情
|
||||
const orderDetail = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>订单信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>订单号:</td><td>` + orderId + `</td></tr>
|
||||
<tr><td>用户:</td><td>demo1</td></tr>
|
||||
<tr><td>状态:</td><td><span class="badge bg-success">已支付</span></td></tr>
|
||||
<tr><td>创建时间:</td><td>2025-06-29 10:30:15</td></tr>
|
||||
<tr><td>支付时间:</td><td>2025-06-29 10:31:20</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>商品信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>iPhone 15 Pro Max</td></tr>
|
||||
<tr><td>单价:</td><td>¥8,888.00</td></tr>
|
||||
<tr><td>数量:</td><td>1</td></tr>
|
||||
<tr><td>总金额:</td><td class="fw-bold">¥8,888.00</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>收货地址</h6>
|
||||
<p>北京市朝阳区xxx街道xxx号xxx小区xxx楼xxx室<br>
|
||||
收货人: 张三 13800138001</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#orderDetailContent').html(orderDetail);
|
||||
$('#orderDetailModal').modal('show');
|
||||
}
|
||||
|
||||
function shipOrder(orderId) {
|
||||
if (confirm('确定要将此订单标记为已发货吗?')) {
|
||||
console.log('发货订单:', orderId);
|
||||
|
||||
setTimeout(function () {
|
||||
alert('订单已标记为已发货!');
|
||||
refreshOrders();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOrder(orderId) {
|
||||
if (confirm('确定要取消此订单吗?此操作不可恢复。')) {
|
||||
console.log('取消订单:', orderId);
|
||||
|
||||
setTimeout(function () {
|
||||
alert('订单已取消!');
|
||||
refreshOrders();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function exportOrders() {
|
||||
console.log('导出订单数据');
|
||||
alert('订单数据导出功能开发中...');
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 待支付
|
||||
case 2:
|
||||
return 'bg-success'; // 已支付
|
||||
case 3:
|
||||
return 'bg-info'; // 已发货
|
||||
case 4:
|
||||
return 'bg-primary'; // 已完成
|
||||
case 5:
|
||||
return 'bg-danger'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
1188
src/main/webapp/WEB-INF/views/admin/products.jsp
Normal file
1188
src/main/webapp/WEB-INF/views/admin/products.jsp
Normal file
File diff suppressed because it is too large
Load Diff
409
src/main/webapp/WEB-INF/views/admin/users.jsp
Normal file
409
src/main/webapp/WEB-INF/views/admin/users.jsp
Normal file
@@ -0,0 +1,409 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">用户管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshUsers()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportUsers()">
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索用户名/邮箱/手机...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchUsers()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterUsers()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="1">正常</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" id="dateFilter" onchange="filterUsers()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortUsers()">
|
||||
<option value="created_at">按注册时间</option>
|
||||
<option value="username">按用户名</option>
|
||||
<option value="last_login">按最后登录</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户统计 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalUsers">0</h5>
|
||||
<p class="card-text">总用户数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="activeUsers">0</h5>
|
||||
<p class="card-text">活跃用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="newUsers">0</h5>
|
||||
<p class="card-text">今日新增</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="onlineUsers">0</h5>
|
||||
<p class="card-text">在线用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>手机号</th>
|
||||
<th>状态</th>
|
||||
<th>注册时间</th>
|
||||
<th>最后登录</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="用户分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadUsers();
|
||||
loadUserStats();
|
||||
});
|
||||
|
||||
function loadUsers(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建请求参数
|
||||
let params = {
|
||||
page: page,
|
||||
size: 10
|
||||
};
|
||||
|
||||
const keyword = $('#searchKeyword').val();
|
||||
const status = $('#statusFilter').val();
|
||||
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
|
||||
if (status && status !== '') {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.get('${pageContext.request.contextPath}/api/admin/users', params)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
renderUsersTable(response.data.users);
|
||||
renderPagination(response.data.currentPage, response.data.totalPages);
|
||||
} else {
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center text-danger">获取用户数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadUserStats() {
|
||||
// 调用真实API获取用户统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/users/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateUserStats(response.data);
|
||||
} else {
|
||||
console.error('获取用户统计数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateUserStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取用户统计数据请求失败');
|
||||
// 显示默认值
|
||||
updateUserStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户统计数据
|
||||
function updateUserStats(stats) {
|
||||
$('#totalUsers').text(formatNumber(stats.totalUsers || 0));
|
||||
$('#activeUsers').text(formatNumber(stats.activeUsers || 0));
|
||||
$('#newUsers').text(formatNumber(stats.newUsers || 0));
|
||||
$('#onlineUsers').text(formatNumber(stats.onlineUsers || 0));
|
||||
}
|
||||
|
||||
function renderUsersTable(users) {
|
||||
let html = '';
|
||||
|
||||
if (users.length === 0) {
|
||||
html = '<tr><td colspan="7" class="text-center">暂无用户数据</td></tr>';
|
||||
} else {
|
||||
users.forEach(user => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + user.id + `</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-2">` + user.username + `</span>
|
||||
` + (user.isOnline ? '<span class="badge bg-success">在线</span>' : '') + `
|
||||
</div>
|
||||
</td>
|
||||
<td>` + (user.email || '-') + `</td>
|
||||
<td>` + (user.phone || '-') + `</td>
|
||||
<td>
|
||||
<span class="badge ` + getUserStatusClass(user.status) + `">
|
||||
` + getUserStatusText(user.status) + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(user.createdAt) + `</td>
|
||||
<td>` + (user.lastLogin ? formatDateTime(user.lastLogin) : '从未登录') + `</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#usersTableBody').html(html);
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshUsers() {
|
||||
$('#usersTableBody').html('<tr><td colspan="7" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadUsers(currentPage);
|
||||
loadUserStats();
|
||||
}
|
||||
|
||||
function searchUsers() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索用户:', keyword);
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
function filterUsers() {
|
||||
const status = $('#statusFilter').val();
|
||||
const date = $('#dateFilter').val();
|
||||
console.log('筛选用户:', {status, date});
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
function sortUsers() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
|
||||
function exportUsers() {
|
||||
console.log('导出用户数据');
|
||||
alert('用户数据导出功能开发中...');
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getUserStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-success'; // 正常
|
||||
case 0:
|
||||
return 'bg-danger'; // 禁用
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getUserStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '正常';
|
||||
case 0:
|
||||
return '禁用';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
244
src/main/webapp/WEB-INF/views/common/footer.jsp
Normal file
244
src/main/webapp/WEB-INF/views/common/footer.jsp
Normal file
@@ -0,0 +1,244 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-dark text-light mt-5 py-4">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-bolt"></i> 秒杀系统</h5>
|
||||
<p class="mb-2">基于Spring Boot + Redis构建的高并发秒杀系统</p>
|
||||
<p class="text-muted small">
|
||||
<i class="fas fa-server"></i> Redis集群 |
|
||||
<i class="fas fa-lock"></i> 分布式锁 |
|
||||
<i class="fas fa-tachometer-alt"></i> 高性能
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>核心功能</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-fire text-danger"></i> 秒杀抢购</li>
|
||||
<li><i class="fas fa-shopping-cart text-primary"></i> 购物车</li>
|
||||
<li><i class="fas fa-list-alt text-success"></i> 订单管理</li>
|
||||
<li><i class="fas fa-chart-line text-warning"></i> 销量排行</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>技术特性</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-database text-info"></i> Redis缓存</li>
|
||||
<li><i class="fas fa-shield-alt text-success"></i> 防超卖机制</li>
|
||||
<li><i class="fas fa-stopwatch text-warning"></i> 接口限流</li>
|
||||
<li><i class="fas fa-code text-primary"></i> Lua脚本</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-0 text-muted">
|
||||
© 2025 秒杀系统演示项目.
|
||||
<span class="text-danger">❤</span>
|
||||
基于Redis集群构建
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="d-flex justify-content-md-end align-items-center">
|
||||
<span class="me-3 text-muted small">
|
||||
<i class="fas fa-users"></i>
|
||||
在线用户: <span id="onlineUserCount">-</span>
|
||||
</span>
|
||||
<span class="me-3 text-muted small">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="currentTime"></span>
|
||||
</span>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-light btn-sm" onclick="checkSystemStatus()">
|
||||
<i class="fas fa-heartbeat"></i> 系统状态
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 系统状态模态框 -->
|
||||
<div class="modal fade" id="systemStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-heartbeat text-success"></i> 系统状态
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-database fa-2x text-success mb-2"></i>
|
||||
<h6>Redis集群</h6>
|
||||
<span class="badge bg-success">正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-server fa-2x text-primary mb-2"></i>
|
||||
<h6>应用服务</h6>
|
||||
<span class="badge bg-primary">运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>实时统计</h6>
|
||||
<div class="row text-center">
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-primary fw-bold" id="totalUsers">-</div>
|
||||
<small class="text-muted">总用户</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-success fw-bold" id="totalProducts">-</div>
|
||||
<small class="text-muted">商品数</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-warning fw-bold" id="totalOrders">-</div>
|
||||
<small class="text-muted">订单数</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-danger fw-bold" id="activeFlashSales">-</div>
|
||||
<small class="text-muted">秒杀中</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" onclick="refreshSystemStatus()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('zh-CN');
|
||||
$('#currentTime').text(timeString);
|
||||
}
|
||||
|
||||
// 获取在线用户数
|
||||
function updateOnlineUserCount() {
|
||||
// 只有在用户登录时才更新在线用户数,并且不在登录页面执行
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
// 检查当前页面是否为登录页面
|
||||
if (window.location.pathname.indexOf('/login') === -1) {
|
||||
$.get('${pageContext.request.contextPath}/api/user/online-stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#onlineUserCount').text(response.data.onlineUserCount);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#onlineUserCount').text('N/A');
|
||||
});
|
||||
}
|
||||
</c:if>
|
||||
}
|
||||
|
||||
// 检查系统状态
|
||||
function checkSystemStatus() {
|
||||
$('#systemStatusModal').modal('show');
|
||||
refreshSystemStatus();
|
||||
}
|
||||
|
||||
// 刷新系统状态
|
||||
function refreshSystemStatus() {
|
||||
// 获取订单统计
|
||||
$.get('${pageContext.request.contextPath}/api/order/statistics')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
const stats = response.data;
|
||||
$('#totalOrders').text(stats.totalOrders || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活跃秒杀数量
|
||||
$.get('${pageContext.request.contextPath}/api/flashsale/active')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#activeFlashSales').text(response.data.length || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟其他统计数据
|
||||
$('#totalUsers').text(Math.floor(Math.random() * 1000) + 500);
|
||||
$('#totalProducts').text(Math.floor(Math.random() * 100) + 50);
|
||||
}
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function () {
|
||||
// 立即更新时间
|
||||
updateCurrentTime();
|
||||
|
||||
// 只有在用户登录时才更新在线用户数
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
updateOnlineUserCount();
|
||||
// 每2分钟更新一次在线用户数(减少频率)
|
||||
setInterval(updateOnlineUserCount, 120000);
|
||||
</c:if>
|
||||
|
||||
// 每秒更新时间
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!document.hidden) {
|
||||
// 页面变为可见时,更新数据
|
||||
updateOnlineUserCount();
|
||||
}
|
||||
});
|
||||
|
||||
// 全局错误处理
|
||||
$(document).ajaxError(function (event, xhr, settings, thrownError) {
|
||||
if (xhr.status === 401) {
|
||||
showMessage('登录已过期,请重新登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 2000);
|
||||
} else if (xhr.status >= 500) {
|
||||
showMessage('服务器错误,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 添加加载动画
|
||||
function showLoading(element) {
|
||||
$(element).html('<i class="fas fa-spinner fa-spin"></i> 加载中...');
|
||||
}
|
||||
|
||||
// 隐藏加载动画
|
||||
function hideLoading(element, originalText) {
|
||||
$(element).html(originalText);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
272
src/main/webapp/WEB-INF/views/common/header.jsp
Normal file
272
src/main/webapp/WEB-INF/views/common/header.jsp
Normal file
@@ -0,0 +1,272 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageTitle} - 秒杀系统</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<style>
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.flash-sale-badge {
|
||||
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 0.7em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.online-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #28a745;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="${pageContext.request.contextPath}/">
|
||||
<i class="fas fa-bolt"></i> 秒杀系统
|
||||
<span class="flash-sale-badge">FLASH SALE</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/">
|
||||
<i class="fas fa-home"></i> 首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/products">
|
||||
<i class="fas fa-shopping-bag"></i> 商品列表
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/flashsales">
|
||||
<i class="fas fa-fire"></i> 秒杀活动
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
<!-- 购物车 -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link position-relative" href="${pageContext.request.contextPath}/cart">
|
||||
<i class="fas fa-shopping-cart"></i> 购物车
|
||||
<span class="cart-badge" id="cartCount">0</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="userDropdown"
|
||||
role="button" data-bs-toggle="dropdown">
|
||||
<div class="user-avatar me-2">
|
||||
${sessionScope.user.username.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<span class="online-indicator"></span>
|
||||
${sessionScope.user.username}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/orders">
|
||||
<i class="fas fa-list-alt"></i> 我的订单
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/profile">
|
||||
<i class="fas fa-user-cog"></i> 个人设置
|
||||
</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#" onclick="logout()">
|
||||
<i class="fas fa-sign-out-alt"></i> 退出登录
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/login">
|
||||
<i class="fas fa-sign-in-alt"></i> 登录
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/register">
|
||||
<i class="fas fa-user-plus"></i> 注册
|
||||
</a>
|
||||
</li>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 消息提示区域 -->
|
||||
<div id="messageContainer" class="container mt-3">
|
||||
<!-- 动态消息将在这里显示 -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局JavaScript函数
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertClass = {
|
||||
'success': 'alert-success',
|
||||
'error': 'alert-danger',
|
||||
'warning': 'alert-warning',
|
||||
'info': 'alert-info'
|
||||
}[type] || 'alert-info';
|
||||
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#messageContainer').html(alertHtml);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
$('.alert').alert('close');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
$.post('${pageContext.request.contextPath}/api/user/logout')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showMessage('退出登录失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
$.get('${pageContext.request.contextPath}/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#cartCount').text(response.data.count);
|
||||
}
|
||||
});
|
||||
</c:if>
|
||||
}
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function () {
|
||||
updateCartCount();
|
||||
|
||||
// 每30秒更新一次购物车数量
|
||||
setInterval(updateCartCount, 30000);
|
||||
});
|
||||
|
||||
// 格式化价格
|
||||
function formatPrice(price) {
|
||||
return '¥' + parseFloat(price).toFixed(2);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 倒计时函数
|
||||
function countdown(endTime, elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
function updateCountdown() {
|
||||
const now = new Date().getTime();
|
||||
const distance = endTime - now;
|
||||
|
||||
if (distance < 0) {
|
||||
element.innerHTML = "已结束";
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
let countdownText = "";
|
||||
if (days > 0) countdownText += days + "天 ";
|
||||
countdownText += String(hours).padStart(2, '0') + ":" +
|
||||
String(minutes).padStart(2, '0') + ":" +
|
||||
String(seconds).padStart(2, '0');
|
||||
|
||||
element.innerHTML = countdownText;
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
|
||||
// 返回清理函数
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
</script>
|
||||
83
src/main/webapp/WEB-INF/views/error.jsp
Normal file
83
src/main/webapp/WEB-INF/views/error.jsp
Normal file
@@ -0,0 +1,83 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="系统错误"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle"></i> 系统错误
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-bug fa-5x text-danger mb-3"></i>
|
||||
<h5>抱歉,系统遇到了一个错误</h5>
|
||||
<p class="text-muted">我们正在努力修复这个问题,请稍后再试。</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息(仅在开发环境显示) -->
|
||||
<c:if test="${not empty error}">
|
||||
<div class="alert alert-warning text-start">
|
||||
<h6><i class="fas fa-info-circle"></i> 错误详情:</h6>
|
||||
<p class="mb-0">${error}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${not empty exception}">
|
||||
<div class="alert alert-danger text-start">
|
||||
<h6><i class="fas fa-bug"></i> 异常信息:</h6>
|
||||
<p class="mb-0">${exception.message}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-primary" onclick="history.back()">
|
||||
<i class="fas fa-arrow-left"></i> 返回上页
|
||||
</button>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-success">
|
||||
<i class="fas fa-home"></i> 返回首页
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="location.reload()">
|
||||
<i class="fas fa-redo"></i> 刷新页面
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常见问题解决方案 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-question-circle"></i> 常见问题解决方案</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>如果页面无法加载:</h6>
|
||||
<ul class="small">
|
||||
<li>检查网络连接</li>
|
||||
<li>清除浏览器缓存</li>
|
||||
<li>尝试刷新页面</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>如果功能异常:</h6>
|
||||
<ul class="small">
|
||||
<li>重新登录账号</li>
|
||||
<li>检查输入信息</li>
|
||||
<li>联系系统管理员</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
472
src/main/webapp/WEB-INF/views/index.jsp
Normal file
472
src/main/webapp/WEB-INF/views/index.jsp
Normal file
@@ -0,0 +1,472 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="首页" />
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<div id="heroCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active"></button>
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="1"></button>
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="2"></button>
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<div class="bg-gradient-danger text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-bolt"></i> 秒杀系统
|
||||
</h1>
|
||||
<p class="lead mb-4">基于Redis集群构建的高并发秒杀系统,支持分布式锁、接口限流、库存预热等核心功能。</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-fire"></i> 立即抢购
|
||||
</a>
|
||||
<a href="${pageContext.request.contextPath}/products" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-shopping-bag"></i> 浏览商品
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-rocket fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<div class="bg-gradient-primary text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-shield-alt"></i> 防超卖机制
|
||||
</h1>
|
||||
<p class="lead mb-4">采用Redis分布式锁和Lua脚本,确保高并发场景下的数据一致性,彻底解决超卖问题。</p>
|
||||
<a href="#features" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-info-circle"></i> 了解更多
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-lock fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<div class="bg-gradient-success text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-tachometer-alt"></i> 高性能缓存
|
||||
</h1>
|
||||
<p class="lead mb-4">Redis集群架构,支持五种数据类型应用,实现毫秒级响应,轻松应对高并发访问。</p>
|
||||
<a href="#performance" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-chart-line"></i> 性能指标
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-database fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#heroCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon"></span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#heroCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container my-5">
|
||||
<!-- 正在进行的秒杀活动 -->
|
||||
<section class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-fire text-danger"></i> 正在秒杀
|
||||
</h2>
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-outline-danger">
|
||||
查看全部 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="activeFlashSales" class="row">
|
||||
<!-- 动态加载秒杀活动 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 热门商品 -->
|
||||
<section class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-star text-warning"></i> 热门商品
|
||||
</h2>
|
||||
<a href="${pageContext.request.contextPath}/products" class="btn btn-outline-primary">
|
||||
查看全部 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="hotProducts" class="row">
|
||||
<!-- 动态加载热门商品 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 系统特性 -->
|
||||
<section id="features" class="mb-5">
|
||||
<h2 class="text-center fw-bold mb-5">
|
||||
<i class="fas fa-cogs"></i> 系统特性
|
||||
</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-bolt fa-3x text-danger mb-3"></i>
|
||||
<h5 class="card-title">秒杀抢购</h5>
|
||||
<p class="card-text text-muted">高并发秒杀系统,支持大量用户同时抢购</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-shield-alt fa-3x text-success mb-3"></i>
|
||||
<h5 class="card-title">防超卖</h5>
|
||||
<p class="card-text text-muted">分布式锁机制,确保库存数据一致性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-database fa-3x text-info mb-3"></i>
|
||||
<h5 class="card-title">Redis缓存</h5>
|
||||
<p class="card-text text-muted">五种数据类型应用,毫秒级响应</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-tachometer-alt fa-3x text-warning mb-3"></i>
|
||||
<h5 class="card-title">接口限流</h5>
|
||||
<p class="card-text text-muted">多种限流策略,防止恶意刷单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 性能指标 -->
|
||||
<section id="performance" class="mb-5">
|
||||
<h2 class="text-center fw-bold mb-5">
|
||||
<i class="fas fa-chart-line"></i> 性能指标
|
||||
</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-primary fw-bold" id="qpsCounter">10000+</h3>
|
||||
<p class="card-text">QPS并发处理</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-success fw-bold" id="responseTime"><100ms</h3>
|
||||
<p class="card-text">平均响应时间</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-warning fw-bold" id="successRate">99.9%</h3>
|
||||
<p class="card-text">系统可用性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-info fw-bold" id="concurrentUsers">50000+</h3>
|
||||
<p class="card-text">并发用户支持</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// 加载正在进行的秒杀活动
|
||||
loadActiveFlashSales();
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
|
||||
// 启动性能指标动画
|
||||
animateCounters();
|
||||
});
|
||||
|
||||
// 加载正在进行的秒杀活动
|
||||
function loadActiveFlashSales() {
|
||||
$.get('${pageContext.request.contextPath}/api/flashsale/active')
|
||||
.done(function(response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderFlashSales(response.data.slice(0, 4)); // 只显示前4个
|
||||
} else {
|
||||
$('#activeFlashSales').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无进行中的秒杀活动</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#activeFlashSales').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
<p class="text-muted mt-2">加载失败,请刷新页面重试</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染秒杀活动
|
||||
function renderFlashSales(flashSales) {
|
||||
let html = '';
|
||||
|
||||
flashSales.forEach(function(flashSale) {
|
||||
const discountPercent = Math.round((1 - flashSale.flashPrice / flashSale.originalPrice) * 100);
|
||||
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100 border-danger">
|
||||
<div class="position-relative">
|
||||
<img src="` + (flashSale.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="position-absolute top-0 start-0 bg-danger text-white px-2 py-1 rounded-end">
|
||||
<small><i class="fas fa-fire"></i> 秒杀中</small>
|
||||
</div>
|
||||
<div class="position-absolute top-0 end-0 bg-warning text-dark px-2 py-1 rounded-start">
|
||||
<small>${discountPercent}% OFF</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + flashSale.productName + `</h6>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<span class="text-danger fw-bold fs-5">¥` + (flashSale.flashPrice ? flashSale.flashPrice.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted text-decoration-line-through ms-2">¥` + (flashSale.originalPrice ? flashSale.originalPrice.toFixed(2) : '0.00') + `</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">剩余: ` + (flashSale.remainingStock || 0) + `件</small>
|
||||
<div class="progress" style="height: 4px;">
|
||||
<div class="progress-bar bg-danger" style="width: ` + ((flashSale.remainingStock || 0) / (flashSale.flashStock || 1) * 100) + `%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-danger fw-bold mb-2" id="countdown_${flashSale.id}">
|
||||
计算中...
|
||||
</div>
|
||||
<button class="btn btn-danger btn-sm w-100" onclick="participateFlashSale(${flashSale.id})">
|
||||
<i class="fas fa-bolt"></i> 立即抢购
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 启动倒计时
|
||||
setTimeout(() => {
|
||||
if (flashSale.timeToEnd > 0) {
|
||||
countdown(Date.now() + flashSale.timeToEnd, 'countdown_' + flashSale.id);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
$('#activeFlashSales').html(html);
|
||||
}
|
||||
|
||||
// 加载热门商品
|
||||
function loadHotProducts() {
|
||||
$.get('${pageContext.request.contextPath}/api/product/hot?limit=8')
|
||||
.done(function(response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderHotProducts(response.data);
|
||||
} else {
|
||||
$('#hotProducts').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无热门商品</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#hotProducts').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
<p class="text-muted mt-2">加载失败,请刷新页面重试</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染热门商品
|
||||
function renderHotProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function(product) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<img src="${product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg'}"
|
||||
class="card-img-top" alt="${product.name}" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + product.name + `</h6>
|
||||
<p class="card-text text-muted small text-truncate">` + (product.description || '暂无描述') + `</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addToCart(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#hotProducts').html(html);
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
if (confirm('确定要参与这个秒杀活动吗?')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('秒杀成功!订单已生成', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showMessage('秒杀失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
function addToCart(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 性能指标动画
|
||||
function animateCounters() {
|
||||
const counters = [
|
||||
{ id: 'qpsCounter', target: 10000, suffix: '+' },
|
||||
{ id: 'concurrentUsers', target: 50000, suffix: '+' }
|
||||
];
|
||||
|
||||
counters.forEach(counter => {
|
||||
animateCounter(counter.id, counter.target, counter.suffix);
|
||||
});
|
||||
}
|
||||
|
||||
function animateCounter(elementId, target, suffix = '') {
|
||||
const element = document.getElementById(elementId);
|
||||
let current = 0;
|
||||
const increment = target / 100;
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
current = target;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.floor(current).toLocaleString() + suffix;
|
||||
}, 20);
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
268
src/main/webapp/WEB-INF/views/login.jsp
Normal file
268
src/main/webapp/WEB-INF/views/login.jsp
Normal file
@@ -0,0 +1,268 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户登录"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-sign-in-alt"></i> 用户登录
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user"></i> 用户名
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="请输入用户名" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock"></i> 密码
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="请输入密码" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="rememberMe">
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">
|
||||
<i class="fas fa-sign-in-alt"></i> 登录
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-2">还没有账号?</p>
|
||||
<a href="${pageContext.request.contextPath}/register" class="btn btn-outline-success">
|
||||
<i class="fas fa-user-plus"></i> 立即注册
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 快速登录演示账号 -->
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">演示账号(快速登录):</small>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="quickLogin('demo1', '123456')">
|
||||
demo1
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="quickLogin('demo2', '123456')">
|
||||
demo2
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="quickLogin('admin', 'admin123')">
|
||||
admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统特性介绍 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle"></i> 系统特性
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<i class="fas fa-bolt fa-2x text-danger mb-2"></i>
|
||||
<h6>秒杀抢购</h6>
|
||||
<small class="text-muted">高并发秒杀系统</small>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<i class="fas fa-shield-alt fa-2x text-success mb-2"></i>
|
||||
<h6>防超卖</h6>
|
||||
<small class="text-muted">分布式锁机制</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-database fa-2x text-info mb-2"></i>
|
||||
<h6>Redis缓存</h6>
|
||||
<small class="text-muted">高性能缓存</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-tachometer-alt fa-2x text-warning mb-2"></i>
|
||||
<h6>接口限流</h6>
|
||||
<small class="text-muted">防刷机制</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码显示/隐藏切换
|
||||
$('#togglePassword').click(function () {
|
||||
const passwordField = $('#password');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (passwordField.attr('type') === 'password') {
|
||||
passwordField.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
$('#loginForm').submit(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = $('#username').val().trim();
|
||||
const password = $('#password').val();
|
||||
|
||||
// 基本验证
|
||||
if (!username) {
|
||||
showFieldError('username', '请输入用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showFieldError('password', '请输入密码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showFieldError('password', '密码长度至少6位');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的错误状态
|
||||
clearFieldErrors();
|
||||
|
||||
// 显示加载状态
|
||||
const loginBtn = $('#loginBtn');
|
||||
const originalText = loginBtn.html();
|
||||
loginBtn.html('<i class="fas fa-spinner fa-spin"></i> 登录中...').prop('disabled', true);
|
||||
|
||||
// 发送登录请求
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/user/login',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('登录成功,正在跳转...', 'success');
|
||||
|
||||
// 保存记住我状态
|
||||
if ($('#rememberMe').is(':checked')) {
|
||||
localStorage.setItem('rememberedUsername', username);
|
||||
} else {
|
||||
localStorage.removeItem('rememberedUsername');
|
||||
}
|
||||
|
||||
// 跳转到首页或之前访问的页面
|
||||
setTimeout(() => {
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('returnUrl');
|
||||
window.location.href = returnUrl || '${pageContext.request.contextPath}/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
const response = xhr.responseJSON;
|
||||
showMessage(response.message || '登录失败', 'error');
|
||||
} else {
|
||||
showMessage('网络错误,请稍后重试', 'error');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
// 恢复按钮状态
|
||||
loginBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载时检查记住的用户名
|
||||
const rememberedUsername = localStorage.getItem('rememberedUsername');
|
||||
if (rememberedUsername) {
|
||||
$('#username').val(rememberedUsername);
|
||||
$('#rememberMe').prop('checked', true);
|
||||
$('#password').focus();
|
||||
} else {
|
||||
$('#username').focus();
|
||||
}
|
||||
|
||||
// 回车键快速登录
|
||||
$(document).keypress(function (e) {
|
||||
if (e.which === 13) { // Enter键
|
||||
$('#loginForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 快速登录演示账号
|
||||
function quickLogin(username, password) {
|
||||
$('#username').val(username);
|
||||
$('#password').val(password);
|
||||
$('#loginForm').submit();
|
||||
}
|
||||
|
||||
// 显示字段错误
|
||||
function showFieldError(fieldName, message) {
|
||||
const field = $('#' + fieldName);
|
||||
field.addClass('is-invalid');
|
||||
field.siblings('.invalid-feedback').text(message);
|
||||
}
|
||||
|
||||
// 清除字段错误
|
||||
function clearFieldErrors() {
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
$('.invalid-feedback').text('');
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
function checkLoginStatus() {
|
||||
$.get('${pageContext.request.contextPath}/api/user/current')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
// 已登录,跳转到首页
|
||||
window.location.href = '${pageContext.request.contextPath}/';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态(延迟执行,避免影响用户输入)
|
||||
// 注释掉自动检查,避免页面刷新影响用户输入
|
||||
// setTimeout(function() {
|
||||
// checkLoginStatus();
|
||||
// }, 5000);
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
383
src/main/webapp/WEB-INF/views/register.jsp
Normal file
383
src/main/webapp/WEB-INF/views/register.jsp
Normal file
@@ -0,0 +1,383 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户注册"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-success text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-user-plus"></i> 用户注册
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="registerForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user"></i> 用户名 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="3-50个字符,支持字母数字下划线" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle"></i> 用户名将作为您的登录凭证
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock"></i> 密码 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="至少6位字符" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
<div class="progress mt-1" style="height: 3px;">
|
||||
<div class="progress-bar" id="passwordStrength" role="progressbar"
|
||||
style="width: 0%"></div>
|
||||
</div>
|
||||
<small class="form-text text-muted" id="passwordStrengthText">密码强度:无</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmPassword" class="form-label">
|
||||
<i class="fas fa-lock"></i> 确认密码 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword"
|
||||
placeholder="请再次输入密码" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope"></i> 邮箱
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
placeholder="example@domain.com">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">
|
||||
<i class="fas fa-phone"></i> 手机号
|
||||
</label>
|
||||
<input type="tel" class="form-control" id="phone" name="phone"
|
||||
placeholder="请输入手机号">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="agreeTerms" required>
|
||||
<label class="form-check-label" for="agreeTerms">
|
||||
我已阅读并同意 <a href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#termsModal">用户协议</a> 和
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#privacyModal">隐私政策</a>
|
||||
</label>
|
||||
<div class="invalid-feedback">请同意用户协议和隐私政策</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success" id="registerBtn">
|
||||
<i class="fas fa-user-plus"></i> 注册
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-2">已有账号?</p>
|
||||
<a href="${pageContext.request.contextPath}/login" class="btn btn-outline-primary">
|
||||
<i class="fas fa-sign-in-alt"></i> 立即登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户协议模态框 -->
|
||||
<div class="modal fade" id="termsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">用户协议</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>1. 服务条款</h6>
|
||||
<p>本系统为秒杀演示系统,仅供学习和演示使用。</p>
|
||||
|
||||
<h6>2. 用户责任</h6>
|
||||
<p>用户应当合理使用系统功能,不得进行恶意操作。</p>
|
||||
|
||||
<h6>3. 隐私保护</h6>
|
||||
<p>我们承诺保护用户隐私,不会泄露用户个人信息。</p>
|
||||
|
||||
<h6>4. 免责声明</h6>
|
||||
<p>本系统仅为演示目的,不承担任何商业责任。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
onclick="$('#agreeTerms').prop('checked', true)">同意
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐私政策模态框 -->
|
||||
<div class="modal fade" id="privacyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">隐私政策</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>信息收集</h6>
|
||||
<p>我们仅收集必要的用户信息用于系统功能实现。</p>
|
||||
|
||||
<h6>信息使用</h6>
|
||||
<p>收集的信息仅用于系统功能,不会用于其他目的。</p>
|
||||
|
||||
<h6>信息保护</h6>
|
||||
<p>我们采用适当的技术措施保护用户信息安全。</p>
|
||||
|
||||
<h6>信息共享</h6>
|
||||
<p>我们不会与第三方共享用户个人信息。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
onclick="$('#agreeTerms').prop('checked', true)">同意
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码显示/隐藏切换
|
||||
$('#togglePassword').click(function () {
|
||||
const passwordField = $('#password');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (passwordField.attr('type') === 'password') {
|
||||
passwordField.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// 密码强度检测
|
||||
$('#password').on('input', function () {
|
||||
const password = $(this).val();
|
||||
const strength = calculatePasswordStrength(password);
|
||||
updatePasswordStrengthUI(strength);
|
||||
});
|
||||
|
||||
// 确认密码验证
|
||||
$('#confirmPassword').on('input', function () {
|
||||
const password = $('#password').val();
|
||||
const confirmPassword = $(this).val();
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
$(this).addClass('is-invalid');
|
||||
$(this).siblings('.invalid-feedback').text('两次输入的密码不一致');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// 用户名实时验证
|
||||
$('#username').on('input', function () {
|
||||
const username = $(this).val();
|
||||
if (username.length >= 3) {
|
||||
checkUsernameAvailability(username);
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
$('#registerForm').submit(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
username: $('#username').val().trim(),
|
||||
password: $('#password').val(),
|
||||
confirmPassword: $('#confirmPassword').val(),
|
||||
email: $('#email').val().trim() || null,
|
||||
phone: $('#phone').val().trim() || null
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
const registerBtn = $('#registerBtn');
|
||||
const originalText = registerBtn.html();
|
||||
registerBtn.html('<i class="fas fa-spinner fa-spin"></i> 注册中...').prop('disabled', true);
|
||||
|
||||
// 发送注册请求
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/user/register',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('注册成功!正在跳转到登录页面...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
const response = xhr.responseJSON;
|
||||
showMessage(response.message || '注册失败', 'error');
|
||||
} else {
|
||||
showMessage('网络错误,请稍后重试', 'error');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
// 恢复按钮状态
|
||||
registerBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
|
||||
// 清除之前的错误状态
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
|
||||
// 用户名验证
|
||||
const username = $('#username').val().trim();
|
||||
if (!username) {
|
||||
showFieldError('username', '请输入用户名');
|
||||
isValid = false;
|
||||
} else if (username.length < 3 || username.length > 50) {
|
||||
showFieldError('username', '用户名长度必须在3-50个字符之间');
|
||||
isValid = false;
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
showFieldError('username', '用户名只能包含字母、数字和下划线');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
const password = $('#password').val();
|
||||
if (!password) {
|
||||
showFieldError('password', '请输入密码');
|
||||
isValid = false;
|
||||
} else if (password.length < 6) {
|
||||
showFieldError('password', '密码长度至少6位');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 确认密码验证
|
||||
const confirmPassword = $('#confirmPassword').val();
|
||||
if (!confirmPassword) {
|
||||
showFieldError('confirmPassword', '请确认密码');
|
||||
isValid = false;
|
||||
} else if (password !== confirmPassword) {
|
||||
showFieldError('confirmPassword', '两次输入的密码不一致');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 邮箱验证(可选)
|
||||
const email = $('#email').val().trim();
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
showFieldError('email', '请输入有效的邮箱地址');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 手机号验证(可选)
|
||||
const phone = $('#phone').val().trim();
|
||||
if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
showFieldError('phone', '请输入有效的手机号');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 协议同意验证
|
||||
if (!$('#agreeTerms').is(':checked')) {
|
||||
$('#agreeTerms').addClass('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// 计算密码强度
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
|
||||
if (password.length >= 6) score += 20;
|
||||
if (password.length >= 8) score += 20;
|
||||
if (/[a-z]/.test(password)) score += 20;
|
||||
if (/[A-Z]/.test(password)) score += 20;
|
||||
if (/[0-9]/.test(password)) score += 10;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score += 10;
|
||||
|
||||
return Math.min(score, 100);
|
||||
}
|
||||
|
||||
// 更新密码强度UI
|
||||
function updatePasswordStrengthUI(strength) {
|
||||
const progressBar = $('#passwordStrength');
|
||||
const strengthText = $('#passwordStrengthText');
|
||||
|
||||
let color, text;
|
||||
|
||||
if (strength < 30) {
|
||||
color = 'bg-danger';
|
||||
text = '弱';
|
||||
} else if (strength < 60) {
|
||||
color = 'bg-warning';
|
||||
text = '中等';
|
||||
} else if (strength < 80) {
|
||||
color = 'bg-info';
|
||||
text = '强';
|
||||
} else {
|
||||
color = 'bg-success';
|
||||
text = '很强';
|
||||
}
|
||||
|
||||
progressBar.removeClass('bg-danger bg-warning bg-info bg-success').addClass(color);
|
||||
progressBar.css('width', strength + '%');
|
||||
strengthText.text('密码强度:' + text);
|
||||
}
|
||||
|
||||
// 检查用户名可用性
|
||||
function checkUsernameAvailability(username) {
|
||||
// 这里可以添加实时检查用户名是否已存在的逻辑
|
||||
// 为了演示,暂时省略
|
||||
}
|
||||
|
||||
// 显示字段错误
|
||||
function showFieldError(fieldName, message) {
|
||||
const field = $('#' + fieldName);
|
||||
field.addClass('is-invalid');
|
||||
field.siblings('.invalid-feedback').text(message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
Reference in New Issue
Block a user