Initial commit: AIGC项目完整代码

This commit is contained in:
AIGC Developer
2025-10-21 16:50:33 +08:00
commit 47c8e02ab0
137 changed files with 30676 additions and 0 deletions

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

View File

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

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

View 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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();
}
}

View File

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

View 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;
}
}

View 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();
}
}

View 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());
}
}
}

View File

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

View 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();
}
}

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

View 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

View 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

View File

@@ -0,0 +1,4 @@
spring.application.name=demo
spring.messages.basename=messages
spring.thymeleaf.cache=false
spring.profiles.active=dev

View 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;

View 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=邮箱格式不正确

View 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

View 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;

View 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
);

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

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

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

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

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

View 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">&nbsp;</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>

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

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

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

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

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

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

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

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

View File

@@ -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() {
}
}