Initial commit
This commit is contained in:
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
yarn-debug.log
|
||||
.pnpm-debug.log
|
||||
.npm
|
||||
.yarn
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
.next/
|
||||
.nuxt/
|
||||
.cache/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
bower_components/
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# 萌贝母婴商城(Spring Boot + Vue + Arco Design + MySQL)
|
||||
|
||||
基于《崔梦雪-开题报告修改版》实现的三端基础版母婴商城,包含顾客端、商家端、管理员端核心功能。
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
- 后端: Spring Boot 3 + Spring Web + Spring Data JPA
|
||||
- 前端: Vue3 + Vite + Arco Design
|
||||
- 数据库: MySQL 8
|
||||
|
||||
## 2. 功能覆盖
|
||||
|
||||
### 顾客
|
||||
- 登录注册
|
||||
- 搜索/浏览商品
|
||||
- 购物车管理(加购、移除、结算)
|
||||
- 下单购买
|
||||
- 收藏管理
|
||||
- 评价商品
|
||||
- 订单管理(退款申请、查看物流、修改地址、删除订单)
|
||||
- 个人信息修改
|
||||
|
||||
### 商家
|
||||
- 数据概览(订单量、销售额、热销商品)
|
||||
- 商品管理(增删改查)
|
||||
- 订单管理(查看、发货、退款处理)
|
||||
- 评价查看
|
||||
- 物流查看
|
||||
- 库存记录查看/删除
|
||||
- 个人信息修改(通过 `/api/auth/me`)
|
||||
|
||||
### 管理员
|
||||
- 数据概览
|
||||
- 订单管理
|
||||
- 审核管理(商家入驻审核)
|
||||
- 用户管理
|
||||
- 轮播图管理
|
||||
- 个人信息修改(通过 `/api/auth/me`)
|
||||
|
||||
## 3. 目录结构
|
||||
|
||||
- `/Users/apple/code/bs/mying/backend` 后端工程
|
||||
- `/Users/apple/code/bs/mying/frontend` 前端工程
|
||||
- `/Users/apple/code/bs/mying/sql/init.sql` 数据库初始化脚本
|
||||
|
||||
## 4. 启动步骤
|
||||
|
||||
1. 创建并初始化数据库:
|
||||
- 执行 `/Users/apple/code/bs/mying/sql/init.sql`
|
||||
2. 修改后端数据库配置:
|
||||
- 文件: `/Users/apple/code/bs/mying/backend/src/main/resources/application.yml`
|
||||
3. 启动后端:
|
||||
```bash
|
||||
cd /Users/apple/code/bs/mying/backend
|
||||
mvn spring-boot:run
|
||||
```
|
||||
4. 启动前端:
|
||||
```bash
|
||||
cd /Users/apple/code/bs/mying/frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
5. 访问前端:
|
||||
- `http://localhost:5173`
|
||||
|
||||
## 5. 默认账号
|
||||
|
||||
- 管理员: `admin / 123456`
|
||||
- 商家: `merchant1 / 123456`
|
||||
- 顾客: `customer1 / 123456`
|
||||
|
||||
## 6. 主要接口前缀
|
||||
|
||||
- `/api/auth` 登录注册与个人资料
|
||||
- `/api/public` 商品与轮播公开接口
|
||||
- `/api/customer` 顾客接口
|
||||
- `/api/merchant` 商家接口
|
||||
- `/api/admin` 管理员接口
|
||||
|
||||
59
backend/pom.xml
Normal file
59
backend/pom.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.maternalmall</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>maternal-mall-backend</name>
|
||||
<description>Maternal mall backend</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MaternalMallApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MaternalMallApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.maternalmall.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApiResponse<T> {
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> ok(T data) {
|
||||
return new ApiResponse<>(0, "ok", data);
|
||||
}
|
||||
|
||||
public static ApiResponse<Void> ok() {
|
||||
return new ApiResponse<>(0, "ok", null);
|
||||
}
|
||||
|
||||
public static ApiResponse<Void> fail(String message) {
|
||||
return new ApiResponse<>(-1, message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.maternalmall.common;
|
||||
|
||||
public class BizException extends RuntimeException {
|
||||
public BizException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.maternalmall.common;
|
||||
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizException.class)
|
||||
public ApiResponse<Void> handleBiz(BizException ex) {
|
||||
return ApiResponse.fail(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ApiResponse<Void> handleValid(MethodArgumentNotValidException ex) {
|
||||
String msg = ex.getBindingResult().getFieldErrors().stream().findFirst()
|
||||
.map(e -> e.getField() + ":" + e.getDefaultMessage()).orElse("参数错误");
|
||||
return ApiResponse.fail(msg);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ApiResponse<Void> handleConstraint(ConstraintViolationException ex) {
|
||||
return ApiResponse.fail(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ApiResponse<Void> handleUnknown(Exception ex) {
|
||||
return ApiResponse.fail(ex.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.config;
|
||||
|
||||
import com.maternalmall.domain.User;
|
||||
|
||||
public class AuthContext {
|
||||
public static final String CURRENT_USER = "CURRENT_USER";
|
||||
|
||||
public static User getUser(jakarta.servlet.http.HttpServletRequest request) {
|
||||
return (User) request.getAttribute(CURRENT_USER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.maternalmall.config;
|
||||
|
||||
import com.maternalmall.domain.User;
|
||||
import com.maternalmall.repository.UserRepository;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Value("${app.token-header:X-Token}")
|
||||
private String tokenHeader;
|
||||
|
||||
private static final Set<String> PUBLIC_PREFIX = Set.of(
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/api/public"
|
||||
);
|
||||
|
||||
public AuthInterceptor(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
// Let browser CORS preflight pass without auth token.
|
||||
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
return true;
|
||||
}
|
||||
|
||||
String uri = request.getRequestURI();
|
||||
if (uri.equals("/") || uri.startsWith("/error") || PUBLIC_PREFIX.stream().anyMatch(uri::startsWith)) {
|
||||
return true;
|
||||
}
|
||||
String token = request.getHeader(tokenHeader);
|
||||
if (token == null || token.isBlank()) {
|
||||
response.setStatus(401);
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"请先登录\",\"data\":null}");
|
||||
return false;
|
||||
}
|
||||
User user = userRepository.findByToken(token).orElse(null);
|
||||
if (user == null || !Boolean.TRUE.equals(user.getEnabled())) {
|
||||
response.setStatus(401);
|
||||
response.getWriter().write("{\"code\":401,\"message\":\"登录状态无效\",\"data\":null}");
|
||||
return false;
|
||||
}
|
||||
request.setAttribute(AuthContext.CURRENT_USER, user);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.maternalmall.config;
|
||||
|
||||
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 WebMvcConfig implements WebMvcConfigurer {
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
public WebMvcConfig(AuthInterceptor authInterceptor) {
|
||||
this.authInterceptor = authInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*").allowedHeaders("*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.maternalmall.controller;
|
||||
|
||||
import com.maternalmall.common.ApiResponse;
|
||||
import com.maternalmall.config.AuthContext;
|
||||
import com.maternalmall.domain.*;
|
||||
import com.maternalmall.service.MallService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
public class AdminController {
|
||||
private final MallService mallService;
|
||||
|
||||
public AdminController(MallService mallService) {
|
||||
this.mallService = mallService;
|
||||
}
|
||||
|
||||
private void check(User user) {
|
||||
mallService.requireRole(user, UserRole.ADMIN);
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
public ApiResponse<Map<String, Object>> overview(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminOverview());
|
||||
}
|
||||
|
||||
@GetMapping("/orders")
|
||||
public ApiResponse<List<Orders>> orders(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminOrders());
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{id}")
|
||||
public ApiResponse<Orders> updateOrder(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminUpdateOrder(id, body.get("status"), body.getOrDefault("logisticsInfo", "")));
|
||||
}
|
||||
|
||||
@GetMapping("/orders/risk")
|
||||
public ApiResponse<List<Map<String, Object>>> riskOrders(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminRiskOrders());
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{id}/refund-audit")
|
||||
public ApiResponse<Orders> auditRefund(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
check(AuthContext.getUser(request));
|
||||
boolean approve = Boolean.parseBoolean(String.valueOf(body.getOrDefault("approve", false)));
|
||||
String remark = String.valueOf(body.getOrDefault("remark", ""));
|
||||
return ApiResponse.ok(mallService.adminAuditRefund(id, approve, remark));
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{id}/ship-audit")
|
||||
public ApiResponse<Orders> auditShipment(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
check(AuthContext.getUser(request));
|
||||
boolean approve = Boolean.parseBoolean(String.valueOf(body.getOrDefault("approve", true)));
|
||||
String remark = String.valueOf(body.getOrDefault("remark", ""));
|
||||
return ApiResponse.ok(mallService.adminAuditShipment(id, approve, remark));
|
||||
}
|
||||
|
||||
@GetMapping("/merchant-applications")
|
||||
public ApiResponse<List<Map<String, Object>>> merchantApplications(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminMerchantApplications());
|
||||
}
|
||||
|
||||
@PutMapping("/merchant-applications/{id}")
|
||||
public ApiResponse<MerchantApplication> auditMerchant(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminAuditApplication(id, body.get("status"), body.getOrDefault("remark", "")));
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public ApiResponse<List<User>> users(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminUsers());
|
||||
}
|
||||
|
||||
@PostMapping("/users")
|
||||
public ApiResponse<User> saveUser(HttpServletRequest request, @RequestBody User user) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminSaveUser(user));
|
||||
}
|
||||
|
||||
@DeleteMapping("/users/{id}")
|
||||
public ApiResponse<Void> deleteUser(HttpServletRequest request, @PathVariable Long id) {
|
||||
check(AuthContext.getUser(request));
|
||||
mallService.adminDeleteUser(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/banners")
|
||||
public ApiResponse<List<Banner>> banners(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminBanners());
|
||||
}
|
||||
|
||||
@PostMapping("/banners")
|
||||
public ApiResponse<Banner> saveBanner(HttpServletRequest request, @RequestBody Banner banner) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminSaveBanner(banner));
|
||||
}
|
||||
|
||||
@DeleteMapping("/banners/{id}")
|
||||
public ApiResponse<Void> deleteBanner(HttpServletRequest request, @PathVariable Long id) {
|
||||
check(AuthContext.getUser(request));
|
||||
mallService.adminDeleteBanner(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/products")
|
||||
public ApiResponse<List<Product>> products(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminProducts());
|
||||
}
|
||||
|
||||
@GetMapping("/products/views")
|
||||
public ApiResponse<List<Map<String, Object>>> productViews(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminProductViews());
|
||||
}
|
||||
|
||||
@PostMapping("/products")
|
||||
public ApiResponse<Product> saveProduct(HttpServletRequest request, @RequestBody Product product) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminSaveProduct(product));
|
||||
}
|
||||
|
||||
@PutMapping("/products/{id}/approve")
|
||||
public ApiResponse<Product> approveProduct(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
check(AuthContext.getUser(request));
|
||||
boolean approved = Boolean.parseBoolean(String.valueOf(body.getOrDefault("approved", true)));
|
||||
return ApiResponse.ok(mallService.adminApproveProduct(id, approved));
|
||||
}
|
||||
|
||||
@DeleteMapping("/products/{id}")
|
||||
public ApiResponse<Void> deleteProduct(HttpServletRequest request, @PathVariable Long id) {
|
||||
check(AuthContext.getUser(request));
|
||||
mallService.adminDeleteProduct(id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/reviews")
|
||||
public ApiResponse<List<Map<String, Object>>> reviews(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminReviews());
|
||||
}
|
||||
|
||||
@GetMapping("/logistics")
|
||||
public ApiResponse<List<Map<String, Object>>> logistics(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminLogistics());
|
||||
}
|
||||
|
||||
@GetMapping("/inventory")
|
||||
public ApiResponse<List<Map<String, Object>>> inventory(HttpServletRequest request) {
|
||||
check(AuthContext.getUser(request));
|
||||
return ApiResponse.ok(mallService.adminInventory());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.maternalmall.controller;
|
||||
|
||||
import com.maternalmall.common.ApiResponse;
|
||||
import com.maternalmall.config.AuthContext;
|
||||
import com.maternalmall.domain.User;
|
||||
import com.maternalmall.repository.UserRepository;
|
||||
import com.maternalmall.service.AuthService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
private final AuthService authService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AuthController(AuthService authService, UserRepository userRepository) {
|
||||
this.authService = authService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ApiResponse<Map<String, Object>> register(@RequestBody Map<String, String> body) {
|
||||
return ApiResponse.ok(authService.register(body.get("username"), body.get("password"), body.getOrDefault("role", "CUSTOMER")));
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<Map<String, Object>> login(@RequestBody Map<String, String> body) {
|
||||
return ApiResponse.ok(authService.login(body.get("username"), body.get("password")));
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<User> me(HttpServletRequest request) {
|
||||
return ApiResponse.ok(AuthContext.getUser(request));
|
||||
}
|
||||
|
||||
@PutMapping("/me")
|
||||
public ApiResponse<User> updateMe(HttpServletRequest request, @RequestBody Map<String, String> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
user.setNickname(body.getOrDefault("nickname", user.getNickname()));
|
||||
user.setPhone(body.getOrDefault("phone", user.getPhone()));
|
||||
user.setAddress(body.getOrDefault("address", user.getAddress()));
|
||||
return ApiResponse.ok(userRepository.save(user));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.maternalmall.controller;
|
||||
|
||||
import com.maternalmall.common.ApiResponse;
|
||||
import com.maternalmall.config.AuthContext;
|
||||
import com.maternalmall.domain.*;
|
||||
import com.maternalmall.service.MallService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/customer")
|
||||
public class CustomerController {
|
||||
private final MallService mallService;
|
||||
|
||||
public CustomerController(MallService mallService) {
|
||||
this.mallService = mallService;
|
||||
}
|
||||
|
||||
@GetMapping("/cart")
|
||||
public ApiResponse<List<CartItem>> cart(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.cartList(user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/cart/views")
|
||||
public ApiResponse<List<Map<String, Object>>> cartViews(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.cartViews(user.getId()));
|
||||
}
|
||||
|
||||
@PostMapping("/cart")
|
||||
public ApiResponse<CartItem> addCart(HttpServletRequest request, @RequestBody Map<String, Object> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.addCart(user.getId(), Long.valueOf(body.get("productId").toString()), Integer.valueOf(body.get("quantity").toString())));
|
||||
}
|
||||
|
||||
@DeleteMapping("/cart/{productId}")
|
||||
public ApiResponse<Void> removeCart(HttpServletRequest request, @PathVariable Long productId) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
mallService.removeCart(user.getId(), productId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/favorites")
|
||||
public ApiResponse<List<Favorite>> favorites(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.favoriteList(user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/favorites/views")
|
||||
public ApiResponse<List<Map<String, Object>>> favoriteViews(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.favoriteViews(user.getId()));
|
||||
}
|
||||
|
||||
@PostMapping("/favorites")
|
||||
public ApiResponse<Favorite> addFavorite(HttpServletRequest request, @RequestBody Map<String, Object> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.addFavorite(user.getId(), Long.valueOf(body.get("productId").toString())));
|
||||
}
|
||||
|
||||
@DeleteMapping("/favorites/{productId}")
|
||||
public ApiResponse<Void> removeFavorite(HttpServletRequest request, @PathVariable Long productId) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
mallService.removeFavorite(user.getId(), productId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/orders/checkout")
|
||||
public ApiResponse<Orders> checkout(HttpServletRequest request, @RequestBody Map<String, String> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.checkout(user.getId(), body.get("address")));
|
||||
}
|
||||
|
||||
@PostMapping("/orders/buy-now")
|
||||
public ApiResponse<Orders> buyNow(HttpServletRequest request, @RequestBody Map<String, Object> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
Long productId = Long.valueOf(body.get("productId").toString());
|
||||
Integer quantity = Integer.valueOf(String.valueOf(body.getOrDefault("quantity", 1)));
|
||||
String address = String.valueOf(body.getOrDefault("address", ""));
|
||||
return ApiResponse.ok(mallService.buyNow(user.getId(), productId, quantity, address));
|
||||
}
|
||||
|
||||
@GetMapping("/orders")
|
||||
public ApiResponse<List<Orders>> orders(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.customerOrders(user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/orders/{orderId}/items")
|
||||
public ApiResponse<List<OrderItem>> orderItems(HttpServletRequest request, @PathVariable Long orderId) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.customerOrderItems(user.getId(), orderId));
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{orderId}/address")
|
||||
public ApiResponse<Orders> updateAddress(HttpServletRequest request, @PathVariable Long orderId, @RequestBody Map<String, String> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.customerUpdateAddress(user.getId(), orderId, body.get("address")));
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{orderId}/refund")
|
||||
public ApiResponse<Orders> refund(HttpServletRequest request, @PathVariable Long orderId, @RequestBody Map<String, String> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.customerRefund(user.getId(), orderId, body.get("reason")));
|
||||
}
|
||||
|
||||
@DeleteMapping("/orders/{orderId}")
|
||||
public ApiResponse<Void> deleteOrder(HttpServletRequest request, @PathVariable Long orderId) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
mallService.customerDeleteOrder(user.getId(), orderId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/orders/{orderId}/logistics")
|
||||
public ApiResponse<List<LogisticsRecord>> logistics(HttpServletRequest request, @PathVariable Long orderId) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.logisticsOfOrder(user.getId(), orderId));
|
||||
}
|
||||
|
||||
@PostMapping("/reviews")
|
||||
public ApiResponse<Review> review(HttpServletRequest request, @RequestBody Map<String, Object> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.CUSTOMER);
|
||||
return ApiResponse.ok(mallService.addReview(
|
||||
user.getId(),
|
||||
Long.valueOf(body.get("orderId").toString()),
|
||||
Long.valueOf(body.get("productId").toString()),
|
||||
Integer.valueOf(body.get("rating").toString()),
|
||||
body.get("content").toString()));
|
||||
}
|
||||
|
||||
@PostMapping("/merchant-applications")
|
||||
public ApiResponse<MerchantApplication> applyMerchant(HttpServletRequest request, @RequestBody Map<String, String> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
return ApiResponse.ok(mallService.applyMerchant(user.getId(), body.getOrDefault("qualification", "")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.maternalmall.controller;
|
||||
|
||||
import com.maternalmall.common.ApiResponse;
|
||||
import com.maternalmall.config.AuthContext;
|
||||
import com.maternalmall.domain.*;
|
||||
import com.maternalmall.service.MallService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant")
|
||||
public class MerchantController {
|
||||
private final MallService mallService;
|
||||
|
||||
public MerchantController(MallService mallService) {
|
||||
this.mallService = mallService;
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
public ApiResponse<Map<String, Object>> overview(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantOverview(user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/products")
|
||||
public ApiResponse<List<Product>> products(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantProducts(user.getId()));
|
||||
}
|
||||
|
||||
@PostMapping("/products")
|
||||
public ApiResponse<Product> saveProduct(HttpServletRequest request, @RequestBody Product product) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantSaveProduct(user.getId(), product));
|
||||
}
|
||||
|
||||
@DeleteMapping("/products/{id}")
|
||||
public ApiResponse<Void> deleteProduct(HttpServletRequest request, @PathVariable Long id) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
mallService.merchantDeleteProduct(user.getId(), id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/orders")
|
||||
public ApiResponse<List<Orders>> orders(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantOrders(user.getId()));
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{id}/ship")
|
||||
public ApiResponse<Orders> ship(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, String> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantShip(user.getId(), id, body.getOrDefault("note", "已发货")));
|
||||
}
|
||||
|
||||
@PutMapping("/orders/{id}/refund")
|
||||
public ApiResponse<Orders> refund(HttpServletRequest request, @PathVariable Long id, @RequestBody Map<String, Object> body) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantProcessRefund(user.getId(), id, Boolean.parseBoolean(body.get("agree").toString())));
|
||||
}
|
||||
|
||||
@GetMapping("/reviews")
|
||||
public ApiResponse<List<Map<String, Object>>> reviews(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantReviews(user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/logistics")
|
||||
public ApiResponse<List<Map<String, Object>>> logistics(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantLogistics(user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/inventory")
|
||||
public ApiResponse<List<Map<String, Object>>> inventory(HttpServletRequest request) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
return ApiResponse.ok(mallService.merchantInventory(user.getId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/inventory/{id}")
|
||||
public ApiResponse<Void> deleteInventory(HttpServletRequest request, @PathVariable Long id) {
|
||||
User user = AuthContext.getUser(request);
|
||||
mallService.requireRole(user, UserRole.MERCHANT);
|
||||
mallService.merchantDeleteInventory(user.getId(), id);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.maternalmall.controller;
|
||||
|
||||
import com.maternalmall.common.ApiResponse;
|
||||
import com.maternalmall.domain.Banner;
|
||||
import com.maternalmall.domain.Product;
|
||||
import com.maternalmall.domain.Review;
|
||||
import com.maternalmall.repository.ReviewRepository;
|
||||
import com.maternalmall.service.MallService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/public")
|
||||
public class PublicController {
|
||||
private final MallService mallService;
|
||||
private final ReviewRepository reviewRepository;
|
||||
|
||||
public PublicController(MallService mallService, ReviewRepository reviewRepository) {
|
||||
this.mallService = mallService;
|
||||
this.reviewRepository = reviewRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/products")
|
||||
public ApiResponse<List<Product>> products(@RequestParam(required = false) String keyword) {
|
||||
return ApiResponse.ok(mallService.searchProducts(keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/products/{id}")
|
||||
public ApiResponse<Product> product(@PathVariable Long id) {
|
||||
return ApiResponse.ok(mallService.getProduct(id));
|
||||
}
|
||||
|
||||
@GetMapping("/products/{id}/reviews")
|
||||
public ApiResponse<List<Review>> reviews(@PathVariable Long id) {
|
||||
return ApiResponse.ok(reviewRepository.findByProductId(id));
|
||||
}
|
||||
|
||||
@GetMapping("/banners")
|
||||
public ApiResponse<List<Banner>> banners() {
|
||||
return ApiResponse.ok(mallService.banners());
|
||||
}
|
||||
}
|
||||
25
backend/src/main/java/com/maternalmall/domain/Banner.java
Normal file
25
backend/src/main/java/com/maternalmall/domain/Banner.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "banner")
|
||||
public class Banner {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
private String imageUrl;
|
||||
|
||||
@Column(length = 255)
|
||||
private String linkUrl;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer sortNo = 0;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean enabled = true;
|
||||
}
|
||||
22
backend/src/main/java/com/maternalmall/domain/CartItem.java
Normal file
22
backend/src/main/java/com/maternalmall/domain/CartItem.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "cart_item", uniqueConstraints = @UniqueConstraint(columnNames = {"customerId", "productId"}))
|
||||
public class CartItem {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long customerId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
}
|
||||
19
backend/src/main/java/com/maternalmall/domain/Favorite.java
Normal file
19
backend/src/main/java/com/maternalmall/domain/Favorite.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "favorite", uniqueConstraints = @UniqueConstraint(columnNames = {"customerId", "productId"}))
|
||||
public class Favorite {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long customerId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long productId;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "inventory_record")
|
||||
public class InventoryRecord {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long merchantId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer changeQty;
|
||||
|
||||
@Column(length = 255)
|
||||
private String note;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "logistics_record")
|
||||
public class LogisticsRecord {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long orderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long merchantId;
|
||||
|
||||
@Column(length = 255, nullable = false)
|
||||
private String status;
|
||||
|
||||
@Column(length = 255)
|
||||
private String note;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "merchant_application")
|
||||
public class MerchantApplication {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(length = 255)
|
||||
private String qualification;
|
||||
|
||||
@Column(length = 30, nullable = false)
|
||||
private String status = "PENDING";
|
||||
|
||||
@Column(length = 255)
|
||||
private String remark;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
30
backend/src/main/java/com/maternalmall/domain/OrderItem.java
Normal file
30
backend/src/main/java/com/maternalmall/domain/OrderItem.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "order_item")
|
||||
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 = 120)
|
||||
private String productName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer quantity;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal unitPrice;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
public enum OrderStatus {
|
||||
PENDING_PAYMENT,
|
||||
PAID,
|
||||
SHIPPED,
|
||||
COMPLETED,
|
||||
REFUND_REQUESTED,
|
||||
REFUNDED,
|
||||
CANCELLED
|
||||
}
|
||||
49
backend/src/main/java/com/maternalmall/domain/Orders.java
Normal file
49
backend/src/main/java/com/maternalmall/domain/Orders.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
public class Orders {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String orderNo;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long customerId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long merchantId;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 30)
|
||||
private OrderStatus status;
|
||||
|
||||
@Column(length = 255)
|
||||
private String address;
|
||||
|
||||
@Column(length = 255)
|
||||
private String logisticsInfo;
|
||||
|
||||
@Column(length = 255)
|
||||
private String refundReason;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
48
backend/src/main/java/com/maternalmall/domain/Product.java
Normal file
48
backend/src/main/java/com/maternalmall/domain/Product.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "product")
|
||||
public class Product {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 120)
|
||||
private String name;
|
||||
|
||||
@Column(length = 50)
|
||||
private String category;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal price;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer stock;
|
||||
|
||||
@Column(length = 255)
|
||||
private String imageUrl;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long merchantId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean approved = false;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
34
backend/src/main/java/com/maternalmall/domain/Review.java
Normal file
34
backend/src/main/java/com/maternalmall/domain/Review.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "review")
|
||||
public class Review {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long orderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long customerId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer rating;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String content;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
48
backend/src/main/java/com/maternalmall/domain/User.java
Normal file
48
backend/src/main/java/com/maternalmall/domain/User.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false, length = 50)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String password;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 20)
|
||||
private UserRole role;
|
||||
|
||||
@Column(length = 50)
|
||||
private String nickname;
|
||||
|
||||
@Column(length = 20)
|
||||
private String phone;
|
||||
|
||||
@Column(length = 255)
|
||||
private String address;
|
||||
|
||||
@Column(length = 100)
|
||||
private String token;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean enabled = true;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.maternalmall.domain;
|
||||
|
||||
public enum UserRole {
|
||||
CUSTOMER,
|
||||
MERCHANT,
|
||||
ADMIN
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.Banner;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BannerRepository extends JpaRepository<Banner, Long> {
|
||||
List<Banner> findByEnabledTrueOrderBySortNoAsc();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.CartItem;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CartItemRepository extends JpaRepository<CartItem, Long> {
|
||||
List<CartItem> findByCustomerId(Long customerId);
|
||||
Optional<CartItem> findByCustomerIdAndProductId(Long customerId, Long productId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
void deleteByCustomerIdAndProductId(Long customerId, Long productId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
void deleteByCustomerId(Long customerId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.Favorite;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FavoriteRepository extends JpaRepository<Favorite, Long> {
|
||||
List<Favorite> findByCustomerId(Long customerId);
|
||||
Optional<Favorite> findByCustomerIdAndProductId(Long customerId, Long productId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
void deleteByCustomerIdAndProductId(Long customerId, Long productId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.InventoryRecord;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface InventoryRecordRepository extends JpaRepository<InventoryRecord, Long> {
|
||||
List<InventoryRecord> findByMerchantId(Long merchantId);
|
||||
List<InventoryRecord> findByProductId(Long productId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.LogisticsRecord;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface LogisticsRecordRepository extends JpaRepository<LogisticsRecord, Long> {
|
||||
List<LogisticsRecord> findByOrderId(Long orderId);
|
||||
List<LogisticsRecord> findByMerchantId(Long merchantId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.MerchantApplication;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MerchantApplicationRepository extends JpaRepository<MerchantApplication, Long> {
|
||||
List<MerchantApplication> findByStatus(String status);
|
||||
boolean existsByUserIdAndStatus(Long userId, String status);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.OrderItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
|
||||
List<OrderItem> findByOrderId(Long orderId);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.OrderStatus;
|
||||
import com.maternalmall.domain.Orders;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OrdersRepository extends JpaRepository<Orders, Long> {
|
||||
List<Orders> findByCustomerId(Long customerId);
|
||||
List<Orders> findByMerchantId(Long merchantId);
|
||||
List<Orders> findByStatus(OrderStatus status);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.Product;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
List<Product> findByApprovedTrueAndNameContainingIgnoreCase(String name);
|
||||
List<Product> findByMerchantId(Long merchantId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.Review;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ReviewRepository extends JpaRepository<Review, Long> {
|
||||
List<Review> findByProductId(Long productId);
|
||||
List<Review> findByCustomerId(Long customerId);
|
||||
boolean existsByOrderIdAndProductIdAndCustomerId(Long orderId, Long productId, Long customerId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maternalmall.repository;
|
||||
|
||||
import com.maternalmall.domain.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
Optional<User> findByToken(String token);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.maternalmall.service;
|
||||
|
||||
import com.maternalmall.common.BizException;
|
||||
import com.maternalmall.domain.User;
|
||||
import com.maternalmall.domain.UserRole;
|
||||
import com.maternalmall.repository.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AuthService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public Map<String, Object> register(String username, String password, String role) {
|
||||
username = username == null ? "" : username.trim();
|
||||
password = password == null ? "" : password.trim();
|
||||
if (username.isEmpty()) {
|
||||
throw new BizException("用户名不能为空");
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
throw new BizException("密码不能为空");
|
||||
}
|
||||
userRepository.findByUsername(username).ifPresent(u -> {
|
||||
throw new BizException("账号已存在");
|
||||
});
|
||||
if (role != null && !role.isBlank() && !"CUSTOMER".equalsIgnoreCase(role)) {
|
||||
throw new BizException("仅支持注册顾客账号,商家需提交入驻申请");
|
||||
}
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(password);
|
||||
user.setRole(UserRole.CUSTOMER);
|
||||
user.setNickname(username);
|
||||
userRepository.save(user);
|
||||
return Map.of("userId", user.getId());
|
||||
}
|
||||
|
||||
public Map<String, Object> login(String username, String password) {
|
||||
User user = userRepository.findByUsername(username).orElseThrow(() -> new BizException("用户不存在"));
|
||||
if (!user.getPassword().equals(password)) {
|
||||
throw new BizException("密码错误");
|
||||
}
|
||||
if (!Boolean.TRUE.equals(user.getEnabled())) {
|
||||
throw new BizException("账号已被禁用,请联系管理员");
|
||||
}
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
user.setToken(token);
|
||||
userRepository.save(user);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("token", token);
|
||||
data.put("id", user.getId());
|
||||
data.put("username", user.getUsername());
|
||||
data.put("role", user.getRole());
|
||||
data.put("nickname", user.getNickname());
|
||||
return data;
|
||||
}
|
||||
}
|
||||
887
backend/src/main/java/com/maternalmall/service/MallService.java
Normal file
887
backend/src/main/java/com/maternalmall/service/MallService.java
Normal file
@@ -0,0 +1,887 @@
|
||||
package com.maternalmall.service;
|
||||
|
||||
import com.maternalmall.common.BizException;
|
||||
import com.maternalmall.domain.*;
|
||||
import com.maternalmall.repository.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class MallService {
|
||||
private final ProductRepository productRepository;
|
||||
private final CartItemRepository cartItemRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final OrdersRepository ordersRepository;
|
||||
private final OrderItemRepository orderItemRepository;
|
||||
private final ReviewRepository reviewRepository;
|
||||
private final LogisticsRecordRepository logisticsRecordRepository;
|
||||
private final InventoryRecordRepository inventoryRecordRepository;
|
||||
private final MerchantApplicationRepository merchantApplicationRepository;
|
||||
private final BannerRepository bannerRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public MallService(ProductRepository productRepository,
|
||||
CartItemRepository cartItemRepository,
|
||||
FavoriteRepository favoriteRepository,
|
||||
OrdersRepository ordersRepository,
|
||||
OrderItemRepository orderItemRepository,
|
||||
ReviewRepository reviewRepository,
|
||||
LogisticsRecordRepository logisticsRecordRepository,
|
||||
InventoryRecordRepository inventoryRecordRepository,
|
||||
MerchantApplicationRepository merchantApplicationRepository,
|
||||
BannerRepository bannerRepository,
|
||||
UserRepository userRepository) {
|
||||
this.productRepository = productRepository;
|
||||
this.cartItemRepository = cartItemRepository;
|
||||
this.favoriteRepository = favoriteRepository;
|
||||
this.ordersRepository = ordersRepository;
|
||||
this.orderItemRepository = orderItemRepository;
|
||||
this.reviewRepository = reviewRepository;
|
||||
this.logisticsRecordRepository = logisticsRecordRepository;
|
||||
this.inventoryRecordRepository = inventoryRecordRepository;
|
||||
this.merchantApplicationRepository = merchantApplicationRepository;
|
||||
this.bannerRepository = bannerRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public void requireRole(User user, UserRole role) {
|
||||
if (user.getRole() != role) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
}
|
||||
|
||||
public List<Product> searchProducts(String keyword) {
|
||||
return productRepository.findByApprovedTrueAndNameContainingIgnoreCase(keyword == null ? "" : keyword);
|
||||
}
|
||||
|
||||
public Product getProduct(Long id) {
|
||||
return productRepository.findById(id).orElseThrow(() -> new BizException("商品不存在"));
|
||||
}
|
||||
|
||||
public List<Banner> banners() {
|
||||
return bannerRepository.findByEnabledTrueOrderBySortNoAsc();
|
||||
}
|
||||
|
||||
public List<CartItem> cartList(Long customerId) {
|
||||
return cartItemRepository.findByCustomerId(customerId);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> cartViews(Long customerId) {
|
||||
List<CartItem> items = cartItemRepository.findByCustomerId(customerId);
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (CartItem item : items) {
|
||||
Product p = getProduct(item.getProductId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("productId", p.getId());
|
||||
row.put("productName", p.getName());
|
||||
row.put("category", p.getCategory());
|
||||
row.put("imageUrl", p.getImageUrl());
|
||||
row.put("unitPrice", p.getPrice());
|
||||
row.put("quantity", item.getQuantity());
|
||||
row.put("subtotal", p.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
|
||||
row.put("stock", p.getStock());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public CartItem addCart(Long customerId, Long productId, Integer quantity) {
|
||||
Product product = getProduct(productId);
|
||||
if (!product.getApproved()) {
|
||||
throw new BizException("商品未上架");
|
||||
}
|
||||
CartItem item = cartItemRepository.findByCustomerIdAndProductId(customerId, productId).orElseGet(CartItem::new);
|
||||
item.setCustomerId(customerId);
|
||||
item.setProductId(productId);
|
||||
item.setQuantity((item.getQuantity() == null ? 0 : item.getQuantity()) + quantity);
|
||||
return cartItemRepository.save(item);
|
||||
}
|
||||
|
||||
public void removeCart(Long customerId, Long productId) {
|
||||
cartItemRepository.deleteByCustomerIdAndProductId(customerId, productId);
|
||||
}
|
||||
|
||||
public List<Favorite> favoriteList(Long customerId) {
|
||||
return favoriteRepository.findByCustomerId(customerId);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> favoriteViews(Long customerId) {
|
||||
List<Favorite> favorites = favoriteRepository.findByCustomerId(customerId);
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (Favorite favorite : favorites) {
|
||||
Product p = getProduct(favorite.getProductId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("productId", p.getId());
|
||||
row.put("productName", p.getName());
|
||||
row.put("category", p.getCategory());
|
||||
row.put("imageUrl", p.getImageUrl());
|
||||
row.put("unitPrice", p.getPrice());
|
||||
row.put("stock", p.getStock());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public Favorite addFavorite(Long customerId, Long productId) {
|
||||
favoriteRepository.findByCustomerIdAndProductId(customerId, productId).ifPresent(f -> {
|
||||
throw new BizException("已收藏");
|
||||
});
|
||||
Favorite favorite = new Favorite();
|
||||
favorite.setCustomerId(customerId);
|
||||
favorite.setProductId(productId);
|
||||
return favoriteRepository.save(favorite);
|
||||
}
|
||||
|
||||
public void removeFavorite(Long customerId, Long productId) {
|
||||
favoriteRepository.deleteByCustomerIdAndProductId(customerId, productId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Orders checkout(Long customerId, String address) {
|
||||
List<CartItem> cartItems = cartItemRepository.findByCustomerId(customerId);
|
||||
if (cartItems.isEmpty()) {
|
||||
throw new BizException("购物车为空");
|
||||
}
|
||||
Long merchantId = null;
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
Orders order = new Orders();
|
||||
order.setOrderNo("MM" + System.currentTimeMillis());
|
||||
order.setCustomerId(customerId);
|
||||
order.setStatus(OrderStatus.PAID);
|
||||
order.setAddress(address);
|
||||
|
||||
for (CartItem cartItem : cartItems) {
|
||||
Product product = getProduct(cartItem.getProductId());
|
||||
if (product.getStock() < cartItem.getQuantity()) {
|
||||
throw new BizException("库存不足: " + product.getName());
|
||||
}
|
||||
if (merchantId == null) {
|
||||
merchantId = product.getMerchantId();
|
||||
} else if (!merchantId.equals(product.getMerchantId())) {
|
||||
throw new BizException("当前基础版本仅支持单商家结算");
|
||||
}
|
||||
product.setStock(product.getStock() - cartItem.getQuantity());
|
||||
productRepository.save(product);
|
||||
|
||||
InventoryRecord record = new InventoryRecord();
|
||||
record.setMerchantId(product.getMerchantId());
|
||||
record.setProductId(product.getId());
|
||||
record.setChangeQty(-cartItem.getQuantity());
|
||||
record.setNote("订单扣减库存");
|
||||
inventoryRecordRepository.save(record);
|
||||
|
||||
BigDecimal line = product.getPrice().multiply(BigDecimal.valueOf(cartItem.getQuantity()));
|
||||
total = total.add(line);
|
||||
}
|
||||
order.setMerchantId(merchantId);
|
||||
order.setTotalAmount(total);
|
||||
ordersRepository.save(order);
|
||||
|
||||
for (CartItem cartItem : cartItems) {
|
||||
Product product = getProduct(cartItem.getProductId());
|
||||
OrderItem item = new OrderItem();
|
||||
item.setOrderId(order.getId());
|
||||
item.setProductId(product.getId());
|
||||
item.setProductName(product.getName());
|
||||
item.setQuantity(cartItem.getQuantity());
|
||||
item.setUnitPrice(product.getPrice());
|
||||
orderItemRepository.save(item);
|
||||
}
|
||||
cartItemRepository.deleteByCustomerId(customerId);
|
||||
return order;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Orders buyNow(Long customerId, Long productId, Integer quantity, String address) {
|
||||
if (quantity == null || quantity <= 0) {
|
||||
throw new BizException("购买数量必须大于0");
|
||||
}
|
||||
Product product = getProduct(productId);
|
||||
if (!Boolean.TRUE.equals(product.getApproved())) {
|
||||
throw new BizException("商品未上架");
|
||||
}
|
||||
if (product.getStock() < quantity) {
|
||||
throw new BizException("库存不足: " + product.getName());
|
||||
}
|
||||
|
||||
product.setStock(product.getStock() - quantity);
|
||||
productRepository.save(product);
|
||||
|
||||
InventoryRecord record = new InventoryRecord();
|
||||
record.setMerchantId(product.getMerchantId());
|
||||
record.setProductId(product.getId());
|
||||
record.setChangeQty(-quantity);
|
||||
record.setNote("立即购买扣减库存");
|
||||
inventoryRecordRepository.save(record);
|
||||
|
||||
Orders order = new Orders();
|
||||
order.setOrderNo("MM" + System.currentTimeMillis());
|
||||
order.setCustomerId(customerId);
|
||||
order.setMerchantId(product.getMerchantId());
|
||||
order.setStatus(OrderStatus.PAID);
|
||||
order.setAddress(address);
|
||||
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
|
||||
ordersRepository.save(order);
|
||||
|
||||
OrderItem item = new OrderItem();
|
||||
item.setOrderId(order.getId());
|
||||
item.setProductId(product.getId());
|
||||
item.setProductName(product.getName());
|
||||
item.setQuantity(quantity);
|
||||
item.setUnitPrice(product.getPrice());
|
||||
orderItemRepository.save(item);
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
public List<Orders> customerOrders(Long customerId) {
|
||||
return ordersRepository.findByCustomerId(customerId);
|
||||
}
|
||||
|
||||
public List<OrderItem> customerOrderItems(Long customerId, Long orderId) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getCustomerId().equals(customerId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
return orderItemRepository.findByOrderId(orderId);
|
||||
}
|
||||
|
||||
public Orders customerUpdateAddress(Long customerId, Long orderId, String address) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getCustomerId().equals(customerId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
if (order.getStatus() == OrderStatus.SHIPPED || order.getStatus() == OrderStatus.COMPLETED) {
|
||||
throw new BizException("该状态不可修改地址");
|
||||
}
|
||||
order.setAddress(address);
|
||||
return ordersRepository.save(order);
|
||||
}
|
||||
|
||||
public Orders customerRefund(Long customerId, Long orderId, String reason) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getCustomerId().equals(customerId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
order.setStatus(OrderStatus.REFUND_REQUESTED);
|
||||
order.setRefundReason(reason);
|
||||
return ordersRepository.save(order);
|
||||
}
|
||||
|
||||
public void customerDeleteOrder(Long customerId, Long orderId) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getCustomerId().equals(customerId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
if (!(order.getStatus() == OrderStatus.COMPLETED || order.getStatus() == OrderStatus.REFUNDED || order.getStatus() == OrderStatus.CANCELLED)) {
|
||||
throw new BizException("当前状态不可删除");
|
||||
}
|
||||
ordersRepository.delete(order);
|
||||
}
|
||||
|
||||
public List<LogisticsRecord> logisticsOfOrder(Long customerId, Long orderId) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getCustomerId().equals(customerId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
return logisticsRecordRepository.findByOrderId(orderId);
|
||||
}
|
||||
|
||||
public Review addReview(Long customerId, Long orderId, Long productId, Integer rating, String content) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getCustomerId().equals(customerId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
if (rating == null || rating < 1 || rating > 5) {
|
||||
throw new BizException("评分范围为1-5分");
|
||||
}
|
||||
if (!(order.getStatus() == OrderStatus.PAID
|
||||
|| order.getStatus() == OrderStatus.SHIPPED
|
||||
|| order.getStatus() == OrderStatus.COMPLETED
|
||||
|| order.getStatus() == OrderStatus.REFUND_REQUESTED
|
||||
|| order.getStatus() == OrderStatus.REFUNDED)) {
|
||||
throw new BizException("仅已购买商品可评价");
|
||||
}
|
||||
boolean ordered = orderItemRepository.findByOrderId(orderId)
|
||||
.stream()
|
||||
.anyMatch(i -> i.getProductId().equals(productId));
|
||||
if (!ordered) {
|
||||
throw new BizException("该商品不在订单内");
|
||||
}
|
||||
if (reviewRepository.existsByOrderIdAndProductIdAndCustomerId(orderId, productId, customerId)) {
|
||||
throw new BizException("该商品已评价");
|
||||
}
|
||||
Review review = new Review();
|
||||
review.setOrderId(orderId);
|
||||
review.setProductId(productId);
|
||||
review.setCustomerId(customerId);
|
||||
review.setRating(rating);
|
||||
review.setContent(content);
|
||||
return reviewRepository.save(review);
|
||||
}
|
||||
|
||||
public Map<String, Object> merchantOverview(Long merchantId) {
|
||||
List<Orders> orders = ordersRepository.findByMerchantId(merchantId);
|
||||
BigDecimal sales = orders.stream()
|
||||
.filter(o -> o.getStatus() == OrderStatus.PAID || o.getStatus() == OrderStatus.SHIPPED || o.getStatus() == OrderStatus.COMPLETED)
|
||||
.map(Orders::getTotalAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
Map<Long, Integer> hotMap = new HashMap<>();
|
||||
Map<String, Long> categoryCount = new HashMap<>();
|
||||
for (Orders order : orders) {
|
||||
for (OrderItem item : orderItemRepository.findByOrderId(order.getId())) {
|
||||
hotMap.put(item.getProductId(), hotMap.getOrDefault(item.getProductId(), 0) + item.getQuantity());
|
||||
}
|
||||
}
|
||||
for (Product p : productRepository.findByMerchantId(merchantId)) {
|
||||
String category = p.getCategory() == null ? "未分类" : p.getCategory();
|
||||
categoryCount.put(category, categoryCount.getOrDefault(category, 0L) + 1);
|
||||
}
|
||||
return Map.of(
|
||||
"orderCount", orders.size(),
|
||||
"salesAmount", sales,
|
||||
"hotProducts", hotMap,
|
||||
"categoryRatio", categoryCount,
|
||||
"notifications", List.of("请及时处理退款申请", "注意库存预警")
|
||||
);
|
||||
}
|
||||
|
||||
public Product merchantSaveProduct(Long merchantId, Product body) {
|
||||
Product product = body;
|
||||
if (product.getId() != null) {
|
||||
Product old = productRepository.findById(product.getId()).orElseThrow(() -> new BizException("商品不存在"));
|
||||
if (!old.getMerchantId().equals(merchantId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
}
|
||||
product.setMerchantId(merchantId);
|
||||
if (product.getApproved() == null) {
|
||||
product.setApproved(false);
|
||||
}
|
||||
Product saved = productRepository.save(product);
|
||||
InventoryRecord record = new InventoryRecord();
|
||||
record.setMerchantId(merchantId);
|
||||
record.setProductId(saved.getId());
|
||||
record.setChangeQty(saved.getStock());
|
||||
record.setNote("商品上架或更新库存");
|
||||
inventoryRecordRepository.save(record);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public List<Product> merchantProducts(Long merchantId) {
|
||||
return productRepository.findByMerchantId(merchantId);
|
||||
}
|
||||
|
||||
public void merchantDeleteProduct(Long merchantId, Long productId) {
|
||||
Product product = getProduct(productId);
|
||||
if (!product.getMerchantId().equals(merchantId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
productRepository.delete(product);
|
||||
}
|
||||
|
||||
public List<Orders> merchantOrders(Long merchantId) {
|
||||
return ordersRepository.findByMerchantId(merchantId);
|
||||
}
|
||||
|
||||
public Orders merchantShip(Long merchantId, Long orderId, String note) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getMerchantId().equals(merchantId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
order.setStatus(OrderStatus.SHIPPED);
|
||||
order.setLogisticsInfo(note);
|
||||
ordersRepository.save(order);
|
||||
|
||||
LogisticsRecord record = new LogisticsRecord();
|
||||
record.setOrderId(orderId);
|
||||
record.setMerchantId(merchantId);
|
||||
record.setStatus("SHIPPED");
|
||||
record.setNote(note);
|
||||
logisticsRecordRepository.save(record);
|
||||
return order;
|
||||
}
|
||||
|
||||
public Orders merchantProcessRefund(Long merchantId, Long orderId, boolean agree) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (!order.getMerchantId().equals(merchantId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
if (order.getStatus() != OrderStatus.REFUND_REQUESTED) {
|
||||
throw new BizException("仅退款申请中的订单可处理");
|
||||
}
|
||||
order.setStatus(agree ? OrderStatus.REFUNDED : OrderStatus.PAID);
|
||||
return ordersRepository.save(order);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> merchantReviews(Long merchantId) {
|
||||
List<Product> products = productRepository.findByMerchantId(merchantId);
|
||||
Set<Long> productIds = new HashSet<>();
|
||||
for (Product product : products) {
|
||||
productIds.add(product.getId());
|
||||
}
|
||||
List<Review> reviews = reviewRepository.findAll();
|
||||
Map<Long, Orders> orderMap = new HashMap<>();
|
||||
for (Orders order : ordersRepository.findAll()) {
|
||||
orderMap.put(order.getId(), order);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (Review r : reviews) {
|
||||
if (!productIds.contains(r.getProductId())) continue;
|
||||
Orders order = orderMap.get(r.getOrderId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", r.getId());
|
||||
row.put("orderId", r.getOrderId());
|
||||
row.put("orderNo", order == null ? "" : order.getOrderNo());
|
||||
row.put("productId", r.getProductId());
|
||||
row.put("productName", products.stream().filter(p -> p.getId().equals(r.getProductId())).findFirst().map(Product::getName).orElse(""));
|
||||
row.put("rating", r.getRating());
|
||||
row.put("content", r.getContent());
|
||||
row.put("createdAt", r.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> merchantLogistics(Long merchantId) {
|
||||
List<LogisticsRecord> records = logisticsRecordRepository.findByMerchantId(merchantId);
|
||||
Map<Long, Orders> orderMap = new HashMap<>();
|
||||
for (Orders order : ordersRepository.findAll()) {
|
||||
orderMap.put(order.getId(), order);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (LogisticsRecord rec : records) {
|
||||
Orders order = orderMap.get(rec.getOrderId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", rec.getId());
|
||||
row.put("orderId", rec.getOrderId());
|
||||
row.put("orderNo", order == null ? "" : order.getOrderNo());
|
||||
row.put("status", rec.getStatus());
|
||||
row.put("note", rec.getNote());
|
||||
row.put("createdAt", rec.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> merchantInventory(Long merchantId) {
|
||||
List<InventoryRecord> records = inventoryRecordRepository.findByMerchantId(merchantId);
|
||||
Map<Long, Product> productMap = new HashMap<>();
|
||||
for (Product product : productRepository.findByMerchantId(merchantId)) {
|
||||
productMap.put(product.getId(), product);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (InventoryRecord rec : records) {
|
||||
Product product = productMap.get(rec.getProductId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", rec.getId());
|
||||
row.put("productId", rec.getProductId());
|
||||
row.put("productName", product == null ? "" : product.getName());
|
||||
row.put("changeQty", rec.getChangeQty());
|
||||
row.put("note", rec.getNote());
|
||||
row.put("createdAt", rec.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public void merchantDeleteInventory(Long merchantId, Long id) {
|
||||
InventoryRecord rec = inventoryRecordRepository.findById(id).orElseThrow(() -> new BizException("记录不存在"));
|
||||
if (!rec.getMerchantId().equals(merchantId)) {
|
||||
throw new BizException("无权限操作");
|
||||
}
|
||||
inventoryRecordRepository.delete(rec);
|
||||
}
|
||||
|
||||
public MerchantApplication applyMerchant(Long userId, String qualification) {
|
||||
User user = userRepository.findById(userId).orElseThrow(() -> new BizException("用户不存在"));
|
||||
if (user.getRole() != UserRole.CUSTOMER) {
|
||||
throw new BizException("仅顾客账号可提交入驻申请");
|
||||
}
|
||||
if (merchantApplicationRepository.existsByUserIdAndStatus(userId, "PENDING")) {
|
||||
throw new BizException("已有待审核申请,请勿重复提交");
|
||||
}
|
||||
MerchantApplication m = new MerchantApplication();
|
||||
m.setUserId(userId);
|
||||
m.setQualification(qualification);
|
||||
m.setStatus("PENDING");
|
||||
return merchantApplicationRepository.save(m);
|
||||
}
|
||||
|
||||
public Map<String, Object> adminOverview() {
|
||||
List<Orders> orders = ordersRepository.findAll();
|
||||
BigDecimal sales = orders.stream().map(Orders::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
Map<String, Long> categoryCount = new HashMap<>();
|
||||
Map<Long, Integer> hotMap = new HashMap<>();
|
||||
for (Product p : productRepository.findAll()) {
|
||||
String category = p.getCategory() == null ? "未分类" : p.getCategory();
|
||||
categoryCount.put(category, categoryCount.getOrDefault(category, 0L) + 1);
|
||||
}
|
||||
for (Orders order : orders) {
|
||||
for (OrderItem item : orderItemRepository.findByOrderId(order.getId())) {
|
||||
hotMap.put(item.getProductId(), hotMap.getOrDefault(item.getProductId(), 0) + item.getQuantity());
|
||||
}
|
||||
}
|
||||
return Map.of(
|
||||
"orderCount", orders.size(),
|
||||
"salesAmount", sales,
|
||||
"categoryRatio", categoryCount,
|
||||
"hotProducts", hotMap
|
||||
);
|
||||
}
|
||||
|
||||
public List<Orders> adminOrders() {
|
||||
return ordersRepository.findAll();
|
||||
}
|
||||
|
||||
public Orders adminUpdateOrder(Long orderId, String status, String logisticsInfo) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
try {
|
||||
order.setStatus(OrderStatus.valueOf(status));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BizException("订单状态不合法");
|
||||
}
|
||||
order.setLogisticsInfo(logisticsInfo);
|
||||
return ordersRepository.save(order);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> adminRiskOrders() {
|
||||
List<Orders> orders = ordersRepository.findAll();
|
||||
List<Map<String, Object>> risks = new ArrayList<>();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (Orders order : orders) {
|
||||
if (order.getTotalAmount().compareTo(new BigDecimal("5000")) >= 0) {
|
||||
risks.add(buildRisk(order, "HIGH_AMOUNT", "订单金额较高,请人工核查"));
|
||||
}
|
||||
if (order.getStatus() == OrderStatus.PAID && order.getCreatedAt() != null && order.getCreatedAt().isBefore(now.minusDays(2))) {
|
||||
risks.add(buildRisk(order, "SHIP_DELAY", "已支付超过48小时未发货"));
|
||||
}
|
||||
if (order.getStatus() == OrderStatus.SHIPPED && logisticsRecordRepository.findByOrderId(order.getId()).isEmpty()) {
|
||||
risks.add(buildRisk(order, "NO_LOGISTICS", "订单标记已发货但无物流轨迹"));
|
||||
}
|
||||
if (order.getStatus() == OrderStatus.REFUND_REQUESTED) {
|
||||
risks.add(buildRisk(order, "REFUND_PENDING", "退款申请待审核"));
|
||||
}
|
||||
}
|
||||
return risks;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildRisk(Orders order, String type, String reason) {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("orderId", order.getId());
|
||||
row.put("orderNo", order.getOrderNo());
|
||||
row.put("status", order.getStatus());
|
||||
row.put("totalAmount", order.getTotalAmount());
|
||||
row.put("customerId", order.getCustomerId());
|
||||
row.put("merchantId", order.getMerchantId());
|
||||
row.put("riskType", type);
|
||||
row.put("riskReason", reason);
|
||||
return row;
|
||||
}
|
||||
|
||||
public Orders adminAuditRefund(Long orderId, boolean approve, String remark) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (order.getStatus() != OrderStatus.REFUND_REQUESTED) {
|
||||
throw new BizException("当前订单不在退款审核状态");
|
||||
}
|
||||
order.setStatus(approve ? OrderStatus.REFUNDED : OrderStatus.PAID);
|
||||
if (remark != null && !remark.trim().isEmpty()) {
|
||||
String old = order.getRefundReason() == null ? "" : order.getRefundReason();
|
||||
String merged = old.isBlank() ? remark.trim() : old + ";管理员备注:" + remark.trim();
|
||||
order.setRefundReason(merged);
|
||||
}
|
||||
Orders saved = ordersRepository.save(order);
|
||||
|
||||
LogisticsRecord record = new LogisticsRecord();
|
||||
record.setOrderId(orderId);
|
||||
record.setMerchantId(order.getMerchantId());
|
||||
record.setStatus(approve ? "REFUND_APPROVED" : "REFUND_REJECTED");
|
||||
record.setNote((approve ? "管理员通过退款审核" : "管理员驳回退款申请") + (remark == null || remark.trim().isEmpty() ? "" : (":" + remark.trim())));
|
||||
logisticsRecordRepository.save(record);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Orders adminAuditShipment(Long orderId, boolean approved, String remark) {
|
||||
Orders order = ordersRepository.findById(orderId).orElseThrow(() -> new BizException("订单不存在"));
|
||||
if (order.getStatus() != OrderStatus.SHIPPED && order.getStatus() != OrderStatus.PAID) {
|
||||
throw new BizException("仅已支付或已发货订单可审核发货");
|
||||
}
|
||||
if (approved && order.getStatus() == OrderStatus.PAID) {
|
||||
order.setStatus(OrderStatus.SHIPPED);
|
||||
}
|
||||
if (!approved && order.getStatus() == OrderStatus.SHIPPED) {
|
||||
order.setStatus(OrderStatus.PAID);
|
||||
}
|
||||
if (remark != null && !remark.trim().isEmpty()) {
|
||||
order.setLogisticsInfo(remark.trim());
|
||||
}
|
||||
Orders saved = ordersRepository.save(order);
|
||||
|
||||
LogisticsRecord record = new LogisticsRecord();
|
||||
record.setOrderId(orderId);
|
||||
record.setMerchantId(order.getMerchantId());
|
||||
record.setStatus(approved ? "SHIP_AUDIT_PASS" : "SHIP_AUDIT_REJECT");
|
||||
record.setNote((approved ? "管理员发货审核通过" : "管理员发货审核驳回") + (remark == null || remark.trim().isEmpty() ? "" : (":" + remark.trim())));
|
||||
logisticsRecordRepository.save(record);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> adminMerchantApplications() {
|
||||
List<MerchantApplication> apps = merchantApplicationRepository.findAll();
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
Map<Long, User> userMap = new HashMap<>();
|
||||
for (User user : userRepository.findAll()) {
|
||||
userMap.put(user.getId(), user);
|
||||
}
|
||||
for (MerchantApplication app : apps) {
|
||||
User applicant = userMap.get(app.getUserId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", app.getId());
|
||||
row.put("userId", app.getUserId());
|
||||
row.put("applicantUsername", applicant == null ? "" : applicant.getUsername());
|
||||
row.put("applicantNickname", applicant == null ? "" : applicant.getNickname());
|
||||
row.put("qualification", app.getQualification());
|
||||
row.put("status", app.getStatus());
|
||||
row.put("remark", app.getRemark());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public MerchantApplication adminAuditApplication(Long id, String status, String remark) {
|
||||
MerchantApplication app = merchantApplicationRepository.findById(id).orElseThrow(() -> new BizException("申请不存在"));
|
||||
app.setStatus(status);
|
||||
app.setRemark(remark);
|
||||
merchantApplicationRepository.save(app);
|
||||
|
||||
if ("APPROVED".equals(status)) {
|
||||
User user = userRepository.findById(app.getUserId()).orElseThrow(() -> new BizException("用户不存在"));
|
||||
user.setRole(UserRole.MERCHANT);
|
||||
userRepository.save(user);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
public List<User> adminUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
public User adminSaveUser(User user) {
|
||||
String username = user.getUsername() == null ? "" : user.getUsername().trim();
|
||||
String password = user.getPassword() == null ? "" : user.getPassword().trim();
|
||||
if (username.isEmpty()) {
|
||||
throw new BizException("用户名不能为空");
|
||||
}
|
||||
if (password.isEmpty()) {
|
||||
throw new BizException("密码不能为空");
|
||||
}
|
||||
|
||||
if (user.getId() == null) {
|
||||
userRepository.findByUsername(username).ifPresent(u -> {
|
||||
throw new BizException("账号已存在");
|
||||
});
|
||||
} else {
|
||||
User existing = userRepository.findById(user.getId()).orElseThrow(() -> new BizException("用户不存在"));
|
||||
userRepository.findByUsername(username).ifPresent(u -> {
|
||||
if (!u.getId().equals(existing.getId())) {
|
||||
throw new BizException("账号已存在");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
user.setUsername(username);
|
||||
user.setPassword(password);
|
||||
if (user.getRole() == null) {
|
||||
user.setRole(UserRole.CUSTOMER);
|
||||
}
|
||||
if (user.getNickname() == null || user.getNickname().trim().isEmpty()) {
|
||||
user.setNickname(username);
|
||||
}
|
||||
if (user.getEnabled() == null) {
|
||||
user.setEnabled(true);
|
||||
}
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public void adminDeleteUser(Long id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public List<Banner> adminBanners() {
|
||||
return bannerRepository.findAll();
|
||||
}
|
||||
|
||||
public Banner adminSaveBanner(Banner banner) {
|
||||
return bannerRepository.save(banner);
|
||||
}
|
||||
|
||||
public void adminDeleteBanner(Long id) {
|
||||
bannerRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public List<Product> adminProducts() {
|
||||
return productRepository.findAll();
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> adminProductViews() {
|
||||
List<Product> products = productRepository.findAll();
|
||||
Map<Long, User> userMap = new HashMap<>();
|
||||
for (User user : userRepository.findAll()) {
|
||||
userMap.put(user.getId(), user);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (Product p : products) {
|
||||
User merchant = userMap.get(p.getMerchantId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", p.getId());
|
||||
row.put("name", p.getName());
|
||||
row.put("category", p.getCategory());
|
||||
row.put("description", p.getDescription());
|
||||
row.put("price", p.getPrice());
|
||||
row.put("stock", p.getStock());
|
||||
row.put("imageUrl", p.getImageUrl());
|
||||
row.put("merchantId", p.getMerchantId());
|
||||
row.put("merchantUsername", merchant == null ? "" : merchant.getUsername());
|
||||
row.put("merchantName", merchant == null ? "" : merchant.getNickname());
|
||||
row.put("approved", p.getApproved());
|
||||
row.put("createdAt", p.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public Product adminSaveProduct(Product input) {
|
||||
if (input.getName() == null || input.getName().trim().isEmpty()) {
|
||||
throw new BizException("商品名称不能为空");
|
||||
}
|
||||
if (input.getMerchantId() == null) {
|
||||
throw new BizException("商家ID不能为空");
|
||||
}
|
||||
if (input.getPrice() == null || input.getStock() == null) {
|
||||
throw new BizException("商品价格或库存不能为空");
|
||||
}
|
||||
Product product = input;
|
||||
if (input.getId() != null) {
|
||||
product = productRepository.findById(input.getId()).orElseThrow(() -> new BizException("商品不存在"));
|
||||
product.setName(input.getName());
|
||||
product.setCategory(input.getCategory());
|
||||
product.setDescription(input.getDescription());
|
||||
product.setPrice(input.getPrice());
|
||||
product.setStock(input.getStock());
|
||||
product.setImageUrl(input.getImageUrl());
|
||||
product.setMerchantId(input.getMerchantId());
|
||||
product.setApproved(Boolean.TRUE.equals(input.getApproved()));
|
||||
} else {
|
||||
if (product.getApproved() == null) {
|
||||
product.setApproved(false);
|
||||
}
|
||||
}
|
||||
return productRepository.save(product);
|
||||
}
|
||||
|
||||
public Product adminApproveProduct(Long productId, boolean approved) {
|
||||
Product product = productRepository.findById(productId).orElseThrow(() -> new BizException("商品不存在"));
|
||||
product.setApproved(approved);
|
||||
return productRepository.save(product);
|
||||
}
|
||||
|
||||
public void adminDeleteProduct(Long id) {
|
||||
productRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> adminReviews() {
|
||||
List<Review> reviews = reviewRepository.findAll();
|
||||
Map<Long, User> userMap = new HashMap<>();
|
||||
for (User user : userRepository.findAll()) {
|
||||
userMap.put(user.getId(), user);
|
||||
}
|
||||
Map<Long, Orders> orderMap = new HashMap<>();
|
||||
for (Orders order : ordersRepository.findAll()) {
|
||||
orderMap.put(order.getId(), order);
|
||||
}
|
||||
Map<Long, Product> productMap = new HashMap<>();
|
||||
for (Product product : productRepository.findAll()) {
|
||||
productMap.put(product.getId(), product);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (Review r : reviews) {
|
||||
User customer = userMap.get(r.getCustomerId());
|
||||
Orders order = orderMap.get(r.getOrderId());
|
||||
Product product = productMap.get(r.getProductId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", r.getId());
|
||||
row.put("orderId", r.getOrderId());
|
||||
row.put("orderNo", order == null ? "" : order.getOrderNo());
|
||||
row.put("productId", r.getProductId());
|
||||
row.put("productName", product == null ? "" : product.getName());
|
||||
row.put("customerId", r.getCustomerId());
|
||||
row.put("customerUsername", customer == null ? "" : customer.getUsername());
|
||||
row.put("rating", r.getRating());
|
||||
row.put("content", r.getContent());
|
||||
row.put("createdAt", r.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> adminLogistics() {
|
||||
List<LogisticsRecord> records = logisticsRecordRepository.findAll();
|
||||
Map<Long, User> userMap = new HashMap<>();
|
||||
for (User user : userRepository.findAll()) {
|
||||
userMap.put(user.getId(), user);
|
||||
}
|
||||
Map<Long, Orders> orderMap = new HashMap<>();
|
||||
for (Orders order : ordersRepository.findAll()) {
|
||||
orderMap.put(order.getId(), order);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (LogisticsRecord rec : records) {
|
||||
User merchant = userMap.get(rec.getMerchantId());
|
||||
Orders order = orderMap.get(rec.getOrderId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", rec.getId());
|
||||
row.put("orderId", rec.getOrderId());
|
||||
row.put("orderNo", order == null ? "" : order.getOrderNo());
|
||||
row.put("merchantId", rec.getMerchantId());
|
||||
row.put("merchantUsername", merchant == null ? "" : merchant.getUsername());
|
||||
row.put("status", rec.getStatus());
|
||||
row.put("note", rec.getNote());
|
||||
row.put("createdAt", rec.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> adminInventory() {
|
||||
List<InventoryRecord> records = inventoryRecordRepository.findAll();
|
||||
Map<Long, User> userMap = new HashMap<>();
|
||||
for (User user : userRepository.findAll()) {
|
||||
userMap.put(user.getId(), user);
|
||||
}
|
||||
Map<Long, Product> productMap = new HashMap<>();
|
||||
for (Product product : productRepository.findAll()) {
|
||||
productMap.put(product.getId(), product);
|
||||
}
|
||||
List<Map<String, Object>> views = new ArrayList<>();
|
||||
for (InventoryRecord rec : records) {
|
||||
User merchant = userMap.get(rec.getMerchantId());
|
||||
Product product = productMap.get(rec.getProductId());
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("id", rec.getId());
|
||||
row.put("productId", rec.getProductId());
|
||||
row.put("productName", product == null ? "" : product.getName());
|
||||
row.put("merchantId", rec.getMerchantId());
|
||||
row.put("merchantUsername", merchant == null ? "" : merchant.getUsername());
|
||||
row.put("changeQty", rec.getChangeQty());
|
||||
row.put("note", rec.getNote());
|
||||
row.put("createdAt", rec.getCreatedAt());
|
||||
views.add(row);
|
||||
}
|
||||
return views;
|
||||
}
|
||||
}
|
||||
20
backend/src/main/resources/application.yml
Normal file
20
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/maternal_mall?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8
|
||||
username: root
|
||||
password: qq5211314
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
jackson:
|
||||
time-zone: Asia/Shanghai
|
||||
|
||||
app:
|
||||
token-header: X-Token
|
||||
20
backend/target/classes/application.yml
Normal file
20
backend/target/classes/application.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/maternal_mall?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8
|
||||
username: root
|
||||
password: qq5211314
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: false
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
jackson:
|
||||
time-zone: Asia/Shanghai
|
||||
|
||||
app:
|
||||
token-header: X-Token
|
||||
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/common/ApiResponse.class
Normal file
BIN
backend/target/classes/com/maternalmall/common/ApiResponse.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/config/AuthContext.class
Normal file
BIN
backend/target/classes/com/maternalmall/config/AuthContext.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/Banner.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/Banner.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/CartItem.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/CartItem.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/Favorite.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/Favorite.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/OrderItem.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/OrderItem.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/OrderStatus.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/OrderStatus.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/Orders.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/Orders.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/Product.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/Product.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/Review.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/Review.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/User.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/User.class
Normal file
Binary file not shown.
BIN
backend/target/classes/com/maternalmall/domain/UserRole.class
Normal file
BIN
backend/target/classes/com/maternalmall/domain/UserRole.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>萌贝母婴商城</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1684
frontend/package-lock.json
generated
Normal file
1684
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "maternal-mall-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.57.0",
|
||||
"axios": "^1.7.9",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.3.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
3
frontend/src/App.vue
Normal file
3
frontend/src/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
24
frontend/src/api/http.js
Normal file
24
frontend/src/api/http.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: 'http://localhost:8080',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers['X-Token'] = token
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
http.interceptors.response.use((resp) => {
|
||||
const data = resp.data
|
||||
if (data.code !== 0) {
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
return data.data
|
||||
})
|
||||
|
||||
export default http
|
||||
65
frontend/src/api/index.js
Normal file
65
frontend/src/api/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import http from './http'
|
||||
|
||||
export const api = {
|
||||
register: (payload) => http.post('/api/auth/register', payload),
|
||||
login: (payload) => http.post('/api/auth/login', payload),
|
||||
me: () => http.get('/api/auth/me'),
|
||||
updateMe: (payload) => http.put('/api/auth/me', payload),
|
||||
|
||||
banners: () => http.get('/api/public/banners'),
|
||||
products: (keyword = '') => http.get('/api/public/products', { params: { keyword } }),
|
||||
|
||||
customerCart: () => http.get('/api/customer/cart'),
|
||||
customerCartViews: () => http.get('/api/customer/cart/views'),
|
||||
addCart: (payload) => http.post('/api/customer/cart', payload),
|
||||
delCart: (productId) => http.delete(`/api/customer/cart/${productId}`),
|
||||
checkout: (payload) => http.post('/api/customer/orders/checkout', payload),
|
||||
customerBuyNow: (payload) => http.post('/api/customer/orders/buy-now', payload),
|
||||
customerOrders: () => http.get('/api/customer/orders'),
|
||||
refundOrder: (id, payload) => http.put(`/api/customer/orders/${id}/refund`, payload),
|
||||
updateOrderAddress: (id, payload) => http.put(`/api/customer/orders/${id}/address`, payload),
|
||||
deleteOrder: (id) => http.delete(`/api/customer/orders/${id}`),
|
||||
orderLogistics: (id) => http.get(`/api/customer/orders/${id}/logistics`),
|
||||
customerFavorites: () => http.get('/api/customer/favorites'),
|
||||
customerFavoriteViews: () => http.get('/api/customer/favorites/views'),
|
||||
addFavorite: (payload) => http.post('/api/customer/favorites', payload),
|
||||
deleteFavorite: (productId) => http.delete(`/api/customer/favorites/${productId}`),
|
||||
addReview: (payload) => http.post('/api/customer/reviews', payload),
|
||||
orderItems: (orderId) => http.get(`/api/customer/orders/${orderId}/items`),
|
||||
applyMerchant: (payload) => http.post('/api/customer/merchant-applications', payload),
|
||||
|
||||
merchantOverview: () => http.get('/api/merchant/overview'),
|
||||
merchantProducts: () => http.get('/api/merchant/products'),
|
||||
saveMerchantProduct: (payload) => http.post('/api/merchant/products', payload),
|
||||
deleteMerchantProduct: (id) => http.delete(`/api/merchant/products/${id}`),
|
||||
merchantOrders: () => http.get('/api/merchant/orders'),
|
||||
shipOrder: (id, payload) => http.put(`/api/merchant/orders/${id}/ship`, payload),
|
||||
merchantRefund: (id, payload) => http.put(`/api/merchant/orders/${id}/refund`, payload),
|
||||
merchantReviews: () => http.get('/api/merchant/reviews'),
|
||||
merchantLogistics: () => http.get('/api/merchant/logistics'),
|
||||
merchantInventory: () => http.get('/api/merchant/inventory'),
|
||||
deleteMerchantInventory: (id) => http.delete(`/api/merchant/inventory/${id}`),
|
||||
|
||||
adminOverview: () => http.get('/api/admin/overview'),
|
||||
adminUsers: () => http.get('/api/admin/users'),
|
||||
adminSaveUser: (payload) => http.post('/api/admin/users', payload),
|
||||
adminDeleteUser: (id) => http.delete(`/api/admin/users/${id}`),
|
||||
adminOrders: () => http.get('/api/admin/orders'),
|
||||
adminUpdateOrder: (id, payload) => http.put(`/api/admin/orders/${id}`, payload),
|
||||
adminOrderRisks: () => http.get('/api/admin/orders/risk'),
|
||||
adminAuditRefund: (id, payload) => http.put(`/api/admin/orders/${id}/refund-audit`, payload),
|
||||
adminAuditShipment: (id, payload) => http.put(`/api/admin/orders/${id}/ship-audit`, payload),
|
||||
adminMerchantApplications: () => http.get('/api/admin/merchant-applications'),
|
||||
adminAuditMerchantApplication: (id, payload) => http.put(`/api/admin/merchant-applications/${id}`, payload),
|
||||
adminBanners: () => http.get('/api/admin/banners'),
|
||||
adminSaveBanner: (payload) => http.post('/api/admin/banners', payload),
|
||||
adminDeleteBanner: (id) => http.delete(`/api/admin/banners/${id}`),
|
||||
adminProducts: () => http.get('/api/admin/products'),
|
||||
adminProductViews: () => http.get('/api/admin/products/views'),
|
||||
adminSaveProduct: (payload) => http.post('/api/admin/products', payload),
|
||||
adminApproveProduct: (id, payload) => http.put(`/api/admin/products/${id}/approve`, payload),
|
||||
adminDeleteProduct: (id) => http.delete(`/api/admin/products/${id}`),
|
||||
adminReviews: () => http.get('/api/admin/reviews'),
|
||||
adminLogistics: () => http.get('/api/admin/logistics'),
|
||||
adminInventory: () => http.get('/api/admin/inventory')
|
||||
}
|
||||
9
frontend/src/main.js
Normal file
9
frontend/src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ArcoVue from '@arco-design/web-vue'
|
||||
import '@arco-design/web-vue/dist/arco.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).use(createPinia()).use(router).use(ArcoVue).mount('#app')
|
||||
29
frontend/src/router/index.js
Normal file
29
frontend/src/router/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import CartView from '../views/CartView.vue'
|
||||
import OrdersView from '../views/OrdersView.vue'
|
||||
import FavoritesView from '../views/FavoritesView.vue'
|
||||
import ProfileView from '../views/ProfileView.vue'
|
||||
import AdminView from '../views/AdminView.vue'
|
||||
import MerchantView from '../views/MerchantView.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', redirect: '/products' },
|
||||
{ path: '/products', component: HomeView },
|
||||
{ path: '/cart', component: CartView },
|
||||
{ path: '/orders', component: OrdersView },
|
||||
{ path: '/favorites', component: FavoritesView },
|
||||
{ path: '/profile', component: ProfileView },
|
||||
{ path: '/login', component: LoginView },
|
||||
{ path: '/admin', component: AdminView },
|
||||
{ path: '/merchant', component: MerchantView },
|
||||
{ path: '/customer', redirect: '/products' }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
21
frontend/src/stores/user.js
Normal file
21
frontend/src/stores/user.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { api } from '../api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
profile: null
|
||||
}),
|
||||
getters: {
|
||||
role: (s) => s.profile?.role,
|
||||
loggedIn: (s) => !!s.profile
|
||||
},
|
||||
actions: {
|
||||
async fetchMe() {
|
||||
this.profile = await api.me()
|
||||
},
|
||||
async logout() {
|
||||
localStorage.removeItem('token')
|
||||
this.profile = null
|
||||
}
|
||||
}
|
||||
})
|
||||
13
frontend/src/style.css
Normal file
13
frontend/src/style.css
Normal file
@@ -0,0 +1,13 @@
|
||||
:root {
|
||||
--bg-main: linear-gradient(135deg, #fff8ed 0%, #f2f7ff 100%);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
background: var(--bg-main);
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
644
frontend/src/views/AdminView.vue
Normal file
644
frontend/src/views/AdminView.vue
Normal file
@@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<a-layout style="min-height: 100vh">
|
||||
<a-layout-sider :width="230" theme="dark" collapsible>
|
||||
<div style="height: 56px; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:600;">超级管理员后台</div>
|
||||
<a-menu :selected-keys="[active]" @menu-item-click="onMenuClick">
|
||||
<a-menu-item key="overview">数据概览</a-menu-item>
|
||||
<a-menu-item key="orders">订单管理</a-menu-item>
|
||||
<a-menu-item key="risk">风控审核</a-menu-item>
|
||||
<a-menu-item key="products">商品管理</a-menu-item>
|
||||
<a-menu-item key="audit">审核管理</a-menu-item>
|
||||
<a-menu-item key="users">用户管理</a-menu-item>
|
||||
<a-menu-item key="banners">轮播图设置</a-menu-item>
|
||||
<a-menu-item key="reviews">评价管理</a-menu-item>
|
||||
<a-menu-item key="logistics">物流总览</a-menu-item>
|
||||
<a-menu-item key="inventory">库存总览</a-menu-item>
|
||||
<a-menu-item key="profile">个人信息</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout>
|
||||
<a-layout-header style="background:#fff; display:flex; justify-content:space-between; align-items:center; padding:0 16px;">
|
||||
<strong>{{ titleMap[active] }}</strong>
|
||||
<a-space>
|
||||
<span>{{ userStore.profile?.nickname }} (ADMIN)</span>
|
||||
<a-button @click="goHome">前台首页</a-button>
|
||||
<a-button status="danger" @click="logout">退出</a-button>
|
||||
</a-space>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content style="padding:16px;">
|
||||
<a-card v-if="active==='overview'" title="平台概览">
|
||||
<a-space style="margin-bottom: 16px">
|
||||
<a-tag color="arcoblue" size="large">订单总量: {{ overview.orderCount || 0 }}</a-tag>
|
||||
<a-tag color="green" size="large">销售额: ¥{{ (overview.salesAmount || 0).toLocaleString() }}</a-tag>
|
||||
<a-button @click="loadOverview">刷新</a-button>
|
||||
</a-space>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-card title="品类占比" size="small">
|
||||
<div ref="categoryChartRef" style="height: 280px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="热销排行 Top10" size="small">
|
||||
<div ref="hotChartRef" style="height: 280px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider />
|
||||
<a-card title="通知公告" size="small">
|
||||
<a-timeline v-if="overview.notifications && overview.notifications.length">
|
||||
<a-timeline-item v-for="(note, idx) in overview.notifications" :key="idx" :color="idx === 0 ? 'red' : 'blue'">
|
||||
{{ note }}
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-empty v-else description="暂无公告" />
|
||||
</a-card>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='orders'" title="订单管理">
|
||||
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="openOrderModal(record)">修改</a-button>
|
||||
<a-button size="mini" v-if="record.status === 'REFUND_REQUESTED'" @click="auditRefund(record, true)">通过退款</a-button>
|
||||
<a-button size="mini" status="danger" v-if="record.status === 'REFUND_REQUESTED'" @click="auditRefund(record, false)">驳回退款</a-button>
|
||||
<a-button size="mini" v-if="record.status === 'SHIPPED' || record.status === 'PAID'" @click="auditShipment(record, true)">发货审核通过</a-button>
|
||||
<a-button size="mini" status="danger" v-if="record.status === 'SHIPPED'" @click="auditShipment(record, false)">发货审核驳回</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='risk'" title="订单风控与异常管控">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-button @click="loadRisks">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="riskColumns" :data="risks" :pagination="{ pageSize: 8 }" />
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='products'" title="商品管理">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-button type="primary" @click="openProductModal()">新增商品</a-button>
|
||||
<a-button @click="loadProducts">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }">
|
||||
<template #approved="{ record }">
|
||||
<a-tag :color="record.approved ? 'green' : 'orange'">{{ record.approved ? '已通过' : '待审核' }}</a-tag>
|
||||
</template>
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="openProductModal(record)">编辑</a-button>
|
||||
<a-button size="mini" @click="toggleApprove(record)">{{ record.approved ? '下架' : '通过' }}</a-button>
|
||||
<a-button size="mini" status="danger" @click="deleteProduct(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='audit'" title="商家审核管理">
|
||||
<a-table :columns="applyColumns" :data="applications" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-button size="mini" type="primary" @click="openAuditModal(record)">审核</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='users'" title="用户管理">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-button type="primary" @click="openUserModal()">新增用户</a-button>
|
||||
<a-button @click="loadUsers">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="userColumns" :data="users" :pagination="{ pageSize: 8 }">
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
|
||||
</template>
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="openUserModal(record)">编辑</a-button>
|
||||
<a-button size="mini" status="danger" @click="deleteUser(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='banners'" title="轮播图设置">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-button type="primary" @click="openBannerModal()">新增轮播图</a-button>
|
||||
<a-button @click="loadBanners">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="bannerColumns" :data="banners" :pagination="false">
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">{{ record.enabled ? '启用' : '禁用' }}</a-tag>
|
||||
</template>
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="openBannerModal(record)">编辑</a-button>
|
||||
<a-button size="mini" status="danger" @click="deleteBanner(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='reviews'" title="评价管理">
|
||||
<a-table :columns="reviewColumns" :data="reviews" :pagination="{ pageSize: 8 }" />
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='logistics'" title="物流总览">
|
||||
<a-table :columns="logisticsColumns" :data="logistics" :pagination="{ pageSize: 8 }" />
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='inventory'" title="库存总览">
|
||||
<a-table :columns="inventoryColumns" :data="inventory" :pagination="{ pageSize: 8 }" />
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='profile'" title="个人信息">
|
||||
<a-form :model="profile" layout="vertical" style="max-width: 480px">
|
||||
<a-form-item label="账号"><a-input v-model="profile.username" disabled /></a-form-item>
|
||||
<a-form-item label="角色"><a-input v-model="profile.role" disabled /></a-form-item>
|
||||
<a-form-item label="昵称"><a-input v-model="profile.nickname" /></a-form-item>
|
||||
<a-form-item label="手机号"><a-input v-model="profile.phone" /></a-form-item>
|
||||
<a-form-item label="地址"><a-input v-model="profile.address" /></a-form-item>
|
||||
<a-button type="primary" @click="saveProfile">保存修改</a-button>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<a-modal v-model:visible="orderModalVisible" title="修改订单" @ok="submitOrderModal">
|
||||
<a-form :model="orderForm" layout="vertical">
|
||||
<a-form-item label="订单状态">
|
||||
<a-select v-model="orderForm.status">
|
||||
<a-option value="PENDING_PAYMENT">PENDING_PAYMENT</a-option>
|
||||
<a-option value="PAID">PAID</a-option>
|
||||
<a-option value="SHIPPED">SHIPPED</a-option>
|
||||
<a-option value="COMPLETED">COMPLETED</a-option>
|
||||
<a-option value="REFUND_REQUESTED">REFUND_REQUESTED</a-option>
|
||||
<a-option value="REFUNDED">REFUNDED</a-option>
|
||||
<a-option value="CANCELLED">CANCELLED</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="物流信息">
|
||||
<a-input v-model="orderForm.logisticsInfo" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="auditModalVisible" title="审核商家" @ok="submitAuditModal">
|
||||
<a-form :model="auditForm" layout="vertical">
|
||||
<a-form-item label="审核结果">
|
||||
<a-select v-model="auditForm.status">
|
||||
<a-option value="APPROVED">通过</a-option>
|
||||
<a-option value="REJECTED">拒绝</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-input v-model="auditForm.remark" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="userModalVisible" :title="userForm.id ? '编辑用户' : '新增用户'" @ok="submitUserModal">
|
||||
<a-form :model="userForm" layout="vertical">
|
||||
<a-form-item label="账号"><a-input v-model="userForm.username" /></a-form-item>
|
||||
<a-form-item label="密码"><a-input v-model="userForm.password" /></a-form-item>
|
||||
<a-form-item label="角色">
|
||||
<a-select v-model="userForm.role">
|
||||
<a-option value="CUSTOMER">CUSTOMER</a-option>
|
||||
<a-option value="MERCHANT">MERCHANT</a-option>
|
||||
<a-option value="ADMIN">ADMIN</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="昵称"><a-input v-model="userForm.nickname" /></a-form-item>
|
||||
<a-form-item label="是否启用"><a-switch v-model="userForm.enabled" /></a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="bannerModalVisible" :title="bannerForm.id ? '编辑轮播图' : '新增轮播图'" @ok="submitBannerModal">
|
||||
<a-form :model="bannerForm" layout="vertical">
|
||||
<a-form-item label="图片URL"><a-input v-model="bannerForm.imageUrl" /></a-form-item>
|
||||
<a-form-item label="跳转URL"><a-input v-model="bannerForm.linkUrl" /></a-form-item>
|
||||
<a-form-item label="排序"><a-input-number v-model="bannerForm.sortNo" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="启用">
|
||||
<a-switch v-model="bannerForm.enabled" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="productModalVisible" :title="productForm.id ? '编辑商品' : '新增商品'" @ok="submitProductModal">
|
||||
<a-form :model="productForm" layout="vertical">
|
||||
<a-form-item label="商品名称"><a-input v-model="productForm.name" /></a-form-item>
|
||||
<a-form-item label="分类"><a-input v-model="productForm.category" /></a-form-item>
|
||||
<a-form-item label="描述"><a-input v-model="productForm.description" /></a-form-item>
|
||||
<a-form-item label="价格"><a-input-number v-model="productForm.price" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="库存"><a-input-number v-model="productForm.stock" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="图片URL"><a-input v-model="productForm.imageUrl" /></a-form-item>
|
||||
<a-form-item label="所属商家">
|
||||
<a-select v-model="productForm.merchantId" placeholder="请选择商家">
|
||||
<a-option v-for="m in merchantOptions" :key="m.value" :value="m.value">{{ m.label }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核通过"><a-switch v-model="productForm.approved" /></a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch, nextTick, onUnmounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const active = ref('overview')
|
||||
|
||||
const categoryChartRef = ref(null)
|
||||
const hotChartRef = ref(null)
|
||||
let categoryChart = null
|
||||
let hotChart = null
|
||||
|
||||
const titleMap = {
|
||||
overview: '数据概览', orders: '订单管理', risk: '风控审核', products: '商品管理', audit: '审核管理',
|
||||
users: '用户管理', banners: '轮播图设置', reviews: '评价管理', logistics: '物流总览', inventory: '库存总览', profile: '个人信息'
|
||||
}
|
||||
|
||||
const overview = reactive({})
|
||||
const orders = ref([])
|
||||
const risks = ref([])
|
||||
const products = ref([])
|
||||
const applications = ref([])
|
||||
const users = ref([])
|
||||
const merchantOptions = ref([])
|
||||
const banners = ref([])
|
||||
const reviews = ref([])
|
||||
const logistics = ref([])
|
||||
const inventory = ref([])
|
||||
const profile = reactive({ username: '', role: '', nickname: '', phone: '', address: '' })
|
||||
|
||||
const orderModalVisible = ref(false)
|
||||
const auditModalVisible = ref(false)
|
||||
const userModalVisible = ref(false)
|
||||
const bannerModalVisible = ref(false)
|
||||
const productModalVisible = ref(false)
|
||||
|
||||
const orderForm = reactive({ id: null, status: 'PAID', logisticsInfo: '' })
|
||||
const auditForm = reactive({ id: null, status: 'APPROVED', remark: '' })
|
||||
const userForm = reactive({ id: null, username: '', password: '123456', role: 'CUSTOMER', nickname: '', enabled: true })
|
||||
const bannerForm = reactive({ id: null, imageUrl: '', linkUrl: '', sortNo: 1, enabled: true })
|
||||
const productForm = reactive({ id: null, name: '', category: '', description: '', price: 0, stock: 0, imageUrl: '', merchantId: null, approved: false })
|
||||
|
||||
const orderColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '金额', dataIndex: 'totalAmount' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '物流', dataIndex: 'logisticsInfo' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
const productColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '商品名', dataIndex: 'name' },
|
||||
{ title: '分类', dataIndex: 'category' },
|
||||
{ title: '价格', dataIndex: 'price' },
|
||||
{ title: '库存', dataIndex: 'stock' },
|
||||
{ title: '所属商家', dataIndex: 'merchantName' },
|
||||
{ title: '商家账号', dataIndex: 'merchantUsername' },
|
||||
{ title: '审核', slotName: 'approved' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
const applyColumns = [
|
||||
{ title: '申请ID', dataIndex: 'id' },
|
||||
{ title: '申请人账号', dataIndex: 'applicantUsername' },
|
||||
{ title: '申请人昵称', dataIndex: 'applicantNickname' },
|
||||
{ title: '资质', dataIndex: 'qualification' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
const userColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '账号', dataIndex: 'username' },
|
||||
{ title: '角色', dataIndex: 'role' },
|
||||
{ title: '昵称', dataIndex: 'nickname' },
|
||||
{ title: '状态', slotName: 'enabled' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
const riskColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '金额', dataIndex: 'totalAmount' },
|
||||
{ title: '风险类型', dataIndex: 'riskType' },
|
||||
{ title: '风险说明', dataIndex: 'riskReason' }
|
||||
]
|
||||
const bannerColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '图片', dataIndex: 'imageUrl' },
|
||||
{ title: '链接', dataIndex: 'linkUrl' },
|
||||
{ title: '排序', dataIndex: 'sortNo' },
|
||||
{ title: '状态', slotName: 'enabled' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
const reviewColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '顾客账号', dataIndex: 'customerUsername' },
|
||||
{ title: '评分', dataIndex: 'rating' },
|
||||
{ title: '内容', dataIndex: 'content' },
|
||||
{ title: '时间', dataIndex: 'createdAt' }
|
||||
]
|
||||
const logisticsColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '商家账号', dataIndex: 'merchantUsername' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '备注', dataIndex: 'note' },
|
||||
{ title: '时间', dataIndex: 'createdAt' }
|
||||
]
|
||||
const inventoryColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '商家账号', dataIndex: 'merchantUsername' },
|
||||
{ title: '变动', dataIndex: 'changeQty' },
|
||||
{ title: '备注', dataIndex: 'note' },
|
||||
{ title: '时间', dataIndex: 'createdAt' }
|
||||
]
|
||||
|
||||
const onMenuClick = (key) => { active.value = key }
|
||||
const goHome = () => router.push('/')
|
||||
const logout = async () => { await userStore.logout(); router.replace('/login') }
|
||||
|
||||
const loadOverview = async () => {
|
||||
Object.assign(overview, await api.adminOverview())
|
||||
await nextTick()
|
||||
initCharts()
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (!overview.categoryRatio) overview.categoryRatio = {}
|
||||
if (!overview.hotProducts) overview.hotProducts = {}
|
||||
|
||||
const categoryData = Object.entries(overview.categoryRatio).map(([name, value]) => ({ name, value }))
|
||||
if (categoryChart) {
|
||||
categoryChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left' },
|
||||
series: [{ type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, data: categoryData }]
|
||||
})
|
||||
} else if (categoryChartRef.value) {
|
||||
categoryChart = echarts.init(categoryChartRef.value)
|
||||
categoryChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left' },
|
||||
series: [{ type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, data: categoryData }]
|
||||
})
|
||||
}
|
||||
|
||||
const hotEntries = Object.entries(overview.hotProducts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
const hotData = hotEntries.map(([name, value]) => ({ name: name.length > 10 ? name.substring(0, 10) + '...' : name, value }))
|
||||
if (hotChart) {
|
||||
hotChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
|
||||
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
|
||||
})
|
||||
} else if (hotChartRef.value) {
|
||||
hotChart = echarts.init(hotChartRef.value)
|
||||
hotChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
|
||||
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
categoryChart?.resize()
|
||||
hotChart?.resize()
|
||||
}
|
||||
|
||||
watch(active, async (v) => {
|
||||
if (v === 'overview') {
|
||||
await loadOverview()
|
||||
await nextTick()
|
||||
initCharts()
|
||||
}
|
||||
if (v === 'orders') return loadOrders()
|
||||
if (v === 'risk') return loadRisks()
|
||||
if (v === 'products') return loadProducts()
|
||||
if (v === 'audit') return loadApplications()
|
||||
if (v === 'users') return loadUsers()
|
||||
if (v === 'banners') return loadBanners()
|
||||
if (v === 'reviews') return loadReviews()
|
||||
if (v === 'logistics') return loadLogistics()
|
||||
if (v === 'inventory') return loadInventory()
|
||||
if (v === 'profile') return loadProfile()
|
||||
})
|
||||
|
||||
const loadOrders = async () => (orders.value = await api.adminOrders())
|
||||
const loadRisks = async () => (risks.value = await api.adminOrderRisks())
|
||||
const loadProducts = async () => (products.value = await api.adminProductViews())
|
||||
const loadApplications = async () => (applications.value = await api.adminMerchantApplications())
|
||||
const loadUsers = async () => (users.value = await api.adminUsers())
|
||||
const loadBanners = async () => (banners.value = await api.adminBanners())
|
||||
const loadReviews = async () => (reviews.value = await api.adminReviews())
|
||||
const loadLogistics = async () => (logistics.value = await api.adminLogistics())
|
||||
const loadInventory = async () => (inventory.value = await api.adminInventory())
|
||||
const loadProfile = async () => {
|
||||
const me = await api.me()
|
||||
profile.username = me.username
|
||||
profile.role = me.role
|
||||
profile.nickname = me.nickname || ''
|
||||
profile.phone = me.phone || ''
|
||||
profile.address = me.address || ''
|
||||
}
|
||||
|
||||
const openOrderModal = (row) => {
|
||||
orderForm.id = row.id
|
||||
orderForm.status = row.status
|
||||
orderForm.logisticsInfo = row.logisticsInfo || ''
|
||||
orderModalVisible.value = true
|
||||
}
|
||||
const submitOrderModal = async () => {
|
||||
await api.adminUpdateOrder(orderForm.id, { status: orderForm.status, logisticsInfo: orderForm.logisticsInfo })
|
||||
Message.success('订单已更新')
|
||||
orderModalVisible.value = false
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const openAuditModal = (row) => {
|
||||
auditForm.id = row.id
|
||||
auditForm.status = row.status === 'APPROVED' ? 'APPROVED' : 'REJECTED'
|
||||
auditForm.remark = row.remark || ''
|
||||
auditModalVisible.value = true
|
||||
}
|
||||
const submitAuditModal = async () => {
|
||||
await api.adminAuditMerchantApplication(auditForm.id, { status: auditForm.status, remark: auditForm.remark })
|
||||
Message.success('审核完成')
|
||||
auditModalVisible.value = false
|
||||
await loadApplications()
|
||||
}
|
||||
|
||||
const openUserModal = (row = null) => {
|
||||
if (row) {
|
||||
userForm.id = row.id
|
||||
userForm.username = row.username
|
||||
userForm.password = row.password || '123456'
|
||||
userForm.role = row.role
|
||||
userForm.nickname = row.nickname || ''
|
||||
userForm.enabled = row.enabled !== false
|
||||
} else {
|
||||
userForm.id = null
|
||||
userForm.username = ''
|
||||
userForm.password = '123456'
|
||||
userForm.role = 'CUSTOMER'
|
||||
userForm.nickname = ''
|
||||
userForm.enabled = true
|
||||
}
|
||||
userModalVisible.value = true
|
||||
}
|
||||
const submitUserModal = async () => {
|
||||
const username = (userForm.username || '').trim()
|
||||
const password = (userForm.password || '').trim()
|
||||
if (!username) return Message.warning('请输入账号')
|
||||
if (!password) return Message.warning('请输入密码')
|
||||
await api.adminSaveUser({ ...userForm, username, password })
|
||||
Message.success(userForm.id ? '用户已更新' : '用户已新增')
|
||||
userModalVisible.value = false
|
||||
await loadUsers()
|
||||
}
|
||||
const deleteUser = async (id) => {
|
||||
await api.adminDeleteUser(id)
|
||||
Message.success('用户已删除')
|
||||
await loadUsers()
|
||||
}
|
||||
|
||||
const auditRefund = async (row, approve) => {
|
||||
const remark = window.prompt(approve ? '请输入退款通过备注(可为空)' : '请输入驳回原因(可为空)', '') || ''
|
||||
await api.adminAuditRefund(row.id, { approve, remark })
|
||||
Message.success(approve ? '退款审核已通过' : '退款审核已驳回')
|
||||
await Promise.all([loadOrders(), loadRisks()])
|
||||
}
|
||||
|
||||
const auditShipment = async (row, approve) => {
|
||||
const remark = window.prompt(approve ? '请输入发货审核备注(可为空)' : '请输入驳回原因(可为空)', '') || ''
|
||||
await api.adminAuditShipment(row.id, { approve, remark })
|
||||
Message.success(approve ? '发货审核已通过' : '发货审核已驳回')
|
||||
await Promise.all([loadOrders(), loadRisks()])
|
||||
}
|
||||
|
||||
const openBannerModal = (row = null) => {
|
||||
if (row) {
|
||||
bannerForm.id = row.id
|
||||
bannerForm.imageUrl = row.imageUrl || ''
|
||||
bannerForm.linkUrl = row.linkUrl || ''
|
||||
bannerForm.sortNo = row.sortNo || 1
|
||||
bannerForm.enabled = !!row.enabled
|
||||
} else {
|
||||
bannerForm.id = null
|
||||
bannerForm.imageUrl = ''
|
||||
bannerForm.linkUrl = ''
|
||||
bannerForm.sortNo = 1
|
||||
bannerForm.enabled = true
|
||||
}
|
||||
bannerModalVisible.value = true
|
||||
}
|
||||
const submitBannerModal = async () => {
|
||||
await api.adminSaveBanner({ ...bannerForm })
|
||||
Message.success(bannerForm.id ? '轮播图已更新' : '轮播图已新增')
|
||||
bannerModalVisible.value = false
|
||||
await loadBanners()
|
||||
}
|
||||
const deleteBanner = async (id) => {
|
||||
await api.adminDeleteBanner(id)
|
||||
Message.success('轮播图已删除')
|
||||
await loadBanners()
|
||||
}
|
||||
|
||||
const openProductModal = (row = null) => {
|
||||
if (row) {
|
||||
productForm.id = row.id
|
||||
productForm.name = row.name || ''
|
||||
productForm.category = row.category || ''
|
||||
productForm.description = row.description || ''
|
||||
productForm.price = row.price || 0
|
||||
productForm.stock = row.stock || 0
|
||||
productForm.imageUrl = row.imageUrl || ''
|
||||
productForm.merchantId = row.merchantId || null
|
||||
productForm.approved = !!row.approved
|
||||
} else {
|
||||
productForm.id = null
|
||||
productForm.name = ''
|
||||
productForm.category = ''
|
||||
productForm.description = ''
|
||||
productForm.price = 0
|
||||
productForm.stock = 0
|
||||
productForm.imageUrl = ''
|
||||
productForm.merchantId = null
|
||||
productForm.approved = false
|
||||
}
|
||||
if (merchantOptions.value.length === 0) {
|
||||
loadMerchantOptions()
|
||||
}
|
||||
productModalVisible.value = true
|
||||
}
|
||||
const submitProductModal = async () => {
|
||||
if (!productForm.merchantId) return Message.warning('请选择所属商家')
|
||||
await api.adminSaveProduct({ ...productForm })
|
||||
Message.success(productForm.id ? '商品已更新' : '商品已新增')
|
||||
productModalVisible.value = false
|
||||
await loadProducts()
|
||||
}
|
||||
const toggleApprove = async (row) => {
|
||||
await api.adminApproveProduct(row.id, { approved: !row.approved })
|
||||
Message.success(!row.approved ? '已通过审核' : '已下架')
|
||||
await loadProducts()
|
||||
}
|
||||
const deleteProduct = async (id) => {
|
||||
await api.adminDeleteProduct(id)
|
||||
Message.success('商品已删除')
|
||||
await loadProducts()
|
||||
}
|
||||
|
||||
const saveProfile = async () => {
|
||||
await api.updateMe({ nickname: profile.nickname, phone: profile.phone, address: profile.address })
|
||||
Message.success('已保存')
|
||||
}
|
||||
|
||||
const loadMerchantOptions = async () => {
|
||||
const list = await api.adminUsers()
|
||||
merchantOptions.value = list
|
||||
.filter((u) => u.role === 'MERCHANT')
|
||||
.map((u) => ({ label: `${u.nickname || u.username} (${u.username})`, value: u.id }))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'ADMIN') {
|
||||
Message.warning('请使用管理员账号访问')
|
||||
return router.replace('/login')
|
||||
}
|
||||
await loadOverview()
|
||||
await loadProfile()
|
||||
await loadMerchantOptions()
|
||||
window.addEventListener('resize', handleResize)
|
||||
} catch (e) {
|
||||
Message.error(e.message)
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
categoryChart?.dispose()
|
||||
hotChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
86
frontend/src/views/CartView.vue
Normal file
86
frontend/src/views/CartView.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
|
||||
<h2>萌贝母婴商城 - 购物车</h2>
|
||||
<a-space>
|
||||
<a-button @click="goProducts">继续购物</a-button>
|
||||
<a-button @click="goOrders">我的订单</a-button>
|
||||
<a-button @click="goFavorites">我的收藏</a-button>
|
||||
<a-button @click="goProfile">个人信息</a-button>
|
||||
<a-button @click="logout">退出</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
|
||||
<a-card title="购物车结算">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-input v-model="address" style="width: 320px" placeholder="收货地址" />
|
||||
<a-button type="primary" @click="checkout">结算购物车</a-button>
|
||||
<a-button @click="loadCart">刷新</a-button>
|
||||
</a-space>
|
||||
|
||||
<a-table :columns="cartColumns" :data="cart" :pagination="false">
|
||||
<template #actions="{ record }">
|
||||
<a-button size="mini" status="danger" @click="removeCart(record.productId)">移除</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const cart = ref([])
|
||||
const address = ref(localStorage.getItem('customer_address') || '辽宁省大连市高新区')
|
||||
|
||||
const cartColumns = [
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '单价', dataIndex: 'unitPrice' },
|
||||
{ title: '数量', dataIndex: 'quantity' },
|
||||
{ title: '小计', dataIndex: 'subtotal' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const goProducts = () => router.push('/products')
|
||||
const goOrders = () => router.push('/orders')
|
||||
const goFavorites = () => router.push('/favorites')
|
||||
const goProfile = () => router.push('/profile')
|
||||
const logout = async () => {
|
||||
await userStore.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
|
||||
const loadCart = async () => {
|
||||
cart.value = await api.customerCartViews()
|
||||
}
|
||||
|
||||
const removeCart = async (productId) => {
|
||||
await api.delCart(productId)
|
||||
Message.success('已移除')
|
||||
await loadCart()
|
||||
}
|
||||
|
||||
const checkout = async () => {
|
||||
if (!address.value) return Message.warning('请填写收货地址')
|
||||
localStorage.setItem('customer_address', address.value)
|
||||
await api.checkout({ address: address.value })
|
||||
Message.success('结算成功')
|
||||
router.push('/orders')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
|
||||
await loadCart()
|
||||
} catch {
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
173
frontend/src/views/CustomerView.vue
Normal file
173
frontend/src/views/CustomerView.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<a-layout style="min-height: 100vh">
|
||||
<a-layout-sider :width="220" theme="dark" collapsible>
|
||||
<div style="height: 56px; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:600;">顾客中心</div>
|
||||
<a-menu :selected-keys="[active]" @menu-item-click="(k)=>active=k">
|
||||
<a-menu-item key="products">商品购买</a-menu-item>
|
||||
<a-menu-item key="cart">购物车</a-menu-item>
|
||||
<a-menu-item key="orders">订单管理</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-layout>
|
||||
<a-layout-header style="background:#fff; display:flex; justify-content:space-between; align-items:center; padding:0 16px;">
|
||||
<strong>{{ titleMap[active] }}</strong>
|
||||
<a-space>
|
||||
<a-input v-model="address" style="width: 280px" placeholder="默认收货地址" />
|
||||
<a-button @click="goHome">去商城</a-button>
|
||||
<a-button status="danger" @click="logout">退出</a-button>
|
||||
</a-space>
|
||||
</a-layout-header>
|
||||
<a-layout-content style="padding:16px;">
|
||||
<a-card v-if="active==='products'" title="商品购买">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-input-search v-model="keyword" placeholder="搜索商品" search-button @search="loadProducts" />
|
||||
<a-button @click="loadProducts">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="addCart(record.id)">加入购物车</a-button>
|
||||
<a-button size="mini" type="primary" @click="buyNow(record.id)">立即购买</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='cart'" title="我的购物车">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-button type="primary" @click="checkout">结算购物车</a-button>
|
||||
<a-button @click="loadCart">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="cartColumns" :data="cart" :pagination="false">
|
||||
<template #actions="{ record }">
|
||||
<a-button size="mini" status="danger" @click="delCart(record.productId)">移除</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='orders'" title="我的订单">
|
||||
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="refund(record.id)">退款</a-button>
|
||||
<a-button size="mini" @click="delOrder(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const active = ref('products')
|
||||
const titleMap = { products: '商品购买', cart: '购物车管理', orders: '订单管理' }
|
||||
|
||||
const address = ref('辽宁省大连市高新区')
|
||||
const keyword = ref('')
|
||||
const products = ref([])
|
||||
const cart = ref([])
|
||||
const orders = ref([])
|
||||
|
||||
const productColumns = [
|
||||
{ title: '名称', dataIndex: 'name' },
|
||||
{ title: '分类', dataIndex: 'category' },
|
||||
{ title: '价格', dataIndex: 'price' },
|
||||
{ title: '库存', dataIndex: 'stock' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const cartColumns = [
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '分类', dataIndex: 'category' },
|
||||
{ title: '单价', dataIndex: 'unitPrice' },
|
||||
{ title: '数量', dataIndex: 'quantity' },
|
||||
{ title: '小计', dataIndex: 'subtotal' },
|
||||
{ title: '库存', dataIndex: 'stock' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const orderColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '金额', dataIndex: 'totalAmount' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const goHome = () => router.push('/')
|
||||
const logout = async () => {
|
||||
await userStore.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
|
||||
const loadProducts = async () => {
|
||||
products.value = await api.products(keyword.value)
|
||||
}
|
||||
const loadCart = async () => {
|
||||
cart.value = await api.customerCartViews()
|
||||
}
|
||||
const loadOrders = async () => {
|
||||
orders.value = await api.customerOrders()
|
||||
}
|
||||
|
||||
const addCart = async (productId) => {
|
||||
await api.addCart({ productId, quantity: 1 })
|
||||
Message.success('已加入购物车')
|
||||
await loadCart()
|
||||
}
|
||||
|
||||
const delCart = async (productId) => {
|
||||
await api.delCart(productId)
|
||||
Message.success('已移除')
|
||||
await loadCart()
|
||||
}
|
||||
|
||||
const buyNow = async (productId) => {
|
||||
if (!address.value) return Message.warning('请先填写收货地址')
|
||||
await api.customerBuyNow({ productId, quantity: 1, address: address.value })
|
||||
Message.success('购买成功')
|
||||
await loadOrders()
|
||||
active.value = 'orders'
|
||||
}
|
||||
|
||||
const checkout = async () => {
|
||||
if (!address.value) return Message.warning('请先填写收货地址')
|
||||
await api.checkout({ address: address.value })
|
||||
Message.success('结算成功')
|
||||
await Promise.all([loadCart(), loadOrders()])
|
||||
active.value = 'orders'
|
||||
}
|
||||
|
||||
const refund = async (id) => {
|
||||
await api.refundOrder(id, { reason: '不想要了' })
|
||||
Message.success('退款申请已提交')
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const delOrder = async (id) => {
|
||||
await api.deleteOrder(id)
|
||||
Message.success('订单已删除')
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
watch(active, async (v) => {
|
||||
if (v === 'products') await loadProducts()
|
||||
if (v === 'cart') await loadCart()
|
||||
if (v === 'orders') await loadOrders()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
|
||||
await Promise.all([loadProducts(), loadCart(), loadOrders()])
|
||||
})
|
||||
</script>
|
||||
86
frontend/src/views/FavoritesView.vue
Normal file
86
frontend/src/views/FavoritesView.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
|
||||
<h2>萌贝母婴商城 - 我的收藏</h2>
|
||||
<a-space>
|
||||
<a-button @click="goProducts">继续购物</a-button>
|
||||
<a-button @click="goCart">购物车</a-button>
|
||||
<a-button @click="goOrders">我的订单</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
|
||||
<a-card title="收藏列表">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-input v-model="address" style="width: 320px" placeholder="收货地址" />
|
||||
<a-button @click="loadFavorites">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="columns" :data="favorites" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="addCart(record.productId)">加入购物车</a-button>
|
||||
<a-button size="mini" type="primary" @click="buyNow(record.productId)">立即购买</a-button>
|
||||
<a-button size="mini" status="danger" @click="removeFavorite(record.productId)">取消收藏</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const favorites = ref([])
|
||||
const address = ref(localStorage.getItem('customer_address') || '辽宁省大连市高新区')
|
||||
|
||||
const columns = [
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '分类', dataIndex: 'category' },
|
||||
{ title: '单价', dataIndex: 'unitPrice' },
|
||||
{ title: '库存', dataIndex: 'stock' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const goProducts = () => router.push('/products')
|
||||
const goCart = () => router.push('/cart')
|
||||
const goOrders = () => router.push('/orders')
|
||||
|
||||
const loadFavorites = async () => {
|
||||
favorites.value = await api.customerFavoriteViews()
|
||||
}
|
||||
|
||||
const removeFavorite = async (productId) => {
|
||||
await api.deleteFavorite(productId)
|
||||
Message.success('已取消收藏')
|
||||
await loadFavorites()
|
||||
}
|
||||
|
||||
const addCart = async (productId) => {
|
||||
await api.addCart({ productId, quantity: 1 })
|
||||
Message.success('已加入购物车')
|
||||
}
|
||||
|
||||
const buyNow = async (productId) => {
|
||||
if (!address.value) return Message.warning('请先填写收货地址')
|
||||
localStorage.setItem('customer_address', address.value)
|
||||
await api.customerBuyNow({ productId, quantity: 1, address: address.value })
|
||||
Message.success('购买成功')
|
||||
router.push('/orders')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
|
||||
await loadFavorites()
|
||||
} catch {
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
132
frontend/src/views/HomeView.vue
Normal file
132
frontend/src/views/HomeView.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
|
||||
<h2>萌贝母婴商城</h2>
|
||||
<a-space>
|
||||
<a-button v-if="!userStore.loggedIn" type="primary" @click="goLogin">登录</a-button>
|
||||
<template v-else>
|
||||
<span>{{ userStore.profile?.nickname }} ({{ userStore.role }})</span>
|
||||
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goCart">购物车</a-button>
|
||||
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goOrders">我的订单</a-button>
|
||||
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goFavorites">我的收藏</a-button>
|
||||
<a-button v-if="userStore.role === 'CUSTOMER'" @click="goProfile">个人信息</a-button>
|
||||
<a-button v-if="userStore.role === 'ADMIN' || userStore.role === 'MERCHANT'" type="primary" @click="goConsole">进入工作台</a-button>
|
||||
<a-button @click="logout">退出</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</a-space>
|
||||
|
||||
<a-carousel auto-play style="height: 220px; margin-bottom: 16px">
|
||||
<a-carousel-item v-for="b in banners" :key="b.id">
|
||||
<img :src="b.imageUrl" style="width: 100%; height: 220px; object-fit: cover" />
|
||||
</a-carousel-item>
|
||||
</a-carousel>
|
||||
|
||||
<a-input-search v-model="keyword" placeholder="搜索商品" search-button @search="loadProducts" />
|
||||
<a-grid :cols="{ xs: 1, md: 3 }" :col-gap="12" :row-gap="12" style="margin-top: 16px">
|
||||
<a-grid-item v-for="p in products" :key="p.id">
|
||||
<a-card>
|
||||
<template #title>{{ p.name }}</template>
|
||||
<div>分类:{{ p.category || '未分类' }}</div>
|
||||
<div>价格:¥{{ p.price }}</div>
|
||||
<div>库存:{{ p.stock }}</div>
|
||||
<div style="margin-top: 8px">
|
||||
<a-space>
|
||||
<a-button size="small" @click="addCart(p.id)" :disabled="userStore.role !== 'CUSTOMER'">加入购物车</a-button>
|
||||
<a-button size="small" type="primary" @click="buyNow(p.id)" :disabled="userStore.role !== 'CUSTOMER'">立即购买</a-button>
|
||||
<a-button size="small" @click="addFavorite(p.id)" :disabled="userStore.role !== 'CUSTOMER'">收藏</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
|
||||
<a-card v-if="userStore.role === 'CUSTOMER'" title="前台购买操作" style="margin-top: 16px">
|
||||
<a-space>
|
||||
<a-input v-model="address" style="width: 320px" placeholder="收货地址" />
|
||||
<span>你可以在此页立即购买,也可以去购物车结算。</span>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const keyword = ref('')
|
||||
const products = ref([])
|
||||
const banners = ref([])
|
||||
const address = ref('辽宁省大连市高新区')
|
||||
|
||||
const goLogin = () => router.push('/login')
|
||||
const goCart = () => router.push('/cart')
|
||||
const goOrders = () => router.push('/orders')
|
||||
const goFavorites = () => router.push('/favorites')
|
||||
const goProfile = () => router.push('/profile')
|
||||
const goConsole = () => {
|
||||
if (userStore.role === 'ADMIN') return router.push('/admin')
|
||||
if (userStore.role === 'MERCHANT') return router.push('/merchant')
|
||||
return router.push('/products')
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
await userStore.logout()
|
||||
Message.success('已退出')
|
||||
}
|
||||
|
||||
const loadProducts = async () => {
|
||||
products.value = await api.products(keyword.value)
|
||||
}
|
||||
|
||||
const loadBanners = async () => {
|
||||
banners.value = await api.banners()
|
||||
if (banners.value.length === 0) {
|
||||
banners.value = [{ id: 0, imageUrl: 'https://picsum.photos/1200/220?baby=1' }]
|
||||
}
|
||||
}
|
||||
|
||||
const addCart = async (productId) => {
|
||||
if (userStore.role !== 'CUSTOMER') return Message.warning('请使用顾客账号操作')
|
||||
await api.addCart({ productId, quantity: 1 })
|
||||
Message.success('已加入购物车')
|
||||
}
|
||||
|
||||
const buyNow = async (productId) => {
|
||||
if (userStore.role !== 'CUSTOMER') return Message.warning('请使用顾客账号操作')
|
||||
if (!address.value) return Message.warning('请先填写收货地址')
|
||||
localStorage.setItem('customer_address', address.value)
|
||||
await api.customerBuyNow({ productId, quantity: 1, address: address.value })
|
||||
Message.success('购买成功')
|
||||
router.push('/orders')
|
||||
}
|
||||
|
||||
const addFavorite = async (productId) => {
|
||||
if (userStore.role !== 'CUSTOMER') return Message.warning('请使用顾客账号操作')
|
||||
await api.addFavorite({ productId })
|
||||
Message.success('已收藏')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadBanners()
|
||||
await loadProducts()
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role === 'CUSTOMER') {
|
||||
address.value = localStorage.getItem('customer_address') || address.value
|
||||
}
|
||||
} catch (e) {
|
||||
userStore.profile = null
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
57
frontend/src/views/LoginView.vue
Normal file
57
frontend/src/views/LoginView.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="container" style="max-width: 460px; margin-top: 80px">
|
||||
<a-card title="萌贝母婴商城 - 登录/注册">
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="账号">
|
||||
<a-input v-model="form.username" placeholder="请输入账号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码">
|
||||
<a-input-password v-model="form.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="onLogin">登录</a-button>
|
||||
<a-button @click="onRegister">注册</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const form = reactive({ username: '', password: '' })
|
||||
|
||||
const routeByRole = (role) => {
|
||||
if (role === 'ADMIN') return '/admin'
|
||||
if (role === 'MERCHANT') return '/merchant'
|
||||
return '/products'
|
||||
}
|
||||
|
||||
const onLogin = async () => {
|
||||
try {
|
||||
const res = await api.login({ username: form.username, password: form.password })
|
||||
localStorage.setItem('token', res.token)
|
||||
await userStore.fetchMe()
|
||||
Message.success('登录成功')
|
||||
router.replace(routeByRole(userStore.role))
|
||||
} catch (e) {
|
||||
Message.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onRegister = async () => {
|
||||
try {
|
||||
await api.register({ username: form.username, password: form.password })
|
||||
Message.success('注册成功,请登录')
|
||||
} catch (e) {
|
||||
Message.error(e.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
355
frontend/src/views/MerchantView.vue
Normal file
355
frontend/src/views/MerchantView.vue
Normal file
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<a-layout style="min-height: 100vh">
|
||||
<a-layout-sider :width="220" theme="dark" collapsible>
|
||||
<div style="height: 56px; display:flex; align-items:center; justify-content:center; color:#fff; font-weight:600;">商家后台</div>
|
||||
<a-menu :selected-keys="[active]" @menu-item-click="(k)=>active=k">
|
||||
<a-menu-item key="overview">数据概览</a-menu-item>
|
||||
<a-menu-item key="products">商品管理</a-menu-item>
|
||||
<a-menu-item key="orders">订单管理</a-menu-item>
|
||||
<a-menu-item key="reviews">评价管理</a-menu-item>
|
||||
<a-menu-item key="logistics">物流管理</a-menu-item>
|
||||
<a-menu-item key="inventory">库存管理</a-menu-item>
|
||||
<a-menu-item key="profile">个人信息</a-menu-item>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-layout>
|
||||
<a-layout-header style="background:#fff; display:flex; justify-content:space-between; align-items:center; padding:0 16px;">
|
||||
<strong>{{ titleMap[active] }}</strong>
|
||||
<a-space>
|
||||
<a-button @click="goHome">去商城</a-button>
|
||||
<a-button status="danger" @click="logout">退出</a-button>
|
||||
</a-space>
|
||||
</a-layout-header>
|
||||
<a-layout-content style="padding:16px;">
|
||||
<a-card v-if="active==='overview'" title="经营概览">
|
||||
<a-space style="margin-bottom: 16px">
|
||||
<a-tag color="arcoblue" size="large">订单量: {{ overview.orderCount || 0 }}</a-tag>
|
||||
<a-tag color="green" size="large">销售额: ¥{{ (overview.salesAmount || 0).toLocaleString() }}</a-tag>
|
||||
<a-button @click="loadOverview">刷新</a-button>
|
||||
</a-space>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-card title="品类占比" size="small">
|
||||
<div ref="categoryChartRef" style="height: 250px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="热销排行 Top10" size="small">
|
||||
<div ref="hotChartRef" style="height: 250px;"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider />
|
||||
<a-card title="通知公告" size="small">
|
||||
<a-timeline v-if="overview.notifications && overview.notifications.length">
|
||||
<a-timeline-item v-for="(note, idx) in overview.notifications" :key="idx" :color="idx === 0 ? 'red' : 'blue'">
|
||||
{{ note }}
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
<a-empty v-else description="暂无公告" />
|
||||
</a-card>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='products'" title="商品管理">
|
||||
<a-space style="margin-bottom: 10px">
|
||||
<a-button type="primary" @click="openProductModal()">新增商品</a-button>
|
||||
<a-button @click="loadProducts">刷新</a-button>
|
||||
</a-space>
|
||||
<a-table :columns="productColumns" :data="products" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="openProductModal(record)">编辑</a-button>
|
||||
<a-button size="mini" status="danger" @click="delProduct(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='orders'" title="订单管理">
|
||||
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" type="primary" @click="ship(record.id)">发货</a-button>
|
||||
<a-button size="mini" @click="refund(record.id, true)">同意退款</a-button>
|
||||
<a-button size="mini" @click="openOrderLogistics(record.id)">查看物流</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='reviews'" title="评价管理">
|
||||
<a-table :columns="reviewColumns" :data="reviews" :pagination="{ pageSize: 8 }" />
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='logistics'" title="物流管理">
|
||||
<a-table :columns="logisticsColumns" :data="logistics" :pagination="{ pageSize: 8 }" />
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='inventory'" title="库存管理">
|
||||
<a-table :columns="inventoryColumns" :data="inventory" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-button size="mini" status="danger" @click="deleteInventory(record.id)">删除</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card v-if="active==='profile'" title="个人信息">
|
||||
<a-form :model="profile" layout="vertical" style="max-width: 480px">
|
||||
<a-form-item label="账号"><a-input v-model="profile.username" disabled /></a-form-item>
|
||||
<a-form-item label="角色"><a-input v-model="profile.role" disabled /></a-form-item>
|
||||
<a-form-item label="昵称"><a-input v-model="profile.nickname" /></a-form-item>
|
||||
<a-form-item label="手机号"><a-input v-model="profile.phone" /></a-form-item>
|
||||
<a-form-item label="地址"><a-input v-model="profile.address" /></a-form-item>
|
||||
<a-button type="primary" @click="saveProfile">保存修改</a-button>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
|
||||
<a-modal v-model:visible="productModalVisible" :title="productForm.id ? '编辑商品' : '新增商品'" @ok="submitProduct">
|
||||
<a-form :model="productForm" layout="vertical">
|
||||
<a-form-item label="商品名称"><a-input v-model="productForm.name" /></a-form-item>
|
||||
<a-form-item label="分类"><a-input v-model="productForm.category" /></a-form-item>
|
||||
<a-form-item label="描述"><a-input v-model="productForm.description" /></a-form-item>
|
||||
<a-form-item label="价格"><a-input-number v-model="productForm.price" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="库存"><a-input-number v-model="productForm.stock" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="图片URL"><a-input v-model="productForm.imageUrl" /></a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="logisticsModalVisible" title="订单物流">
|
||||
<a-table :columns="logisticsColumns" :data="filteredLogistics" :pagination="false" />
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch, nextTick, onUnmounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const active = ref('overview')
|
||||
const titleMap = {
|
||||
overview: '数据概览', products: '商品管理', orders: '订单管理', reviews: '评价管理',
|
||||
logistics: '物流管理', inventory: '库存管理', profile: '个人信息'
|
||||
}
|
||||
|
||||
const categoryChartRef = ref(null)
|
||||
const hotChartRef = ref(null)
|
||||
let categoryChart = null
|
||||
let hotChart = null
|
||||
|
||||
const overview = reactive({})
|
||||
const products = ref([])
|
||||
const orders = ref([])
|
||||
const reviews = ref([])
|
||||
const logistics = ref([])
|
||||
const inventory = ref([])
|
||||
|
||||
const productModalVisible = ref(false)
|
||||
const logisticsModalVisible = ref(false)
|
||||
|
||||
const productForm = reactive({ id: null, name: '', category: '', description: '', price: 0, stock: 0, imageUrl: '', approved: false })
|
||||
const profile = reactive({ username: '', role: '', nickname: '', phone: '', address: '' })
|
||||
const filteredLogistics = ref([])
|
||||
|
||||
const productColumns = [
|
||||
{ title: 'ID', dataIndex: 'id' },
|
||||
{ title: '名称', dataIndex: 'name' },
|
||||
{ title: '分类', dataIndex: 'category' },
|
||||
{ title: '价格', dataIndex: 'price' },
|
||||
{ title: '库存', dataIndex: 'stock' },
|
||||
{ title: '审核', dataIndex: 'approved' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const orderColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '金额', dataIndex: 'totalAmount' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const reviewColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '评分', dataIndex: 'rating' },
|
||||
{ title: '内容', dataIndex: 'content' },
|
||||
{ title: '时间', dataIndex: 'createdAt' }
|
||||
]
|
||||
|
||||
const logisticsColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '备注', dataIndex: 'note' },
|
||||
{ title: '时间', dataIndex: 'createdAt' }
|
||||
]
|
||||
|
||||
const inventoryColumns = [
|
||||
{ title: '商品名称', dataIndex: 'productName' },
|
||||
{ title: '变动', dataIndex: 'changeQty' },
|
||||
{ title: '备注', dataIndex: 'note' },
|
||||
{ title: '时间', dataIndex: 'createdAt' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const goHome = () => router.push('/products')
|
||||
const logout = async () => { await userStore.logout(); router.replace('/login') }
|
||||
|
||||
const loadOverview = async () => {
|
||||
Object.assign(overview, await api.merchantOverview())
|
||||
await nextTick()
|
||||
initCharts()
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (!overview.categoryRatio) overview.categoryRatio = {}
|
||||
if (!overview.hotProducts) overview.hotProducts = {}
|
||||
|
||||
const categoryData = Object.entries(overview.categoryRatio).map(([name, value]) => ({ name, value }))
|
||||
if (categoryChart) {
|
||||
categoryChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left' },
|
||||
series: [{ type: 'pie', radius: ['40%', '70%'], data: categoryData }]
|
||||
})
|
||||
} else if (categoryChartRef.value) {
|
||||
categoryChart = echarts.init(categoryChartRef.value)
|
||||
categoryChart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||
legend: { orient: 'vertical', left: 'left' },
|
||||
series: [{ type: 'pie', radius: ['40%', '70%'], data: categoryData }]
|
||||
})
|
||||
}
|
||||
|
||||
const hotEntries = Object.entries(overview.hotProducts)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
const hotData = hotEntries.map(([name, value]) => ({ name: name.length > 10 ? name.substring(0, 10) + '...' : name, value }))
|
||||
if (hotChart) {
|
||||
hotChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
|
||||
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
|
||||
})
|
||||
} else if (hotChartRef.value) {
|
||||
hotChart = echarts.init(hotChartRef.value)
|
||||
hotChart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'value' },
|
||||
yAxis: { type: 'category', data: hotData.map(d => d.name).reverse() },
|
||||
series: [{ type: 'bar', data: hotData.map(d => d.value).reverse() }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
categoryChart?.resize()
|
||||
hotChart?.resize()
|
||||
}
|
||||
|
||||
watch(active, async (v) => {
|
||||
if (v === 'overview') {
|
||||
await loadOverview()
|
||||
await nextTick()
|
||||
initCharts()
|
||||
}
|
||||
if (v === 'products') return loadProducts()
|
||||
if (v === 'orders') return loadOrders()
|
||||
if (v === 'reviews') return loadReviews()
|
||||
if (v === 'logistics') return loadLogistics()
|
||||
if (v === 'inventory') return loadInventory()
|
||||
})
|
||||
|
||||
const openProductModal = (row = null) => {
|
||||
if (row) {
|
||||
productForm.id = row.id
|
||||
productForm.name = row.name
|
||||
productForm.category = row.category
|
||||
productForm.description = row.description
|
||||
productForm.price = row.price
|
||||
productForm.stock = row.stock
|
||||
productForm.imageUrl = row.imageUrl
|
||||
} else {
|
||||
productForm.id = null
|
||||
productForm.name = ''
|
||||
productForm.category = ''
|
||||
productForm.description = ''
|
||||
productForm.price = 0
|
||||
productForm.stock = 0
|
||||
productForm.imageUrl = ''
|
||||
}
|
||||
productModalVisible.value = true
|
||||
}
|
||||
|
||||
const submitProduct = async () => {
|
||||
await api.saveMerchantProduct({ ...productForm })
|
||||
Message.success('商品已保存')
|
||||
productModalVisible.value = false
|
||||
await loadProducts()
|
||||
}
|
||||
|
||||
const delProduct = async (id) => {
|
||||
await api.deleteMerchantProduct(id)
|
||||
Message.success('商品已删除')
|
||||
await loadProducts()
|
||||
}
|
||||
|
||||
const ship = async (id) => {
|
||||
await api.shipOrder(id, { note: '已发货' })
|
||||
Message.success('发货成功')
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const refund = async (id, agree) => {
|
||||
await api.merchantRefund(id, { agree })
|
||||
Message.success('退款已处理')
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const deleteInventory = async (id) => {
|
||||
await api.deleteMerchantInventory(id)
|
||||
Message.success('库存记录已删除')
|
||||
await loadInventory()
|
||||
}
|
||||
|
||||
const openOrderLogistics = async (orderId) => {
|
||||
await loadLogistics()
|
||||
filteredLogistics.value = logistics.value.filter((l) => l.orderId === orderId)
|
||||
logisticsModalVisible.value = true
|
||||
}
|
||||
|
||||
const loadProfile = async () => {
|
||||
const me = await api.me()
|
||||
profile.username = me.username
|
||||
profile.role = me.role
|
||||
profile.nickname = me.nickname || ''
|
||||
profile.phone = me.phone || ''
|
||||
profile.address = me.address || ''
|
||||
}
|
||||
|
||||
const saveProfile = async () => {
|
||||
await api.updateMe({ nickname: profile.nickname, phone: profile.phone, address: profile.address })
|
||||
Message.success('已保存')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'MERCHANT') return router.replace('/login')
|
||||
await loadOverview()
|
||||
await loadProfile()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
categoryChart?.dispose()
|
||||
hotChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
154
frontend/src/views/OrdersView.vue
Normal file
154
frontend/src/views/OrdersView.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
|
||||
<h2>萌贝母婴商城 - 我的订单</h2>
|
||||
<a-space>
|
||||
<a-button @click="goProducts">继续购物</a-button>
|
||||
<a-button @click="goCart">购物车</a-button>
|
||||
<a-button @click="goFavorites">我的收藏</a-button>
|
||||
<a-button @click="goProfile">个人信息</a-button>
|
||||
<a-button @click="loadOrders">刷新</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
|
||||
<a-card title="订单列表">
|
||||
<a-table :columns="orderColumns" :data="orders" :pagination="{ pageSize: 8 }">
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button size="mini" @click="openLogistics(record.id)">查看物流</a-button>
|
||||
<a-button size="mini" @click="openAddress(record)">修改地址</a-button>
|
||||
<a-button size="mini" @click="openReview(record.id)">评价</a-button>
|
||||
<a-button size="mini" @click="refund(record.id)">退款</a-button>
|
||||
<a-button size="mini" @click="deleteOrder(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-modal v-model:visible="addressModalVisible" title="修改收货地址" @ok="submitAddress">
|
||||
<a-input v-model="addressForm.address" placeholder="请输入新的收货地址" />
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="logisticsModalVisible" title="物流信息">
|
||||
<a-table :columns="logisticsColumns" :data="logistics" :pagination="false" />
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:visible="reviewModalVisible" title="提交评价" @ok="submitReview">
|
||||
<a-form :model="reviewForm" layout="vertical">
|
||||
<a-form-item label="商品">
|
||||
<a-select v-model="reviewForm.productId">
|
||||
<a-option v-for="item in orderItems" :key="item.productId" :value="item.productId">{{ item.productName }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="评分">
|
||||
<a-rate v-model="reviewForm.rating" :count="5" />
|
||||
</a-form-item>
|
||||
<a-form-item label="内容">
|
||||
<a-textarea v-model="reviewForm.content" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const orders = ref([])
|
||||
|
||||
const orderColumns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '金额', dataIndex: 'totalAmount' },
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '地址', dataIndex: 'address' },
|
||||
{ title: '操作', slotName: 'actions' }
|
||||
]
|
||||
|
||||
const goProducts = () => router.push('/products')
|
||||
const goCart = () => router.push('/cart')
|
||||
const goFavorites = () => router.push('/favorites')
|
||||
const goProfile = () => router.push('/profile')
|
||||
|
||||
const loadOrders = async () => {
|
||||
orders.value = await api.customerOrders()
|
||||
}
|
||||
|
||||
const addressModalVisible = ref(false)
|
||||
const logisticsModalVisible = ref(false)
|
||||
const reviewModalVisible = ref(false)
|
||||
|
||||
const addressForm = reactive({ id: null, address: '' })
|
||||
const logistics = ref([])
|
||||
const logisticsColumns = [
|
||||
{ title: '状态', dataIndex: 'status' },
|
||||
{ title: '备注', dataIndex: 'note' },
|
||||
{ title: '时间', dataIndex: 'createdAt' }
|
||||
]
|
||||
|
||||
const orderItems = ref([])
|
||||
const reviewForm = reactive({ orderId: null, productId: null, rating: 5, content: '' })
|
||||
|
||||
const refund = async (id) => {
|
||||
await api.refundOrder(id, { reason: '不想要了' })
|
||||
Message.success('已提交退款申请')
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const deleteOrder = async (id) => {
|
||||
await api.deleteOrder(id)
|
||||
Message.success('订单已删除')
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const openAddress = (order) => {
|
||||
addressForm.id = order.id
|
||||
addressForm.address = order.address || ''
|
||||
addressModalVisible.value = true
|
||||
}
|
||||
|
||||
const submitAddress = async () => {
|
||||
await api.updateOrderAddress(addressForm.id, { address: addressForm.address })
|
||||
Message.success('地址已更新')
|
||||
addressModalVisible.value = false
|
||||
await loadOrders()
|
||||
}
|
||||
|
||||
const openLogistics = async (orderId) => {
|
||||
logistics.value = await api.orderLogistics(orderId)
|
||||
logisticsModalVisible.value = true
|
||||
}
|
||||
|
||||
const openReview = async (orderId) => {
|
||||
orderItems.value = await api.orderItems(orderId)
|
||||
if (orderItems.value.length > 0) {
|
||||
reviewForm.productId = orderItems.value[0].productId
|
||||
}
|
||||
reviewForm.orderId = orderId
|
||||
reviewForm.rating = 5
|
||||
reviewForm.content = ''
|
||||
reviewModalVisible.value = true
|
||||
}
|
||||
|
||||
const submitReview = async () => {
|
||||
if (!reviewForm.productId) return Message.warning('请选择商品')
|
||||
await api.addReview({ orderId: reviewForm.orderId, productId: reviewForm.productId, rating: reviewForm.rating, content: reviewForm.content })
|
||||
Message.success('评价已提交')
|
||||
reviewModalVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
|
||||
await loadOrders()
|
||||
} catch {
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
75
frontend/src/views/ProfileView.vue
Normal file
75
frontend/src/views/ProfileView.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-space style="width: 100%; justify-content: space-between; margin-bottom: 12px">
|
||||
<h2>萌贝母婴商城 - 个人信息</h2>
|
||||
<a-space>
|
||||
<a-button @click="goProducts">继续购物</a-button>
|
||||
<a-button @click="goOrders">我的订单</a-button>
|
||||
</a-space>
|
||||
</a-space>
|
||||
|
||||
<a-card title="个人资料">
|
||||
<a-form :model="form" layout="vertical" style="max-width: 480px">
|
||||
<a-form-item label="账号"><a-input v-model="form.username" disabled /></a-form-item>
|
||||
<a-form-item label="角色"><a-input v-model="form.role" disabled /></a-form-item>
|
||||
<a-form-item label="昵称"><a-input v-model="form.nickname" /></a-form-item>
|
||||
<a-form-item label="手机号"><a-input v-model="form.phone" /></a-form-item>
|
||||
<a-form-item label="收货地址"><a-input v-model="form.address" /></a-form-item>
|
||||
<a-button type="primary" @click="save">保存修改</a-button>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<a-card title="商家入驻申请" style="margin-top: 12px">
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-textarea v-model="qualification" :rows="3" placeholder="请填写商家资质、经营能力说明" />
|
||||
<a-button type="primary" @click="submitMerchantApplication">提交入驻申请</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { api } from '../api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const form = reactive({ username: '', role: '', nickname: '', phone: '', address: '' })
|
||||
const qualification = ref('')
|
||||
|
||||
const goProducts = () => router.push('/products')
|
||||
const goOrders = () => router.push('/orders')
|
||||
|
||||
const load = async () => {
|
||||
const me = await api.me()
|
||||
form.username = me.username
|
||||
form.role = me.role
|
||||
form.nickname = me.nickname || ''
|
||||
form.phone = me.phone || ''
|
||||
form.address = me.address || ''
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
await api.updateMe({ nickname: form.nickname, phone: form.phone, address: form.address })
|
||||
Message.success('已保存')
|
||||
}
|
||||
|
||||
const submitMerchantApplication = async () => {
|
||||
await api.applyMerchant({ qualification: qualification.value })
|
||||
qualification.value = ''
|
||||
Message.success('入驻申请已提交,等待管理员审核')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await userStore.fetchMe()
|
||||
if (userStore.role !== 'CUSTOMER') return router.replace('/login')
|
||||
await load()
|
||||
} catch {
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user