Initial commit

This commit is contained in:
wangziqi
2026-02-09 09:51:14 -08:00
commit a7ce0a089e
104 changed files with 6470 additions and 0 deletions

48
.gitignore vendored Normal file
View 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
View 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
View 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>

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package com.maternalmall.common;
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,11 @@
package com.maternalmall.domain;
public enum OrderStatus {
PENDING_PAYMENT,
PAID,
SHIPPED,
COMPLETED,
REFUND_REQUESTED,
REFUNDED,
CANCELLED
}

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

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

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

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

View File

@@ -0,0 +1,7 @@
package com.maternalmall.domain;
public enum UserRole {
CUSTOMER,
MERCHANT,
ADMIN
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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

View 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

12
frontend/index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

24
frontend/package.json Normal file
View 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
View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

24
frontend/src/api/http.js Normal file
View 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
View 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
View 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')

View 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

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

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

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

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

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

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

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

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

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

View 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