add
This commit is contained in:
11
backend/src/main/java/com/flower/FlowerApplication.java
Normal file
11
backend/src/main/java/com/flower/FlowerApplication.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.flower;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class FlowerApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(FlowerApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.flower.admin;
|
||||
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.order.OrderRepository;
|
||||
import com.flower.security.AdminOnly;
|
||||
import com.flower.user.UserRepository;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/dashboard")
|
||||
@AdminOnly
|
||||
public class AdminDashboardController {
|
||||
private final OrderRepository orderRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AdminDashboardController(OrderRepository orderRepository, UserRepository userRepository) {
|
||||
this.orderRepository = orderRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<DashboardStats> stats() {
|
||||
long totalOrders = orderRepository.count();
|
||||
long totalUsers = userRepository.count();
|
||||
BigDecimal totalSales = orderRepository.findAll().stream()
|
||||
.filter(order -> "PAID".equals(order.getStatus()) || "SHIPPED".equals(order.getStatus())
|
||||
|| "COMPLETED".equals(order.getStatus()))
|
||||
.map(order -> order.getTotalAmount() == null ? BigDecimal.ZERO : order.getTotalAmount())
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
DashboardStats stats = new DashboardStats();
|
||||
stats.setTotalOrders(totalOrders);
|
||||
stats.setTotalUsers(totalUsers);
|
||||
stats.setTotalSales(totalSales);
|
||||
return ApiResponse.ok(stats);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DashboardStats {
|
||||
private long totalOrders;
|
||||
private long totalUsers;
|
||||
private BigDecimal totalSales;
|
||||
}
|
||||
}
|
||||
14
backend/src/main/java/com/flower/common/ApiException.java
Normal file
14
backend/src/main/java/com/flower/common/ApiException.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.flower.common;
|
||||
|
||||
public class ApiException extends RuntimeException {
|
||||
private final int code;
|
||||
|
||||
public ApiException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
25
backend/src/main/java/com/flower/common/ApiResponse.java
Normal file
25
backend/src/main/java/com/flower/common/ApiResponse.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.flower.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.code = 0;
|
||||
response.message = "ok";
|
||||
response.data = data;
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.code = code;
|
||||
response.message = message;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.flower.common;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ApiException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handleApi(ApiException ex) {
|
||||
return ApiResponse.error(ex.getCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handleValid(MethodArgumentNotValidException ex) {
|
||||
String message = ex.getBindingResult().getFieldErrors().isEmpty()
|
||||
? "参数错误"
|
||||
: ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
|
||||
return ApiResponse.error(400, message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ApiResponse<Void> handleOther(Exception ex) {
|
||||
return ApiResponse.error(500, ex.getMessage());
|
||||
}
|
||||
}
|
||||
21
backend/src/main/java/com/flower/common/PasswordUtil.java
Normal file
21
backend/src/main/java/com/flower/common/PasswordUtil.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.flower.common;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class PasswordUtil {
|
||||
public static String sha256(String raw) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("SHA-256 not supported", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
backend/src/main/java/com/flower/common/QrCodeUtil.java
Normal file
21
backend/src/main/java/com/flower/common/QrCodeUtil.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.flower.common;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.MultiFormatWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class QrCodeUtil {
|
||||
public static byte[] generatePng(String content, int size) {
|
||||
try {
|
||||
BitMatrix matrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, size, size);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
MatrixToImageWriter.writeToStream(matrix, "PNG", outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new ApiException(500, "二维码生成失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
33
backend/src/main/java/com/flower/confession/Barrage.java
Normal file
33
backend/src/main/java/com/flower/confession/Barrage.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.flower.confession;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "barrages")
|
||||
public class Barrage {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long confessionId;
|
||||
|
||||
@Column(length = 32)
|
||||
private String sender;
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String content;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.confession;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BarrageRepository extends JpaRepository<Barrage, Long> {
|
||||
List<Barrage> findByConfessionIdOrderByCreatedAtAsc(Long confessionId);
|
||||
}
|
||||
40
backend/src/main/java/com/flower/confession/Confession.java
Normal file
40
backend/src/main/java/com/flower/confession/Confession.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.flower.confession;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "confessions")
|
||||
public class Confession {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long orderId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 32)
|
||||
private String code;
|
||||
|
||||
@Column(length = 64)
|
||||
private String title;
|
||||
|
||||
@Column(length = 2000)
|
||||
private String message;
|
||||
|
||||
@Column(length = 255)
|
||||
private String imageUrl;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.flower.confession;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.common.QrCodeUtil;
|
||||
import com.flower.order.Order;
|
||||
import com.flower.order.OrderRepository;
|
||||
import com.flower.security.AuthContext;
|
||||
import com.flower.security.PublicEndpoint;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/confessions")
|
||||
public class ConfessionController {
|
||||
private final ConfessionRepository confessionRepository;
|
||||
private final BarrageRepository barrageRepository;
|
||||
private final OrderRepository orderRepository;
|
||||
private final String baseUrl;
|
||||
|
||||
public ConfessionController(ConfessionRepository confessionRepository,
|
||||
BarrageRepository barrageRepository,
|
||||
OrderRepository orderRepository,
|
||||
@Value("${app.qr.base-url}") String baseUrl) {
|
||||
this.confessionRepository = confessionRepository;
|
||||
this.barrageRepository = barrageRepository;
|
||||
this.orderRepository = orderRepository;
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<Confession> create(@RequestBody CreateConfessionRequest request) {
|
||||
Order order = orderRepository.findById(request.getOrderId())
|
||||
.orElseThrow(() -> new ApiException(404, "订单不存在"));
|
||||
if (!order.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
Confession confession = new Confession();
|
||||
confession.setOrderId(order.getId());
|
||||
confession.setUserId(order.getUserId());
|
||||
confession.setTitle(request.getTitle());
|
||||
confession.setMessage(request.getMessage());
|
||||
confession.setImageUrl(request.getImageUrl());
|
||||
confession.setCode(UUID.randomUUID().toString().replace("-", "").substring(0, 12));
|
||||
return ApiResponse.ok(confessionRepository.save(confession));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping("/{code}")
|
||||
public ApiResponse<Confession> get(@PathVariable String code) {
|
||||
return ApiResponse.ok(confessionRepository.findByCode(code)
|
||||
.orElseThrow(() -> new ApiException(404, "页面不存在")));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping("/{code}/barrages")
|
||||
public ApiResponse<List<Barrage>> listBarrages(@PathVariable String code) {
|
||||
Confession confession = confessionRepository.findByCode(code)
|
||||
.orElseThrow(() -> new ApiException(404, "页面不存在"));
|
||||
return ApiResponse.ok(barrageRepository.findByConfessionIdOrderByCreatedAtAsc(confession.getId()));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@PostMapping("/{code}/barrages")
|
||||
public ApiResponse<Barrage> createBarrage(@PathVariable String code, @RequestBody CreateBarrageRequest request) {
|
||||
Confession confession = confessionRepository.findByCode(code)
|
||||
.orElseThrow(() -> new ApiException(404, "页面不存在"));
|
||||
Barrage barrage = new Barrage();
|
||||
barrage.setConfessionId(confession.getId());
|
||||
barrage.setSender(request.getSender());
|
||||
barrage.setContent(request.getContent());
|
||||
return ApiResponse.ok(barrageRepository.save(barrage));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping(value = "/{code}/qr", produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public ResponseEntity<byte[]> qr(@PathVariable String code) {
|
||||
String url = baseUrl.endsWith("/") ? baseUrl + code : baseUrl + "/" + code;
|
||||
byte[] png = QrCodeUtil.generatePng(url, 300);
|
||||
return ResponseEntity.ok(png);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateConfessionRequest {
|
||||
private Long orderId;
|
||||
private String title;
|
||||
private String message;
|
||||
private String imageUrl;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateBarrageRequest {
|
||||
private String sender;
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.confession;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ConfessionRepository extends JpaRepository<Confession, Long> {
|
||||
Optional<Confession> findByCode(String code);
|
||||
}
|
||||
33
backend/src/main/java/com/flower/config/WebConfig.java
Normal file
33
backend/src/main/java/com/flower/config/WebConfig.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.flower.config;
|
||||
|
||||
import com.flower.security.AuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
private final AuthInterceptor authInterceptor;
|
||||
private final String allowedOrigins;
|
||||
|
||||
public WebConfig(AuthInterceptor authInterceptor,
|
||||
@Value("${app.cors.allowed-origins}") String allowedOrigins) {
|
||||
this.authInterceptor = authInterceptor;
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor).addPathPatterns("/api/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(allowedOrigins.split(","))
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
}
|
||||
56
backend/src/main/java/com/flower/order/Order.java
Normal file
56
backend/src/main/java/com/flower/order/Order.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.flower.order;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 32)
|
||||
private String orderNo;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 16)
|
||||
private String status;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Column(length = 32)
|
||||
private String receiverName;
|
||||
|
||||
@Column(length = 32)
|
||||
private String receiverPhone;
|
||||
|
||||
@Column(length = 128)
|
||||
private String receiverAddress;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
private LocalDateTime payAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
186
backend/src/main/java/com/flower/order/OrderController.java
Normal file
186
backend/src/main/java/com/flower/order/OrderController.java
Normal file
@@ -0,0 +1,186 @@
|
||||
package com.flower.order;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.product.Product;
|
||||
import com.flower.product.ProductRepository;
|
||||
import com.flower.security.AdminOnly;
|
||||
import com.flower.security.AuthContext;
|
||||
import com.flower.user.Address;
|
||||
import com.flower.user.AddressRepository;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
public class OrderController {
|
||||
private final OrderRepository orderRepository;
|
||||
private final OrderItemRepository orderItemRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final AddressRepository addressRepository;
|
||||
|
||||
public OrderController(OrderRepository orderRepository,
|
||||
OrderItemRepository orderItemRepository,
|
||||
ProductRepository productRepository,
|
||||
AddressRepository addressRepository) {
|
||||
this.orderRepository = orderRepository;
|
||||
this.orderItemRepository = orderItemRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.addressRepository = addressRepository;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<OrderDetail> create(@Valid @RequestBody CreateOrderRequest request) {
|
||||
Address address = addressRepository.findById(request.getAddressId())
|
||||
.orElseThrow(() -> new ApiException(404, "地址不存在"));
|
||||
if (!address.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
List<OrderItem> items = new ArrayList<>();
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (CreateOrderItem item : request.getItems()) {
|
||||
Product product = productRepository.findById(item.getProductId())
|
||||
.orElseThrow(() -> new ApiException(404, "商品不存在"));
|
||||
if (!"ON".equals(product.getStatus())) {
|
||||
throw new ApiException(400, "商品已下架");
|
||||
}
|
||||
if (product.getStock() < item.getQuantity()) {
|
||||
throw new ApiException(400, "库存不足");
|
||||
}
|
||||
product.setStock(product.getStock() - item.getQuantity());
|
||||
productRepository.save(product);
|
||||
OrderItem orderItem = new OrderItem();
|
||||
orderItem.setProductId(product.getId());
|
||||
orderItem.setProductName(product.getName());
|
||||
orderItem.setProductPrice(product.getPrice());
|
||||
orderItem.setQuantity(item.getQuantity());
|
||||
orderItem.setProductCover(product.getCoverUrl());
|
||||
items.add(orderItem);
|
||||
total = total.add(product.getPrice().multiply(new BigDecimal(item.getQuantity())));
|
||||
}
|
||||
Order order = new Order();
|
||||
order.setOrderNo("NO" + UUID.randomUUID().toString().replace("-", "").substring(0, 12));
|
||||
order.setUserId(AuthContext.get().getId());
|
||||
order.setStatus("CREATED");
|
||||
order.setTotalAmount(total);
|
||||
order.setReceiverName(address.getRecipientName());
|
||||
order.setReceiverPhone(address.getPhone());
|
||||
order.setReceiverAddress(String.join(" ", safe(address.getProvince()),
|
||||
safe(address.getCity()), safe(address.getDistrict()), safe(address.getDetail())));
|
||||
orderRepository.save(order);
|
||||
for (OrderItem item : items) {
|
||||
item.setOrderId(order.getId());
|
||||
orderItemRepository.save(item);
|
||||
}
|
||||
return ApiResponse.ok(new OrderDetail(order, items));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<List<OrderDetail>> listMine() {
|
||||
List<Order> orders = orderRepository.findByUserIdOrderByCreatedAtDesc(AuthContext.get().getId());
|
||||
List<OrderDetail> result = new ArrayList<>();
|
||||
for (Order order : orders) {
|
||||
result.add(new OrderDetail(order, orderItemRepository.findByOrderId(order.getId())));
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<OrderDetail> detail(@PathVariable Long id) {
|
||||
Order order = orderRepository.findById(id).orElseThrow(() -> new ApiException(404, "订单不存在"));
|
||||
if (!order.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
return ApiResponse.ok(new OrderDetail(order, orderItemRepository.findByOrderId(id)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/cancel")
|
||||
public ApiResponse<Order> cancel(@PathVariable Long id) {
|
||||
Order order = orderRepository.findById(id).orElseThrow(() -> new ApiException(404, "订单不存在"));
|
||||
if (!order.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
if (!"CREATED".equals(order.getStatus())) {
|
||||
throw new ApiException(400, "订单无法取消");
|
||||
}
|
||||
order.setStatus("CANCELED");
|
||||
orderRepository.save(order);
|
||||
return ApiResponse.ok(order);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/pay")
|
||||
public ApiResponse<Order> pay(@PathVariable Long id) {
|
||||
Order order = orderRepository.findById(id).orElseThrow(() -> new ApiException(404, "订单不存在"));
|
||||
if (!order.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
if (!"CREATED".equals(order.getStatus())) {
|
||||
throw new ApiException(400, "订单无法支付");
|
||||
}
|
||||
order.setStatus("PAID");
|
||||
order.setPayAt(LocalDateTime.now());
|
||||
orderRepository.save(order);
|
||||
return ApiResponse.ok(order);
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@GetMapping("/admin/all")
|
||||
public ApiResponse<List<OrderDetail>> listAll() {
|
||||
List<Order> orders = orderRepository.findAll();
|
||||
List<OrderDetail> result = new ArrayList<>();
|
||||
for (Order order : orders) {
|
||||
result.add(new OrderDetail(order, orderItemRepository.findByOrderId(order.getId())));
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PutMapping("/admin/{id}/status")
|
||||
public ApiResponse<Order> updateStatus(@PathVariable Long id, @RequestBody UpdateStatusRequest request) {
|
||||
Order order = orderRepository.findById(id).orElseThrow(() -> new ApiException(404, "订单不存在"));
|
||||
order.setStatus(request.getStatus());
|
||||
orderRepository.save(order);
|
||||
return ApiResponse.ok(order);
|
||||
}
|
||||
|
||||
private String safe(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateOrderRequest {
|
||||
private Long addressId;
|
||||
@NotEmpty
|
||||
private List<CreateOrderItem> items;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CreateOrderItem {
|
||||
private Long productId;
|
||||
private Integer quantity;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateStatusRequest {
|
||||
private String status;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OrderDetail {
|
||||
private Order order;
|
||||
private List<OrderItem> items;
|
||||
|
||||
public OrderDetail(Order order, List<OrderItem> items) {
|
||||
this.order = order;
|
||||
this.items = items;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
backend/src/main/java/com/flower/order/OrderItem.java
Normal file
33
backend/src/main/java/com/flower/order/OrderItem.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.flower.order;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "order_items")
|
||||
public class OrderItem {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long orderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String productName;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal productPrice;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
|
||||
@Column(length = 255)
|
||||
private String productCover;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.order;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
|
||||
List<OrderItem> findByOrderId(Long orderId);
|
||||
}
|
||||
11
backend/src/main/java/com/flower/order/OrderRepository.java
Normal file
11
backend/src/main/java/com/flower/order/OrderRepository.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.flower.order;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface OrderRepository extends JpaRepository<Order, Long> {
|
||||
Optional<Order> findByOrderNo(String orderNo);
|
||||
List<Order> findByUserIdOrderByCreatedAtDesc(Long userId);
|
||||
}
|
||||
19
backend/src/main/java/com/flower/product/Category.java
Normal file
19
backend/src/main/java/com/flower/product/Category.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.flower.product;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "categories")
|
||||
public class Category {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String name;
|
||||
|
||||
private Integer sortOrder = 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.flower.product;
|
||||
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.security.AdminOnly;
|
||||
import com.flower.security.PublicEndpoint;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/categories")
|
||||
public class CategoryController {
|
||||
private final CategoryRepository categoryRepository;
|
||||
|
||||
public CategoryController(CategoryRepository categoryRepository) {
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping
|
||||
public ApiResponse<List<Category>> list() {
|
||||
return ApiResponse.ok(categoryRepository.findAll());
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PostMapping
|
||||
public ApiResponse<Category> create(@RequestBody Category category) {
|
||||
return ApiResponse.ok(categoryRepository.save(category));
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Category> update(@PathVariable Long id, @RequestBody Category category) {
|
||||
category.setId(id);
|
||||
return ApiResponse.ok(categoryRepository.save(category));
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Void> delete(@PathVariable Long id) {
|
||||
categoryRepository.deleteById(id);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.flower.product;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CategoryRepository extends JpaRepository<Category, Long> {
|
||||
}
|
||||
52
backend/src/main/java/com/flower/product/Product.java
Normal file
52
backend/src/main/java/com/flower/product/Product.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.flower.product;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long categoryId;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String name;
|
||||
|
||||
@Column(length = 2000)
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal price;
|
||||
|
||||
private Integer stock = 0;
|
||||
|
||||
@Column(length = 255)
|
||||
private String coverUrl;
|
||||
|
||||
@Column(length = 16)
|
||||
private String status = "ON";
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.flower.product;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.security.AdminOnly;
|
||||
import com.flower.security.PublicEndpoint;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/products")
|
||||
public class ProductController {
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
public ProductController(ProductRepository productRepository) {
|
||||
this.productRepository = productRepository;
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping
|
||||
public ApiResponse<List<Product>> list(@RequestParam(required = false) Long categoryId) {
|
||||
if (categoryId == null) {
|
||||
return ApiResponse.ok(productRepository.findByStatus("ON"));
|
||||
}
|
||||
return ApiResponse.ok(productRepository.findByCategoryIdAndStatus(categoryId, "ON"));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping("/{id}")
|
||||
public ApiResponse<Product> detail(@PathVariable Long id) {
|
||||
return ApiResponse.ok(productRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(404, "商品不存在")));
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@GetMapping("/admin/all")
|
||||
public ApiResponse<List<Product>> adminList() {
|
||||
return ApiResponse.ok(productRepository.findAll());
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PostMapping
|
||||
public ApiResponse<Product> create(@RequestBody Product product) {
|
||||
return ApiResponse.ok(productRepository.save(product));
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Product> update(@PathVariable Long id, @RequestBody Product product) {
|
||||
product.setId(id);
|
||||
return ApiResponse.ok(productRepository.save(product));
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Void> delete(@PathVariable Long id) {
|
||||
productRepository.deleteById(id);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.flower.product;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
List<Product> findByStatus(String status);
|
||||
List<Product> findByCategoryIdAndStatus(Long categoryId, String status);
|
||||
}
|
||||
41
backend/src/main/java/com/flower/review/Review.java
Normal file
41
backend/src/main/java/com/flower/review/Review.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.flower.review;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "reviews")
|
||||
public class Review {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long orderId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Integer rating;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String content;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String images;
|
||||
|
||||
@Column(length = 16)
|
||||
private String status = "PENDING";
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.flower.review;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.security.AdminOnly;
|
||||
import com.flower.security.AuthContext;
|
||||
import com.flower.security.PublicEndpoint;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/reviews")
|
||||
public class ReviewController {
|
||||
private final ReviewRepository reviewRepository;
|
||||
|
||||
public ReviewController(ReviewRepository reviewRepository) {
|
||||
this.reviewRepository = reviewRepository;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<Review> create(@RequestBody Review review) {
|
||||
review.setUserId(AuthContext.get().getId());
|
||||
return ApiResponse.ok(reviewRepository.save(review));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@GetMapping("/product/{productId}")
|
||||
public ApiResponse<List<Review>> listByProduct(@PathVariable Long productId) {
|
||||
return ApiResponse.ok(reviewRepository.findByProductIdAndStatus(productId, "APPROVED"));
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@GetMapping("/admin/all")
|
||||
public ApiResponse<List<Review>> listAll() {
|
||||
return ApiResponse.ok(reviewRepository.findAll());
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PutMapping("/admin/{id}/status")
|
||||
public ApiResponse<Review> updateStatus(@PathVariable Long id, @RequestBody UpdateStatusRequest request) {
|
||||
Review review = reviewRepository.findById(id).orElseThrow(() -> new ApiException(404, "评价不存在"));
|
||||
review.setStatus(request.getStatus());
|
||||
reviewRepository.save(review);
|
||||
return ApiResponse.ok(review);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateStatusRequest {
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.review;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ReviewRepository extends JpaRepository<Review, Long> {
|
||||
List<Review> findByProductIdAndStatus(Long productId, String status);
|
||||
}
|
||||
11
backend/src/main/java/com/flower/security/AdminOnly.java
Normal file
11
backend/src/main/java/com/flower/security/AdminOnly.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.flower.security;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AdminOnly {
|
||||
}
|
||||
29
backend/src/main/java/com/flower/security/AuthContext.java
Normal file
29
backend/src/main/java/com/flower/security/AuthContext.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.flower.security;
|
||||
|
||||
import com.flower.user.User;
|
||||
|
||||
public class AuthContext {
|
||||
private static final ThreadLocal<User> HOLDER = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> TOKEN = new ThreadLocal<>();
|
||||
|
||||
public static void set(User user) {
|
||||
HOLDER.set(user);
|
||||
}
|
||||
|
||||
public static User get() {
|
||||
return HOLDER.get();
|
||||
}
|
||||
|
||||
public static void setToken(String token) {
|
||||
TOKEN.set(token);
|
||||
}
|
||||
|
||||
public static String getToken() {
|
||||
return TOKEN.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
HOLDER.remove();
|
||||
TOKEN.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.flower.security;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.user.Session;
|
||||
import com.flower.user.SessionRepository;
|
||||
import com.flower.user.User;
|
||||
import com.flower.user.UserRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
private final SessionRepository sessionRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AuthInterceptor(SessionRepository sessionRepository, UserRepository userRepository) {
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
HandlerMethod method = (HandlerMethod) handler;
|
||||
if (method.hasMethodAnnotation(PublicEndpoint.class)) {
|
||||
return true;
|
||||
}
|
||||
String token = extractToken(request);
|
||||
if (!StringUtils.hasText(token)) {
|
||||
throw new ApiException(401, "未登录");
|
||||
}
|
||||
Optional<Session> session = sessionRepository.findByTokenAndExpiredFalse(token);
|
||||
if (!session.isPresent()) {
|
||||
throw new ApiException(401, "登录已过期");
|
||||
}
|
||||
User user = userRepository.findById(session.get().getUserId())
|
||||
.orElseThrow(() -> new ApiException(404, "用户不存在"));
|
||||
if (Boolean.TRUE.equals(user.getDisabled())) {
|
||||
throw new ApiException(403, "账号已禁用");
|
||||
}
|
||||
AuthContext.set(user);
|
||||
AuthContext.setToken(token);
|
||||
if (method.hasMethodAnnotation(AdminOnly.class) || method.getBeanType().isAnnotationPresent(AdminOnly.class)) {
|
||||
if (!"ADMIN".equals(user.getRole())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
AuthContext.clear();
|
||||
}
|
||||
|
||||
private String extractToken(HttpServletRequest request) {
|
||||
String auth = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(auth) && auth.startsWith("Bearer ")) {
|
||||
return auth.substring(7);
|
||||
}
|
||||
return request.getHeader("X-Token");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.flower.security;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PublicEndpoint {
|
||||
}
|
||||
37
backend/src/main/java/com/flower/user/Address.java
Normal file
37
backend/src/main/java/com/flower/user/Address.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.flower.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "addresses")
|
||||
public class Address {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String recipientName;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String phone;
|
||||
|
||||
@Column(length = 32)
|
||||
private String province;
|
||||
|
||||
@Column(length = 32)
|
||||
private String city;
|
||||
|
||||
@Column(length = 32)
|
||||
private String district;
|
||||
|
||||
@Column(length = 255)
|
||||
private String detail;
|
||||
|
||||
private Boolean isDefault = false;
|
||||
}
|
||||
68
backend/src/main/java/com/flower/user/AddressController.java
Normal file
68
backend/src/main/java/com/flower/user/AddressController.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.flower.user;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.security.AuthContext;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/addresses")
|
||||
public class AddressController {
|
||||
private final AddressRepository addressRepository;
|
||||
|
||||
public AddressController(AddressRepository addressRepository) {
|
||||
this.addressRepository = addressRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<List<Address>> list() {
|
||||
return ApiResponse.ok(addressRepository.findByUserId(AuthContext.get().getId()));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<Address> create(@RequestBody Address address) {
|
||||
address.setUserId(AuthContext.get().getId());
|
||||
if (Boolean.TRUE.equals(address.getIsDefault())) {
|
||||
clearDefault();
|
||||
}
|
||||
return ApiResponse.ok(addressRepository.save(address));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ApiResponse<Address> update(@PathVariable Long id, @RequestBody Address address) {
|
||||
Address exists = addressRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(404, "地址不存在"));
|
||||
if (!exists.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
address.setId(id);
|
||||
address.setUserId(exists.getUserId());
|
||||
if (Boolean.TRUE.equals(address.getIsDefault())) {
|
||||
clearDefault();
|
||||
}
|
||||
return ApiResponse.ok(addressRepository.save(address));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Void> delete(@PathVariable Long id) {
|
||||
Address exists = addressRepository.findById(id)
|
||||
.orElseThrow(() -> new ApiException(404, "地址不存在"));
|
||||
if (!exists.getUserId().equals(AuthContext.get().getId())) {
|
||||
throw new ApiException(403, "无权限");
|
||||
}
|
||||
addressRepository.deleteById(id);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
private void clearDefault() {
|
||||
List<Address> addresses = addressRepository.findByUserId(AuthContext.get().getId());
|
||||
for (Address item : addresses) {
|
||||
if (Boolean.TRUE.equals(item.getIsDefault())) {
|
||||
item.setIsDefault(false);
|
||||
addressRepository.save(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.user;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AddressRepository extends JpaRepository<Address, Long> {
|
||||
List<Address> findByUserId(Long userId);
|
||||
}
|
||||
99
backend/src/main/java/com/flower/user/AuthController.java
Normal file
99
backend/src/main/java/com/flower/user/AuthController.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.flower.user;
|
||||
|
||||
import com.flower.common.ApiException;
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.common.PasswordUtil;
|
||||
import com.flower.security.AuthContext;
|
||||
import com.flower.security.PublicEndpoint;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
private final UserRepository userRepository;
|
||||
private final SessionRepository sessionRepository;
|
||||
|
||||
public AuthController(UserRepository userRepository, SessionRepository sessionRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@PostMapping("/register")
|
||||
public ApiResponse<Map<String, Object>> register(@Valid @RequestBody RegisterRequest request) {
|
||||
userRepository.findByUsername(request.getUsername()).ifPresent(u -> {
|
||||
throw new ApiException(400, "账号已存在");
|
||||
});
|
||||
User user = new User();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPasswordHash(PasswordUtil.sha256(request.getPassword()));
|
||||
user.setRole("USER");
|
||||
user.setNickname(request.getNickname());
|
||||
userRepository.save(user);
|
||||
return ApiResponse.ok(buildToken(user));
|
||||
}
|
||||
|
||||
@PublicEndpoint
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<Map<String, Object>> login(@Valid @RequestBody LoginRequest request) {
|
||||
User user = userRepository.findByUsername(request.getUsername())
|
||||
.orElseThrow(() -> new ApiException(404, "账号不存在"));
|
||||
if (!PasswordUtil.sha256(request.getPassword()).equals(user.getPasswordHash())) {
|
||||
throw new ApiException(400, "密码错误");
|
||||
}
|
||||
if (Boolean.TRUE.equals(user.getDisabled())) {
|
||||
throw new ApiException(403, "账号已禁用");
|
||||
}
|
||||
return ApiResponse.ok(buildToken(user));
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public ApiResponse<Void> logout() {
|
||||
sessionRepository.findByTokenAndExpiredFalse(AuthContext.getToken())
|
||||
.ifPresent(session -> {
|
||||
session.setExpired(true);
|
||||
sessionRepository.save(session);
|
||||
});
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<User> me() {
|
||||
return ApiResponse.ok(AuthContext.get());
|
||||
}
|
||||
|
||||
private Map<String, Object> buildToken(User user) {
|
||||
Session session = new Session();
|
||||
session.setUserId(user.getId());
|
||||
session.setToken(UUID.randomUUID().toString().replace("-", ""));
|
||||
sessionRepository.save(session);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("token", session.getToken());
|
||||
result.put("user", user);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class RegisterRequest {
|
||||
@NotBlank(message = "账号不能为空")
|
||||
private String username;
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
private String nickname;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LoginRequest {
|
||||
@NotBlank(message = "账号不能为空")
|
||||
private String username;
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
}
|
||||
}
|
||||
32
backend/src/main/java/com/flower/user/Session.java
Normal file
32
backend/src/main/java/com/flower/user/Session.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.flower.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "sessions")
|
||||
public class Session {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 64)
|
||||
private String token;
|
||||
|
||||
private Boolean expired = false;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.user;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SessionRepository extends JpaRepository<Session, Long> {
|
||||
Optional<Session> findByTokenAndExpiredFalse(String token);
|
||||
}
|
||||
47
backend/src/main/java/com/flower/user/User.java
Normal file
47
backend/src/main/java/com/flower/user/User.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.flower.user;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 32)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String passwordHash;
|
||||
|
||||
@Column(nullable = false, length = 16)
|
||||
private String role;
|
||||
|
||||
@Column(length = 32)
|
||||
private String nickname;
|
||||
|
||||
@Column(length = 32)
|
||||
private String phone;
|
||||
|
||||
@Column(length = 64)
|
||||
private String email;
|
||||
|
||||
@Column(length = 255)
|
||||
private String avatarUrl;
|
||||
|
||||
private Boolean disabled = false;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
public void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
backend/src/main/java/com/flower/user/UserController.java
Normal file
73
backend/src/main/java/com/flower/user/UserController.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package com.flower.user;
|
||||
|
||||
import com.flower.common.ApiResponse;
|
||||
import com.flower.security.AdminOnly;
|
||||
import com.flower.security.AuthContext;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<User> me() {
|
||||
return ApiResponse.ok(AuthContext.get());
|
||||
}
|
||||
|
||||
@PutMapping("/me")
|
||||
public ApiResponse<User> updateMe(@RequestBody UpdateProfileRequest request) {
|
||||
User user = AuthContext.get();
|
||||
if (request.getNickname() != null) {
|
||||
user.setNickname(request.getNickname());
|
||||
}
|
||||
if (request.getPhone() != null) {
|
||||
user.setPhone(request.getPhone());
|
||||
}
|
||||
if (request.getEmail() != null) {
|
||||
user.setEmail(request.getEmail());
|
||||
}
|
||||
if (request.getAvatarUrl() != null) {
|
||||
user.setAvatarUrl(request.getAvatarUrl());
|
||||
}
|
||||
userRepository.save(user);
|
||||
return ApiResponse.ok(user);
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@GetMapping
|
||||
public ApiResponse<List<User>> list() {
|
||||
return ApiResponse.ok(userRepository.findAll());
|
||||
}
|
||||
|
||||
@AdminOnly
|
||||
@PutMapping("/{id}/status")
|
||||
public ApiResponse<User> updateStatus(@PathVariable Long id, @RequestBody UpdateStatusRequest request) {
|
||||
User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("用户不存在"));
|
||||
user.setDisabled(request.getDisabled());
|
||||
userRepository.save(user);
|
||||
return ApiResponse.ok(user);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateProfileRequest {
|
||||
private String nickname;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String avatarUrl;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateStatusRequest {
|
||||
@NotNull
|
||||
private Boolean disabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.flower.user;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
}
|
||||
Reference in New Issue
Block a user