Initial commit: AIGC项目完整代码
This commit is contained in:
13
demo/src/main/java/com/example/demo/DemoApplication.java
Normal file
13
demo/src/main/java/com/example/demo/DemoApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.hibernate5.jakarta.Hibernate5JakartaModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// 注册Hibernate模块来处理代理对象
|
||||
mapper.registerModule(new Hibernate5JakartaModule());
|
||||
|
||||
// 注册Java时间模块
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// 禁用将日期写为时间戳
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
// 配置Hibernate模块
|
||||
Hibernate5JakartaModule hibernateModule = new Hibernate5JakartaModule();
|
||||
hibernateModule.disable(Hibernate5JakartaModule.Feature.USE_TRANSIENT_ANNOTATION);
|
||||
mapper.registerModule(hibernateModule);
|
||||
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
103
demo/src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
103
demo/src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import com.example.demo.security.PlainTextPasswordEncoder;
|
||||
import com.example.demo.security.JwtAuthenticationFilter;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
import com.example.demo.service.UserService;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new PlainTextPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtUtils jwtUtils, UserService userService) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,使用JWT
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login", "/register", "/api/public/**", "/api/auth/**", "/css/**", "/js/**", "/h2-console/**").permitAll()
|
||||
.requestMatchers("/api/orders/stats").permitAll() // 统计接口允许匿名访问
|
||||
.requestMatchers("/api/orders/**").authenticated() // 订单接口需要认证
|
||||
.requestMatchers("/api/payments/**").authenticated() // 支付接口需要认证
|
||||
.requestMatchers("/api/dashboard/**").hasRole("ADMIN") // 仪表盘API需要管理员权限
|
||||
.requestMatchers("/settings", "/settings/**").hasRole("ADMIN")
|
||||
.requestMatchers("/users/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login")
|
||||
.defaultSuccessUrl("/", true)
|
||||
.permitAll()
|
||||
)
|
||||
.logout(Customizer.withDefaults());
|
||||
|
||||
// 添加JWT过滤器
|
||||
http.addFilterBefore(jwtAuthenticationFilter(jwtUtils, userService), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
// H2 控制台需要以下设置
|
||||
http.headers(headers -> headers.frameOptions(frame -> frame.disable()));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config, DaoAuthenticationProvider authenticationProvider) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
// 只允许前端开发服务器
|
||||
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://127.0.0.1:3000"));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setExposedHeaders(Arrays.asList("Authorization"));
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationFilter jwtAuthenticationFilter(JwtUtils jwtUtils, UserService userService) {
|
||||
return new JwtAuthenticationFilter(jwtUtils, userService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
demo/src/main/java/com/example/demo/config/WebMvcConfig.java
Normal file
37
demo/src/main/java/com/example/demo/config/WebMvcConfig.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||
slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
|
||||
return slr;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||
lci.setParamName("lang");
|
||||
return lci;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.UserService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/users")
|
||||
@Validated
|
||||
public class AdminUserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public AdminUserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String list(Model model) {
|
||||
List<User> users = userService.findAll();
|
||||
model.addAttribute("users", users);
|
||||
return "users/list";
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
public String createForm(Model model) {
|
||||
model.addAttribute("user", new User());
|
||||
return "users/form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String create(@RequestParam String username,
|
||||
@RequestParam String email,
|
||||
@RequestParam String password,
|
||||
@RequestParam String role) {
|
||||
userService.create(username, email, password);
|
||||
// 创建后更新角色
|
||||
User user = userService.findByUsername(username);
|
||||
userService.update(user.getId(), username, email, null, role);
|
||||
return "redirect:/users";
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/edit")
|
||||
public String editForm(@PathVariable Long id, Model model) {
|
||||
User user = userService.findById(id);
|
||||
model.addAttribute("user", user);
|
||||
return "users/form";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String update(@PathVariable Long id,
|
||||
@RequestParam String username,
|
||||
@RequestParam String email,
|
||||
@RequestParam(required = false) String password,
|
||||
@RequestParam String role) {
|
||||
userService.update(id, username, email, password, role);
|
||||
return "redirect:/users";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/delete")
|
||||
public String delete(@PathVariable Long id) {
|
||||
userService.delete(id);
|
||||
return "redirect:/users";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthApiController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthApiController.class);
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private AuthenticationManager authenticationManager;
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<Map<String, Object>> login(@RequestBody Map<String, String> credentials,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
try {
|
||||
String username = credentials.get("username");
|
||||
String password = credentials.get("password");
|
||||
|
||||
if (username == null || password == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户名和密码不能为空"));
|
||||
}
|
||||
|
||||
// 使用Spring Security进行认证
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(username, password);
|
||||
Authentication authentication = authenticationManager.authenticate(authToken);
|
||||
|
||||
User user = userService.findByUsername(username);
|
||||
|
||||
// 生成JWT Token
|
||||
String token = jwtUtils.generateToken(username, user.getRole(), user.getId());
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("message", "登录成功");
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("user", user);
|
||||
data.put("token", token);
|
||||
body.put("data", data);
|
||||
|
||||
logger.info("用户登录成功:{}", username);
|
||||
return ResponseEntity.ok(body);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("登录失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户名或密码错误"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<Map<String, Object>> register(@Valid @RequestBody User user) {
|
||||
try {
|
||||
if (userService.findByUsername(user.getUsername()) != null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户名已存在"));
|
||||
}
|
||||
|
||||
if (userService.findByEmail(user.getEmail()) != null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("邮箱已存在"));
|
||||
}
|
||||
|
||||
User savedUser = userService.save(user);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "注册成功");
|
||||
response.put("data", savedUser);
|
||||
|
||||
logger.info("用户注册成功:{}", user.getUsername());
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("注册失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("注册失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<Map<String, Object>> logout() {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "登出成功");
|
||||
|
||||
logger.info("用户登出");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<Map<String, Object>> getCurrentUser(Authentication authentication) {
|
||||
try {
|
||||
logger.info("获取当前用户信息 - authentication: {}", authentication);
|
||||
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String username = authentication.getName();
|
||||
logger.info("当前用户名: {}", username);
|
||||
|
||||
try {
|
||||
User user = userService.findByUsername(username);
|
||||
logger.info("找到用户: {}", user.getUsername());
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", user);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("查找用户失败: {}", username, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户不存在: " + username));
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("用户未认证");
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("用户未登录"));
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取用户信息失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取用户信息失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户名是否存在
|
||||
*/
|
||||
@GetMapping("/public/users/exists/username")
|
||||
public ResponseEntity<Map<String, Object>> checkUsernameExists(@RequestParam String value) {
|
||||
try {
|
||||
boolean exists = userService.findByUsername(value) != null;
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", Map.of("exists", exists));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("检查用户名失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("检查用户名失败"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
*/
|
||||
@GetMapping("/public/users/exists/email")
|
||||
public ResponseEntity<Map<String, Object>> checkEmailExists(@RequestParam String value) {
|
||||
try {
|
||||
boolean exists = userService.findByEmail(value) != null;
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", Map.of("exists", exists));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("检查邮箱失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("检查邮箱失败"));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> createErrorResponse(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", message);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import com.example.demo.service.UserService;
|
||||
|
||||
@Controller
|
||||
@Validated
|
||||
public class AuthController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public AuthController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
public static class RegisterForm {
|
||||
@NotBlank
|
||||
@Size(min = 3, max = 50)
|
||||
public String username;
|
||||
|
||||
@NotBlank
|
||||
@Email
|
||||
public String email;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 6, max = 100)
|
||||
public String password;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 6, max = 100)
|
||||
public String confirmPassword;
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public String loginPage() {
|
||||
return "login";
|
||||
}
|
||||
|
||||
@GetMapping("/register")
|
||||
public String registerPage(Model model) {
|
||||
model.addAttribute("form", new RegisterForm());
|
||||
return "register";
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public String doRegister(@Valid @ModelAttribute("form") RegisterForm form, BindingResult bindingResult, Model model) {
|
||||
if (!bindingResult.hasFieldErrors("password") && !bindingResult.hasFieldErrors("confirmPassword")) {
|
||||
if (!form.password.equals(form.confirmPassword)) {
|
||||
bindingResult.rejectValue("confirmPassword", "register.password.mismatch", "两次输入的密码不一致");
|
||||
}
|
||||
}
|
||||
if (bindingResult.hasErrors()) {
|
||||
return "register";
|
||||
}
|
||||
try {
|
||||
userService.register(form.username, form.email, form.password);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
model.addAttribute("error", ex.getMessage());
|
||||
return "register";
|
||||
}
|
||||
return "redirect:/login?registered";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.service.DashboardService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/dashboard")
|
||||
@CrossOrigin(origins = "*")
|
||||
public class DashboardApiController {
|
||||
|
||||
@Autowired
|
||||
private DashboardService dashboardService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘概览数据
|
||||
*/
|
||||
@GetMapping("/overview")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getOverview() {
|
||||
try {
|
||||
Map<String, Object> overview = dashboardService.getDashboardOverview();
|
||||
return ResponseEntity.ok(overview);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日活数据
|
||||
*/
|
||||
@GetMapping("/daily-active-users")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getDailyActiveUsers() {
|
||||
try {
|
||||
Map<String, Object> data = dashboardService.getDailyActiveUsers();
|
||||
return ResponseEntity.ok(data);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收入趋势数据
|
||||
*/
|
||||
@GetMapping("/revenue-trend")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getRevenueTrend() {
|
||||
try {
|
||||
Map<String, Object> data = dashboardService.getRevenueTrend();
|
||||
return ResponseEntity.ok(data);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单状态分布
|
||||
*/
|
||||
@GetMapping("/order-status-distribution")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getOrderStatusDistribution() {
|
||||
try {
|
||||
Map<String, Object> data = dashboardService.getOrderStatusDistribution();
|
||||
return ResponseEntity.ok(data);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付方式分布
|
||||
*/
|
||||
@GetMapping("/payment-method-distribution")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getPaymentMethodDistribution() {
|
||||
try {
|
||||
Map<String, Object> data = dashboardService.getPaymentMethodDistribution();
|
||||
return ResponseEntity.ok(data);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近订单列表
|
||||
*/
|
||||
@GetMapping("/recent-orders")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getRecentOrders() {
|
||||
try {
|
||||
Map<String, Object> data = dashboardService.getRecentOrders();
|
||||
return ResponseEntity.ok(data);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有仪表盘数据
|
||||
*/
|
||||
@GetMapping("/all")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getAllDashboardData() {
|
||||
try {
|
||||
Map<String, Object> allData = Map.of(
|
||||
"overview", dashboardService.getDashboardOverview(),
|
||||
"dailyActiveUsers", dashboardService.getDailyActiveUsers(),
|
||||
"revenueTrend", dashboardService.getRevenueTrend(),
|
||||
"orderStatusDistribution", dashboardService.getOrderStatusDistribution(),
|
||||
"paymentMethodDistribution", dashboardService.getPaymentMethodDistribution(),
|
||||
"recentOrders", dashboardService.getRecentOrders()
|
||||
);
|
||||
return ResponseEntity.ok(allData);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home() {
|
||||
return "home";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.*;
|
||||
import com.example.demo.service.OrderService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
public class OrderApiController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OrderApiController.class);
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getOrders(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||
@RequestParam(defaultValue = "desc") String sortDir,
|
||||
@RequestParam(required = false) OrderStatus status,
|
||||
@RequestParam(required = false) String search,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
// 检查认证信息
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未认证,请重新登录");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
User user = (User) authentication.getPrincipal();
|
||||
if (user == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户信息获取失败,请重新登录");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
// 创建分页和排序
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(sortDir), sortBy);
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
// 获取订单列表
|
||||
Page<Order> orderPage;
|
||||
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||
// 管理员可以查看所有订单
|
||||
orderPage = orderService.findAllOrders(pageable, status, search);
|
||||
} else {
|
||||
// 普通用户只能查看自己的订单
|
||||
orderPage = orderService.findOrdersByUser(user, pageable, status, search);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", orderPage);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取订单列表失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取订单列表失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getOrderById(@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Order order = orderService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限访问此订单"));
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", order);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取订单详情失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取订单详情失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<Map<String, Object>> createOrder(@Valid @RequestBody Order order,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
// 检查认证信息
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未认证,请重新登录");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
User user = (User) authentication.getPrincipal();
|
||||
if (user == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户信息获取失败,请重新登录");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
order.setUser(user);
|
||||
|
||||
Order createdOrder = orderService.createOrder(order);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单创建成功");
|
||||
response.put("data", createdOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("创建订单失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
@PostMapping("/{id}/status")
|
||||
public ResponseEntity<Map<String, Object>> updateOrderStatus(@PathVariable Long id,
|
||||
@RequestBody Map<String, String> request,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Order order = orderService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
|
||||
OrderStatus status = OrderStatus.valueOf(request.get("status"));
|
||||
String notes = request.get("notes");
|
||||
|
||||
Order updatedOrder = orderService.updateOrderStatus(id, status);
|
||||
|
||||
if (notes != null && !notes.trim().isEmpty()) {
|
||||
updatedOrder.setNotes((updatedOrder.getNotes() != null ? updatedOrder.getNotes() + "\n" : "") + notes);
|
||||
orderService.createOrder(updatedOrder);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单状态更新成功");
|
||||
response.put("data", updatedOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("更新订单状态失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("更新订单状态失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
@PostMapping("/{id}/cancel")
|
||||
public ResponseEntity<Map<String, Object>> cancelOrder(@PathVariable Long id,
|
||||
@RequestBody Map<String, String> request,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Order order = orderService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
|
||||
String reason = request.get("reason");
|
||||
Order cancelledOrder = orderService.cancelOrder(id, reason);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单取消成功");
|
||||
response.put("data", cancelledOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("取消订单失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("取消订单失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发货
|
||||
*/
|
||||
@PostMapping("/{id}/ship")
|
||||
public ResponseEntity<Map<String, Object>> shipOrder(@PathVariable Long id,
|
||||
@RequestBody Map<String, String> request,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
|
||||
// 只有管理员可以发货
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
|
||||
String trackingNumber = request.get("trackingNumber");
|
||||
Order shippedOrder = orderService.shipOrder(id, trackingNumber);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单发货成功");
|
||||
response.put("data", shippedOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("订单发货失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("订单发货失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成订单
|
||||
*/
|
||||
@PostMapping("/{id}/complete")
|
||||
public ResponseEntity<Map<String, Object>> completeOrder(@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
|
||||
// 只有管理员可以完成订单
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
|
||||
Order completedOrder = orderService.completeOrder(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "订单完成成功");
|
||||
response.put("data", completedOrder);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("完成订单失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("完成订单失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单支付
|
||||
*/
|
||||
@PostMapping("/{id}/pay")
|
||||
public ResponseEntity<Map<String, Object>> createOrderPayment(@PathVariable Long id,
|
||||
@RequestBody Map<String, String> request,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Order order = orderService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!order.getUser().getId().equals(user.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("无权限操作此订单"));
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (!order.canPay()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("订单当前状态不允许支付"));
|
||||
}
|
||||
|
||||
PaymentMethod paymentMethod = PaymentMethod.valueOf(request.get("paymentMethod"));
|
||||
|
||||
// 这里应该调用支付服务创建支付
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付创建成功");
|
||||
|
||||
// 模拟支付URL
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("paymentId", "payment-" + System.currentTimeMillis());
|
||||
data.put("paymentUrl", "/payment/" + paymentMethod.name().toLowerCase() + "/create?orderId=" + id);
|
||||
response.put("data", data);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单支付失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("创建订单支付失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<Map<String, Object>> getOrderStats(Authentication authentication) {
|
||||
try {
|
||||
// 检查认证信息
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户未认证,请重新登录");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
User user = (User) authentication.getPrincipal();
|
||||
if (user == null) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", "用户信息获取失败,请重新登录");
|
||||
return ResponseEntity.status(401).body(response);
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
Map<String, Object> stats;
|
||||
if (user.getRole().equals("ROLE_ADMIN")) {
|
||||
// 管理员查看所有订单统计
|
||||
stats = orderService.getOrderStats();
|
||||
} else {
|
||||
// 普通用户查看自己的订单统计
|
||||
stats = orderService.getOrderStatsByUser(user);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取订单统计失败:", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取订单统计失败:" + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> createErrorResponse(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", message);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.*;
|
||||
import com.example.demo.service.OrderService;
|
||||
import com.example.demo.service.PaymentService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/orders")
|
||||
public class OrderController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
private PaymentService paymentService;
|
||||
|
||||
/**
|
||||
* 显示订单列表
|
||||
*/
|
||||
@GetMapping
|
||||
public String orderList(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||
@RequestParam(defaultValue = "desc") String sortDir,
|
||||
@RequestParam(required = false) OrderStatus status,
|
||||
@RequestParam(required = false) String search,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Sort sort = sortDir.equalsIgnoreCase("desc") ?
|
||||
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
Page<Order> orders;
|
||||
if (status != null) {
|
||||
orders = orderService.findByStatus(status, pageable);
|
||||
} else {
|
||||
orders = orderService.findByUserId(user.getId(), pageable);
|
||||
}
|
||||
|
||||
model.addAttribute("orders", orders);
|
||||
model.addAttribute("currentPage", page);
|
||||
model.addAttribute("totalPages", orders.getTotalPages());
|
||||
model.addAttribute("totalElements", orders.getTotalElements());
|
||||
model.addAttribute("status", status);
|
||||
model.addAttribute("search", search);
|
||||
model.addAttribute("sortBy", sortBy);
|
||||
model.addAttribute("sortDir", sortDir);
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
|
||||
return "orders/list";
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取订单列表失败:", e);
|
||||
model.addAttribute("error", "获取订单列表失败:" + e.getMessage());
|
||||
return "orders/list";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示订单详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public String orderDetail(@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Optional<Order> orderOpt = orderService.findById(id);
|
||||
|
||||
if (!orderOpt.isPresent()) {
|
||||
model.addAttribute("error", "订单不存在");
|
||||
return "orders/detail";
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限:用户只能查看自己的订单,管理员可以查看所有订单
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限访问此订单");
|
||||
return "orders/detail";
|
||||
}
|
||||
|
||||
model.addAttribute("order", order);
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
|
||||
return "orders/detail";
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取订单详情失败:", e);
|
||||
model.addAttribute("error", "获取订单详情失败:" + e.getMessage());
|
||||
return "orders/detail";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示创建订单表单
|
||||
*/
|
||||
@GetMapping("/create")
|
||||
public String showCreateOrderForm(Model model) {
|
||||
Order order = new Order();
|
||||
order.setOrderItems(new ArrayList<>());
|
||||
order.setOrderItems(new ArrayList<>());
|
||||
|
||||
// 添加一个空的订单项
|
||||
OrderItem item = new OrderItem();
|
||||
item.setQuantity(1);
|
||||
item.setUnitPrice(BigDecimal.ZERO);
|
||||
order.getOrderItems().add(item);
|
||||
|
||||
model.addAttribute("order", order);
|
||||
model.addAttribute("orderTypes", OrderType.values());
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
|
||||
return "orders/form";
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建订单
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public String createOrder(@Valid @ModelAttribute Order order,
|
||||
BindingResult result,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
if (result.hasErrors()) {
|
||||
model.addAttribute("orderTypes", OrderType.values());
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
return "orders/form";
|
||||
}
|
||||
|
||||
User user = (User) authentication.getPrincipal();
|
||||
order.setUser(user);
|
||||
|
||||
// 验证订单项
|
||||
if (order.getOrderItems() == null || order.getOrderItems().isEmpty()) {
|
||||
model.addAttribute("error", "订单必须包含至少一个商品");
|
||||
model.addAttribute("orderTypes", OrderType.values());
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
return "orders/form";
|
||||
}
|
||||
|
||||
// 过滤掉空的订单项
|
||||
order.getOrderItems().removeIf(item ->
|
||||
item.getProductName() == null || item.getProductName().trim().isEmpty() ||
|
||||
item.getQuantity() == null || item.getQuantity() <= 0 ||
|
||||
item.getUnitPrice() == null || item.getUnitPrice().compareTo(BigDecimal.ZERO) <= 0);
|
||||
|
||||
if (order.getOrderItems().isEmpty()) {
|
||||
model.addAttribute("error", "订单必须包含至少一个有效商品");
|
||||
model.addAttribute("orderTypes", OrderType.values());
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
return "orders/form";
|
||||
}
|
||||
|
||||
Order createdOrder = orderService.createOrder(order);
|
||||
model.addAttribute("success", "订单创建成功,订单号:" + createdOrder.getOrderNumber());
|
||||
|
||||
return "redirect:/orders/" + createdOrder.getId();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单失败:", e);
|
||||
model.addAttribute("error", "创建订单失败:" + e.getMessage());
|
||||
model.addAttribute("orderTypes", OrderType.values());
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
return "orders/form";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
@PostMapping("/{id}/status")
|
||||
public String updateOrderStatus(@PathVariable Long id,
|
||||
@RequestParam OrderStatus status,
|
||||
@RequestParam(required = false) String notes,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Optional<Order> orderOpt = orderService.findById(id);
|
||||
|
||||
if (!orderOpt.isPresent()) {
|
||||
model.addAttribute("error", "订单不存在");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
Order updatedOrder = orderService.updateOrderStatus(id, status);
|
||||
|
||||
if (notes != null && !notes.trim().isEmpty()) {
|
||||
updatedOrder.setNotes((updatedOrder.getNotes() != null ? updatedOrder.getNotes() + "\n" : "") + notes);
|
||||
orderService.createOrder(updatedOrder); // 保存备注
|
||||
}
|
||||
|
||||
model.addAttribute("success", "订单状态更新成功");
|
||||
return "redirect:/orders/" + id;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("更新订单状态失败:", e);
|
||||
model.addAttribute("error", "更新订单状态失败:" + e.getMessage());
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
@PostMapping("/{id}/cancel")
|
||||
public String cancelOrder(@PathVariable Long id,
|
||||
@RequestParam(required = false) String reason,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Optional<Order> orderOpt = orderService.findById(id);
|
||||
|
||||
if (!orderOpt.isPresent()) {
|
||||
model.addAttribute("error", "订单不存在");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限
|
||||
if (!user.getRole().equals("ROLE_ADMIN") && !order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
Order cancelledOrder = orderService.cancelOrder(id, reason);
|
||||
model.addAttribute("success", "订单取消成功");
|
||||
|
||||
return "redirect:/orders/" + id;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("取消订单失败:", e);
|
||||
model.addAttribute("error", "取消订单失败:" + e.getMessage());
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发货
|
||||
*/
|
||||
@PostMapping("/{id}/ship")
|
||||
public String shipOrder(@PathVariable Long id,
|
||||
@RequestParam(required = false) String trackingNumber,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
|
||||
// 只有管理员可以发货
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
|
||||
Order shippedOrder = orderService.shipOrder(id, trackingNumber);
|
||||
model.addAttribute("success", "订单发货成功");
|
||||
|
||||
return "redirect:/orders/" + id;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("订单发货失败:", e);
|
||||
model.addAttribute("error", "订单发货失败:" + e.getMessage());
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成订单
|
||||
*/
|
||||
@PostMapping("/{id}/complete")
|
||||
public String completeOrder(@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
|
||||
// 只有管理员可以完成订单
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
|
||||
Order completedOrder = orderService.completeOrder(id);
|
||||
model.addAttribute("success", "订单完成成功");
|
||||
|
||||
return "redirect:/orders/" + id;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("完成订单失败:", e);
|
||||
model.addAttribute("error", "完成订单失败:" + e.getMessage());
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为订单创建支付
|
||||
*/
|
||||
@PostMapping("/{id}/pay")
|
||||
public String createPaymentForOrder(@PathVariable Long id,
|
||||
@RequestParam PaymentMethod paymentMethod,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Optional<Order> orderOpt = orderService.findById(id);
|
||||
|
||||
if (!orderOpt.isPresent()) {
|
||||
model.addAttribute("error", "订单不存在");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
Order order = orderOpt.get();
|
||||
|
||||
// 检查权限
|
||||
if (!order.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限操作此订单");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
// 检查订单状态
|
||||
if (!order.canPay()) {
|
||||
model.addAttribute("error", "订单当前状态不允许支付");
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
|
||||
// 创建支付记录
|
||||
Payment savedPayment = paymentService.createOrderPayment(order, paymentMethod);
|
||||
|
||||
// 根据支付方式跳转到相应的支付页面
|
||||
if (paymentMethod == PaymentMethod.ALIPAY) {
|
||||
return "redirect:/payment/alipay/create?paymentId=" + savedPayment.getId();
|
||||
} else if (paymentMethod == PaymentMethod.PAYPAL) {
|
||||
return "redirect:/payment/paypal/create?paymentId=" + savedPayment.getId();
|
||||
} else {
|
||||
model.addAttribute("error", "不支持的支付方式");
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单支付失败:", e);
|
||||
model.addAttribute("error", "创建订单支付失败:" + e.getMessage());
|
||||
return "redirect:/orders/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员订单管理页面
|
||||
*/
|
||||
@GetMapping("/admin")
|
||||
public String adminOrderList(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "createdAt") String sortBy,
|
||||
@RequestParam(defaultValue = "desc") String sortDir,
|
||||
@RequestParam(required = false) OrderStatus status,
|
||||
@RequestParam(required = false) String search,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
|
||||
// 只有管理员可以访问
|
||||
if (!user.getRole().equals("ROLE_ADMIN")) {
|
||||
model.addAttribute("error", "无权限访问");
|
||||
return "redirect:/orders";
|
||||
}
|
||||
|
||||
Sort sort = sortDir.equalsIgnoreCase("desc") ?
|
||||
Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
Page<Order> orders;
|
||||
if (status != null) {
|
||||
orders = orderService.findByStatus(status, pageable);
|
||||
} else {
|
||||
orders = orderService.findAll(pageable);
|
||||
}
|
||||
|
||||
model.addAttribute("orders", orders);
|
||||
model.addAttribute("currentPage", page);
|
||||
model.addAttribute("totalPages", orders.getTotalPages());
|
||||
model.addAttribute("totalElements", orders.getTotalElements());
|
||||
model.addAttribute("status", status);
|
||||
model.addAttribute("search", search);
|
||||
model.addAttribute("sortBy", sortBy);
|
||||
model.addAttribute("sortDir", sortDir);
|
||||
model.addAttribute("orderStatuses", OrderStatus.values());
|
||||
model.addAttribute("isAdmin", true);
|
||||
|
||||
return "orders/admin";
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取管理员订单列表失败:", e);
|
||||
model.addAttribute("error", "获取订单列表失败:" + e.getMessage());
|
||||
return "orders/admin";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
import com.example.demo.service.PaymentService;
|
||||
import com.example.demo.service.AlipayService;
|
||||
import com.example.demo.service.PayPalService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/payments")
|
||||
public class PaymentApiController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PaymentApiController.class);
|
||||
|
||||
@Autowired
|
||||
private PaymentService paymentService;
|
||||
|
||||
@Autowired
|
||||
private AlipayService alipayService;
|
||||
|
||||
@Autowired
|
||||
private PayPalService payPalService;
|
||||
|
||||
/**
|
||||
* 获取用户的支付记录
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getUserPayments(
|
||||
Authentication authentication,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String search) {
|
||||
try {
|
||||
List<Payment> payments;
|
||||
|
||||
// 检查用户是否已登录
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String username = authentication.getName();
|
||||
payments = paymentService.findByUsername(username);
|
||||
} else {
|
||||
// 未登录用户返回空列表
|
||||
payments = new ArrayList<>();
|
||||
}
|
||||
|
||||
// 简单的筛选逻辑
|
||||
if (status != null && !status.isEmpty()) {
|
||||
payments = payments.stream()
|
||||
.filter(p -> p.getStatus().name().equals(status))
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (search != null && !search.isEmpty()) {
|
||||
payments = payments.stream()
|
||||
.filter(p -> p.getOrderId().contains(search))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "获取支付记录成功");
|
||||
response.put("data", payments);
|
||||
response.put("total", payments.size());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取支付记录失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取支付记录失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取支付详情
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getPaymentById(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
Payment payment = paymentService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限访问此支付记录"));
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取支付详情失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取支付详情失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<Map<String, Object>> createPayment(
|
||||
@RequestBody Map<String, Object> paymentData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String username;
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
username = authentication.getName();
|
||||
} else {
|
||||
// 未登录用户使用匿名用户名
|
||||
username = "anonymous_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
String orderId = (String) paymentData.get("orderId");
|
||||
String amountStr = (String) paymentData.get("amount");
|
||||
String method = (String) paymentData.get("method");
|
||||
|
||||
if (orderId == null || amountStr == null || method == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("订单号、金额和支付方式不能为空"));
|
||||
}
|
||||
|
||||
Payment payment = paymentService.createPayment(username, orderId, amountStr, method);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付创建成功");
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建支付失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("创建支付失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
*/
|
||||
@PutMapping("/{id}/status")
|
||||
public ResponseEntity<Map<String, Object>> updatePaymentStatus(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> statusData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String status = statusData.get("status");
|
||||
if (status == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("状态不能为空"));
|
||||
}
|
||||
|
||||
Payment payment = paymentService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限修改此支付记录"));
|
||||
}
|
||||
|
||||
payment.setStatus(PaymentStatus.valueOf(status));
|
||||
paymentService.save(payment);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "状态更新成功");
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("更新支付状态失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("更新支付状态失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认支付成功
|
||||
*/
|
||||
@PostMapping("/{id}/success")
|
||||
public ResponseEntity<Map<String, Object>> confirmPaymentSuccess(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> successData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String externalTransactionId = successData.get("externalTransactionId");
|
||||
|
||||
Payment payment = paymentService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限操作此支付记录"));
|
||||
}
|
||||
|
||||
paymentService.confirmPaymentSuccess(id, externalTransactionId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付确认成功");
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("确认支付成功失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("确认支付成功失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认支付失败
|
||||
*/
|
||||
@PostMapping("/{id}/failure")
|
||||
public ResponseEntity<Map<String, Object>> confirmPaymentFailure(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> failureData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String failureReason = failureData.get("failureReason");
|
||||
|
||||
Payment payment = paymentService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限操作此支付记录"));
|
||||
}
|
||||
|
||||
paymentService.confirmPaymentFailure(id, failureReason);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付失败确认成功");
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("确认支付失败失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("确认支付失败失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付统计
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<Map<String, Object>> getPaymentStats(Authentication authentication) {
|
||||
try {
|
||||
String username = authentication.getName();
|
||||
Map<String, Object> stats = paymentService.getUserPaymentStats(username);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("data", stats);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取支付统计失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("获取支付统计失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试支付记录
|
||||
*/
|
||||
@PostMapping("/create-test")
|
||||
public ResponseEntity<Map<String, Object>> createTestPayment(
|
||||
@RequestBody Map<String, Object> paymentData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String username;
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
username = authentication.getName();
|
||||
} else {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("请先登录后再创建支付记录"));
|
||||
}
|
||||
|
||||
String amountStr = (String) paymentData.get("amount");
|
||||
String method = (String) paymentData.get("method");
|
||||
|
||||
if (amountStr == null || method == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("金额和支付方式不能为空"));
|
||||
}
|
||||
|
||||
// 生成测试订单号
|
||||
String testOrderId = "TEST_" + System.currentTimeMillis();
|
||||
Payment payment = paymentService.createPayment(username, testOrderId, amountStr, method);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "测试支付记录创建成功");
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建测试支付记录失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("创建测试支付记录失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试支付完成(用于测试自动创建订单功能)
|
||||
*/
|
||||
@PostMapping("/{id}/test-complete")
|
||||
public ResponseEntity<Map<String, Object>> testPaymentComplete(
|
||||
@PathVariable Long id,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
Payment payment = paymentService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(authentication.getName())) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限操作此支付记录"));
|
||||
}
|
||||
|
||||
// 模拟支付成功
|
||||
String mockTransactionId = "TEST_" + System.currentTimeMillis();
|
||||
paymentService.confirmPaymentSuccess(id, mockTransactionId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付完成测试成功,订单已自动创建");
|
||||
response.put("data", payment);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("测试支付完成失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("测试支付完成失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付宝支付
|
||||
*/
|
||||
@PostMapping("/alipay/create")
|
||||
public ResponseEntity<Map<String, Object>> createAlipayPayment(
|
||||
@RequestBody Map<String, Object> paymentData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String username;
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
username = authentication.getName();
|
||||
} else {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("请先登录后再创建支付"));
|
||||
}
|
||||
|
||||
Long paymentId = Long.valueOf(paymentData.get("paymentId").toString());
|
||||
Payment payment = paymentService.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(username)) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限操作此支付记录"));
|
||||
}
|
||||
|
||||
// 调用支付宝接口创建支付
|
||||
String paymentUrl = alipayService.createPayment(payment);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "支付宝支付创建成功");
|
||||
response.put("data", Map.of("paymentUrl", paymentUrl));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建支付宝支付失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("创建支付宝支付失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建PayPal支付
|
||||
*/
|
||||
@PostMapping("/paypal/create")
|
||||
public ResponseEntity<Map<String, Object>> createPayPalPayment(
|
||||
@RequestBody Map<String, Object> paymentData,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
String username;
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
username = authentication.getName();
|
||||
} else {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("请先登录后再创建支付"));
|
||||
}
|
||||
|
||||
Long paymentId = Long.valueOf(paymentData.get("paymentId").toString());
|
||||
Payment payment = paymentService.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getUsername().equals(username)) {
|
||||
return ResponseEntity.status(403)
|
||||
.body(createErrorResponse("无权限操作此支付记录"));
|
||||
}
|
||||
|
||||
// 调用PayPal接口创建支付
|
||||
String paymentUrl = payPalService.createPayment(payment);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "PayPal支付创建成功");
|
||||
response.put("data", Map.of("paymentUrl", paymentUrl));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建PayPal支付失败", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(createErrorResponse("创建PayPal支付失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝异步通知
|
||||
*/
|
||||
@PostMapping("/alipay/notify")
|
||||
public ResponseEntity<String> handleAlipayNotify(@RequestParam Map<String, String> params) {
|
||||
try {
|
||||
logger.info("收到支付宝异步通知:{}", params);
|
||||
|
||||
boolean success = alipayService.handleNotify(params);
|
||||
|
||||
if (success) {
|
||||
return ResponseEntity.ok("success");
|
||||
} else {
|
||||
return ResponseEntity.ok("fail");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("处理支付宝异步通知失败:", e);
|
||||
return ResponseEntity.ok("fail");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝同步返回
|
||||
*/
|
||||
@GetMapping("/alipay/return")
|
||||
public ResponseEntity<String> handleAlipayReturn(@RequestParam Map<String, String> params) {
|
||||
try {
|
||||
logger.info("收到支付宝同步返回:{}", params);
|
||||
|
||||
boolean success = alipayService.handleReturn(params);
|
||||
|
||||
if (success) {
|
||||
return ResponseEntity.ok("支付成功,正在跳转...");
|
||||
} else {
|
||||
return ResponseEntity.ok("支付验证失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("处理支付宝同步返回失败:", e);
|
||||
return ResponseEntity.ok("支付处理异常");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> createErrorResponse(String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", message);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentMethod;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.AlipayService;
|
||||
import com.example.demo.service.PayPalService;
|
||||
import com.example.demo.service.PaymentService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/payment")
|
||||
public class PaymentController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PaymentController.class);
|
||||
|
||||
@Autowired
|
||||
private PaymentService paymentService;
|
||||
|
||||
@Autowired
|
||||
private AlipayService alipayService;
|
||||
|
||||
@Autowired
|
||||
private PayPalService payPalService;
|
||||
|
||||
/**
|
||||
* 显示支付页面
|
||||
*/
|
||||
@GetMapping("/create")
|
||||
public String showPaymentForm(Model model) {
|
||||
model.addAttribute("payment", new Payment());
|
||||
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||
return "payment/form";
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付请求
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public String createPayment(@Valid @ModelAttribute Payment payment,
|
||||
BindingResult result,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
|
||||
if (result.hasErrors()) {
|
||||
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||
return "payment/form";
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置当前用户
|
||||
User user = (User) authentication.getPrincipal();
|
||||
payment.setUser(user);
|
||||
|
||||
// 设置默认货币
|
||||
if (payment.getCurrency() == null || payment.getCurrency().isEmpty()) {
|
||||
payment.setCurrency("CNY");
|
||||
}
|
||||
|
||||
// 根据支付方式创建支付
|
||||
String redirectUrl;
|
||||
if (payment.getPaymentMethod() == PaymentMethod.ALIPAY) {
|
||||
redirectUrl = alipayService.createPayment(payment);
|
||||
return "redirect:" + redirectUrl;
|
||||
} else if (payment.getPaymentMethod() == PaymentMethod.PAYPAL) {
|
||||
redirectUrl = payPalService.createPayment(payment);
|
||||
return "redirect:" + redirectUrl;
|
||||
} else {
|
||||
model.addAttribute("error", "不支持的支付方式");
|
||||
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||
return "payment/form";
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建支付订单失败:", e);
|
||||
model.addAttribute("error", "创建支付订单失败:" + e.getMessage());
|
||||
model.addAttribute("paymentMethods", PaymentMethod.values());
|
||||
return "payment/form";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝异步通知
|
||||
*/
|
||||
@PostMapping("/alipay/notify")
|
||||
@ResponseBody
|
||||
public String alipayNotify(HttpServletRequest request) {
|
||||
try {
|
||||
Map<String, String> params = request.getParameterMap().entrySet().stream()
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue()[0]
|
||||
));
|
||||
|
||||
boolean success = alipayService.handleNotify(params);
|
||||
return success ? "success" : "fail";
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理支付宝异步通知失败:", e);
|
||||
return "fail";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝同步返回
|
||||
*/
|
||||
@GetMapping("/alipay/return")
|
||||
public String alipayReturn(HttpServletRequest request, Model model) {
|
||||
try {
|
||||
Map<String, String> params = request.getParameterMap().entrySet().stream()
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue()[0]
|
||||
));
|
||||
|
||||
boolean success = alipayService.handleReturn(params);
|
||||
|
||||
if (success) {
|
||||
String outTradeNo = params.get("out_trade_no");
|
||||
Payment payment = paymentService.findByOrderId(outTradeNo)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
model.addAttribute("payment", payment);
|
||||
model.addAttribute("success", true);
|
||||
return "payment/result";
|
||||
} else {
|
||||
model.addAttribute("error", "支付验证失败");
|
||||
return "payment/result";
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理支付宝同步返回失败:", e);
|
||||
model.addAttribute("error", "支付处理失败:" + e.getMessage());
|
||||
return "payment/result";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal支付返回
|
||||
*/
|
||||
@GetMapping("/paypal/return")
|
||||
public String paypalReturn(@RequestParam("paymentId") String paymentId,
|
||||
@RequestParam("PayerID") String payerId,
|
||||
Model model) {
|
||||
try {
|
||||
boolean success = payPalService.executePayment(paymentId, payerId);
|
||||
|
||||
if (success) {
|
||||
Payment payment = paymentService.findByExternalTransactionId(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
model.addAttribute("payment", payment);
|
||||
model.addAttribute("success", true);
|
||||
return "payment/result";
|
||||
} else {
|
||||
model.addAttribute("error", "支付执行失败");
|
||||
return "payment/result";
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理PayPal支付返回失败:", e);
|
||||
model.addAttribute("error", "支付处理失败:" + e.getMessage());
|
||||
return "payment/result";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal支付取消
|
||||
*/
|
||||
@GetMapping("/paypal/cancel")
|
||||
public String paypalCancel(Model model) {
|
||||
model.addAttribute("error", "支付已取消");
|
||||
return "payment/result";
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal Webhook通知
|
||||
*/
|
||||
@PostMapping("/paypal/webhook")
|
||||
@ResponseBody
|
||||
public String paypalWebhook(HttpServletRequest request) {
|
||||
try {
|
||||
Map<String, String> params = request.getParameterMap().entrySet().stream()
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> entry.getValue()[0]
|
||||
));
|
||||
|
||||
boolean success = payPalService.handleWebhook(params);
|
||||
return success ? "success" : "fail";
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理PayPal Webhook失败:", e);
|
||||
return "fail";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付记录列表
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
public String paymentHistory(Authentication authentication, Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
List<Payment> payments = paymentService.findByUserId(user.getId());
|
||||
model.addAttribute("payments", payments);
|
||||
return "payment/history";
|
||||
} catch (Exception e) {
|
||||
logger.error("获取支付记录失败:", e);
|
||||
model.addAttribute("error", "获取支付记录失败");
|
||||
return "payment/history";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付详情
|
||||
*/
|
||||
@GetMapping("/detail/{id}")
|
||||
public String paymentDetail(@PathVariable Long id,
|
||||
Authentication authentication,
|
||||
Model model) {
|
||||
try {
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Payment payment = paymentService.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在"));
|
||||
|
||||
// 检查权限
|
||||
if (!payment.getUser().getId().equals(user.getId())) {
|
||||
model.addAttribute("error", "无权限访问此支付记录");
|
||||
return "payment/detail";
|
||||
}
|
||||
|
||||
model.addAttribute("payment", payment);
|
||||
return "payment/detail";
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("获取支付详情失败:", e);
|
||||
model.addAttribute("error", "获取支付详情失败");
|
||||
return "payment/detail";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.example.demo.repository.UserRepository;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/public")
|
||||
public class PublicApiController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public PublicApiController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/users/exists/username")
|
||||
public Map<String, Boolean> existsUsername(@RequestParam("value") String username) {
|
||||
return Map.of("exists", userRepository.existsByUsername(username));
|
||||
}
|
||||
|
||||
@GetMapping("/users/exists/email")
|
||||
public Map<String, Boolean> existsEmail(@RequestParam("value") String email) {
|
||||
return Map.of("exists", userRepository.existsByEmail(email));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.model.SystemSettings;
|
||||
import com.example.demo.service.SystemSettingsService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/settings")
|
||||
public class SettingsController {
|
||||
|
||||
private final SystemSettingsService settingsService;
|
||||
|
||||
public SettingsController(SystemSettingsService settingsService) {
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String showForm(Model model) {
|
||||
SystemSettings settings = settingsService.getOrCreate();
|
||||
model.addAttribute("pageTitle", "系统设置");
|
||||
model.addAttribute("settings", settings);
|
||||
return "settings/form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String save(@Valid @ModelAttribute("settings") SystemSettings form,
|
||||
BindingResult bindingResult,
|
||||
Model model) {
|
||||
if (bindingResult.hasErrors()) {
|
||||
model.addAttribute("pageTitle", "系统设置");
|
||||
return "settings/form";
|
||||
}
|
||||
settingsService.update(form);
|
||||
model.addAttribute("success", "保存成功");
|
||||
return "redirect:/settings";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
314
demo/src/main/java/com/example/demo/model/Order.java
Normal file
314
demo/src/main/java/com/example/demo/model/Order.java
Normal file
@@ -0,0 +1,314 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
public class Order {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 50, unique = true)
|
||||
private String orderNumber;
|
||||
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.01", message = "订单金额必须大于0")
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@NotBlank
|
||||
@Column(nullable = false, length = 3)
|
||||
private String currency;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private OrderStatus status;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private OrderType orderType;
|
||||
|
||||
@Column(length = 500)
|
||||
private String description;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String notes;
|
||||
|
||||
@Column(name = "shipping_address", length = 1000)
|
||||
private String shippingAddress;
|
||||
|
||||
@Column(name = "billing_address", length = 1000)
|
||||
private String billingAddress;
|
||||
|
||||
@Column(name = "contact_phone", length = 20)
|
||||
private String contactPhone;
|
||||
|
||||
@Column(name = "contact_email", length = 100)
|
||||
private String contactEmail;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "paid_at")
|
||||
private LocalDateTime paidAt;
|
||||
|
||||
@Column(name = "shipped_at")
|
||||
private LocalDateTime shippedAt;
|
||||
|
||||
@Column(name = "delivered_at")
|
||||
private LocalDateTime deliveredAt;
|
||||
|
||||
@Column(name = "cancelled_at")
|
||||
private LocalDateTime cancelledAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<OrderItem> orderItems = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<Payment> payments = new ArrayList<>();
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
if (orderNumber == null) {
|
||||
orderNumber = generateOrderNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
private String generateOrderNumber() {
|
||||
return "ORD" + System.currentTimeMillis() + String.format("%03d", (int)(Math.random() * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算订单总金额
|
||||
*/
|
||||
public BigDecimal calculateTotalAmount() {
|
||||
return orderItems.stream()
|
||||
.map(OrderItem::getSubtotal)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以支付
|
||||
*/
|
||||
public boolean canPay() {
|
||||
return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以取消
|
||||
*/
|
||||
public boolean canCancel() {
|
||||
return status == OrderStatus.PENDING || status == OrderStatus.CONFIRMED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以发货
|
||||
*/
|
||||
public boolean canShip() {
|
||||
return status == OrderStatus.PAID || status == OrderStatus.CONFIRMED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以完成
|
||||
*/
|
||||
public boolean canComplete() {
|
||||
return status == OrderStatus.SHIPPED;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(String orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public void setCurrency(String currency) {
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
public OrderStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(OrderStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public OrderType getOrderType() {
|
||||
return orderType;
|
||||
}
|
||||
|
||||
public void setOrderType(OrderType orderType) {
|
||||
this.orderType = orderType;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(String notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public String getShippingAddress() {
|
||||
return shippingAddress;
|
||||
}
|
||||
|
||||
public void setShippingAddress(String shippingAddress) {
|
||||
this.shippingAddress = shippingAddress;
|
||||
}
|
||||
|
||||
public String getBillingAddress() {
|
||||
return billingAddress;
|
||||
}
|
||||
|
||||
public void setBillingAddress(String billingAddress) {
|
||||
this.billingAddress = billingAddress;
|
||||
}
|
||||
|
||||
public String getContactPhone() {
|
||||
return contactPhone;
|
||||
}
|
||||
|
||||
public void setContactPhone(String contactPhone) {
|
||||
this.contactPhone = contactPhone;
|
||||
}
|
||||
|
||||
public String getContactEmail() {
|
||||
return contactEmail;
|
||||
}
|
||||
|
||||
public void setContactEmail(String contactEmail) {
|
||||
this.contactEmail = contactEmail;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getPaidAt() {
|
||||
return paidAt;
|
||||
}
|
||||
|
||||
public void setPaidAt(LocalDateTime paidAt) {
|
||||
this.paidAt = paidAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getShippedAt() {
|
||||
return shippedAt;
|
||||
}
|
||||
|
||||
public void setShippedAt(LocalDateTime shippedAt) {
|
||||
this.shippedAt = shippedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getDeliveredAt() {
|
||||
return deliveredAt;
|
||||
}
|
||||
|
||||
public void setDeliveredAt(LocalDateTime deliveredAt) {
|
||||
this.deliveredAt = deliveredAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getCancelledAt() {
|
||||
return cancelledAt;
|
||||
}
|
||||
|
||||
public void setCancelledAt(LocalDateTime cancelledAt) {
|
||||
this.cancelledAt = cancelledAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public List<OrderItem> getOrderItems() {
|
||||
return orderItems;
|
||||
}
|
||||
|
||||
public void setOrderItems(List<OrderItem> orderItems) {
|
||||
this.orderItems = orderItems;
|
||||
}
|
||||
|
||||
public List<Payment> getPayments() {
|
||||
return payments;
|
||||
}
|
||||
|
||||
public void setPayments(List<Payment> payments) {
|
||||
this.payments = payments;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
demo/src/main/java/com/example/demo/model/OrderItem.java
Normal file
134
demo/src/main/java/com/example/demo/model/OrderItem.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "order_items")
|
||||
public class OrderItem {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Column(nullable = false, length = 100)
|
||||
private String productName;
|
||||
|
||||
@Column(length = 500)
|
||||
private String productDescription;
|
||||
|
||||
@Column(length = 200)
|
||||
private String productSku;
|
||||
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.01", message = "单价必须大于0")
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@NotNull
|
||||
@Min(value = 1, message = "数量必须大于0")
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.00", message = "小计不能为负数")
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal subtotal;
|
||||
|
||||
@Column(length = 100)
|
||||
private String productImage;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id", nullable = false)
|
||||
private Order order;
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
protected void calculateSubtotal() {
|
||||
if (unitPrice != null && quantity != null) {
|
||||
subtotal = unitPrice.multiply(BigDecimal.valueOf(quantity));
|
||||
}
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getProductName() {
|
||||
return productName;
|
||||
}
|
||||
|
||||
public void setProductName(String productName) {
|
||||
this.productName = productName;
|
||||
}
|
||||
|
||||
public String getProductDescription() {
|
||||
return productDescription;
|
||||
}
|
||||
|
||||
public void setProductDescription(String productDescription) {
|
||||
this.productDescription = productDescription;
|
||||
}
|
||||
|
||||
public String getProductSku() {
|
||||
return productSku;
|
||||
}
|
||||
|
||||
public void setProductSku(String productSku) {
|
||||
this.productSku = productSku;
|
||||
}
|
||||
|
||||
public BigDecimal getUnitPrice() {
|
||||
return unitPrice;
|
||||
}
|
||||
|
||||
public void setUnitPrice(BigDecimal unitPrice) {
|
||||
this.unitPrice = unitPrice;
|
||||
}
|
||||
|
||||
public Integer getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public void setQuantity(Integer quantity) {
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
public BigDecimal getSubtotal() {
|
||||
return subtotal;
|
||||
}
|
||||
|
||||
public void setSubtotal(BigDecimal subtotal) {
|
||||
this.subtotal = subtotal;
|
||||
}
|
||||
|
||||
public String getProductImage() {
|
||||
return productImage;
|
||||
}
|
||||
|
||||
public void setProductImage(String productImage) {
|
||||
this.productImage = productImage;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public Order getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public void setOrder(Order order) {
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
demo/src/main/java/com/example/demo/model/OrderStatus.java
Normal file
26
demo/src/main/java/com/example/demo/model/OrderStatus.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
public enum OrderStatus {
|
||||
PENDING("待支付"),
|
||||
CONFIRMED("已确认"),
|
||||
PAID("已支付"),
|
||||
PROCESSING("处理中"),
|
||||
SHIPPED("已发货"),
|
||||
DELIVERED("已送达"),
|
||||
COMPLETED("已完成"),
|
||||
CANCELLED("已取消"),
|
||||
REFUNDED("已退款");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
OrderStatus(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
20
demo/src/main/java/com/example/demo/model/OrderType.java
Normal file
20
demo/src/main/java/com/example/demo/model/OrderType.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
public enum OrderType {
|
||||
PRODUCT("商品订单"),
|
||||
SERVICE("服务订单"),
|
||||
SUBSCRIPTION("订阅订单"),
|
||||
DIGITAL("数字商品"),
|
||||
PHYSICAL("实体商品"),
|
||||
PAYMENT("支付订单");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
OrderType(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
227
demo/src/main/java/com/example/demo/model/Payment.java
Normal file
227
demo/src/main/java/com/example/demo/model/Payment.java
Normal file
@@ -0,0 +1,227 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "payments")
|
||||
public class Payment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Column(nullable = false, length = 50)
|
||||
private String orderId;
|
||||
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.01", message = "金额必须大于0")
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@NotBlank
|
||||
@Column(nullable = false, length = 3)
|
||||
private String currency;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private PaymentMethod paymentMethod;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private PaymentStatus status;
|
||||
|
||||
@Column(length = 500)
|
||||
private String description;
|
||||
|
||||
@Column(length = 100)
|
||||
private String externalTransactionId;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String callbackUrl;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String returnUrl;
|
||||
|
||||
@Column(length = 2000)
|
||||
private String paymentUrl;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "paid_at")
|
||||
private LocalDateTime paidAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id_ref")
|
||||
private Order order;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
if (status == null) {
|
||||
status = PaymentStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以支付
|
||||
*/
|
||||
public boolean canPay() {
|
||||
return status == PaymentStatus.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以退款
|
||||
*/
|
||||
public boolean canRefund() {
|
||||
return status == PaymentStatus.SUCCESS;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public void setOrderId(String orderId) {
|
||||
this.orderId = orderId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public void setCurrency(String currency) {
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
public PaymentMethod getPaymentMethod() {
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
public void setPaymentMethod(PaymentMethod paymentMethod) {
|
||||
this.paymentMethod = paymentMethod;
|
||||
}
|
||||
|
||||
public PaymentStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(PaymentStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getExternalTransactionId() {
|
||||
return externalTransactionId;
|
||||
}
|
||||
|
||||
public void setExternalTransactionId(String externalTransactionId) {
|
||||
this.externalTransactionId = externalTransactionId;
|
||||
}
|
||||
|
||||
public String getCallbackUrl() {
|
||||
return callbackUrl;
|
||||
}
|
||||
|
||||
public void setCallbackUrl(String callbackUrl) {
|
||||
this.callbackUrl = callbackUrl;
|
||||
}
|
||||
|
||||
public String getReturnUrl() {
|
||||
return returnUrl;
|
||||
}
|
||||
|
||||
public void setReturnUrl(String returnUrl) {
|
||||
this.returnUrl = returnUrl;
|
||||
}
|
||||
|
||||
public String getPaymentUrl() {
|
||||
return paymentUrl;
|
||||
}
|
||||
|
||||
public void setPaymentUrl(String paymentUrl) {
|
||||
this.paymentUrl = paymentUrl;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getPaidAt() {
|
||||
return paidAt;
|
||||
}
|
||||
|
||||
public void setPaidAt(LocalDateTime paidAt) {
|
||||
this.paidAt = paidAt;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public Order getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public void setOrder(Order order) {
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
17
demo/src/main/java/com/example/demo/model/PaymentMethod.java
Normal file
17
demo/src/main/java/com/example/demo/model/PaymentMethod.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
public enum PaymentMethod {
|
||||
ALIPAY("支付宝"),
|
||||
PAYPAL("PayPal");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
PaymentMethod(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
21
demo/src/main/java/com/example/demo/model/PaymentStatus.java
Normal file
21
demo/src/main/java/com/example/demo/model/PaymentStatus.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
public enum PaymentStatus {
|
||||
PENDING("待支付"),
|
||||
PROCESSING("处理中"),
|
||||
SUCCESS("支付成功"),
|
||||
FAILED("支付失败"),
|
||||
CANCELLED("已取消"),
|
||||
REFUNDED("已退款");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
PaymentStatus(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
158
demo/src/main/java/com/example/demo/model/SystemSettings.java
Normal file
158
demo/src/main/java/com/example/demo/model/SystemSettings.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Entity
|
||||
@Table(name = "system_settings")
|
||||
public class SystemSettings {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 标准版价格(单位:元) */
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Column(nullable = false)
|
||||
private Integer standardPriceCny = 0;
|
||||
|
||||
/** 专业版价格(单位:元) */
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Column(nullable = false)
|
||||
private Integer proPriceCny = 0;
|
||||
|
||||
/** 每次生成消耗的资源点数量 */
|
||||
@NotNull
|
||||
@Min(0)
|
||||
@Column(nullable = false)
|
||||
private Integer pointsPerGeneration = 1;
|
||||
|
||||
/** 站点名称 */
|
||||
@Column(nullable = false, length = 100)
|
||||
private String siteName = "AIGC Demo";
|
||||
|
||||
/** 站点副标题 */
|
||||
@Column(nullable = false, length = 150)
|
||||
private String siteSubtitle = "现代化的Spring Boot应用演示";
|
||||
|
||||
/** 是否开放注册 */
|
||||
@NotNull
|
||||
@Column(nullable = false)
|
||||
private Boolean registrationOpen = true;
|
||||
|
||||
/** 维护模式开关 */
|
||||
@NotNull
|
||||
@Column(nullable = false)
|
||||
private Boolean maintenanceMode = false;
|
||||
|
||||
/** 支付渠道开关 */
|
||||
@NotNull
|
||||
@Column(nullable = false)
|
||||
private Boolean enableAlipay = true;
|
||||
|
||||
@NotNull
|
||||
@Column(nullable = false)
|
||||
private Boolean enablePaypal = true;
|
||||
|
||||
/** 联系邮箱 */
|
||||
@Column(length = 120)
|
||||
private String contactEmail = "support@example.com";
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getStandardPriceCny() {
|
||||
return standardPriceCny;
|
||||
}
|
||||
|
||||
public void setStandardPriceCny(Integer standardPriceCny) {
|
||||
this.standardPriceCny = standardPriceCny;
|
||||
}
|
||||
|
||||
public Integer getProPriceCny() {
|
||||
return proPriceCny;
|
||||
}
|
||||
|
||||
public void setProPriceCny(Integer proPriceCny) {
|
||||
this.proPriceCny = proPriceCny;
|
||||
}
|
||||
|
||||
public Integer getPointsPerGeneration() {
|
||||
return pointsPerGeneration;
|
||||
}
|
||||
|
||||
public void setPointsPerGeneration(Integer pointsPerGeneration) {
|
||||
this.pointsPerGeneration = pointsPerGeneration;
|
||||
}
|
||||
|
||||
public String getSiteName() {
|
||||
return siteName;
|
||||
}
|
||||
|
||||
public void setSiteName(String siteName) {
|
||||
this.siteName = siteName;
|
||||
}
|
||||
|
||||
public String getSiteSubtitle() {
|
||||
return siteSubtitle;
|
||||
}
|
||||
|
||||
public void setSiteSubtitle(String siteSubtitle) {
|
||||
this.siteSubtitle = siteSubtitle;
|
||||
}
|
||||
|
||||
public Boolean getRegistrationOpen() {
|
||||
return registrationOpen;
|
||||
}
|
||||
|
||||
public void setRegistrationOpen(Boolean registrationOpen) {
|
||||
this.registrationOpen = registrationOpen;
|
||||
}
|
||||
|
||||
public Boolean getMaintenanceMode() {
|
||||
return maintenanceMode;
|
||||
}
|
||||
|
||||
public void setMaintenanceMode(Boolean maintenanceMode) {
|
||||
this.maintenanceMode = maintenanceMode;
|
||||
}
|
||||
|
||||
public Boolean getEnableAlipay() {
|
||||
return enableAlipay;
|
||||
}
|
||||
|
||||
public void setEnableAlipay(Boolean enableAlipay) {
|
||||
this.enableAlipay = enableAlipay;
|
||||
}
|
||||
|
||||
public Boolean getEnablePaypal() {
|
||||
return enablePaypal;
|
||||
}
|
||||
|
||||
public void setEnablePaypal(Boolean enablePaypal) {
|
||||
this.enablePaypal = enablePaypal;
|
||||
}
|
||||
|
||||
public String getContactEmail() {
|
||||
return contactEmail;
|
||||
}
|
||||
|
||||
public void setContactEmail(String contactEmail) {
|
||||
this.contactEmail = contactEmail;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
112
demo/src/main/java/com/example/demo/model/User.java
Normal file
112
demo/src/main/java/com/example/demo/model/User.java
Normal file
@@ -0,0 +1,112 @@
|
||||
package com.example.demo.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 3, max = 50)
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String username;
|
||||
|
||||
@NotBlank
|
||||
@Email
|
||||
@Column(nullable = false, unique = true, length = 100)
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
@Size(min = 6, max = 100)
|
||||
@Column(nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
@NotBlank
|
||||
@Column(nullable = false, length = 30)
|
||||
private String role = "ROLE_USER";
|
||||
|
||||
@Min(0)
|
||||
@Column(nullable = false)
|
||||
private Integer points = 50; // 默认50积分
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public Integer getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public void setPoints(Integer points) {
|
||||
this.points = points;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import com.example.demo.model.OrderItem;
|
||||
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.util.List;
|
||||
|
||||
@Repository
|
||||
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
|
||||
|
||||
/**
|
||||
* 根据订单ID查找订单项
|
||||
*/
|
||||
List<OrderItem> findByOrderId(Long orderId);
|
||||
|
||||
/**
|
||||
* 根据产品SKU查找订单项
|
||||
*/
|
||||
List<OrderItem> findByProductSku(String productSku);
|
||||
|
||||
/**
|
||||
* 根据产品名称模糊查询订单项
|
||||
*/
|
||||
@Query("SELECT oi FROM OrderItem oi WHERE oi.productName LIKE %:productName%")
|
||||
List<OrderItem> findByProductNameContaining(@Param("productName") String productName);
|
||||
|
||||
/**
|
||||
* 统计指定产品的销售数量
|
||||
*/
|
||||
@Query("SELECT SUM(oi.quantity) FROM OrderItem oi WHERE oi.productSku = :productSku")
|
||||
Long sumQuantityByProductSku(@Param("productSku") String productSku);
|
||||
|
||||
/**
|
||||
* 统计指定产品的销售金额
|
||||
*/
|
||||
@Query("SELECT SUM(oi.subtotal) FROM OrderItem oi WHERE oi.productSku = :productSku")
|
||||
java.math.BigDecimal sumSubtotalByProductSku(@Param("productSku") String productSku);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import com.example.demo.model.Order;
|
||||
import com.example.demo.model.OrderStatus;
|
||||
import com.example.demo.model.OrderType;
|
||||
import com.example.demo.model.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.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
|
||||
/**
|
||||
* 根据订单号查找订单
|
||||
*/
|
||||
Optional<Order> findByOrderNumber(String orderNumber);
|
||||
|
||||
/**
|
||||
* 根据用户ID查找订单
|
||||
*/
|
||||
List<Order> findByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找订单
|
||||
*/
|
||||
Page<Order> findByUserId(Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据订单状态查找订单
|
||||
*/
|
||||
List<Order> findByStatus(OrderStatus status);
|
||||
|
||||
/**
|
||||
* 根据订单状态分页查找订单
|
||||
*/
|
||||
Page<Order> findByStatus(OrderStatus status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据订单类型查找订单
|
||||
*/
|
||||
List<Order> findByOrderType(OrderType orderType);
|
||||
|
||||
/**
|
||||
* 根据用户ID和订单状态查找订单
|
||||
*/
|
||||
List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
|
||||
|
||||
/**
|
||||
* 根据创建时间范围查找订单
|
||||
*/
|
||||
List<Order> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* 根据用户ID和创建时间范围查找订单
|
||||
*/
|
||||
List<Order> findByUserIdAndCreatedAtBetween(Long userId, LocalDateTime startDate, LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* 统计用户订单数量
|
||||
*/
|
||||
long countByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 统计指定状态的订单数量
|
||||
*/
|
||||
long countByStatus(OrderStatus status);
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的订单数量
|
||||
*/
|
||||
long countByUserIdAndStatus(Long userId, OrderStatus status);
|
||||
|
||||
/**
|
||||
* 查找用户的最近订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.user.id = :userId ORDER BY o.createdAt DESC")
|
||||
List<Order> findRecentOrdersByUserId(@Param("userId") Long userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 查找待支付的订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.status IN ('PENDING', 'CONFIRMED') AND o.createdAt < :expireTime")
|
||||
List<Order> findExpiredPendingOrders(@Param("expireTime") LocalDateTime expireTime);
|
||||
|
||||
/**
|
||||
* 根据订单号模糊查询
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.orderNumber LIKE %:orderNumber%")
|
||||
List<Order> findByOrderNumberContaining(@Param("orderNumber") String orderNumber);
|
||||
|
||||
/**
|
||||
* 根据用户邮箱查找订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.contactEmail = :email")
|
||||
List<Order> findByContactEmail(@Param("email") String email);
|
||||
|
||||
/**
|
||||
* 根据用户手机号查找订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.contactPhone = :phone")
|
||||
List<Order> findByContactPhone(@Param("phone") String phone);
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的订单数量
|
||||
*/
|
||||
@Query("SELECT COUNT(o) FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate")
|
||||
long countByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的订单总金额
|
||||
*/
|
||||
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.createdAt BETWEEN :startDate AND :endDate AND o.status = 'COMPLETED'")
|
||||
BigDecimal sumTotalAmountByCreatedAtBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
|
||||
|
||||
/**
|
||||
* 查找需要自动取消的订单
|
||||
*/
|
||||
@Query("SELECT o FROM Order o WHERE o.status = 'PENDING' AND o.createdAt < :cancelTime")
|
||||
List<Order> findOrdersToAutoCancel(@Param("cancelTime") LocalDateTime cancelTime);
|
||||
|
||||
// 新增的查询方法
|
||||
|
||||
/**
|
||||
* 根据状态和订单号模糊查询(分页)
|
||||
*/
|
||||
Page<Order> findByStatusAndOrderNumberContainingIgnoreCase(OrderStatus status, String orderNumber, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据订单号模糊查询(分页)
|
||||
*/
|
||||
Page<Order> findByOrderNumberContainingIgnoreCase(String orderNumber, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户查找订单(分页)
|
||||
*/
|
||||
Page<Order> findByUser(User user, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户和状态查找订单(分页)
|
||||
*/
|
||||
Page<Order> findByUserAndStatus(User user, OrderStatus status, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户和订单号模糊查询(分页)
|
||||
*/
|
||||
Page<Order> findByUserAndOrderNumberContainingIgnoreCase(User user, String orderNumber, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 根据用户、状态和订单号模糊查询(分页)
|
||||
*/
|
||||
Page<Order> findByUserAndStatusAndOrderNumberContainingIgnoreCase(User user, OrderStatus status, String orderNumber, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 统计指定时间之后的订单数量
|
||||
*/
|
||||
long countByCreatedAtAfter(LocalDateTime dateTime);
|
||||
|
||||
/**
|
||||
* 统计用户指定时间之后的订单数量
|
||||
*/
|
||||
long countByUserAndCreatedAtAfter(User user, LocalDateTime dateTime);
|
||||
|
||||
/**
|
||||
* 统计用户的订单数量
|
||||
*/
|
||||
long countByUser(User user);
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的订单数量
|
||||
*/
|
||||
long countByUserAndStatus(User user, OrderStatus status);
|
||||
|
||||
/**
|
||||
* 统计指定状态的订单总金额
|
||||
*/
|
||||
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.status = :status")
|
||||
BigDecimal sumTotalAmountByStatus(@Param("status") OrderStatus status);
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的订单总金额
|
||||
*/
|
||||
@Query("SELECT SUM(o.totalAmount) FROM Order o WHERE o.user = :user AND o.status = :status")
|
||||
BigDecimal sumTotalAmountByUserAndStatus(@Param("user") User user, @Param("status") OrderStatus status);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
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.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PaymentRepository extends JpaRepository<Payment, Long> {
|
||||
|
||||
Optional<Payment> findByOrderId(String orderId);
|
||||
|
||||
Optional<Payment> findByExternalTransactionId(String externalTransactionId);
|
||||
|
||||
List<Payment> findByUserId(Long userId);
|
||||
|
||||
List<Payment> findByStatus(PaymentStatus status);
|
||||
|
||||
@Query("SELECT p FROM Payment p WHERE p.user.id = :userId ORDER BY p.createdAt DESC")
|
||||
List<Payment> findByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId);
|
||||
|
||||
@Query("SELECT COUNT(p) FROM Payment p WHERE p.status = :status")
|
||||
long countByStatus(@Param("status") PaymentStatus status);
|
||||
|
||||
/**
|
||||
* 统计用户支付记录数量
|
||||
*/
|
||||
long countByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的支付记录数量
|
||||
*/
|
||||
long countByUserIdAndStatus(Long userId, PaymentStatus status);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import com.example.demo.model.SystemSettings;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface SystemSettingsRepository extends JpaRepository<SystemSettings, Long> {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
Optional<User> findByEmail(String email);
|
||||
boolean existsByUsername(String username);
|
||||
boolean existsByEmail(String email);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.example.demo.security;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.service.UserService;
|
||||
import com.example.demo.util.JwtUtils;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
|
||||
|
||||
private final JwtUtils jwtUtils;
|
||||
private final UserService userService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserService userService) {
|
||||
this.jwtUtils = jwtUtils;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
logger.debug("JWT过滤器处理请求: {}", request.getRequestURI());
|
||||
|
||||
try {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
logger.debug("Authorization头: {}", authHeader);
|
||||
|
||||
String token = jwtUtils.extractTokenFromHeader(authHeader);
|
||||
logger.debug("提取的token: {}", token != null ? "存在" : "不存在");
|
||||
|
||||
if (token != null) {
|
||||
String username = jwtUtils.getUsernameFromToken(token);
|
||||
logger.debug("从token获取用户名: {}", username);
|
||||
|
||||
if (username != null && jwtUtils.validateToken(token, username)) {
|
||||
User user = userService.findByUsername(username);
|
||||
logger.debug("查找用户: {}", user != null ? user.getUsername() : "未找到");
|
||||
|
||||
if (user != null) {
|
||||
// 创建用户权限列表
|
||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||
authorities.add(new SimpleGrantedAuthority(user.getRole()));
|
||||
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(user, null, authorities);
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
logger.debug("JWT认证成功,用户: {}, 角色: {}", username, user.getRole());
|
||||
} else {
|
||||
logger.warn("JWT中的用户不存在: {}", username);
|
||||
}
|
||||
} else {
|
||||
logger.debug("JWT验证失败,用户: {}", username);
|
||||
}
|
||||
} else {
|
||||
logger.debug("没有token");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("JWT认证过程中发生异常: {}", e.getMessage(), e);
|
||||
// 清除可能存在的认证信息
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.demo.security;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PlainTextPasswordEncoder implements PasswordEncoder {
|
||||
|
||||
@Override
|
||||
public String encode(CharSequence rawPassword) {
|
||||
return rawPassword.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(CharSequence rawPassword, String encodedPassword) {
|
||||
return rawPassword.toString().equals(encodedPassword);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.demo.security;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.repository.UserRepository;
|
||||
|
||||
@Service
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserDetailsServiceImpl(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
|
||||
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getUsername(),
|
||||
user.getPasswordHash(),
|
||||
List.of(new SimpleGrantedAuthority(user.getRole()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
212
demo/src/main/java/com/example/demo/service/AlipayService.java
Normal file
212
demo/src/main/java/com/example/demo/service/AlipayService.java
Normal file
@@ -0,0 +1,212 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.AlipayClient;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
import com.alipay.api.domain.AlipayTradePagePayModel;
|
||||
import com.alipay.api.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradePagePayRequest;
|
||||
import com.alipay.api.response.AlipayTradePagePayResponse;
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentMethod;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
import com.example.demo.repository.PaymentRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AlipayService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AlipayService.class);
|
||||
|
||||
private final PaymentRepository paymentRepository;
|
||||
|
||||
@Value("${alipay.app-id}")
|
||||
private String appId;
|
||||
|
||||
@Value("${alipay.private-key}")
|
||||
private String privateKey;
|
||||
|
||||
@Value("${alipay.public-key}")
|
||||
private String publicKey;
|
||||
|
||||
@Value("${alipay.gateway-url}")
|
||||
private String gatewayUrl;
|
||||
|
||||
@Value("${alipay.charset}")
|
||||
private String charset;
|
||||
|
||||
@Value("${alipay.sign-type}")
|
||||
private String signType;
|
||||
|
||||
@Value("${alipay.notify-url}")
|
||||
private String notifyUrl;
|
||||
|
||||
@Value("${alipay.return-url}")
|
||||
private String returnUrl;
|
||||
|
||||
public AlipayService(PaymentRepository paymentRepository) {
|
||||
this.paymentRepository = paymentRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付宝支付订单
|
||||
*/
|
||||
public String createPayment(Payment payment) {
|
||||
try {
|
||||
// 设置支付状态
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
payment.setPaymentMethod(PaymentMethod.ALIPAY);
|
||||
payment.setOrderId(generateOrderId());
|
||||
payment.setCallbackUrl(notifyUrl);
|
||||
payment.setReturnUrl(returnUrl);
|
||||
|
||||
// 保存支付记录
|
||||
paymentRepository.save(payment);
|
||||
|
||||
// 创建支付宝客户端
|
||||
AlipayClient alipayClient = new DefaultAlipayClient(
|
||||
gatewayUrl, appId, privateKey, "json", charset, publicKey, signType);
|
||||
|
||||
// 设置请求参数
|
||||
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
|
||||
request.setReturnUrl(returnUrl);
|
||||
request.setNotifyUrl(notifyUrl);
|
||||
|
||||
// 设置业务参数
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
model.setOutTradeNo(payment.getOrderId());
|
||||
model.setTotalAmount(payment.getAmount().toString());
|
||||
model.setSubject(payment.getDescription() != null ? payment.getDescription() : "商品支付");
|
||||
model.setBody(payment.getDescription());
|
||||
model.setProductCode("FAST_INSTANT_TRADE_PAY");
|
||||
|
||||
request.setBizModel(model);
|
||||
|
||||
// 调用API
|
||||
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
|
||||
|
||||
if (response.isSuccess()) {
|
||||
logger.info("支付宝支付订单创建成功,订单号:{}", payment.getOrderId());
|
||||
// 返回支付URL,前端可以跳转到这个URL进行支付
|
||||
String paymentUrl = gatewayUrl + "?" + response.getBody();
|
||||
return paymentUrl;
|
||||
} else {
|
||||
logger.error("支付宝支付订单创建失败:{}", response.getMsg());
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
paymentRepository.save(payment);
|
||||
throw new RuntimeException("支付订单创建失败:" + response.getMsg());
|
||||
}
|
||||
|
||||
} catch (AlipayApiException e) {
|
||||
logger.error("支付宝API调用异常:", e);
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
paymentRepository.save(payment);
|
||||
throw new RuntimeException("支付服务异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付宝异步通知
|
||||
*/
|
||||
public boolean handleNotify(Map<String, String> params) {
|
||||
try {
|
||||
// 验证签名
|
||||
boolean signVerified = AlipaySignature.rsaCheckV1(
|
||||
params, publicKey, charset, signType);
|
||||
|
||||
if (!signVerified) {
|
||||
logger.warn("支付宝异步通知签名验证失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
String tradeStatus = params.get("trade_status");
|
||||
String outTradeNo = params.get("out_trade_no");
|
||||
String tradeNo = params.get("trade_no");
|
||||
|
||||
logger.info("收到支付宝异步通知,订单号:{},交易状态:{}", outTradeNo, tradeStatus);
|
||||
|
||||
// 查找支付记录
|
||||
Payment payment = paymentRepository.findByOrderId(outTradeNo)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + outTradeNo));
|
||||
|
||||
// 更新支付状态
|
||||
switch (tradeStatus) {
|
||||
case "TRADE_SUCCESS":
|
||||
case "TRADE_FINISHED":
|
||||
payment.setStatus(PaymentStatus.SUCCESS);
|
||||
payment.setExternalTransactionId(tradeNo);
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
break;
|
||||
case "TRADE_CLOSED":
|
||||
payment.setStatus(PaymentStatus.CANCELLED);
|
||||
break;
|
||||
default:
|
||||
payment.setStatus(PaymentStatus.PROCESSING);
|
||||
break;
|
||||
}
|
||||
|
||||
paymentRepository.save(payment);
|
||||
logger.info("支付状态更新成功,订单号:{},状态:{}", outTradeNo, payment.getStatus());
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理支付宝异步通知异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付宝同步返回
|
||||
*/
|
||||
public boolean handleReturn(Map<String, String> params) {
|
||||
try {
|
||||
// 验证签名
|
||||
boolean signVerified = AlipaySignature.rsaCheckV1(
|
||||
params, publicKey, charset, signType);
|
||||
|
||||
if (!signVerified) {
|
||||
logger.warn("支付宝同步返回签名验证失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
String outTradeNo = params.get("out_trade_no");
|
||||
String tradeNo = params.get("trade_no");
|
||||
|
||||
logger.info("收到支付宝同步返回,订单号:{},交易号:{}", outTradeNo, tradeNo);
|
||||
|
||||
// 查找支付记录
|
||||
Payment payment = paymentRepository.findByOrderId(outTradeNo)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + outTradeNo));
|
||||
|
||||
// 如果状态还是待支付,更新为处理中
|
||||
if (payment.getStatus() == PaymentStatus.PENDING) {
|
||||
payment.setStatus(PaymentStatus.PROCESSING);
|
||||
payment.setExternalTransactionId(tradeNo);
|
||||
paymentRepository.save(payment);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理支付宝同步返回异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
private String generateOrderId() {
|
||||
return "ALI" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.Order;
|
||||
import com.example.demo.model.OrderStatus;
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
import com.example.demo.repository.OrderRepository;
|
||||
import com.example.demo.repository.PaymentRepository;
|
||||
import com.example.demo.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class DashboardService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private PaymentRepository paymentRepository;
|
||||
|
||||
/**
|
||||
* 获取仪表盘概览数据
|
||||
*/
|
||||
public Map<String, Object> getDashboardOverview() {
|
||||
Map<String, Object> overview = new HashMap<>();
|
||||
|
||||
// 用户总数
|
||||
long totalUsers = userRepository.count();
|
||||
overview.put("totalUsers", totalUsers);
|
||||
|
||||
// 付费用户数(有成功支付记录的不同用户数)
|
||||
long payingUsers = paymentRepository.findAll().stream()
|
||||
.filter(payment -> payment.getStatus() == PaymentStatus.SUCCESS)
|
||||
.map(payment -> payment.getUser().getId())
|
||||
.distinct()
|
||||
.count();
|
||||
overview.put("payingUsers", payingUsers);
|
||||
|
||||
// 今日收入(今日成功支付的金额)
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime todayEnd = LocalDate.now().atTime(23, 59, 59);
|
||||
|
||||
List<Payment> todayPayments = paymentRepository.findAll().stream()
|
||||
.filter(payment -> payment.getStatus() == PaymentStatus.SUCCESS)
|
||||
.filter(payment -> payment.getPaidAt() != null &&
|
||||
payment.getPaidAt().isAfter(todayStart) &&
|
||||
payment.getPaidAt().isBefore(todayEnd))
|
||||
.toList();
|
||||
|
||||
BigDecimal todayRevenue = todayPayments.stream()
|
||||
.map(Payment::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
overview.put("todayRevenue", todayRevenue);
|
||||
|
||||
// 转化率(付费用户数 / 总用户数)
|
||||
double conversionRate = totalUsers > 0 ? (double) payingUsers / totalUsers * 100 : 0;
|
||||
overview.put("conversionRate", Math.round(conversionRate * 100.0) / 100.0);
|
||||
|
||||
// 今日新增用户
|
||||
long todayNewUsers = userRepository.findAll().stream()
|
||||
.filter(user -> user.getCreatedAt() != null &&
|
||||
user.getCreatedAt().isAfter(todayStart) &&
|
||||
user.getCreatedAt().isBefore(todayEnd))
|
||||
.count();
|
||||
overview.put("todayNewUsers", todayNewUsers);
|
||||
|
||||
return overview;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日活数据(最近30天)
|
||||
*/
|
||||
public Map<String, Object> getDailyActiveUsers() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
List<Map<String, Object>> dailyData = new ArrayList<>();
|
||||
LocalDate endDate = LocalDate.now();
|
||||
|
||||
for (int i = 29; i >= 0; i--) {
|
||||
LocalDate date = endDate.minusDays(i);
|
||||
LocalDateTime startOfDay = date.atStartOfDay();
|
||||
LocalDateTime endOfDay = date.atTime(23, 59, 59);
|
||||
|
||||
// 统计当天有订单的用户数作为日活
|
||||
long dailyActiveUsers = orderRepository.findAll().stream()
|
||||
.filter(order -> order.getCreatedAt() != null &&
|
||||
order.getCreatedAt().isAfter(startOfDay) &&
|
||||
order.getCreatedAt().isBefore(endOfDay))
|
||||
.map(Order::getUser)
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
Map<String, Object> dayData = new HashMap<>();
|
||||
dayData.put("date", date.format(DateTimeFormatter.ofPattern("MM-dd")));
|
||||
dayData.put("activeUsers", dailyActiveUsers);
|
||||
dailyData.add(dayData);
|
||||
}
|
||||
|
||||
result.put("dailyData", dailyData);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取收入趋势数据(最近30天)
|
||||
*/
|
||||
public Map<String, Object> getRevenueTrend() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
List<Map<String, Object>> revenueData = new ArrayList<>();
|
||||
LocalDate endDate = LocalDate.now();
|
||||
|
||||
for (int i = 29; i >= 0; i--) {
|
||||
LocalDate date = endDate.minusDays(i);
|
||||
LocalDateTime startOfDay = date.atStartOfDay();
|
||||
LocalDateTime endOfDay = date.atTime(23, 59, 59);
|
||||
|
||||
// 统计当天成功支付的金额
|
||||
BigDecimal dailyRevenue = paymentRepository.findAll().stream()
|
||||
.filter(payment -> payment.getStatus() == PaymentStatus.SUCCESS)
|
||||
.filter(payment -> payment.getPaidAt() != null &&
|
||||
payment.getPaidAt().isAfter(startOfDay) &&
|
||||
payment.getPaidAt().isBefore(endOfDay))
|
||||
.map(Payment::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
Map<String, Object> dayData = new HashMap<>();
|
||||
dayData.put("date", date.format(DateTimeFormatter.ofPattern("MM-dd")));
|
||||
dayData.put("revenue", dailyRevenue);
|
||||
revenueData.add(dayData);
|
||||
}
|
||||
|
||||
result.put("revenueData", revenueData);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单状态分布
|
||||
*/
|
||||
public Map<String, Object> getOrderStatusDistribution() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
List<Map<String, Object>> statusData = new ArrayList<>();
|
||||
for (OrderStatus status : OrderStatus.values()) {
|
||||
long count = orderRepository.countByStatus(status);
|
||||
Map<String, Object> statusInfo = new HashMap<>();
|
||||
statusInfo.put("status", status.name());
|
||||
statusInfo.put("count", count);
|
||||
statusData.add(statusInfo);
|
||||
}
|
||||
|
||||
result.put("statusData", statusData);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支付方式分布
|
||||
*/
|
||||
public Map<String, Object> getPaymentMethodDistribution() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
List<Map<String, Object>> methodData = new ArrayList<>();
|
||||
// 这里需要根据PaymentMethod枚举来统计,暂时简化处理
|
||||
Map<String, Long> methodCounts = new HashMap<>();
|
||||
|
||||
paymentRepository.findAll().forEach(payment -> {
|
||||
String method = payment.getPaymentMethod().name();
|
||||
methodCounts.put(method, methodCounts.getOrDefault(method, 0L) + 1);
|
||||
});
|
||||
|
||||
methodCounts.forEach((method, count) -> {
|
||||
Map<String, Object> methodInfo = new HashMap<>();
|
||||
methodInfo.put("method", method);
|
||||
methodInfo.put("count", count);
|
||||
methodData.add(methodInfo);
|
||||
});
|
||||
|
||||
result.put("methodData", methodData);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近订单列表
|
||||
*/
|
||||
public Map<String, Object> getRecentOrders() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
List<Order> recentOrders = orderRepository.findAll().stream()
|
||||
.sorted((o1, o2) -> o2.getCreatedAt().compareTo(o1.getCreatedAt()))
|
||||
.limit(10)
|
||||
.toList();
|
||||
|
||||
List<Map<String, Object>> orderList = new ArrayList<>();
|
||||
recentOrders.forEach(order -> {
|
||||
Map<String, Object> orderInfo = new HashMap<>();
|
||||
orderInfo.put("id", order.getId());
|
||||
orderInfo.put("orderNumber", order.getOrderNumber());
|
||||
orderInfo.put("totalAmount", order.getTotalAmount());
|
||||
orderInfo.put("status", order.getStatus().name());
|
||||
orderInfo.put("createdAt", order.getCreatedAt());
|
||||
orderInfo.put("username", order.getUser().getUsername());
|
||||
orderList.add(orderInfo);
|
||||
});
|
||||
|
||||
result.put("orders", orderList);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
525
demo/src/main/java/com/example/demo/service/OrderService.java
Normal file
525
demo/src/main/java/com/example/demo/service/OrderService.java
Normal file
@@ -0,0 +1,525 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.*;
|
||||
import com.example.demo.repository.OrderItemRepository;
|
||||
import com.example.demo.repository.OrderRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class OrderService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderItemRepository orderItemRepository;
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
public Order createOrder(Order order) {
|
||||
try {
|
||||
// 设置订单状态
|
||||
if (order.getStatus() == null) {
|
||||
order.setStatus(OrderStatus.PENDING);
|
||||
}
|
||||
|
||||
// 设置默认货币
|
||||
if (order.getCurrency() == null || order.getCurrency().isEmpty()) {
|
||||
order.setCurrency("CNY");
|
||||
}
|
||||
|
||||
// 设置默认订单类型
|
||||
if (order.getOrderType() == null) {
|
||||
order.setOrderType(OrderType.PRODUCT);
|
||||
}
|
||||
|
||||
// 计算订单总金额
|
||||
if (order.getOrderItems() != null && !order.getOrderItems().isEmpty()) {
|
||||
BigDecimal totalAmount = order.getOrderItems().stream()
|
||||
.map(OrderItem::getSubtotal)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
order.setTotalAmount(totalAmount);
|
||||
}
|
||||
|
||||
// 临时保存订单项,避免级联保存问题
|
||||
List<OrderItem> tempOrderItems = order.getOrderItems();
|
||||
order.setOrderItems(new ArrayList<>()); // 清空订单项
|
||||
|
||||
// 先保存订单(获得ID)
|
||||
Order savedOrder = orderRepository.save(order);
|
||||
|
||||
// 再保存订单项,设置正确的order_id
|
||||
if (tempOrderItems != null && !tempOrderItems.isEmpty()) {
|
||||
List<OrderItem> savedOrderItems = new ArrayList<>();
|
||||
for (OrderItem item : tempOrderItems) {
|
||||
item.setOrder(savedOrder); // 设置关联的订单
|
||||
OrderItem savedItem = orderItemRepository.save(item);
|
||||
savedOrderItems.add(savedItem);
|
||||
}
|
||||
savedOrder.setOrderItems(savedOrderItems);
|
||||
}
|
||||
|
||||
logger.info("订单创建成功,订单号:{}", savedOrder.getOrderNumber());
|
||||
return savedOrder;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单失败:", e);
|
||||
throw new RuntimeException("创建订单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Order> findById(Long id) {
|
||||
return orderRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单号查找订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Order> findByOrderNumber(String orderNumber) {
|
||||
return orderRepository.findByOrderNumber(orderNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查找订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Order> findByUserId(Long userId) {
|
||||
return orderRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID分页查找订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Order> findByUserId(Long userId, Pageable pageable) {
|
||||
return orderRepository.findByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Order> findAll() {
|
||||
return orderRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查找所有订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Order> findAll(Pageable pageable) {
|
||||
return orderRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态查找订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Order> findByStatus(OrderStatus status) {
|
||||
return orderRepository.findByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态分页查找订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Order> findByStatus(OrderStatus status, Pageable pageable) {
|
||||
return orderRepository.findByStatus(status, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
public Order updateOrderStatus(Long orderId, OrderStatus newStatus) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
OrderStatus oldStatus = order.getStatus();
|
||||
order.setStatus(newStatus);
|
||||
|
||||
// 设置相应的时间戳
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
switch (newStatus) {
|
||||
case PAID:
|
||||
order.setPaidAt(now);
|
||||
break;
|
||||
case SHIPPED:
|
||||
order.setShippedAt(now);
|
||||
break;
|
||||
case DELIVERED:
|
||||
order.setDeliveredAt(now);
|
||||
break;
|
||||
case COMPLETED:
|
||||
order.setDeliveredAt(now);
|
||||
break;
|
||||
case CANCELLED:
|
||||
order.setCancelledAt(now);
|
||||
break;
|
||||
}
|
||||
|
||||
Order updatedOrder = orderRepository.save(order);
|
||||
logger.info("订单状态更新成功,订单号:{},状态:{} -> {}",
|
||||
order.getOrderNumber(), oldStatus, newStatus);
|
||||
|
||||
return updatedOrder;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("更新订单状态失败:", e);
|
||||
throw new RuntimeException("更新订单状态失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
*/
|
||||
public Order cancelOrder(Long orderId, String reason) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
if (!order.canCancel()) {
|
||||
throw new RuntimeException("订单当前状态不允许取消");
|
||||
}
|
||||
|
||||
order.setStatus(OrderStatus.CANCELLED);
|
||||
order.setCancelledAt(LocalDateTime.now());
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
order.setNotes((order.getNotes() != null ? order.getNotes() + "\n" : "") +
|
||||
"取消原因:" + reason);
|
||||
}
|
||||
|
||||
Order cancelledOrder = orderRepository.save(order);
|
||||
logger.info("订单取消成功,订单号:{}", order.getOrderNumber());
|
||||
|
||||
return cancelledOrder;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("取消订单失败:", e);
|
||||
throw new RuntimeException("取消订单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认订单支付
|
||||
*/
|
||||
public Order confirmPayment(Long orderId, String paymentId) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
if (!order.canPay()) {
|
||||
throw new RuntimeException("订单当前状态不允许支付");
|
||||
}
|
||||
|
||||
order.setStatus(OrderStatus.PAID);
|
||||
order.setPaidAt(LocalDateTime.now());
|
||||
|
||||
Order paidOrder = orderRepository.save(order);
|
||||
logger.info("订单支付确认成功,订单号:{},支付ID:{}",
|
||||
order.getOrderNumber(), paymentId);
|
||||
|
||||
return paidOrder;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("确认订单支付失败:", e);
|
||||
throw new RuntimeException("确认订单支付失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发货
|
||||
*/
|
||||
public Order shipOrder(Long orderId, String trackingNumber) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
if (!order.canShip()) {
|
||||
throw new RuntimeException("订单当前状态不允许发货");
|
||||
}
|
||||
|
||||
order.setStatus(OrderStatus.SHIPPED);
|
||||
order.setShippedAt(LocalDateTime.now());
|
||||
if (trackingNumber != null && !trackingNumber.isEmpty()) {
|
||||
order.setNotes((order.getNotes() != null ? order.getNotes() + "\n" : "") +
|
||||
"物流单号:" + trackingNumber);
|
||||
}
|
||||
|
||||
Order shippedOrder = orderRepository.save(order);
|
||||
logger.info("订单发货成功,订单号:{}", order.getOrderNumber());
|
||||
|
||||
return shippedOrder;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("订单发货失败:", e);
|
||||
throw new RuntimeException("订单发货失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成订单
|
||||
*/
|
||||
public Order completeOrder(Long orderId) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
if (!order.canComplete()) {
|
||||
throw new RuntimeException("订单当前状态不允许完成");
|
||||
}
|
||||
|
||||
order.setStatus(OrderStatus.COMPLETED);
|
||||
order.setDeliveredAt(LocalDateTime.now());
|
||||
|
||||
Order completedOrder = orderRepository.save(order);
|
||||
logger.info("订单完成成功,订单号:{}", order.getOrderNumber());
|
||||
|
||||
return completedOrder;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("完成订单失败:", e);
|
||||
throw new RuntimeException("完成订单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计订单数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByUserId(Long userId) {
|
||||
return orderRepository.countByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定状态的订单数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByStatus(OrderStatus status) {
|
||||
return orderRepository.countByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的订单数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByUserIdAndStatus(Long userId, OrderStatus status) {
|
||||
return orderRepository.countByUserIdAndStatus(userId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户的最近订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Order> findRecentOrdersByUserId(Long userId, Pageable pageable) {
|
||||
return orderRepository.findRecentOrdersByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找过期的待支付订单
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Order> findExpiredPendingOrders(int expireMinutes) {
|
||||
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(expireMinutes);
|
||||
return orderRepository.findExpiredPendingOrders(expireTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动取消过期订单
|
||||
*/
|
||||
public int autoCancelExpiredOrders(int expireMinutes) {
|
||||
try {
|
||||
List<Order> expiredOrders = findExpiredPendingOrders(expireMinutes);
|
||||
int cancelledCount = 0;
|
||||
|
||||
for (Order order : expiredOrders) {
|
||||
try {
|
||||
cancelOrder(order.getId(), "系统自动取消:订单超时未支付");
|
||||
cancelledCount++;
|
||||
} catch (Exception e) {
|
||||
logger.warn("自动取消订单失败,订单号:{}", order.getOrderNumber(), e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("自动取消过期订单完成,取消数量:{}", cancelledCount);
|
||||
return cancelledCount;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("自动取消过期订单失败:", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除订单(软删除,仅管理员可操作)
|
||||
*/
|
||||
public void deleteOrder(Long orderId) {
|
||||
try {
|
||||
Order order = orderRepository.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("订单不存在:" + orderId));
|
||||
|
||||
// 只有已取消或已退款的订单才能删除
|
||||
if (order.getStatus() != OrderStatus.CANCELLED && order.getStatus() != OrderStatus.REFUNDED) {
|
||||
throw new RuntimeException("只有已取消或已退款的订单才能删除");
|
||||
}
|
||||
|
||||
orderRepository.delete(order);
|
||||
logger.info("订单删除成功,订单号:{}", order.getOrderNumber());
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("删除订单失败:", e);
|
||||
throw new RuntimeException("删除订单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存订单
|
||||
*/
|
||||
public Order save(Order order) {
|
||||
try {
|
||||
Order savedOrder = orderRepository.save(order);
|
||||
logger.info("订单保存成功,订单ID:{}", savedOrder.getId());
|
||||
return savedOrder;
|
||||
} catch (Exception e) {
|
||||
logger.error("保存订单失败:", e);
|
||||
throw new RuntimeException("保存订单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存订单项
|
||||
*/
|
||||
public OrderItem saveOrderItem(OrderItem orderItem) {
|
||||
try {
|
||||
OrderItem savedOrderItem = orderItemRepository.save(orderItem);
|
||||
logger.info("订单项保存成功,订单项ID:{}", savedOrderItem.getId());
|
||||
return savedOrderItem;
|
||||
} catch (Exception e) {
|
||||
logger.error("保存订单项失败:", e);
|
||||
throw new RuntimeException("保存订单项失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查找所有订单(支持状态和搜索筛选)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Order> findAllOrders(Pageable pageable, OrderStatus status, String search) {
|
||||
if (status != null && search != null && !search.trim().isEmpty()) {
|
||||
return orderRepository.findByStatusAndOrderNumberContainingIgnoreCase(status, search, pageable);
|
||||
} else if (status != null) {
|
||||
return orderRepository.findByStatus(status, pageable);
|
||||
} else if (search != null && !search.trim().isEmpty()) {
|
||||
return orderRepository.findByOrderNumberContainingIgnoreCase(search, pageable);
|
||||
} else {
|
||||
return orderRepository.findAll(pageable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查找用户的订单(支持状态和搜索筛选)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Order> findOrdersByUser(User user, Pageable pageable, OrderStatus status, String search) {
|
||||
if (status != null && search != null && !search.trim().isEmpty()) {
|
||||
return orderRepository.findByUserAndStatusAndOrderNumberContainingIgnoreCase(user, status, search, pageable);
|
||||
} else if (status != null) {
|
||||
return orderRepository.findByUserAndStatus(user, status, pageable);
|
||||
} else if (search != null && !search.trim().isEmpty()) {
|
||||
return orderRepository.findByUserAndOrderNumberContainingIgnoreCase(user, search, pageable);
|
||||
} else {
|
||||
return orderRepository.findByUser(user, pageable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有订单统计
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getOrderStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 总订单数
|
||||
long totalOrders = orderRepository.count();
|
||||
stats.put("totalOrders", totalOrders);
|
||||
|
||||
// 各状态订单数
|
||||
stats.put("pendingOrders", orderRepository.countByStatus(OrderStatus.PENDING));
|
||||
stats.put("confirmedOrders", orderRepository.countByStatus(OrderStatus.CONFIRMED));
|
||||
stats.put("paidOrders", orderRepository.countByStatus(OrderStatus.PAID));
|
||||
stats.put("processingOrders", orderRepository.countByStatus(OrderStatus.PROCESSING));
|
||||
stats.put("shippedOrders", orderRepository.countByStatus(OrderStatus.SHIPPED));
|
||||
stats.put("deliveredOrders", orderRepository.countByStatus(OrderStatus.DELIVERED));
|
||||
stats.put("completedOrders", orderRepository.countByStatus(OrderStatus.COMPLETED));
|
||||
stats.put("cancelledOrders", orderRepository.countByStatus(OrderStatus.CANCELLED));
|
||||
stats.put("refundedOrders", orderRepository.countByStatus(OrderStatus.REFUNDED));
|
||||
|
||||
// 今日订单数
|
||||
LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
long todayOrders = orderRepository.countByCreatedAtAfter(today);
|
||||
stats.put("todayOrders", todayOrders);
|
||||
|
||||
// 总金额
|
||||
BigDecimal totalAmount = orderRepository.sumTotalAmountByStatus(OrderStatus.COMPLETED);
|
||||
stats.put("totalAmount", totalAmount != null ? totalAmount : BigDecimal.ZERO);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户订单统计
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getOrderStatsByUser(User user) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// 用户总订单数
|
||||
long totalOrders = orderRepository.countByUser(user);
|
||||
stats.put("totalOrders", totalOrders);
|
||||
|
||||
// 用户各状态订单数
|
||||
stats.put("pendingOrders", orderRepository.countByUserAndStatus(user, OrderStatus.PENDING));
|
||||
stats.put("confirmedOrders", orderRepository.countByUserAndStatus(user, OrderStatus.CONFIRMED));
|
||||
stats.put("paidOrders", orderRepository.countByUserAndStatus(user, OrderStatus.PAID));
|
||||
stats.put("processingOrders", orderRepository.countByUserAndStatus(user, OrderStatus.PROCESSING));
|
||||
stats.put("shippedOrders", orderRepository.countByUserAndStatus(user, OrderStatus.SHIPPED));
|
||||
stats.put("deliveredOrders", orderRepository.countByUserAndStatus(user, OrderStatus.DELIVERED));
|
||||
stats.put("completedOrders", orderRepository.countByUserAndStatus(user, OrderStatus.COMPLETED));
|
||||
stats.put("cancelledOrders", orderRepository.countByUserAndStatus(user, OrderStatus.CANCELLED));
|
||||
stats.put("refundedOrders", orderRepository.countByUserAndStatus(user, OrderStatus.REFUNDED));
|
||||
|
||||
// 用户今日订单数
|
||||
LocalDateTime today = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
long todayOrders = orderRepository.countByUserAndCreatedAtAfter(user, today);
|
||||
stats.put("todayOrders", todayOrders);
|
||||
|
||||
// 用户总金额
|
||||
BigDecimal totalAmount = orderRepository.sumTotalAmountByUserAndStatus(user, OrderStatus.COMPLETED);
|
||||
stats.put("totalAmount", totalAmount != null ? totalAmount : BigDecimal.ZERO);
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
202
demo/src/main/java/com/example/demo/service/PayPalService.java
Normal file
202
demo/src/main/java/com/example/demo/service/PayPalService.java
Normal file
@@ -0,0 +1,202 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.Payment;
|
||||
import com.example.demo.model.PaymentMethod;
|
||||
import com.example.demo.model.PaymentStatus;
|
||||
import com.example.demo.repository.PaymentRepository;
|
||||
import com.paypal.api.payments.*;
|
||||
import com.paypal.base.rest.APIContext;
|
||||
import com.paypal.base.rest.PayPalRESTException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class PayPalService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PayPalService.class);
|
||||
|
||||
private final PaymentRepository paymentRepository;
|
||||
|
||||
@Value("${paypal.client-id}")
|
||||
private String clientId;
|
||||
|
||||
@Value("${paypal.client-secret}")
|
||||
private String clientSecret;
|
||||
|
||||
@Value("${paypal.mode}")
|
||||
private String mode;
|
||||
|
||||
@Value("${paypal.return-url}")
|
||||
private String returnUrl;
|
||||
|
||||
@Value("${paypal.cancel-url}")
|
||||
private String cancelUrl;
|
||||
|
||||
public PayPalService(PaymentRepository paymentRepository) {
|
||||
this.paymentRepository = paymentRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建PayPal支付订单
|
||||
*/
|
||||
public String createPayment(Payment payment) {
|
||||
try {
|
||||
// 设置支付状态
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
payment.setPaymentMethod(PaymentMethod.PAYPAL);
|
||||
payment.setOrderId(generateOrderId());
|
||||
payment.setReturnUrl(returnUrl);
|
||||
|
||||
// 保存支付记录
|
||||
paymentRepository.save(payment);
|
||||
|
||||
// 创建API上下文
|
||||
APIContext apiContext = new APIContext(clientId, clientSecret, mode);
|
||||
|
||||
// 创建支付金额
|
||||
Amount amount = new Amount();
|
||||
amount.setCurrency(payment.getCurrency());
|
||||
amount.setTotal(payment.getAmount().toString());
|
||||
|
||||
// 创建交易
|
||||
Transaction transaction = new Transaction();
|
||||
transaction.setAmount(amount);
|
||||
transaction.setDescription(payment.getDescription() != null ? payment.getDescription() : "商品支付");
|
||||
|
||||
// 创建交易列表
|
||||
List<Transaction> transactions = new ArrayList<>();
|
||||
transactions.add(transaction);
|
||||
|
||||
// 创建支付者
|
||||
Payer payer = new Payer();
|
||||
payer.setPaymentMethod("paypal");
|
||||
|
||||
// 创建支付
|
||||
com.paypal.api.payments.Payment paypalPayment = new com.paypal.api.payments.Payment();
|
||||
paypalPayment.setIntent("sale");
|
||||
paypalPayment.setPayer(payer);
|
||||
paypalPayment.setTransactions(transactions);
|
||||
|
||||
// 设置重定向URL
|
||||
RedirectUrls redirectUrls = new RedirectUrls();
|
||||
redirectUrls.setReturnUrl(returnUrl + "?paymentId={PAY_ID}&PayerID={PAYER_ID}");
|
||||
redirectUrls.setCancelUrl(cancelUrl);
|
||||
paypalPayment.setRedirectUrls(redirectUrls);
|
||||
|
||||
// 创建支付
|
||||
com.paypal.api.payments.Payment createdPayment = paypalPayment.create(apiContext);
|
||||
|
||||
// 更新支付记录
|
||||
payment.setExternalTransactionId(createdPayment.getId());
|
||||
paymentRepository.save(payment);
|
||||
|
||||
logger.info("PayPal支付订单创建成功,订单号:{},PayPal ID:{}",
|
||||
payment.getOrderId(), createdPayment.getId());
|
||||
|
||||
// 获取批准URL
|
||||
for (Links link : createdPayment.getLinks()) {
|
||||
if ("approval_url".equals(link.getRel())) {
|
||||
return link.getHref();
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("未找到PayPal批准URL");
|
||||
|
||||
} catch (PayPalRESTException e) {
|
||||
logger.error("PayPal API调用异常:", e);
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
paymentRepository.save(payment);
|
||||
throw new RuntimeException("PayPal支付服务异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行PayPal支付
|
||||
*/
|
||||
public boolean executePayment(String paymentId, String payerId) {
|
||||
try {
|
||||
// 创建API上下文
|
||||
APIContext apiContext = new APIContext(clientId, clientSecret, mode);
|
||||
|
||||
// 创建支付执行
|
||||
PaymentExecution paymentExecution = new PaymentExecution();
|
||||
paymentExecution.setPayerId(payerId);
|
||||
|
||||
// 获取支付信息
|
||||
com.paypal.api.payments.Payment payment = com.paypal.api.payments.Payment.get(apiContext, paymentId);
|
||||
|
||||
// 执行支付
|
||||
com.paypal.api.payments.Payment executedPayment = payment.execute(apiContext, paymentExecution);
|
||||
|
||||
// 查找支付记录
|
||||
Payment paymentRecord = paymentRepository.findByExternalTransactionId(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
// 更新支付状态
|
||||
if ("approved".equals(executedPayment.getState())) {
|
||||
paymentRecord.setStatus(PaymentStatus.SUCCESS);
|
||||
paymentRecord.setPaidAt(LocalDateTime.now());
|
||||
logger.info("PayPal支付成功,订单号:{}", paymentRecord.getOrderId());
|
||||
} else {
|
||||
paymentRecord.setStatus(PaymentStatus.FAILED);
|
||||
logger.warn("PayPal支付失败,订单号:{},状态:{}",
|
||||
paymentRecord.getOrderId(), executedPayment.getState());
|
||||
}
|
||||
|
||||
paymentRepository.save(paymentRecord);
|
||||
return true;
|
||||
|
||||
} catch (PayPalRESTException e) {
|
||||
logger.error("PayPal支付执行异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理PayPal Webhook通知
|
||||
*/
|
||||
public boolean handleWebhook(Map<String, String> params) {
|
||||
try {
|
||||
String eventType = params.get("event_type");
|
||||
String resourceType = params.get("resource_type");
|
||||
|
||||
logger.info("收到PayPal Webhook通知,事件类型:{},资源类型:{}", eventType, resourceType);
|
||||
|
||||
if ("PAYMENT.SALE.COMPLETED".equals(eventType) && "sale".equals(resourceType)) {
|
||||
String paymentId = params.get("resource.id");
|
||||
|
||||
// 查找支付记录
|
||||
Payment payment = paymentRepository.findByExternalTransactionId(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
// 更新支付状态
|
||||
payment.setStatus(PaymentStatus.SUCCESS);
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
paymentRepository.save(payment);
|
||||
|
||||
logger.info("PayPal Webhook处理成功,订单号:{}", payment.getOrderId());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理PayPal Webhook异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
*/
|
||||
private String generateOrderId() {
|
||||
return "PP" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
465
demo/src/main/java/com/example/demo/service/PaymentService.java
Normal file
465
demo/src/main/java/com/example/demo/service/PaymentService.java
Normal file
@@ -0,0 +1,465 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.*;
|
||||
import com.example.demo.repository.PaymentRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class PaymentService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
|
||||
|
||||
@Autowired
|
||||
private PaymentRepository paymentRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private AlipayService alipayService;
|
||||
|
||||
/**
|
||||
* 保存支付记录
|
||||
*/
|
||||
public Payment save(Payment payment) {
|
||||
try {
|
||||
Payment savedPayment = paymentRepository.save(payment);
|
||||
logger.info("支付记录保存成功,支付ID:{}", savedPayment.getId());
|
||||
return savedPayment;
|
||||
} catch (Exception e) {
|
||||
logger.error("保存支付记录失败:", e);
|
||||
throw new RuntimeException("保存支付记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findById(Long id) {
|
||||
return paymentRepository.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据订单ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findByOrderId(String orderId) {
|
||||
return paymentRepository.findByOrderId(orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据外部交易ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findByExternalTransactionId(String externalTransactionId) {
|
||||
return paymentRepository.findByExternalTransactionId(externalTransactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByUserId(Long userId) {
|
||||
return paymentRepository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找所有支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findAll() {
|
||||
return paymentRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByStatus(PaymentStatus status) {
|
||||
return paymentRepository.findByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
*/
|
||||
public Payment updatePaymentStatus(Long paymentId, PaymentStatus newStatus) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
PaymentStatus oldStatus = payment.getStatus();
|
||||
payment.setStatus(newStatus);
|
||||
|
||||
if (newStatus == PaymentStatus.SUCCESS) {
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
|
||||
// 更新关联订单状态
|
||||
if (payment.getOrder() != null) {
|
||||
orderService.confirmPayment(payment.getOrder().getId(), payment.getExternalTransactionId());
|
||||
}
|
||||
}
|
||||
|
||||
Payment updatedPayment = paymentRepository.save(payment);
|
||||
logger.info("支付状态更新成功,支付ID:{},状态:{} -> {}",
|
||||
paymentId, oldStatus, newStatus);
|
||||
|
||||
return updatedPayment;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("更新支付状态失败:", e);
|
||||
throw new RuntimeException("更新支付状态失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认支付成功
|
||||
*/
|
||||
public Payment confirmPaymentSuccess(Long paymentId, String externalTransactionId) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
payment.setStatus(PaymentStatus.SUCCESS);
|
||||
payment.setPaidAt(LocalDateTime.now());
|
||||
payment.setExternalTransactionId(externalTransactionId);
|
||||
|
||||
Payment confirmedPayment = paymentRepository.save(payment);
|
||||
|
||||
// 更新关联订单状态
|
||||
if (payment.getOrder() != null) {
|
||||
orderService.confirmPayment(payment.getOrder().getId(), externalTransactionId);
|
||||
} else {
|
||||
// 如果没有关联订单,自动创建一个订单
|
||||
createOrderFromPayment(confirmedPayment);
|
||||
}
|
||||
|
||||
// 根据支付金额增加积分
|
||||
addPointsForPayment(confirmedPayment);
|
||||
|
||||
logger.info("支付确认成功,支付ID:{},外部交易ID:{}", paymentId, externalTransactionId);
|
||||
return confirmedPayment;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("确认支付成功失败:", e);
|
||||
throw new RuntimeException("确认支付成功失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认支付失败
|
||||
*/
|
||||
public Payment confirmPaymentFailure(Long paymentId, String failureReason) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
payment.setStatus(PaymentStatus.FAILED);
|
||||
if (failureReason != null && !failureReason.isEmpty()) {
|
||||
payment.setDescription((payment.getDescription() != null ? payment.getDescription() + "\n" : "") +
|
||||
"失败原因:" + failureReason);
|
||||
}
|
||||
|
||||
Payment failedPayment = paymentRepository.save(payment);
|
||||
logger.info("支付确认失败,支付ID:{},失败原因:{}", paymentId, failureReason);
|
||||
|
||||
return failedPayment;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("确认支付失败失败:", e);
|
||||
throw new RuntimeException("确认支付失败失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单支付
|
||||
*/
|
||||
public Payment createOrderPayment(Order order, PaymentMethod paymentMethod) {
|
||||
try {
|
||||
Payment payment = new Payment();
|
||||
payment.setOrderId(order.getOrderNumber());
|
||||
payment.setAmount(order.getTotalAmount());
|
||||
payment.setCurrency(order.getCurrency());
|
||||
payment.setPaymentMethod(paymentMethod);
|
||||
payment.setDescription("订单支付 - " + order.getOrderNumber());
|
||||
payment.setUser(order.getUser());
|
||||
payment.setOrder(order);
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
|
||||
Payment savedPayment = paymentRepository.save(payment);
|
||||
logger.info("订单支付创建成功,订单号:{},支付ID:{}", order.getOrderNumber(), savedPayment.getId());
|
||||
|
||||
return savedPayment;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("创建订单支付失败:", e);
|
||||
throw new RuntimeException("创建订单支付失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计支付记录数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByUserId(Long userId) {
|
||||
return paymentRepository.countByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定状态的支付记录数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByStatus(PaymentStatus status) {
|
||||
return paymentRepository.countByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户指定状态的支付记录数量
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countByUserIdAndStatus(Long userId, PaymentStatus status) {
|
||||
return paymentRepository.countByUserIdAndStatus(userId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除支付记录
|
||||
*/
|
||||
public void deletePayment(Long paymentId) {
|
||||
try {
|
||||
Payment payment = paymentRepository.findById(paymentId)
|
||||
.orElseThrow(() -> new RuntimeException("支付记录不存在:" + paymentId));
|
||||
|
||||
// 只有失败的支付记录才能删除
|
||||
if (payment.getStatus() != PaymentStatus.FAILED) {
|
||||
throw new RuntimeException("只有失败的支付记录才能删除");
|
||||
}
|
||||
|
||||
paymentRepository.delete(payment);
|
||||
logger.info("支付记录删除成功,支付ID:{}", paymentId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("删除支付记录失败:", e);
|
||||
throw new RuntimeException("删除支付记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查找支付记录
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Payment> findByUsername(String username) {
|
||||
try {
|
||||
logger.info("PaymentService: 开始查找用户 {} 的支付记录", username);
|
||||
|
||||
// 先查找用户
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) {
|
||||
logger.error("PaymentService: 用户 {} 不存在", username);
|
||||
throw new RuntimeException("用户不存在: " + username);
|
||||
}
|
||||
|
||||
logger.info("PaymentService: 找到用户 {}, ID: {}", username, user.getId());
|
||||
|
||||
// 查找支付记录
|
||||
List<Payment> payments = paymentRepository.findByUserIdOrderByCreatedAtDesc(user.getId());
|
||||
logger.info("PaymentService: 用户 {} 的支付记录数量: {}", username, payments.size());
|
||||
|
||||
return payments;
|
||||
} catch (Exception e) {
|
||||
logger.error("PaymentService: 根据用户名查找支付记录失败,用户名: {}, 错误: {}", username, e.getMessage(), e);
|
||||
throw new RuntimeException("查找支付记录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建支付
|
||||
*/
|
||||
public Payment createPayment(String username, String orderId, String amountStr, String method) {
|
||||
try {
|
||||
logger.info("创建支付 - 用户名: {}, 订单ID: {}, 金额: {}, 支付方式: {}", username, orderId, amountStr, method);
|
||||
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) {
|
||||
logger.error("用户不存在: {}", username);
|
||||
// 如果是匿名用户,创建一个临时用户记录
|
||||
if (username.startsWith("anonymous_")) {
|
||||
user = createAnonymousUser(username);
|
||||
} else {
|
||||
throw new RuntimeException("用户不存在: " + username);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("找到用户: {}", user.getUsername());
|
||||
|
||||
BigDecimal amount = new BigDecimal(amountStr);
|
||||
PaymentMethod paymentMethod = PaymentMethod.valueOf(method);
|
||||
|
||||
logger.info("金额: {}, 支付方式: {}", amount, paymentMethod);
|
||||
|
||||
Payment payment = new Payment();
|
||||
payment.setUser(user);
|
||||
payment.setOrderId(orderId);
|
||||
payment.setAmount(amount);
|
||||
payment.setPaymentMethod(paymentMethod);
|
||||
payment.setStatus(PaymentStatus.PENDING);
|
||||
payment.setCreatedAt(LocalDateTime.now());
|
||||
|
||||
Payment savedPayment = save(payment);
|
||||
logger.info("支付记录创建成功: {}", savedPayment.getId());
|
||||
|
||||
// 根据支付方式调用相应的支付服务
|
||||
if (paymentMethod == PaymentMethod.ALIPAY) {
|
||||
try {
|
||||
String paymentUrl = alipayService.createPayment(savedPayment);
|
||||
savedPayment.setPaymentUrl(paymentUrl);
|
||||
save(savedPayment);
|
||||
logger.info("支付宝支付URL生成成功: {}", paymentUrl);
|
||||
} catch (Exception e) {
|
||||
logger.error("调用支付宝支付接口失败:", e);
|
||||
// 不抛出异常,让前端处理
|
||||
}
|
||||
}
|
||||
|
||||
return savedPayment;
|
||||
} catch (Exception e) {
|
||||
logger.error("创建支付失败:", e);
|
||||
throw new RuntimeException("创建支付失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建匿名用户
|
||||
*/
|
||||
private User createAnonymousUser(String username) {
|
||||
try {
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(username + "@anonymous.com");
|
||||
user.setPasswordHash("anonymous");
|
||||
user.setRole("ROLE_USER");
|
||||
|
||||
return userService.save(user);
|
||||
} catch (Exception e) {
|
||||
logger.error("创建匿名用户失败:", e);
|
||||
throw new RuntimeException("创建匿名用户失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户支付统计
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getUserPaymentStats(String username) {
|
||||
try {
|
||||
User user = userService.findByUsername(username);
|
||||
if (user == null) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
Long userId = user.getId();
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("totalPayments", paymentRepository.countByUserId(userId));
|
||||
stats.put("successfulPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.SUCCESS));
|
||||
stats.put("pendingPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.PENDING));
|
||||
stats.put("failedPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.FAILED));
|
||||
stats.put("cancelledPayments", paymentRepository.countByUserIdAndStatus(userId, PaymentStatus.CANCELLED));
|
||||
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
logger.error("获取用户支付统计失败:", e);
|
||||
throw new RuntimeException("获取支付统计失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据支付金额增加积分
|
||||
*/
|
||||
private void addPointsForPayment(Payment payment) {
|
||||
try {
|
||||
BigDecimal amount = payment.getAmount();
|
||||
Integer pointsToAdd = 0;
|
||||
|
||||
// 根据支付金额确定积分奖励
|
||||
if (amount.compareTo(new BigDecimal("59.00")) >= 0 && amount.compareTo(new BigDecimal("259.00")) < 0) {
|
||||
// 标准版订阅 (59-258元) - 200积分
|
||||
pointsToAdd = 200;
|
||||
} else if (amount.compareTo(new BigDecimal("259.00")) >= 0) {
|
||||
// 专业版订阅 (259元以上) - 1000积分
|
||||
pointsToAdd = 1000;
|
||||
}
|
||||
|
||||
if (pointsToAdd > 0) {
|
||||
userService.addPoints(payment.getUser().getId(), pointsToAdd);
|
||||
logger.info("用户 {} 支付 {} 元,获得 {} 积分",
|
||||
payment.getUser().getUsername(), amount, pointsToAdd);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("增加积分失败:", e);
|
||||
// 不抛出异常,避免影响支付流程
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从支付记录自动创建订单
|
||||
*/
|
||||
private void createOrderFromPayment(Payment payment) {
|
||||
try {
|
||||
// 生成订单号
|
||||
String orderNumber = "ORD" + System.currentTimeMillis();
|
||||
|
||||
// 创建订单
|
||||
Order order = new Order();
|
||||
order.setUser(payment.getUser());
|
||||
order.setOrderNumber(orderNumber);
|
||||
order.setTotalAmount(payment.getAmount());
|
||||
order.setCurrency("CNY");
|
||||
order.setStatus(OrderStatus.PAID); // 支付成功,订单状态为已支付
|
||||
order.setOrderType(OrderType.PAYMENT); // 订单类型为支付订单
|
||||
order.setCreatedAt(LocalDateTime.now());
|
||||
order.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
// 保存订单
|
||||
Order savedOrder = orderService.save(order);
|
||||
|
||||
// 创建订单项
|
||||
OrderItem orderItem = new OrderItem();
|
||||
orderItem.setOrder(savedOrder);
|
||||
orderItem.setProductName("支付服务 - " + payment.getPaymentMethod().name());
|
||||
orderItem.setProductDescription("通过" + payment.getPaymentMethod().name() + "完成的支付服务");
|
||||
orderItem.setQuantity(1);
|
||||
orderItem.setUnitPrice(payment.getAmount());
|
||||
orderItem.setSubtotal(payment.getAmount());
|
||||
|
||||
// 保存订单项
|
||||
orderService.saveOrderItem(orderItem);
|
||||
|
||||
// 更新支付记录,关联到新创建的订单
|
||||
payment.setOrder(savedOrder);
|
||||
paymentRepository.save(payment);
|
||||
|
||||
logger.info("从支付记录自动创建订单成功,支付ID:{},订单ID:{},订单号:{}",
|
||||
payment.getId(), savedOrder.getId(), orderNumber);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("从支付记录创建订单失败:", e);
|
||||
throw new RuntimeException("创建订单失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.model.SystemSettings;
|
||||
import com.example.demo.repository.SystemSettingsRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SystemSettingsService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SystemSettingsService.class);
|
||||
|
||||
private final SystemSettingsRepository repository;
|
||||
|
||||
public SystemSettingsService(SystemSettingsRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取唯一的系统设置;若不存在则初始化默认值。
|
||||
*/
|
||||
@Transactional
|
||||
public SystemSettings getOrCreate() {
|
||||
List<SystemSettings> all = repository.findAll();
|
||||
if (all.isEmpty()) {
|
||||
SystemSettings defaults = new SystemSettings();
|
||||
defaults.setStandardPriceCny(9); // 默认标准版 9 元
|
||||
defaults.setProPriceCny(29); // 默认专业版 29 元
|
||||
defaults.setPointsPerGeneration(1); // 默认每次消耗 1 点
|
||||
defaults.setSiteName("AIGC Demo");
|
||||
defaults.setSiteSubtitle("现代化的Spring Boot应用演示");
|
||||
defaults.setRegistrationOpen(true);
|
||||
defaults.setMaintenanceMode(false);
|
||||
defaults.setEnableAlipay(true);
|
||||
defaults.setEnablePaypal(true);
|
||||
defaults.setContactEmail("support@example.com");
|
||||
SystemSettings saved = repository.save(defaults);
|
||||
logger.info("Initialized default SystemSettings: std={}, pro={}, points={}",
|
||||
saved.getStandardPriceCny(), saved.getProPriceCny(), saved.getPointsPerGeneration());
|
||||
return saved;
|
||||
}
|
||||
return all.get(0);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public SystemSettings update(SystemSettings updated) {
|
||||
// 确保只有一条记录
|
||||
SystemSettings current = getOrCreate();
|
||||
current.setStandardPriceCny(updated.getStandardPriceCny());
|
||||
current.setProPriceCny(updated.getProPriceCny());
|
||||
current.setPointsPerGeneration(updated.getPointsPerGeneration());
|
||||
current.setSiteName(updated.getSiteName());
|
||||
current.setSiteSubtitle(updated.getSiteSubtitle());
|
||||
current.setRegistrationOpen(updated.getRegistrationOpen());
|
||||
current.setMaintenanceMode(updated.getMaintenanceMode());
|
||||
current.setEnableAlipay(updated.getEnableAlipay());
|
||||
current.setEnablePaypal(updated.getEnablePaypal());
|
||||
current.setContactEmail(updated.getContactEmail());
|
||||
return repository.save(current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
156
demo/src/main/java/com/example/demo/service/UserService.java
Normal file
156
demo/src/main/java/com/example/demo/service/UserService.java
Normal file
@@ -0,0 +1,156 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.model.User;
|
||||
import com.example.demo.repository.UserRepository;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User register(String username, String email, String rawPassword) {
|
||||
if (userRepository.existsByUsername(username)) {
|
||||
throw new IllegalArgumentException("用户名已存在");
|
||||
}
|
||||
if (userRepository.existsByEmail(email)) {
|
||||
throw new IllegalArgumentException("邮箱已被使用");
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
user.setPasswordHash(rawPassword);
|
||||
// 注册时默认为普通用户
|
||||
user.setRole("ROLE_USER");
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public java.util.List<User> findAll() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public User findById(Long id) {
|
||||
return userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("用户不存在"));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public User findByUsername(String username) {
|
||||
return userRepository.findByUsername(username).orElseThrow(() -> new IllegalArgumentException("用户不存在"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User create(String username, String email, String rawPassword) {
|
||||
return register(username, email, rawPassword);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User update(Long id, String username, String email, String rawPasswordNullable) {
|
||||
return update(id, username, email, rawPasswordNullable, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User update(Long id, String username, String email, String rawPasswordNullable, String role) {
|
||||
User user = findById(id);
|
||||
if (!user.getUsername().equals(username) && userRepository.existsByUsername(username)) {
|
||||
throw new IllegalArgumentException("用户名已存在");
|
||||
}
|
||||
if (!user.getEmail().equals(email) && userRepository.existsByEmail(email)) {
|
||||
throw new IllegalArgumentException("邮箱已被使用");
|
||||
}
|
||||
user.setUsername(username);
|
||||
user.setEmail(email);
|
||||
if (role != null && !role.isBlank()) {
|
||||
user.setRole(role);
|
||||
}
|
||||
if (rawPasswordNullable != null && !rawPasswordNullable.isBlank()) {
|
||||
user.setPasswordHash(rawPasswordNullable);
|
||||
}
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查密码是否匹配(明文比较)
|
||||
*/
|
||||
public boolean checkPassword(String rawPassword, String storedPassword) {
|
||||
return rawPassword.equals(storedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查找用户
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public User findByEmail(String email) {
|
||||
return userRepository.findByEmail(email).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户
|
||||
*/
|
||||
@Transactional
|
||||
public User save(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加用户积分
|
||||
*/
|
||||
@Transactional
|
||||
public User addPoints(Long userId, Integer points) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||
|
||||
int newPoints = user.getPoints() + points;
|
||||
if (newPoints < 0) {
|
||||
throw new RuntimeException("积分不能为负数");
|
||||
}
|
||||
|
||||
user.setPoints(newPoints);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少用户积分
|
||||
*/
|
||||
@Transactional
|
||||
public User deductPoints(Long userId, Integer points) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||
|
||||
int newPoints = user.getPoints() - points;
|
||||
if (newPoints < 0) {
|
||||
throw new RuntimeException("积分不足");
|
||||
}
|
||||
|
||||
user.setPoints(newPoints);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户积分
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Integer getUserPoints(Long userId) {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||
return user.getPoints();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
154
demo/src/main/java/com/example/demo/util/JwtUtils.java
Normal file
154
demo/src/main/java/com/example/demo/util/JwtUtils.java
Normal file
@@ -0,0 +1,154 @@
|
||||
package com.example.demo.util;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class JwtUtils {
|
||||
|
||||
@Value("${jwt.secret:aigc-demo-secret-key-for-jwt-token-generation}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400000}") // 24小时,单位毫秒
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
* 生成JWT Token
|
||||
*/
|
||||
public String generateToken(String username, String role, Long userId) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("username", username);
|
||||
claims.put("role", role);
|
||||
claims.put("userId", userId);
|
||||
|
||||
return createToken(claims, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Token
|
||||
*/
|
||||
private String createToken(Map<String, Object> claims, String subject) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.claims(claims)
|
||||
.subject(subject)
|
||||
.issuedAt(now)
|
||||
.expiration(expiryDate)
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
return claims.get("userId", Long.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取角色
|
||||
*/
|
||||
public String getRoleFromToken(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
return claims.get("role", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Token中获取过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token中的指定声明
|
||||
*/
|
||||
public <T> T getClaimFromToken(String token, ClaimsResolver<T> claimsResolver) {
|
||||
final Claims claims = getAllClaimsFromToken(token);
|
||||
return claimsResolver.resolve(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Token中的所有声明
|
||||
*/
|
||||
private Claims getAllClaimsFromToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Token是否过期
|
||||
*/
|
||||
public Boolean isTokenExpired(String token) {
|
||||
final Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token
|
||||
*/
|
||||
public Boolean validateToken(String token, String username) {
|
||||
try {
|
||||
final String tokenUsername = getUsernameFromToken(token);
|
||||
return (username.equals(tokenUsername) && !isTokenExpired(token));
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
// 确保密钥长度至少为256位(32字节)
|
||||
byte[] keyBytes = secret.getBytes();
|
||||
if (keyBytes.length < 32) {
|
||||
// 如果密钥太短,使用SHA-256哈希扩展
|
||||
try {
|
||||
java.security.MessageDigest sha = java.security.MessageDigest.getInstance("SHA-256");
|
||||
keyBytes = sha.digest(keyBytes);
|
||||
} catch (java.security.NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取Token
|
||||
*/
|
||||
public String extractTokenFromHeader(String authHeader) {
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明解析器接口
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ClaimsResolver<T> {
|
||||
T resolve(Claims claims);
|
||||
}
|
||||
}
|
||||
44
demo/src/main/resources/application-dev.properties
Normal file
44
demo/src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,44 @@
|
||||
# MySQL DataSource (DEV) - 使用环境变量
|
||||
spring.datasource.url=${DB_URL:jdbc:mysql://localhost:3306/aigc?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true}
|
||||
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.username=${DB_USERNAME:root}
|
||||
spring.datasource.password=${DB_PASSWORD:177615}
|
||||
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
# 初始化脚本仅在开发环境开启
|
||||
spring.sql.init.mode=always
|
||||
spring.sql.init.platform=mysql
|
||||
|
||||
# 支付宝配置 (开发环境 - 沙箱测试)
|
||||
alipay.app-id=2021000000000000
|
||||
alipay.private-key=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
|
||||
alipay.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
alipay.gateway-url=https://openapi.alipaydev.com/gateway.do
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=http://localhost:8080/api/payments/alipay/notify
|
||||
alipay.return-url=http://localhost:8080/api/payments/alipay/return
|
||||
|
||||
# PayPal配置 (开发环境 - 沙箱模式)
|
||||
paypal.client-id=your_paypal_sandbox_client_id
|
||||
paypal.client-secret=your_paypal_sandbox_client_secret
|
||||
paypal.mode=sandbox
|
||||
paypal.return-url=http://localhost:8080/api/payments/paypal/return
|
||||
paypal.cancel-url=http://localhost:8080/api/payments/paypal/cancel
|
||||
|
||||
# JWT配置 - 使用环境变量
|
||||
jwt.secret=${JWT_SECRET:aigc-demo-secret-key-for-jwt-token-generation-very-long-secret-key}
|
||||
jwt.expiration=${JWT_EXPIRATION:604800000}
|
||||
|
||||
# 日志配置
|
||||
logging.level.com.example.demo.security.JwtAuthenticationFilter=DEBUG
|
||||
logging.level.com.example.demo.util.JwtUtils=DEBUG
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
52
demo/src/main/resources/application-prod.properties
Normal file
52
demo/src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,52 @@
|
||||
# 生产环境配置
|
||||
spring.h2.console.enabled=false
|
||||
|
||||
# MySQL DataSource (PROD) - 使用环境变量
|
||||
spring.datasource.url=${DB_URL}
|
||||
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.username=${DB_USERNAME}
|
||||
spring.datasource.password=${DB_PASSWORD}
|
||||
|
||||
# 强烈建议生产环境禁用自动建表
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.format_sql=false
|
||||
|
||||
# 禁用 SQL 脚本自动运行
|
||||
spring.sql.init.mode=never
|
||||
|
||||
# Thymeleaf 可启用缓存
|
||||
spring.thymeleaf.cache=true
|
||||
|
||||
# 支付宝配置 (生产环境)
|
||||
alipay.app-id=${ALIPAY_APP_ID}
|
||||
alipay.private-key=${ALIPAY_PRIVATE_KEY}
|
||||
alipay.public-key=${ALIPAY_PUBLIC_KEY}
|
||||
alipay.gateway-url=https://openapi.alipay.com/gateway.do
|
||||
alipay.charset=UTF-8
|
||||
alipay.sign-type=RSA2
|
||||
alipay.notify-url=${ALIPAY_NOTIFY_URL}
|
||||
alipay.return-url=${ALIPAY_RETURN_URL}
|
||||
|
||||
# PayPal配置 (生产环境)
|
||||
paypal.client-id=${PAYPAL_CLIENT_ID}
|
||||
paypal.client-secret=${PAYPAL_CLIENT_SECRET}
|
||||
paypal.mode=live
|
||||
paypal.return-url=${PAYPAL_RETURN_URL}
|
||||
paypal.cancel-url=${PAYPAL_CANCEL_URL}
|
||||
|
||||
# JWT配置 - 使用环境变量
|
||||
jwt.secret=${JWT_SECRET}
|
||||
jwt.expiration=${JWT_EXPIRATION:604800000}
|
||||
|
||||
# 生产环境日志配置
|
||||
logging.level.root=INFO
|
||||
logging.level.com.example.demo=INFO
|
||||
logging.level.org.springframework.security=WARN
|
||||
logging.file.name=${LOG_FILE_PATH:./logs/application.log}
|
||||
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
4
demo/src/main/resources/application.properties
Normal file
4
demo/src/main/resources/application.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
spring.application.name=demo
|
||||
spring.messages.basename=messages
|
||||
spring.thymeleaf.cache=false
|
||||
spring.profiles.active=dev
|
||||
19
demo/src/main/resources/data.sql
Normal file
19
demo/src/main/resources/data.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 示例用户数据
|
||||
-- 用户名: demo, 密码: demo
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('demo', 'demo@example.com', 'demo', 'ROLE_USER', 100);
|
||||
|
||||
-- 用户名: admin, 密码: admin123
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('admin', 'admin@example.com', 'admin123', 'ROLE_ADMIN', 200);
|
||||
|
||||
-- 测试用户1: 用户名: testuser, 密码: test123
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('testuser', 'testuser@example.com', 'test123', 'ROLE_USER', 75);
|
||||
|
||||
-- 测试用户2: 用户名: mingzi_FBx7foZYDS7inLQb, 密码: 123456 (对应个人主页的用户名)
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('mingzi_FBx7foZYDS7inLQb', 'mingzi@example.com', '123456', 'ROLE_USER', 25);
|
||||
|
||||
-- 手机号测试用户: 用户名: 15538239326, 密码: 0627
|
||||
INSERT IGNORE INTO users (username, email, password_hash, role, points) VALUES ('15538239326', '15538239326@example.com', '0627', 'ROLE_USER', 50);
|
||||
|
||||
-- 更新现有用户的积分(如果还没有设置)
|
||||
UPDATE users SET points = 50 WHERE points IS NULL OR points = 0;
|
||||
|
||||
28
demo/src/main/resources/messages.properties
Normal file
28
demo/src/main/resources/messages.properties
Normal file
@@ -0,0 +1,28 @@
|
||||
title.login=登录
|
||||
title.register=注册
|
||||
title.home=首页
|
||||
|
||||
label.username=用户名
|
||||
label.email=邮箱
|
||||
label.password=密码
|
||||
label.password.confirm=确认密码
|
||||
label.remember=记住我
|
||||
|
||||
btn.login=登录
|
||||
btn.register=注册
|
||||
btn.logout=退出
|
||||
|
||||
hint.noaccount=还没有账号?去注册
|
||||
hint.haveaccount=已有账号?去登录
|
||||
hint.redirect.home=登录成功将跳转到 /
|
||||
hint.h2=开发调试可访问 /h2-console
|
||||
|
||||
msg.register.success=注册成功,请登录
|
||||
msg.login.error=用户名或密码错误
|
||||
msg.logout.success=您已退出登录
|
||||
|
||||
register.password.mismatch=两次输入的密码不一致
|
||||
javax.validation.constraints.NotBlank.message=不能为空
|
||||
javax.validation.constraints.Size.message=长度不符合要求
|
||||
javax.validation.constraints.Email.message=邮箱格式不正确
|
||||
|
||||
29
demo/src/main/resources/messages_en.properties
Normal file
29
demo/src/main/resources/messages_en.properties
Normal file
@@ -0,0 +1,29 @@
|
||||
title.login=Login
|
||||
title.register=Sign up
|
||||
title.home=Home
|
||||
|
||||
label.username=Username
|
||||
label.email=Email
|
||||
label.password=Password
|
||||
label.password.confirm=Confirm password
|
||||
label.remember=Remember me
|
||||
|
||||
btn.login=Login
|
||||
btn.register=Sign up
|
||||
btn.logout=Logout
|
||||
|
||||
hint.noaccount=No account? Sign up
|
||||
hint.haveaccount=Have an account? Login
|
||||
hint.redirect.home=After login, you will be redirected to /
|
||||
hint.h2=Dev console: /h2-console
|
||||
|
||||
msg.register.success=Registered successfully, please login
|
||||
msg.login.error=Incorrect username or password
|
||||
msg.logout.success=You have been logged out
|
||||
|
||||
register.password.mismatch=Passwords do not match
|
||||
javax.validation.constraints.NotBlank.message=Must not be blank
|
||||
javax.validation.constraints.Size.message=Length is out of range
|
||||
javax.validation.constraints.Email.message=Invalid email format
|
||||
|
||||
|
||||
19
demo/src/main/resources/migration_add_created_at.sql
Normal file
19
demo/src/main/resources/migration_add_created_at.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 数据库迁移脚本:为users表添加created_at字段
|
||||
-- 如果users表不存在created_at字段,则添加它
|
||||
|
||||
-- 检查字段是否存在,如果不存在则添加
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'users'
|
||||
AND COLUMN_NAME = 'created_at') = 0,
|
||||
'ALTER TABLE users ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
||||
'SELECT "created_at column already exists" as message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为现有用户设置创建时间(如果为NULL)
|
||||
UPDATE users SET created_at = CURRENT_TIMESTAMP WHERE created_at IS NULL;
|
||||
64
demo/src/main/resources/schema.sql
Normal file
64
demo/src/main/resources/schema.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(100) NOT NULL,
|
||||
role VARCHAR(30) NOT NULL DEFAULT 'ROLE_USER',
|
||||
points INT NOT NULL DEFAULT 50,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
payment_method VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
description VARCHAR(500),
|
||||
external_transaction_id VARCHAR(100),
|
||||
callback_url VARCHAR(1000),
|
||||
return_url VARCHAR(1000),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP NULL,
|
||||
user_id BIGINT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
order_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
total_amount DECIMAL(10,2) NOT NULL,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
order_type VARCHAR(20) NOT NULL DEFAULT 'PRODUCT',
|
||||
description VARCHAR(500),
|
||||
notes TEXT,
|
||||
shipping_address TEXT,
|
||||
billing_address TEXT,
|
||||
contact_phone VARCHAR(20),
|
||||
contact_email VARCHAR(100),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
paid_at TIMESTAMP NULL,
|
||||
shipped_at TIMESTAMP NULL,
|
||||
delivered_at TIMESTAMP NULL,
|
||||
cancelled_at TIMESTAMP NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
product_name VARCHAR(100) NOT NULL,
|
||||
product_description VARCHAR(500),
|
||||
product_sku VARCHAR(200),
|
||||
unit_price DECIMAL(10,2) NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
subtotal DECIMAL(10,2) NOT NULL,
|
||||
product_image VARCHAR(100),
|
||||
order_id BIGINT NOT NULL,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
194
demo/src/main/resources/templates/home.html
Normal file
194
demo/src/main/resources/templates/home.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>首页</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Welcome Section -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body text-center py-5">
|
||||
<h1 class="display-4 mb-3">
|
||||
<i class="fas fa-rocket me-3"></i>欢迎使用 AIGC Demo
|
||||
</h1>
|
||||
<p class="lead mb-4">现代化的Spring Boot应用,集成用户管理、支付系统等功能</p>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-4 mb-3">
|
||||
<i class="fas fa-users fa-2x mb-2"></i>
|
||||
<h5>用户管理</h5>
|
||||
<small>完整的用户注册、登录、权限管理</small>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<i class="fas fa-credit-card fa-2x mb-2"></i>
|
||||
<h5>支付系统</h5>
|
||||
<small>支持支付宝、PayPal多种支付方式</small>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<i class="fas fa-shield-alt fa-2x mb-2"></i>
|
||||
<h5>安全可靠</h5>
|
||||
<small>Spring Security安全框架保护</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-bolt me-2"></i>快速操作
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-credit-card fa-3x text-primary mb-3"></i>
|
||||
<h5 class="card-title">支付接入</h5>
|
||||
<p class="card-text">创建新的支付订单,支持支付宝和PayPal</p>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>创建支付
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-users fa-3x text-success mb-3"></i>
|
||||
<h5 class="card-title">用户管理</h5>
|
||||
<p class="card-text">管理系统用户,查看用户列表和权限</p>
|
||||
<a th:href="@{/users}" class="btn btn-success">
|
||||
<i class="fas fa-cog me-2"></i>管理用户
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3" sec:authorize="isAuthenticated()">
|
||||
<div class="card h-100 text-center">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-history fa-3x text-info mb-3"></i>
|
||||
<h5 class="card-title">支付记录</h5>
|
||||
<p class="card-text">查看您的支付历史和交易详情</p>
|
||||
<a th:href="@{/payment/history}" class="btn btn-info">
|
||||
<i class="fas fa-list me-2"></i>查看记录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Stats -->
|
||||
<div class="row mb-5" sec:authorize="hasRole('ADMIN')">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-chart-bar me-2"></i>系统统计
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">总用户数</h6>
|
||||
<h3 class="mb-0" th:text="${userCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-users stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card" style="background: linear-gradient(135deg, #198754, #146c43);">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">支付订单</h6>
|
||||
<h3 class="mb-0" th:text="${paymentCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-credit-card stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card" style="background: linear-gradient(135deg, #ffc107, #e0a800);">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">成功支付</h6>
|
||||
<h3 class="mb-0" th:text="${successPaymentCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-check-circle stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stats-card" style="background: linear-gradient(135deg, #0dcaf0, #0aa2c0);">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">在线用户</h6>
|
||||
<h3 class="mb-0" th:text="${onlineUserCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-user-check stats-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activities -->
|
||||
<div class="row" sec:authorize="isAuthenticated()">
|
||||
<div class="col-12">
|
||||
<h3 class="mb-4">
|
||||
<i class="fas fa-clock me-2"></i>最近活动
|
||||
</h3>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-muted mb-3">最近支付</h6>
|
||||
<div th:if="${recentPayments != null and !recentPayments.empty}">
|
||||
<div th:each="payment : ${recentPayments}" class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<small class="text-muted" th:text="${payment.orderId}"></small>
|
||||
<div>
|
||||
<span th:text="${payment.currency}"></span>
|
||||
<span th:text="${payment.amount}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}" class="badge bg-success" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}" class="badge bg-danger" th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${recentPayments == null or recentPayments.empty}" class="text-muted">
|
||||
<i class="fas fa-inbox me-2"></i>暂无支付记录
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-muted mb-3">系统信息</h6>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">当前用户:</small>
|
||||
<span sec:authentication="name" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">用户角色:</small>
|
||||
<span sec:authentication="authorities" class="fw-bold"></span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">登录时间:</small>
|
||||
<span class="fw-bold" th:text="${#temporals.format(#temporals.createNow(), 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
354
demo/src/main/resources/templates/layout.html
Normal file
354
demo/src/main/resources/templates/layout.html
Normal file
@@ -0,0 +1,354 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title th:text="${pageTitle} + ' - ' + ${siteName}">AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--light-color: #f8f9fa;
|
||||
--dark-color: #212529;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link:hover {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-height: calc(100vh - 200px);
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.footer {
|
||||
background-color: var(--dark-color);
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: var(--secondary-color);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover,
|
||||
.sidebar .nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, var(--primary-color), #0056b3);
|
||||
color: white;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stats-card .stats-icon {
|
||||
font-size: 2.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.table {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: var(--light-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Page specific styles -->
|
||||
<th:block th:fragment="page-styles">
|
||||
<!-- Additional page-specific styles -->
|
||||
</th:block>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" th:href="@{/}">
|
||||
<i class="fas fa-rocket me-2"></i>AIGC Demo
|
||||
</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" th:href="@{/}" th:classappend="${#httpServletRequest.requestURI == '/'} ? 'active' : ''">
|
||||
<i class="fas fa-home me-1"></i>首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="hasRole('ADMIN')">
|
||||
<a class="nav-link" th:href="@{/settings}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/settings')} ? 'active' : ''">
|
||||
<i class="fas fa-gear me-1"></i>系统设置
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="hasRole('ADMIN')">
|
||||
<a class="nav-link" th:href="@{/users}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/users')} ? 'active' : ''">
|
||||
<i class="fas fa-users me-1"></i>用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="isAuthenticated()">
|
||||
<a class="nav-link" th:href="@{/payment/create}" th:classappend="${#strings.startsWith(#httpServletRequest.requestURI, '/payment')} ? 'active' : ''">
|
||||
<i class="fas fa-credit-card me-1"></i>支付管理
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown" sec:authorize="isAuthenticated()">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user me-1"></i>
|
||||
<span sec:authentication="name">用户</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" th:href="@{/payment/history}">
|
||||
<i class="fas fa-history me-2"></i>支付记录
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form th:action="@{/logout}" method="post" class="d-inline">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>退出登录
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item" sec:authorize="!isAuthenticated()">
|
||||
<a class="nav-link" th:href="@{/login}">
|
||||
<i class="fas fa-sign-in-alt me-1"></i>登录
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<div class="container">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" th:if="${breadcrumbs}">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item" th:each="crumb, iterStat : ${breadcrumbs}"
|
||||
th:classappend="${iterStat.last} ? 'active' : ''">
|
||||
<a th:if="${!iterStat.last}" th:href="${crumb.url}" th:text="${crumb.name}"></a>
|
||||
<span th:if="${iterStat.last}" th:text="${crumb.name}"></span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-4" th:if="${pageTitle}">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1 class="h3 mb-0" th:text="${pageTitle}">页面标题</h1>
|
||||
<div th:fragment="page-actions">
|
||||
<!-- Page specific action buttons -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div th:if="${success}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<span th:text="${success}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
<div th:if="${info}" class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span th:text="${info}"></span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div th:fragment="content">
|
||||
<!-- Page specific content will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-rocket me-2"></i>AIGC Demo</h5>
|
||||
<p class="mb-0">现代化的Spring Boot应用演示</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<p class="mb-0">
|
||||
<i class="fas fa-code me-1"></i>
|
||||
基于 Spring Boot 3.5.6 + JDK 21
|
||||
</p>
|
||||
<small class="text-muted">© 2024 AIGC Demo. All rights reserved.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script>
|
||||
// Global JavaScript functions
|
||||
function showLoading(element) {
|
||||
const loading = element.querySelector('.loading');
|
||||
if (loading) {
|
||||
loading.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading(element) {
|
||||
const loading = element.querySelector('.loading');
|
||||
if (loading) {
|
||||
loading.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmAction(message, callback) {
|
||||
if (confirm(message)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-hide alerts after 5 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(function(alert) {
|
||||
setTimeout(function() {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
function validateForm(form) {
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(function(field) {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Page specific scripts -->
|
||||
<th:block th:fragment="page-scripts">
|
||||
<!-- Additional page-specific scripts -->
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
292
demo/src/main/resources/templates/login.html
Normal file
292
demo/src/main/resources/templates/login.html
Normal file
@@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录 - AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-header h2 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-floating {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: #667eea;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, #d4edda, #c3e6cb);
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.login-footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.language-switcher a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.language-switcher a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h2><i class="fas fa-rocket me-2"></i>AIGC Demo</h2>
|
||||
<p>欢迎回来,请登录您的账户</p>
|
||||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div id="msg" class="alert alert-success" style="display: none;"></div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${param.error}" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="#{msg.login.error}">用户名或密码错误</span>
|
||||
</div>
|
||||
|
||||
<!-- Logout Message -->
|
||||
<div th:if="${param.logout}" class="alert alert-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<span th:text="#{msg.logout.success}">您已退出登录</span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/login}" method="post" id="loginForm">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="用户名" required autocomplete="username">
|
||||
<label for="username">
|
||||
<i class="fas fa-user me-2"></i>用户名
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="密码" required autocomplete="current-password">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock me-2"></i>密码
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="remember-me" name="remember-me">
|
||||
<label class="form-check-label" for="remember-me">
|
||||
<i class="fas fa-clock me-2"></i>记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login" id="loginBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>登录中...
|
||||
</span>
|
||||
<span class="login-text">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>登录
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p class="mb-2">
|
||||
<span th:text="#{hint.noaccount}">还没有账号?</span>
|
||||
<a th:href="@{/register}">立即注册</a>
|
||||
</p>
|
||||
|
||||
<div class="language-switcher">
|
||||
<a th:href="@{|?lang=zh_CN|}">中文</a> |
|
||||
<a th:href="@{|?lang=en|}">English</a>
|
||||
</div>
|
||||
|
||||
<div class="demo-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>演示账户:</strong><br>
|
||||
用户名: demo, 密码: demo<br>
|
||||
用户名: admin, 密码: admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Show registration success message
|
||||
function fromQuery(name) {
|
||||
const url = new URL(window.location.href);
|
||||
return url.searchParams.get(name);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const registered = fromQuery('registered');
|
||||
if (registered) {
|
||||
const msgDiv = document.getElementById('msg');
|
||||
msgDiv.textContent = '注册成功,请登录';
|
||||
msgDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const loading = loginBtn.querySelector('.loading');
|
||||
const loginText = loginBtn.querySelector('.login-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
loginText.style.display = 'none';
|
||||
loginBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Auto-hide alerts after 5 seconds
|
||||
setTimeout(function() {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(function(alert) {
|
||||
alert.style.transition = 'opacity 0.5s ease';
|
||||
alert.style.opacity = '0';
|
||||
setTimeout(function() {
|
||||
alert.style.display = 'none';
|
||||
}, 500);
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Form validation
|
||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
e.preventDefault();
|
||||
alert('请填写用户名和密码');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
563
demo/src/main/resources/templates/orders/admin.html
Normal file
563
demo/src/main/resources/templates/orders/admin.html
Normal file
@@ -0,0 +1,563 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>订单管理 - 管理员</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Page Actions -->
|
||||
<div th:fragment="page-actions">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-filter me-2"></i>筛选
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin}">全部订单</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=PENDING)}">待支付</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=PAID)}">已支付</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=SHIPPED)}">已发货</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=COMPLETED)}">已完成</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{/orders/admin(status=CANCELLED)}">已取消</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-success" onclick="exportOrders()">
|
||||
<i class="fas fa-download me-2"></i>导出
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Orders Statistics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">总订单数</h6>
|
||||
<h3 class="mb-0" th:text="${totalElements}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-shopping-cart fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">待支付</h6>
|
||||
<h3 class="mb-0" th:text="${pendingCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-clock fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">已完成</h6>
|
||||
<h3 class="mb-0" th:text="${completedCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-check-circle fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="mb-1">今日订单</h6>
|
||||
<h3 class="mb-0" th:text="${todayCount ?: 0}">0</h3>
|
||||
</div>
|
||||
<i class="fas fa-calendar-day fa-2x opacity-75"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>订单管理
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<select class="form-select form-select-sm" id="statusFilter">
|
||||
<option value="">全部状态</option>
|
||||
<option th:each="status : ${orderStatuses}"
|
||||
th:value="${status.name()}"
|
||||
th:text="${status.displayName}"
|
||||
th:selected="${status == param.status}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="input-group input-group-sm" style="max-width: 200px;">
|
||||
<input type="text" class="form-control" placeholder="搜索订单号..." id="searchInput">
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="ordersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="120">订单号</th>
|
||||
<th width="100">用户</th>
|
||||
<th width="100">金额</th>
|
||||
<th width="100">状态</th>
|
||||
<th width="100">类型</th>
|
||||
<th width="120">创建时间</th>
|
||||
<th width="200">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="order : ${orders.content}" class="order-row">
|
||||
<td>
|
||||
<a th:href="@{|/orders/${order.id}|}" class="text-decoration-none">
|
||||
<span th:text="${order.orderNumber}">ORD123456789</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle me-2">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold" th:text="${order.user.username}">用户名</div>
|
||||
<small class="text-muted" th:text="${order.user.email}">邮箱</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${order.totalAmount}">0.00</span>
|
||||
</td>
|
||||
<td>
|
||||
<span th:if="${order.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${order.status.displayName}">待支付</span>
|
||||
<span th:if="${order.status.name() == 'CONFIRMED'}" class="badge bg-info" th:text="${order.status.displayName}">已确认</span>
|
||||
<span th:if="${order.status.name() == 'PAID'}" class="badge bg-primary" th:text="${order.status.displayName}">已支付</span>
|
||||
<span th:if="${order.status.name() == 'PROCESSING'}" class="badge bg-secondary" th:text="${order.status.displayName}">处理中</span>
|
||||
<span th:if="${order.status.name() == 'SHIPPED'}" class="badge bg-success" th:text="${order.status.displayName}">已发货</span>
|
||||
<span th:if="${order.status.name() == 'DELIVERED'}" class="badge bg-success" th:text="${order.status.displayName}">已送达</span>
|
||||
<span th:if="${order.status.name() == 'COMPLETED'}" class="badge bg-success" th:text="${order.status.displayName}">已完成</span>
|
||||
<span th:if="${order.status.name() == 'CANCELLED'}" class="badge bg-danger" th:text="${order.status.displayName}">已取消</span>
|
||||
<span th:if="${order.status.name() == 'REFUNDED'}" class="badge bg-secondary" th:text="${order.status.displayName}">已退款</span>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${order.orderType.displayName}">商品订单</span>
|
||||
</td>
|
||||
<td>
|
||||
<small th:text="${#temporals.format(order.createdAt, 'yyyy-MM-dd HH:mm')}">2024-01-01 10:00</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a th:href="@{|/orders/${order.id}|}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li th:if="${order.canShip()}" class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'shipOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-truck me-2"></i>发货
|
||||
</li>
|
||||
<li th:if="${order.canComplete()}" class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'completeOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-check me-2"></i>完成订单
|
||||
</li>
|
||||
<li th:if="${order.canCancel()}" class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'cancelOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-times me-2"></i>取消订单
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="dropdown-item" style="cursor: pointer;"
|
||||
th:onclick="'updateStatus(' + ${order.id} + ')'">
|
||||
<i class="fas fa-edit me-2"></i>更新状态
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
共 <span th:text="${totalElements}">0</span> 个订单,
|
||||
第 <span th:text="${currentPage + 1}">1</span> 页,
|
||||
共 <span th:text="${totalPages}">1</span> 页
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<nav aria-label="订单分页">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item" th:classappend="${currentPage == 0} ? 'disabled' : ''">
|
||||
<a class="page-link" th:href="@{/orders/admin(page=${currentPage - 1}, size=${orders.size}, sortBy=${sortBy}, sortDir=${sortDir}, status=${param.status})}">上一页</a>
|
||||
</li>
|
||||
<li class="page-item" th:each="i : ${#numbers.sequence(0, totalPages - 1)}"
|
||||
th:classappend="${i == currentPage} ? 'active' : ''">
|
||||
<a class="page-link" th:href="@{/orders/admin(page=${i}, size=${orders.size}, sortBy=${sortBy}, sortDir=${sortDir}, status=${param.status})}"
|
||||
th:text="${i + 1}">1</a>
|
||||
</li>
|
||||
<li class="page-item" th:classappend="${currentPage == totalPages - 1} ? 'disabled' : ''">
|
||||
<a class="page-link" th:href="@{/orders/admin(page=${currentPage + 1}, size=${orders.size}, sortBy=${sortBy}, sortDir=${sortDir}, status=${param.status})}">下一页</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Status Modal -->
|
||||
<div class="modal fade" id="updateStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-edit me-2"></i>更新订单状态
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="updateStatusForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">新状态</label>
|
||||
<select class="form-select" id="newStatus" required>
|
||||
<option th:each="status : ${orderStatuses}"
|
||||
th:value="${status.name()}"
|
||||
th:text="${status.displayName}">状态</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="statusNotes" class="form-label">备注(可选)</label>
|
||||
<textarea class="form-control" id="statusNotes" rows="3"
|
||||
placeholder="请输入状态更新备注..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-primary">确认更新</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Order Modal -->
|
||||
<div class="modal fade" id="cancelOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>取消订单
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="cancelOrderForm">
|
||||
<div class="modal-body">
|
||||
<p>确定要取消此订单吗?此操作不可撤销。</p>
|
||||
<div class="mb-3">
|
||||
<label for="cancelReason" class="form-label">取消原因(可选)</label>
|
||||
<textarea class="form-control" id="cancelReason" rows="3"
|
||||
placeholder="请输入取消原因..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-danger">确认取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship Order Modal -->
|
||||
<div class="modal fade" id="shipOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-truck me-2"></i>订单发货
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="shipOrderForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="trackingNumber" class="form-label">物流单号(可选)</label>
|
||||
<input type="text" class="form-control" id="trackingNumber"
|
||||
placeholder="请输入物流单号...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-info">确认发货</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.order-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentOrderId = null;
|
||||
|
||||
// Status filter
|
||||
document.getElementById('statusFilter').addEventListener('change', function() {
|
||||
const status = this.value;
|
||||
const url = new URL(window.location);
|
||||
if (status) {
|
||||
url.searchParams.set('status', status);
|
||||
} else {
|
||||
url.searchParams.delete('status');
|
||||
}
|
||||
url.searchParams.set('page', '0'); // Reset to first page
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('searchInput').addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('.order-row');
|
||||
|
||||
rows.forEach(function(row) {
|
||||
const orderNumber = row.cells[0].textContent.toLowerCase();
|
||||
const username = row.cells[1].textContent.toLowerCase();
|
||||
|
||||
if (orderNumber.includes(searchTerm) || username.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update status
|
||||
function updateStatus(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('updateStatusModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Cancel order
|
||||
function cancelOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('cancelOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Ship order
|
||||
function shipOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('shipOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Complete order
|
||||
function completeOrder(orderId) {
|
||||
if (confirm('确定要完成此订单吗?')) {
|
||||
fetch(`/orders/${orderId}/complete`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('完成订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('完成订单失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export orders
|
||||
function exportOrders() {
|
||||
const url = new URL(window.location);
|
||||
url.pathname = '/orders/export';
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
// Update status form submission
|
||||
document.getElementById('updateStatusForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const status = document.getElementById('newStatus').value;
|
||||
const notes = document.getElementById('statusNotes').value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('status', status);
|
||||
if (notes) {
|
||||
formData.append('notes', notes);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/status`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('更新状态失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('更新状态失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Cancel order form submission
|
||||
document.getElementById('cancelOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const reason = document.getElementById('cancelReason').value;
|
||||
const formData = new FormData();
|
||||
if (reason) {
|
||||
formData.append('reason', reason);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/cancel`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('取消订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('取消订单失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Ship order form submission
|
||||
document.getElementById('shipOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const trackingNumber = document.getElementById('trackingNumber').value;
|
||||
const formData = new FormData();
|
||||
if (trackingNumber) {
|
||||
formData.append('trackingNumber', trackingNumber);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/ship`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('发货失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('发货失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(function() {
|
||||
// Only refresh if no modal is open and no form is being edited
|
||||
if (!document.querySelector('.modal.show') && !document.querySelector('form:focus')) {
|
||||
location.reload();
|
||||
}
|
||||
}, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
479
demo/src/main/resources/templates/orders/detail.html
Normal file
479
demo/src/main/resources/templates/orders/detail.html
Normal file
@@ -0,0 +1,479 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>订单详情</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a th:href="@{/}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a th:href="@{/orders}">订单管理</a></li>
|
||||
<li class="breadcrumb-item active" th:text="${order.orderNumber}">订单详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Order Details -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Order Info Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-cart me-2"></i>订单信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<span th:if="${order.status.name() == 'PENDING'}" class="badge bg-warning fs-6" th:text="${order.status.displayName}">待支付</span>
|
||||
<span th:if="${order.status.name() == 'CONFIRMED'}" class="badge bg-info fs-6" th:text="${order.status.displayName}">已确认</span>
|
||||
<span th:if="${order.status.name() == 'PAID'}" class="badge bg-primary fs-6" th:text="${order.status.displayName}">已支付</span>
|
||||
<span th:if="${order.status.name() == 'PROCESSING'}" class="badge bg-secondary fs-6" th:text="${order.status.displayName}">处理中</span>
|
||||
<span th:if="${order.status.name() == 'SHIPPED'}" class="badge bg-success fs-6" th:text="${order.status.displayName}">已发货</span>
|
||||
<span th:if="${order.status.name() == 'DELIVERED'}" class="badge bg-success fs-6" th:text="${order.status.displayName}">已送达</span>
|
||||
<span th:if="${order.status.name() == 'COMPLETED'}" class="badge bg-success fs-6" th:text="${order.status.displayName}">已完成</span>
|
||||
<span th:if="${order.status.name() == 'CANCELLED'}" class="badge bg-danger fs-6" th:text="${order.status.displayName}">已取消</span>
|
||||
<span th:if="${order.status.name() == 'REFUNDED'}" class="badge bg-secondary fs-6" th:text="${order.status.displayName}">已退款</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">订单号:</td>
|
||||
<td th:text="${order.orderNumber}">ORD123456789</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">订单类型:</td>
|
||||
<td th:text="${order.orderType.displayName}">商品订单</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">创建时间:</td>
|
||||
<td th:text="${#temporals.format(order.createdAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-01 10:00:00</td>
|
||||
</tr>
|
||||
<tr th:if="${order.paidAt}">
|
||||
<td class="fw-bold">支付时间:</td>
|
||||
<td th:text="${#temporals.format(order.paidAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-01 10:30:00</td>
|
||||
</tr>
|
||||
<tr th:if="${order.shippedAt}">
|
||||
<td class="fw-bold">发货时间:</td>
|
||||
<td th:text="${#temporals.format(order.shippedAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-02 09:00:00</td>
|
||||
</tr>
|
||||
<tr th:if="${order.deliveredAt}">
|
||||
<td class="fw-bold">送达时间:</td>
|
||||
<td th:text="${#temporals.format(order.deliveredAt, 'yyyy-MM-dd HH:mm:ss')}">2024-01-03 14:00:00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">订单金额:</td>
|
||||
<td class="fs-5 text-primary">
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${order.totalAmount}">0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${order.description}">
|
||||
<td class="fw-bold">订单描述:</td>
|
||||
<td th:text="${order.description}">订单描述</td>
|
||||
</tr>
|
||||
<tr th:if="${order.contactEmail}">
|
||||
<td class="fw-bold">联系邮箱:</td>
|
||||
<td th:text="${order.contactEmail}">user@example.com</td>
|
||||
</tr>
|
||||
<tr th:if="${order.contactPhone}">
|
||||
<td class="fw-bold">联系电话:</td>
|
||||
<td th:text="${order.contactPhone}">13800138000</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Notes -->
|
||||
<div th:if="${order.notes}" class="mt-3">
|
||||
<h6 class="fw-bold">订单备注:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre th:text="${order.notes}" class="mb-0">订单备注内容</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>订单商品
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>商品名称</th>
|
||||
<th width="100">单价</th>
|
||||
<th width="80">数量</th>
|
||||
<th width="100">小计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="item : ${order.orderItems}">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div th:if="${item.productImage}" class="me-3">
|
||||
<img th:src="${item.productImage}" class="img-thumbnail"
|
||||
style="width: 50px; height: 50px; object-fit: cover;">
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold" th:text="${item.productName}">商品名称</div>
|
||||
<div th:if="${item.productDescription}" class="text-muted small"
|
||||
th:text="${item.productDescription}">商品描述</div>
|
||||
<div th:if="${item.productSku}" class="text-muted small">
|
||||
SKU: <span th:text="${item.productSku}">SKU123</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${item.unitPrice}">0.00</span>
|
||||
</td>
|
||||
<td th:text="${item.quantity}">1</td>
|
||||
<td>
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${item.subtotal}">0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="table-active">
|
||||
<th colspan="3" class="text-end">订单总计:</th>
|
||||
<th class="text-primary">
|
||||
<span th:text="${order.currency}">CNY</span>
|
||||
<span th:text="${order.totalAmount}">0.00</span>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="card mb-4" th:if="${order.shippingAddress or order.billingAddress}">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>地址信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6" th:if="${order.shippingAddress}">
|
||||
<h6 class="fw-bold">收货地址:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre th:text="${order.shippingAddress}" class="mb-0">收货地址</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" th:if="${order.billingAddress}">
|
||||
<h6 class="fw-bold">账单地址:</h6>
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre th:text="${order.billingAddress}" class="mb-0">账单地址</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Order Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-cogs me-2"></i>订单操作
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<!-- Pay Button -->
|
||||
<div th:if="${order.canPay()}" class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-credit-card me-2"></i>立即支付
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" th:href="@{|/orders/${order.id}/pay?paymentMethod=ALIPAY|}">
|
||||
<i class="fab fa-alipay me-2"></i>支付宝支付
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" th:href="@{|/orders/${order.id}/pay?paymentMethod=PAYPAL|}">
|
||||
<i class="fab fa-paypal me-2"></i>PayPal支付
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<button th:if="${order.canCancel()}" type="button" class="btn btn-danger"
|
||||
th:onclick="'cancelOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-times me-2"></i>取消订单
|
||||
</button>
|
||||
|
||||
<!-- Admin Actions -->
|
||||
<div sec:authorize="hasRole('ADMIN')" class="mt-3">
|
||||
<hr>
|
||||
<h6 class="fw-bold">管理员操作</h6>
|
||||
|
||||
<!-- Ship Button -->
|
||||
<button th:if="${order.canShip()}" type="button" class="btn btn-info mb-2"
|
||||
th:onclick="'shipOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-truck me-2"></i>发货
|
||||
</button>
|
||||
|
||||
<!-- Complete Button -->
|
||||
<button th:if="${order.canComplete()}" type="button" class="btn btn-success mb-2"
|
||||
th:onclick="'completeOrder(' + ${order.id} + ')'">
|
||||
<i class="fas fa-check me-2"></i>完成订单
|
||||
</button>
|
||||
|
||||
<!-- Status Update -->
|
||||
<div class="mt-3">
|
||||
<label class="form-label">更新状态</label>
|
||||
<div class="input-group">
|
||||
<select class="form-select" id="statusSelect">
|
||||
<option th:each="status : ${orderStatuses}"
|
||||
th:value="${status.name()}"
|
||||
th:text="${status.displayName}"
|
||||
th:selected="${status == order.status}"></option>
|
||||
</select>
|
||||
<button class="btn btn-outline-primary" type="button"
|
||||
th:onclick="'updateStatus(' + ${order.id} + ')'">
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment History -->
|
||||
<div class="card" th:if="${order.payments != null and !order.payments.empty}">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-history me-2"></i>支付记录
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div th:each="payment : ${order.payments}" class="border-bottom pb-2 mb-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span th:text="${payment.paymentMethod.displayName}">支付宝</span>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}" class="badge bg-success" th:text="${payment.status.displayName}">成功</span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${payment.status.displayName}">待支付</span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}" class="badge bg-danger" th:text="${payment.status.displayName}">失败</span>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
<span th:text="${payment.currency}">CNY</span>
|
||||
<span th:text="${payment.amount}">0.00</span>
|
||||
<span th:text="${#temporals.format(payment.createdAt, 'MM-dd HH:mm')}">01-01 10:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cancel Order Modal -->
|
||||
<div class="modal fade" id="cancelOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>取消订单
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="cancelOrderForm">
|
||||
<div class="modal-body">
|
||||
<p>确定要取消此订单吗?此操作不可撤销。</p>
|
||||
<div class="mb-3">
|
||||
<label for="cancelReason" class="form-label">取消原因(可选)</label>
|
||||
<textarea class="form-control" id="cancelReason" rows="3"
|
||||
placeholder="请输入取消原因..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-danger">确认取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship Order Modal -->
|
||||
<div class="modal fade" id="shipOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-truck me-2"></i>订单发货
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="shipOrderForm">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="trackingNumber" class="form-label">物流单号(可选)</label>
|
||||
<input type="text" class="form-control" id="trackingNumber"
|
||||
placeholder="请输入物流单号...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="submit" class="btn btn-info">确认发货</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentOrderId = null;
|
||||
|
||||
// Cancel order
|
||||
function cancelOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('cancelOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Ship order
|
||||
function shipOrder(orderId) {
|
||||
currentOrderId = orderId;
|
||||
const modal = new bootstrap.Modal(document.getElementById('shipOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Complete order
|
||||
function completeOrder(orderId) {
|
||||
if (confirm('确定要完成此订单吗?')) {
|
||||
fetch(`/orders/${orderId}/complete`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('完成订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('完成订单失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update status
|
||||
function updateStatus(orderId) {
|
||||
const status = document.getElementById('statusSelect').value;
|
||||
const notes = prompt('请输入备注(可选):');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('status', status);
|
||||
if (notes) {
|
||||
formData.append('notes', notes);
|
||||
}
|
||||
|
||||
fetch(`/orders/${orderId}/status`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('更新状态失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('更新状态失败');
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel order form submission
|
||||
document.getElementById('cancelOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const reason = document.getElementById('cancelReason').value;
|
||||
const formData = new FormData();
|
||||
if (reason) {
|
||||
formData.append('reason', reason);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/cancel`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('取消订单失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('取消订单失败');
|
||||
});
|
||||
});
|
||||
|
||||
// Ship order form submission
|
||||
document.getElementById('shipOrderForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!currentOrderId) return;
|
||||
|
||||
const trackingNumber = document.getElementById('trackingNumber').value;
|
||||
const formData = new FormData();
|
||||
if (trackingNumber) {
|
||||
formData.append('trackingNumber', trackingNumber);
|
||||
}
|
||||
|
||||
fetch(`/orders/${currentOrderId}/ship`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('发货失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('发货失败');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
518
demo/src/main/resources/templates/orders/form.html
Normal file
518
demo/src/main/resources/templates/orders/form.html
Normal file
@@ -0,0 +1,518 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>创建订单</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a th:href="@{/}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a th:href="@{/orders}">订单管理</a></li>
|
||||
<li class="breadcrumb-item active">创建订单</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Order Form -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<form th:action="@{/orders/create}" method="post" th:object="${order}" id="orderForm">
|
||||
<!-- Order Basic Info -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>订单基本信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="orderType" class="form-label">
|
||||
<i class="fas fa-tag me-2"></i>订单类型
|
||||
</label>
|
||||
<select class="form-select" id="orderType" th:field="*{orderType}" required>
|
||||
<option th:each="type : ${orderTypes}"
|
||||
th:value="${type.name()}"
|
||||
th:text="${type.displayName}">商品订单</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="currency" class="form-label">
|
||||
<i class="fas fa-coins me-2"></i>货币
|
||||
</label>
|
||||
<select class="form-select" id="currency" th:field="*{currency}">
|
||||
<option value="CNY">人民币 (CNY)</option>
|
||||
<option value="USD">美元 (USD)</option>
|
||||
<option value="EUR">欧元 (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">
|
||||
<i class="fas fa-file-text me-2"></i>订单描述
|
||||
</label>
|
||||
<textarea class="form-control" id="description" th:field="*{description}"
|
||||
rows="3" placeholder="请输入订单描述..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Items -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-basket me-2"></i>订单商品
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addItemBtn">
|
||||
<i class="fas fa-plus me-1"></i>添加商品
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="orderItems">
|
||||
<div class="order-item border rounded p-3 mb-3" data-index="0">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">商品名称 *</label>
|
||||
<input type="text" class="form-control item-name" name="orderItems[0].productName"
|
||||
placeholder="请输入商品名称" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" class="form-control item-sku" name="orderItems[0].productSku"
|
||||
placeholder="商品SKU">
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label class="form-label">单价 *</label>
|
||||
<input type="number" class="form-control item-price" name="orderItems[0].unitPrice"
|
||||
step="0.01" min="0" placeholder="0.00" required>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<label class="form-label">数量 *</label>
|
||||
<input type="number" class="form-control item-quantity" name="orderItems[0].quantity"
|
||||
min="1" value="1" required>
|
||||
</div>
|
||||
<div class="col-md-1 mb-3">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger d-block remove-item"
|
||||
style="display: none;">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label class="form-label">商品描述</label>
|
||||
<textarea class="form-control item-description" name="orderItems[0].productDescription"
|
||||
rows="2" placeholder="请输入商品描述..."></textarea>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">小计</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text currency-symbol">CNY</span>
|
||||
<input type="text" class="form-control item-subtotal" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Total -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8"></div>
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">订单总计:</span>
|
||||
<span class="fs-5 text-primary">
|
||||
<span class="currency-symbol">CNY</span>
|
||||
<span id="totalAmount">0.00</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-user me-2"></i>联系信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contactEmail" class="form-label">
|
||||
<i class="fas fa-envelope me-2"></i>联系邮箱
|
||||
</label>
|
||||
<input type="email" class="form-control" id="contactEmail" th:field="*{contactEmail}"
|
||||
placeholder="请输入联系邮箱">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contactPhone" class="form-label">
|
||||
<i class="fas fa-phone me-2"></i>联系电话
|
||||
</label>
|
||||
<input type="tel" class="form-control" id="contactPhone" th:field="*{contactPhone}"
|
||||
placeholder="请输入联系电话">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>地址信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="shippingAddress" class="form-label">
|
||||
<i class="fas fa-truck me-2"></i>收货地址
|
||||
</label>
|
||||
<textarea class="form-control" id="shippingAddress" th:field="*{shippingAddress}"
|
||||
rows="3" placeholder="请输入收货地址..."></textarea>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="billingAddress" class="form-label">
|
||||
<i class="fas fa-file-invoice me-2"></i>账单地址
|
||||
</label>
|
||||
<textarea class="form-control" id="billingAddress" th:field="*{billingAddress}"
|
||||
rows="3" placeholder="请输入账单地址..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a th:href="@{/orders}" class="btn btn-secondary me-md-2">
|
||||
<i class="fas fa-times me-2"></i>取消
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>创建中...
|
||||
</span>
|
||||
<span class="submit-text">
|
||||
<i class="fas fa-save me-2"></i>创建订单
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Order Summary -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-calculator me-2"></i>订单摘要
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="orderSummary">
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-shopping-basket fa-2x mb-2"></i>
|
||||
<p>请添加商品到订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-question-circle me-2"></i>帮助信息
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small">
|
||||
<h6>订单类型说明:</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><span class="badge bg-primary me-2">商品订单</span>实体商品订单</li>
|
||||
<li><span class="badge bg-info me-2">服务订单</span>服务类订单</li>
|
||||
<li><span class="badge bg-success me-2">订阅订单</span>订阅服务订单</li>
|
||||
<li><span class="badge bg-warning me-2">数字商品</span>数字产品订单</li>
|
||||
<li><span class="badge bg-secondary me-2">实体商品</span>实体产品订单</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="mt-3">注意事项:</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li>• 订单创建后状态为"待支付"</li>
|
||||
<li>• 支持支付宝和PayPal支付</li>
|
||||
<li>• 订单超时未支付将自动取消</li>
|
||||
<li>• 请确保联系信息准确无误</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.order-item {
|
||||
background-color: #f8f9fa;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.order-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.item-subtotal {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let itemIndex = 0;
|
||||
|
||||
// Add new order item
|
||||
document.getElementById('addItemBtn').addEventListener('click', function() {
|
||||
itemIndex++;
|
||||
const template = document.querySelector('.order-item').cloneNode(true);
|
||||
|
||||
// Update indices
|
||||
template.setAttribute('data-index', itemIndex);
|
||||
template.querySelectorAll('input, textarea').forEach(function(input) {
|
||||
const name = input.getAttribute('name');
|
||||
if (name) {
|
||||
input.setAttribute('name', name.replace('[0]', '[' + itemIndex + ']'));
|
||||
}
|
||||
});
|
||||
|
||||
// Clear values
|
||||
template.querySelectorAll('input, textarea').forEach(function(input) {
|
||||
if (input.type !== 'number' || input.classList.contains('item-quantity')) {
|
||||
input.value = '';
|
||||
}
|
||||
if (input.classList.contains('item-quantity')) {
|
||||
input.value = '1';
|
||||
}
|
||||
});
|
||||
|
||||
// Show remove button
|
||||
template.querySelector('.remove-item').style.display = 'block';
|
||||
|
||||
// Add event listeners
|
||||
addItemEventListeners(template);
|
||||
|
||||
document.getElementById('orderItems').appendChild(template);
|
||||
updateOrderSummary();
|
||||
});
|
||||
|
||||
// Remove order item
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.remove-item')) {
|
||||
const item = e.target.closest('.order-item');
|
||||
item.remove();
|
||||
updateOrderSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners to order item
|
||||
function addItemEventListeners(item) {
|
||||
const priceInput = item.querySelector('.item-price');
|
||||
const quantityInput = item.querySelector('.item-quantity');
|
||||
const subtotalInput = item.querySelector('.item-subtotal');
|
||||
|
||||
function updateSubtotal() {
|
||||
const price = parseFloat(priceInput.value) || 0;
|
||||
const quantity = parseInt(quantityInput.value) || 0;
|
||||
const subtotal = price * quantity;
|
||||
subtotalInput.value = subtotal.toFixed(2);
|
||||
updateOrderSummary();
|
||||
}
|
||||
|
||||
priceInput.addEventListener('input', updateSubtotal);
|
||||
quantityInput.addEventListener('input', updateSubtotal);
|
||||
}
|
||||
|
||||
// Add event listeners to existing items
|
||||
document.querySelectorAll('.order-item').forEach(addItemEventListeners);
|
||||
|
||||
// Update order summary
|
||||
function updateOrderSummary() {
|
||||
const items = document.querySelectorAll('.order-item');
|
||||
const summary = document.getElementById('orderSummary');
|
||||
let total = 0;
|
||||
let itemCount = 0;
|
||||
|
||||
items.forEach(function(item) {
|
||||
const price = parseFloat(item.querySelector('.item-price').value) || 0;
|
||||
const quantity = parseInt(item.querySelector('.item-quantity').value) || 0;
|
||||
const subtotal = price * quantity;
|
||||
|
||||
if (price > 0 && quantity > 0) {
|
||||
total += subtotal;
|
||||
itemCount++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('totalAmount').textContent = total.toFixed(2);
|
||||
|
||||
if (itemCount > 0) {
|
||||
let summaryHtml = '<div class="mb-3">';
|
||||
items.forEach(function(item, index) {
|
||||
const name = item.querySelector('.item-name').value;
|
||||
const price = parseFloat(item.querySelector('.item-price').value) || 0;
|
||||
const quantity = parseInt(item.querySelector('.item-quantity').value) || 0;
|
||||
const subtotal = price * quantity;
|
||||
|
||||
if (name && price > 0 && quantity > 0) {
|
||||
summaryHtml += `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<div class="fw-bold">${name}</div>
|
||||
<small class="text-muted">${price.toFixed(2)} × ${quantity}</small>
|
||||
</div>
|
||||
<span class="fw-bold">${subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
summaryHtml += '</div>';
|
||||
summaryHtml += `
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">总计:</span>
|
||||
<span class="fs-5 text-primary">${total.toFixed(2)}</span>
|
||||
</div>
|
||||
`;
|
||||
summary.innerHTML = summaryHtml;
|
||||
} else {
|
||||
summary.innerHTML = `
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="fas fa-shopping-basket fa-2x mb-2"></i>
|
||||
<p>请添加商品到订单</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Currency change
|
||||
document.getElementById('currency').addEventListener('change', function() {
|
||||
const currency = this.value;
|
||||
document.querySelectorAll('.currency-symbol').forEach(function(symbol) {
|
||||
symbol.textContent = currency;
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('orderForm').addEventListener('submit', function(e) {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = submitBtn.querySelector('.loading');
|
||||
const submitText = submitBtn.querySelector('.submit-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
submitText.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Validate form
|
||||
const items = document.querySelectorAll('.order-item');
|
||||
let hasValidItem = false;
|
||||
|
||||
items.forEach(function(item) {
|
||||
const name = item.querySelector('.item-name').value.trim();
|
||||
const price = parseFloat(item.querySelector('.item-price').value) || 0;
|
||||
const quantity = parseInt(item.querySelector('.item-quantity').value) || 0;
|
||||
|
||||
if (name && price > 0 && quantity > 0) {
|
||||
hasValidItem = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasValidItem) {
|
||||
e.preventDefault();
|
||||
alert('请至少添加一个有效商品');
|
||||
loading.classList.remove('show');
|
||||
submitText.style.display = 'inline-block';
|
||||
submitBtn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft
|
||||
const formData = {};
|
||||
const form = document.getElementById('orderForm');
|
||||
|
||||
form.addEventListener('input', function(e) {
|
||||
if (e.target.name && e.target.name.includes('orderItems')) {
|
||||
return; // Skip order items for now
|
||||
}
|
||||
|
||||
formData[e.target.name] = e.target.value;
|
||||
localStorage.setItem('orderFormDraft', JSON.stringify(formData));
|
||||
});
|
||||
|
||||
// Restore draft on page load
|
||||
const savedDraft = localStorage.getItem('orderFormDraft');
|
||||
if (savedDraft) {
|
||||
try {
|
||||
const draft = JSON.parse(savedDraft);
|
||||
Object.keys(draft).forEach(function(key) {
|
||||
const field = form.querySelector(`[name="${key}"]`);
|
||||
if (field && draft[key]) {
|
||||
field.value = draft[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to restore form draft:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft on successful submission
|
||||
form.addEventListener('submit', function() {
|
||||
localStorage.removeItem('orderFormDraft');
|
||||
});
|
||||
|
||||
// Initial update
|
||||
updateOrderSummary();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
152
demo/src/main/resources/templates/payment/detail.html
Normal file
152
demo/src/main/resources/templates/payment/detail.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>支付详情</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.detail-card {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.info-row {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card detail-card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h4 class="mb-0"><i class="fas fa-receipt me-2"></i>支付详情</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment}">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>订单号:</strong>
|
||||
<span th:text="${payment.orderId}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>支付状态:</strong>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}"
|
||||
class="badge bg-success status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}"
|
||||
class="badge bg-danger status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}"
|
||||
class="badge bg-warning status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PROCESSING'}"
|
||||
class="badge bg-info status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'CANCELLED'}"
|
||||
class="badge bg-secondary status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'REFUNDED'}"
|
||||
class="badge bg-dark status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>支付金额:</strong>
|
||||
<span class="h5 text-primary">
|
||||
<span th:text="${payment.currency}"></span>
|
||||
<span th:text="${payment.amount}"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>支付方式:</strong>
|
||||
<span th:text="${payment.paymentMethod.displayName}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.description}" class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="info-row">
|
||||
<strong>支付描述:</strong>
|
||||
<span th:text="${payment.description}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.externalTransactionId}" class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="info-row">
|
||||
<strong>外部交易ID:</strong>
|
||||
<span th:text="${payment.externalTransactionId}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>创建时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.createdAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-row">
|
||||
<strong>更新时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.updatedAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.paidAt}" class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="info-row">
|
||||
<strong>支付时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.paidAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a th:href="@{/payment/history}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>返回列表
|
||||
</a>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>新建支付
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
157
demo/src/main/resources/templates/payment/form.html
Normal file
157
demo/src/main/resources/templates/payment/form.html
Normal file
@@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>支付接入 - 选择支付方式</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.payment-card {
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
.payment-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
||||
}
|
||||
.payment-card.selected {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.payment-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.alipay-icon {
|
||||
color: #1677ff;
|
||||
}
|
||||
.paypal-icon {
|
||||
color: #0070ba;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="mb-0"><i class="fas fa-credit-card me-2"></i>支付接入</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/payment/create}" th:object="${payment}" method="post">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card payment-card h-100" onclick="selectPaymentMethod('ALIPAY')">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-mobile-alt payment-icon alipay-icon"></i>
|
||||
<h5 class="card-title">支付宝</h5>
|
||||
<p class="card-text">安全便捷的移动支付</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card payment-card h-100" onclick="selectPaymentMethod('PAYPAL')">
|
||||
<div class="card-body text-center">
|
||||
<i class="fab fa-paypal payment-icon paypal-icon"></i>
|
||||
<h5 class="card-title">PayPal</h5>
|
||||
<p class="card-text">全球领先的在线支付</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">支付金额</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">¥</span>
|
||||
<input type="number" class="form-control" id="amount" name="amount"
|
||||
th:field="*{amount}" step="0.01" min="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="currency" class="form-label">货币类型</label>
|
||||
<select class="form-select" id="currency" name="currency" th:field="*{currency}">
|
||||
<option value="CNY">人民币 (CNY)</option>
|
||||
<option value="USD">美元 (USD)</option>
|
||||
<option value="EUR">欧元 (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">支付描述</label>
|
||||
<textarea class="form-control" id="description" name="description"
|
||||
th:field="*{description}" rows="3" placeholder="请输入支付描述..."></textarea>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="paymentMethod" name="paymentMethod" th:field="*{paymentMethod}">
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submitBtn" disabled>
|
||||
<i class="fas fa-lock me-2"></i>确认支付
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
function selectPaymentMethod(method) {
|
||||
// 移除所有选中状态
|
||||
document.querySelectorAll('.payment-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
// 添加选中状态
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// 设置隐藏字段值
|
||||
document.getElementById('paymentMethod').value = method;
|
||||
|
||||
// 启用提交按钮
|
||||
document.getElementById('submitBtn').disabled = false;
|
||||
|
||||
// 根据支付方式调整货币选项
|
||||
const currencySelect = document.getElementById('currency');
|
||||
if (method === 'ALIPAY') {
|
||||
currencySelect.value = 'CNY';
|
||||
currencySelect.options[0].selected = true;
|
||||
} else if (method === 'PAYPAL') {
|
||||
currencySelect.value = 'USD';
|
||||
currencySelect.options[1].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
const amount = document.getElementById('amount').value;
|
||||
const paymentMethod = document.getElementById('paymentMethod').value;
|
||||
|
||||
if (!paymentMethod) {
|
||||
e.preventDefault();
|
||||
alert('请选择支付方式');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount || parseFloat(amount) <= 0) {
|
||||
e.preventDefault();
|
||||
alert('请输入有效的支付金额');
|
||||
return;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
118
demo/src/main/resources/templates/payment/history.html
Normal file
118
demo/src/main/resources/templates/payment/history.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>支付记录</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.payment-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.payment-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="fas fa-history me-2"></i>支付记录</h2>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>新建支付
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}" class="alert alert-danger" role="alert">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payments != null and !payments.empty}">
|
||||
<div class="row">
|
||||
<div th:each="payment : ${payments}" class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card payment-card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h6 class="card-title mb-0" th:text="${payment.orderId}"></h6>
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}"
|
||||
class="badge bg-success status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}"
|
||||
class="badge bg-danger status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}"
|
||||
class="badge bg-warning status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PROCESSING'}"
|
||||
class="badge bg-info status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'CANCELLED'}"
|
||||
class="badge bg-secondary status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'REFUNDED'}"
|
||||
class="badge bg-dark status-badge"
|
||||
th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<strong class="text-primary" th:text="${payment.currency}"></strong>
|
||||
<span class="h5" th:text="${payment.amount}"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-credit-card me-1"></i>
|
||||
<span th:text="${payment.paymentMethod.displayName}"></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.description}" class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-file-text me-1"></i>
|
||||
<span th:text="${payment.description}"></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-clock me-1"></i>
|
||||
<span th:text="${#temporals.format(payment.createdAt, 'yyyy-MM-dd HH:mm')}"></span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<a th:href="@{/payment/detail/{id}(id=${payment.id})}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>查看详情
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${payments == null or payments.empty}" class="text-center py-5">
|
||||
<i class="fas fa-receipt fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无支付记录</h4>
|
||||
<p class="text-muted">您还没有任何支付记录</p>
|
||||
<a th:href="@{/payment/create}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>创建支付
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
107
demo/src/main/resources/templates/payment/result.html
Normal file
107
demo/src/main/resources/templates/payment/result.html
Normal file
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>支付结果</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.result-card {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.success-icon {
|
||||
color: #28a745;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.error-icon {
|
||||
color: #dc3545;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.payment-info {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card result-card shadow">
|
||||
<div class="card-body text-center">
|
||||
<div th:if="${success}">
|
||||
<i class="fas fa-check-circle success-icon mb-3"></i>
|
||||
<h3 class="text-success mb-3">支付成功!</h3>
|
||||
<p class="text-muted mb-4">您的支付已完成,感谢您的使用。</p>
|
||||
</div>
|
||||
|
||||
<div th:if="${error}">
|
||||
<i class="fas fa-times-circle error-icon mb-3"></i>
|
||||
<h3 class="text-danger mb-3">支付失败</h3>
|
||||
<p class="text-muted mb-4" th:text="${error}"></p>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment}" class="payment-info mb-4">
|
||||
<h5 class="mb-3">支付详情</h5>
|
||||
<div class="row text-start">
|
||||
<div class="col-6">
|
||||
<strong>订单号:</strong>
|
||||
</div>
|
||||
<div class="col-6" th:text="${payment.orderId}"></div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>支付金额:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span th:text="${payment.currency}"></span>
|
||||
<span th:text="${payment.amount}"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>支付方式:</strong>
|
||||
</div>
|
||||
<div class="col-6" th:text="${payment.paymentMethod.displayName}"></div>
|
||||
|
||||
<div class="col-6">
|
||||
<strong>支付状态:</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span th:if="${payment.status.name() == 'SUCCESS'}" class="badge bg-success" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'FAILED'}" class="badge bg-danger" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PENDING'}" class="badge bg-warning" th:text="${payment.status.displayName}"></span>
|
||||
<span th:if="${payment.status.name() == 'PROCESSING'}" class="badge bg-info" th:text="${payment.status.displayName}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.description}" class="col-12 mt-2">
|
||||
<strong>支付描述:</strong>
|
||||
<span th:text="${payment.description}"></span>
|
||||
</div>
|
||||
|
||||
<div th:if="${payment.paidAt}" class="col-12 mt-2">
|
||||
<strong>支付时间:</strong>
|
||||
<span th:text="${#temporals.format(payment.paidAt, 'yyyy-MM-dd HH:mm:ss')}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a th:href="@{/payment/history}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-history me-2"></i>查看支付记录
|
||||
</a>
|
||||
<a th:href="@{/}" class="btn btn-primary">
|
||||
<i class="fas fa-home me-2"></i>返回首页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
477
demo/src/main/resources/templates/register.html
Normal file
477
demo/src/main/resources/templates/register.html
Normal file
@@ -0,0 +1,477 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户注册 - AIGC Demo</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.register-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.register-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.register-header h2 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.register-header p {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-floating {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 1rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.form-control.is-invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.form-control.is-valid {
|
||||
border-color: #198754;
|
||||
}
|
||||
|
||||
.btn-register {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: white;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-register:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(40, 167, 69, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-register:disabled {
|
||||
opacity: 0.6;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(135deg, #f8d7da, #f5c6cb);
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #dc3545;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.valid-feedback {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #198754;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.register-footer {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.register-footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.register-footer a:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.language-switcher a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.language-switcher a:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.strength-fill {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.strength-weak { background: #dc3545; width: 25%; }
|
||||
.strength-fair { background: #ffc107; width: 50%; }
|
||||
.strength-good { background: #17a2b8; width: 75%; }
|
||||
.strength-strong { background: #28a745; width: 100%; }
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #28a745;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #dc3545;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-container">
|
||||
<div class="register-header">
|
||||
<h2><i class="fas fa-user-plus me-2"></i>创建账户</h2>
|
||||
<p>加入我们,开始您的AIGC之旅</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div th:if="${error}" class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<span th:text="${error}"></span>
|
||||
</div>
|
||||
|
||||
<form th:action="@{/register}" method="post" th:object="${form}" id="registerForm">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="username" th:field="*{username}"
|
||||
placeholder="用户名" required minlength="3" maxlength="50" autocomplete="username">
|
||||
<label for="username">
|
||||
<i class="fas fa-user me-2"></i>用户名
|
||||
</label>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></div>
|
||||
<div class="valid-feedback" th:if="${!#fields.hasErrors('username') and form.username != null}">
|
||||
<i class="fas fa-check check-icon"></i>用户名可用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control" id="email" th:field="*{email}"
|
||||
placeholder="邮箱地址" required autocomplete="email">
|
||||
<label for="email">
|
||||
<i class="fas fa-envelope me-2"></i>邮箱地址
|
||||
</label>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
|
||||
<div class="valid-feedback" th:if="${!#fields.hasErrors('email') and form.email != null}">
|
||||
<i class="fas fa-check check-icon"></i>邮箱格式正确
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="password" th:field="*{password}"
|
||||
placeholder="密码" required minlength="6" maxlength="100" autocomplete="new-password">
|
||||
<label for="password">
|
||||
<i class="fas fa-lock me-2"></i>密码
|
||||
</label>
|
||||
<div class="password-strength">
|
||||
<div class="strength-bar">
|
||||
<div class="strength-fill" id="strengthFill"></div>
|
||||
</div>
|
||||
<small class="text-muted" id="strengthText">密码强度</small>
|
||||
</div>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="password" class="form-control" id="confirmPassword" th:field="*{confirmPassword}"
|
||||
placeholder="确认密码" required minlength="6" maxlength="100" autocomplete="new-password">
|
||||
<label for="confirmPassword">
|
||||
<i class="fas fa-lock me-2"></i>确认密码
|
||||
</label>
|
||||
<div class="invalid-feedback" th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{confirmPassword}"></div>
|
||||
<div class="valid-feedback" id="passwordMatch" style="display: none;">
|
||||
<i class="fas fa-check check-icon"></i>密码匹配
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-register" id="registerBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>注册中...
|
||||
</span>
|
||||
<span class="register-text">
|
||||
<i class="fas fa-user-plus me-2"></i>创建账户
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="register-footer">
|
||||
<p class="mb-2">
|
||||
<span th:text="#{hint.haveaccount}">已有账号?</span>
|
||||
<a th:href="@{/login}">立即登录</a>
|
||||
</p>
|
||||
|
||||
<div class="language-switcher">
|
||||
<a th:href="@{|?lang=zh_CN|}">中文</a> |
|
||||
<a th:href="@{|?lang=en|}">English</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Password strength checker
|
||||
function checkPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
let strengthText = '';
|
||||
let strengthClass = '';
|
||||
|
||||
if (password.length >= 6) strength++;
|
||||
if (password.match(/[a-z]/)) strength++;
|
||||
if (password.match(/[A-Z]/)) strength++;
|
||||
if (password.match(/[0-9]/)) strength++;
|
||||
if (password.match(/[^a-zA-Z0-9]/)) strength++;
|
||||
|
||||
switch (strength) {
|
||||
case 0:
|
||||
case 1:
|
||||
strengthText = '密码强度:弱';
|
||||
strengthClass = 'strength-weak';
|
||||
break;
|
||||
case 2:
|
||||
strengthText = '密码强度:一般';
|
||||
strengthClass = 'strength-fair';
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
strengthText = '密码强度:良好';
|
||||
strengthClass = 'strength-good';
|
||||
break;
|
||||
case 5:
|
||||
strengthText = '密码强度:强';
|
||||
strengthClass = 'strength-strong';
|
||||
break;
|
||||
}
|
||||
|
||||
return { strengthText, strengthClass };
|
||||
}
|
||||
|
||||
// Password strength indicator
|
||||
document.getElementById('password').addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const strengthFill = document.getElementById('strengthFill');
|
||||
const strengthText = document.getElementById('strengthText');
|
||||
|
||||
if (password.length > 0) {
|
||||
const { strengthText: text, strengthClass: className } = checkPasswordStrength(password);
|
||||
strengthFill.className = 'strength-fill ' + className;
|
||||
strengthText.textContent = text;
|
||||
} else {
|
||||
strengthFill.className = 'strength-fill';
|
||||
strengthText.textContent = '密码强度';
|
||||
}
|
||||
});
|
||||
|
||||
// Password confirmation check
|
||||
document.getElementById('confirmPassword').addEventListener('input', function() {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = this.value;
|
||||
const passwordMatch = document.getElementById('passwordMatch');
|
||||
|
||||
if (confirmPassword.length > 0) {
|
||||
if (password === confirmPassword) {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
passwordMatch.style.display = 'block';
|
||||
} else {
|
||||
this.classList.remove('is-valid');
|
||||
this.classList.add('is-invalid');
|
||||
passwordMatch.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
this.classList.remove('is-valid', 'is-invalid');
|
||||
passwordMatch.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Username uniqueness check
|
||||
async function checkUsernameUniqueness(username) {
|
||||
if (!username.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/public/users/exists/username?value=' + encodeURIComponent(username));
|
||||
const data = await response.json();
|
||||
|
||||
const usernameField = document.getElementById('username');
|
||||
if (data.exists) {
|
||||
usernameField.classList.add('is-invalid');
|
||||
usernameField.classList.remove('is-valid');
|
||||
} else {
|
||||
usernameField.classList.remove('is-invalid');
|
||||
usernameField.classList.add('is-valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Username check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Email uniqueness check
|
||||
async function checkEmailUniqueness(email) {
|
||||
if (!email.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/public/users/exists/email?value=' + encodeURIComponent(email));
|
||||
const data = await response.json();
|
||||
|
||||
const emailField = document.getElementById('email');
|
||||
if (data.exists) {
|
||||
emailField.classList.add('is-invalid');
|
||||
emailField.classList.remove('is-valid');
|
||||
} else {
|
||||
emailField.classList.remove('is-invalid');
|
||||
emailField.classList.add('is-valid');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Email check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Add debounced event listeners
|
||||
const debouncedUsernameCheck = debounce(checkUsernameUniqueness, 500);
|
||||
const debouncedEmailCheck = debounce(checkEmailUniqueness, 500);
|
||||
|
||||
document.getElementById('username').addEventListener('blur', function() {
|
||||
debouncedUsernameCheck(this.value);
|
||||
});
|
||||
|
||||
document.getElementById('email').addEventListener('blur', function() {
|
||||
debouncedEmailCheck(this.value);
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||||
const registerBtn = document.getElementById('registerBtn');
|
||||
const loading = registerBtn.querySelector('.loading');
|
||||
const registerText = registerBtn.querySelector('.register-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
registerText.style.display = 'none';
|
||||
registerBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('registerForm').addEventListener('submit', function(e) {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const confirmPassword = document.getElementById('confirmPassword').value.trim();
|
||||
|
||||
if (!username || !email || !password || !confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('请填写所有必填字段');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('两次输入的密码不一致');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
e.preventDefault();
|
||||
alert('密码长度至少6位');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
90
demo/src/main/resources/templates/settings/form.html
Normal file
90
demo/src/main/resources/templates/settings/form.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>系统设置</title>
|
||||
</head>
|
||||
<body>
|
||||
<th:block th:replace="layout :: content">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-gear me-2"></i>系统设置
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form th:action="@{/settings}" th:object="${settings}" method="post">
|
||||
<h5 class="mb-3">基础信息</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">站点名称</label>
|
||||
<input type="text" class="form-control" th:field="*{siteName}" maxlength="100" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">站点副标题</label>
|
||||
<input type="text" class="form-control" th:field="*{siteSubtitle}" maxlength="150" required>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{registrationOpen}">
|
||||
<label class="form-check-label">开放注册</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{maintenanceMode}">
|
||||
<label class="form-check-label">维护模式</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{enableAlipay}">
|
||||
<label class="form-check-label">启用支付宝</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" th:field="*{enablePaypal}">
|
||||
<label class="form-check-label">启用 PayPal</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">联系邮箱</label>
|
||||
<input type="email" class="form-control" th:field="*{contactEmail}" maxlength="120">
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3">套餐与扣点</h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">标准版价格(元)</label>
|
||||
<input type="number" class="form-control" th:field="*{standardPriceCny}" min="0" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">专业版价格(元)</label>
|
||||
<input type="number" class="form-control" th:field="*{proPriceCny}" min="0" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">每次生成消耗资源点</label>
|
||||
<input type="number" class="form-control" th:field="*{pointsPerGeneration}" min="0" required>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>保存
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
343
demo/src/main/resources/templates/users/form.html
Normal file
343
demo/src/main/resources/templates/users/form.html
Normal file
@@ -0,0 +1,343 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title th:text="${user.id} != null ? '编辑用户' : '新增用户'">用户表单</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a th:href="@{/}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a th:href="@{/users}">用户管理</a></li>
|
||||
<li class="breadcrumb-item active" th:text="${user.id} != null ? '编辑用户' : '新增用户'">用户表单</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- User Form -->
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-user me-2"></i>
|
||||
<span th:text="${user.id} != null ? '编辑用户' : '新增用户'">用户表单</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form th:action="${user.id} != null ? @{|/users/${user.id}|} : @{/users}"
|
||||
method="post" th:object="${user}" id="userForm">
|
||||
|
||||
<!-- Username -->
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user me-2"></i>用户名
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
th:value="${user.username}" required minlength="3" maxlength="50"
|
||||
autocomplete="username" placeholder="请输入用户名">
|
||||
<div class="form-text">用户名长度3-50个字符,支持字母、数字、下划线</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope me-2"></i>邮箱地址
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
th:value="${user.email}" required autocomplete="email"
|
||||
placeholder="请输入邮箱地址">
|
||||
<div class="form-text">请输入有效的邮箱地址</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock me-2"></i>密码
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
minlength="6" maxlength="100" autocomplete="new-password"
|
||||
placeholder="请输入密码">
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<span th:if="${user.id} != null">留空则不修改密码</span>
|
||||
<span th:if="${user.id} == null">密码长度6-100个字符</span>
|
||||
</div>
|
||||
|
||||
<!-- Password Strength Indicator -->
|
||||
<div class="password-strength mt-2" th:if="${user.id} == null">
|
||||
<div class="strength-bar">
|
||||
<div class="strength-fill" id="strengthFill"></div>
|
||||
</div>
|
||||
<small class="text-muted" id="strengthText">密码强度</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Role -->
|
||||
<div class="mb-4">
|
||||
<label for="role" class="form-label">
|
||||
<i class="fas fa-shield-alt me-2"></i>用户角色
|
||||
</label>
|
||||
<select class="form-select" id="role" name="role" th:value="${user.role}">
|
||||
<option value="ROLE_USER">普通用户</option>
|
||||
<option value="ROLE_MEMBER">会员用户</option>
|
||||
<option value="ROLE_ADMIN">管理员</option>
|
||||
</select>
|
||||
<div class="form-text">选择用户的权限级别</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a th:href="@{/users}" class="btn btn-secondary me-md-2">
|
||||
<i class="fas fa-times me-2"></i>取消
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<span class="loading">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>保存中...
|
||||
</span>
|
||||
<span class="submit-text">
|
||||
<i class="fas fa-save me-2"></i>保存
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Info Card (for edit mode) -->
|
||||
<div class="card mt-4" th:if="${user.id} != null">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>用户信息
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">用户ID</small>
|
||||
<div th:text="${user.id}">1</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">创建时间</small>
|
||||
<div>2024-01-01 10:00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.password-strength {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.strength-fill {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.strength-weak { background: #dc3545; width: 25%; }
|
||||
.strength-fair { background: #ffc107; width: 50%; }
|
||||
.strength-good { background: #17a2b8; width: 75%; }
|
||||
.strength-strong { background: #28a745; width: 100%; }
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Password strength checker
|
||||
function checkPasswordStrength(password) {
|
||||
let strength = 0;
|
||||
let strengthText = '';
|
||||
let strengthClass = '';
|
||||
|
||||
if (password.length >= 6) strength++;
|
||||
if (password.match(/[a-z]/)) strength++;
|
||||
if (password.match(/[A-Z]/)) strength++;
|
||||
if (password.match(/[0-9]/)) strength++;
|
||||
if (password.match(/[^a-zA-Z0-9]/)) strength++;
|
||||
|
||||
switch (strength) {
|
||||
case 0:
|
||||
case 1:
|
||||
strengthText = '密码强度:弱';
|
||||
strengthClass = 'strength-weak';
|
||||
break;
|
||||
case 2:
|
||||
strengthText = '密码强度:一般';
|
||||
strengthClass = 'strength-fair';
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
strengthText = '密码强度:良好';
|
||||
strengthClass = 'strength-good';
|
||||
break;
|
||||
case 5:
|
||||
strengthText = '密码强度:强';
|
||||
strengthClass = 'strength-strong';
|
||||
break;
|
||||
}
|
||||
|
||||
return { strengthText, strengthClass };
|
||||
}
|
||||
|
||||
// Password strength indicator (only for new users)
|
||||
const passwordField = document.getElementById('password');
|
||||
const strengthFill = document.getElementById('strengthFill');
|
||||
const strengthText = document.getElementById('strengthText');
|
||||
|
||||
if (strengthFill && strengthText) {
|
||||
passwordField.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
|
||||
if (password.length > 0) {
|
||||
const { strengthText: text, strengthClass: className } = checkPasswordStrength(password);
|
||||
strengthFill.className = 'strength-fill ' + className;
|
||||
strengthText.textContent = text;
|
||||
} else {
|
||||
strengthFill.className = 'strength-fill';
|
||||
strengthText.textContent = '密码强度';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle password visibility
|
||||
document.getElementById('togglePassword').addEventListener('click', function() {
|
||||
const passwordField = document.getElementById('password');
|
||||
const icon = this.querySelector('i');
|
||||
|
||||
if (passwordField.type === 'password') {
|
||||
passwordField.type = 'text';
|
||||
icon.classList.remove('fa-eye');
|
||||
icon.classList.add('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.type = 'password';
|
||||
icon.classList.remove('fa-eye-slash');
|
||||
icon.classList.add('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission with loading state
|
||||
document.getElementById('userForm').addEventListener('submit', function(e) {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = submitBtn.querySelector('.loading');
|
||||
const submitText = submitBtn.querySelector('.submit-text');
|
||||
|
||||
// Show loading state
|
||||
loading.classList.add('show');
|
||||
submitText.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('userForm').addEventListener('submit', function(e) {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value.trim();
|
||||
const isEdit = document.querySelector('form').action.includes('/edit');
|
||||
|
||||
if (!username || !email) {
|
||||
e.preventDefault();
|
||||
alert('请填写用户名和邮箱');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isEdit && !password) {
|
||||
e.preventDefault();
|
||||
alert('新用户必须设置密码');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password && password.length < 6) {
|
||||
e.preventDefault();
|
||||
alert('密码长度至少6位');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
e.preventDefault();
|
||||
alert('请输入有效的邮箱地址');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft (for new users)
|
||||
if (!document.querySelector('form').action.includes('/edit')) {
|
||||
const formData = {};
|
||||
|
||||
['username', 'email', 'password'].forEach(function(fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
if (field) {
|
||||
field.addEventListener('input', function() {
|
||||
formData[fieldName] = this.value;
|
||||
localStorage.setItem('userFormDraft', JSON.stringify(formData));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Restore draft on page load
|
||||
const savedDraft = localStorage.getItem('userFormDraft');
|
||||
if (savedDraft) {
|
||||
try {
|
||||
const draft = JSON.parse(savedDraft);
|
||||
Object.keys(draft).forEach(function(key) {
|
||||
const field = document.getElementById(key);
|
||||
if (field && draft[key]) {
|
||||
field.value = draft[key];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to restore form draft:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear draft on successful submission
|
||||
document.getElementById('userForm').addEventListener('submit', function() {
|
||||
localStorage.removeItem('userFormDraft');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
262
demo/src/main/resources/templates/users/list.html
Normal file
262
demo/src/main/resources/templates/users/list.html
Normal file
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" th:replace="~{layout :: layout(~{::title}, ~{::content})}">
|
||||
<head>
|
||||
<title>用户管理</title>
|
||||
</head>
|
||||
<body>
|
||||
<div th:fragment="content">
|
||||
<!-- Page Actions -->
|
||||
<div th:fragment="page-actions">
|
||||
<a th:href="@{/users/new}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>新增用户
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-users me-2"></i>用户列表
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group" style="max-width: 300px;">
|
||||
<input type="text" class="form-control" placeholder="搜索用户..." id="searchInput">
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="usersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="80">ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th width="120">角色</th>
|
||||
<th width="120">状态</th>
|
||||
<th width="200">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="u : ${users}" class="user-row">
|
||||
<td th:text="${u.id}">1</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle me-2">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<span th:text="${u.username}">user</span>
|
||||
</div>
|
||||
</td>
|
||||
<td th:text="${u.email}">email</td>
|
||||
<td>
|
||||
<span th:if="${u.role == 'ROLE_USER'}" class="badge bg-secondary">普通用户</span>
|
||||
<span th:if="${u.role == 'ROLE_MEMBER'}" class="badge bg-info">会员用户</span>
|
||||
<span th:if="${u.role == 'ROLE_ADMIN'}" class="badge bg-danger">管理员</span>
|
||||
<span th:if="${u.role != 'ROLE_USER' and u.role != 'ROLE_MEMBER' and u.role != 'ROLE_ADMIN'}"
|
||||
class="badge bg-warning" th:text="${u.role}">其他</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">正常</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a th:href="@{|/users/${u.id}/edit|}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
th:onclick="'showUserDetails(' + ${u.id} + ')'">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<form th:action="@{|/users/${u.id}/delete|}" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
th:onclick="'return confirmDelete(\'' + ${u.username} + '\')'">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<small class="text-muted">
|
||||
共 <span th:text="${users.size()}">0</span> 个用户
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<nav aria-label="用户分页">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">上一页</span>
|
||||
</li>
|
||||
<li class="page-item active">
|
||||
<span class="page-link">1</span>
|
||||
</li>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">下一页</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Details Modal -->
|
||||
<div class="modal fade" id="userDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-user me-2"></i>用户详情
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="userDetailsContent">
|
||||
<!-- User details will be loaded here -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatar-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Search functionality
|
||||
document.getElementById('searchInput').addEventListener('input', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('.user-row');
|
||||
|
||||
rows.forEach(function(row) {
|
||||
const username = row.cells[1].textContent.toLowerCase();
|
||||
const email = row.cells[2].textContent.toLowerCase();
|
||||
|
||||
if (username.includes(searchTerm) || email.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Show user details
|
||||
function showUserDetails(userId) {
|
||||
// In a real application, you would fetch user details via AJAX
|
||||
const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
|
||||
const content = document.getElementById('userDetailsContent');
|
||||
|
||||
// For demo purposes, show static content
|
||||
content.innerHTML = `
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="avatar-circle mx-auto mb-3" style="width: 80px; height: 80px; font-size: 2rem;">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<h6>用户ID: ${userId}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>用户名:</strong></td>
|
||||
<td>demo_user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>邮箱:</strong></td>
|
||||
<td>demo@example.com</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>角色:</strong></td>
|
||||
<td><span class="badge bg-secondary">普通用户</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>注册时间:</strong></td>
|
||||
<td>2024-01-01 10:00:00</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>最后登录:</strong></td>
|
||||
<td>2024-01-15 14:30:00</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
function confirmDelete(username) {
|
||||
return confirm(`确定要删除用户 "${username}" 吗?此操作不可撤销。`);
|
||||
}
|
||||
|
||||
// Auto-refresh table every 30 seconds
|
||||
setInterval(function() {
|
||||
// In a real application, you would refresh the table data
|
||||
console.log('Auto-refreshing user table...');
|
||||
}, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class DemoApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user