Initial commit: backend code

This commit is contained in:
Agent
2026-03-20 04:59:00 +00:00
commit e7c7f3b174
42 changed files with 2855 additions and 0 deletions

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN apk add --no-cache maven && \
mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

122
pom.xml Normal file
View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>building</artifactId>
<version>1.0.0</version>
<name>building</name>
<description>建材销售管家后端服务</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<jwt.version>0.11.5</jwt.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.43</version>
</dependency>
<!-- Test -->
<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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,17 @@
package com.example.building;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 建材销售管家启动类
*/
@SpringBootApplication
@MapperScan("com.example.building.mapper")
public class BuildingApplication {
public static void main(String[] args) {
SpringApplication.run(BuildingApplication.class, args);
}
}

View File

@@ -0,0 +1,133 @@
package com.example.building.common;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
* 用于生成和验证JWT Token
*/
@Component
public class JwtUtil {
@Value("${jwt.secret:building-materials-secret-key-2024}")
private String secret;
@Value("${jwt.expiration:7200000}")
private Long expiration;
@Value("${jwt.refresh-expiration:604800000}")
private Long refreshExpiration;
/**
* 生成JWT Token
*/
public String generateToken(String userId, String username, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
claims.put("role", role);
return createToken(claims, userId);
}
/**
* 创建Token
*/
private String createToken(Map<String, Object> claims, String subject) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 生成刷新Token
*/
public String generateRefreshToken(String userId) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 解析Token
*/
public Claims parseToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 验证Token是否有效
*/
public boolean validateToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取用户ID
*/
public String getUserId(String token) {
Claims claims = parseToken(token);
return claims.get("userId", String.class);
}
/**
* 获取用户名
*/
public String getUsername(String token) {
Claims claims = parseToken(token);
return claims.get("username", String.class);
}
/**
* 获取用户角色
*/
public String getRole(String token) {
Claims claims = parseToken(token);
return claims.get("role", String.class);
}
/**
* 判断Token是否过期
*/
public boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
}

View File

@@ -0,0 +1,60 @@
package com.example.building.common;
import lombok.Data;
import java.io.Serializable;
/**
* 统一响应结果类
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 状态码: 0成功, 非0失败
*/
private Integer code;
/**
* 提示信息
*/
private String message;
/**
* 业务数据
*/
private T data;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(0);
result.setMessage("成功");
result.setData(data);
return result;
}
public static <T> Result<T> success(T data, String message) {
Result<T> result = new Result<>();
result.setCode(0);
result.setMessage(message);
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
return error(3000, message);
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}

View File

@@ -0,0 +1,23 @@
package com.example.building.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
* 允许前端应用跨域访问后端API
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}

View File

@@ -0,0 +1,48 @@
package com.example.building.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
* MyBatis-Plus配置
* 配置分页插件和自动填充
*/
@Configuration
public class MybatisPlusConfig {
/**
* 配置分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
return interceptor;
}
/**
* 自动填充处理器
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createdAt", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updatedAt", LocalDateTime.class, LocalDateTime.now());
}
};
}
}

View File

@@ -0,0 +1,44 @@
package com.example.building.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
* 配置RedisTemplate的序列化方式
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 配置ObjectMapper
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
// 使用Jackson2JsonRedisSerializer序列化value
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
// 设置key和value的序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -0,0 +1,84 @@
package com.example.building.controller;
import com.example.building.common.Result;
import com.example.building.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 认证控制器
* 支持:手机号验证码登录、微信扫码登录、支付宝扫码登录
*/
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private AuthService authService;
/**
* 发送验证码
*/
@PostMapping("/send-code")
public Result<Void> sendCode(@RequestParam String phone) {
authService.sendCode(phone);
return Result.success();
}
/**
* 手机号验证码登录
*/
@PostMapping("/phone-login")
public Result<Map<String, Object>> phoneLogin(@RequestParam String phone, @RequestParam String code) {
Map<String, Object> result = authService.phoneLogin(phone, code);
return Result.success(result);
}
/**
* 微信登录
*/
@PostMapping("/wechat")
public Result<Map<String, Object>> wechatLogin(@RequestParam String code) {
Map<String, Object> result = authService.wechatLogin(code);
return Result.success(result);
}
/**
* 支付宝登录
*/
@PostMapping("/alipay")
public Result<Map<String, Object>> alipayLogin(@RequestParam String code) {
Map<String, Object> result = authService.alipayLogin(code);
return Result.success(result);
}
/**
* 刷新Token
*/
@PostMapping("/refresh")
public Result<Map<String, Object>> refresh(@RequestParam String refreshToken) {
Map<String, Object> result = authService.refreshToken(refreshToken);
return Result.success(result);
}
/**
* 获取当前用户
*/
@GetMapping("/me")
public Result<Map<String, Object>> me(@RequestHeader("X-User-Id") String userId) {
Map<String, Object> result = authService.getCurrentUser(userId);
return Result.success(result);
}
/**
* 退出登录
*/
@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String token) {
String jwtToken = token.replace("Bearer ", "");
authService.logout(jwtToken);
return Result.success();
}
}

View File

@@ -0,0 +1,65 @@
package com.example.building.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.common.Result;
import com.example.building.entity.Customer;
import com.example.building.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 客户控制器
*/
@RestController
@RequestMapping("/api/v1/customers")
public class CustomerController {
@Autowired
private CustomerService customerService;
/**
* 获取客户列表
*/
@GetMapping
public Result<Page<Customer>> getCustomers(
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
return Result.success(customerService.getCustomers(keyword, page, pageSize));
}
/**
* 获取客户详情
*/
@GetMapping("/{id}")
public Result<Customer> getCustomer(@PathVariable String id) {
return Result.success(customerService.getCustomer(id));
}
/**
* 新增客户
*/
@PostMapping
public Result<Customer> createCustomer(@RequestBody Customer customer,
@RequestHeader("X-User-Id") String userId) {
customer.setCreatedBy(userId);
return Result.success(customerService.createCustomer(customer));
}
/**
* 修改客户
*/
@PutMapping("/{id}")
public Result<Customer> updateCustomer(@PathVariable String id, @RequestBody Customer customer) {
return Result.success(customerService.updateCustomer(id, customer));
}
/**
* 删除客户
*/
@DeleteMapping("/{id}")
public Result<Void> deleteCustomer(@PathVariable String id) {
customerService.deleteCustomer(id);
return Result.success();
}
}

View File

@@ -0,0 +1,92 @@
package com.example.building.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.common.Result;
import com.example.building.dto.CreateOrderRequest;
import com.example.building.entity.Order;
import com.example.building.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 订单控制器
* 核心业务:
* - 订单创建:计算原价(total_amount)、优惠金额(discount_amount)、实付金额(actual_amount)
* - 订单原价 = 商品标价 × 数量之和
* - 实付金额 = 原价 - 优惠金额
*/
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
* 核心逻辑:
* 1. 计算订单原价(total_amount) = Σ(item.price × item.quantity)
* 2. 计算优惠金额(discount_amount) = total_amount × (100 - discount_rate) / 100
* 3. 计算实付金额(actual_amount) = total_amount - discount_amount
*/
@PostMapping
public Result<Order> createOrder(@RequestBody CreateOrderRequest request,
@RequestHeader("X-User-Id") String operatorId,
@RequestHeader("X-Username") String operatorName) {
return Result.success(orderService.createOrder(request, operatorId, operatorName));
}
/**
* 获取订单列表
*/
@GetMapping
public Result<Page<Order>> getOrders(
@RequestParam(required = false) String customerId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
return Result.success(orderService.getOrders(customerId, status, startDate, endDate, page, pageSize));
}
/**
* 获取订单详情
*/
@GetMapping("/{id}")
public Result<Map<String, Object>> getOrderDetail(@PathVariable String id) {
return Result.success(orderService.getOrderDetail(id));
}
/**
* 取消订单
*/
@PutMapping("/{id}/cancel")
public Result<Void> cancelOrder(@PathVariable String id,
@RequestHeader("X-User-Id") String operatorId) {
orderService.cancelOrder(id, operatorId);
return Result.success();
}
/**
* 退款
*/
@PutMapping("/{id}/refund")
public Result<Void> refundOrder(@PathVariable String id,
@RequestHeader("X-User-Id") String operatorId) {
orderService.refundOrder(id, operatorId);
return Result.success();
}
/**
* 订单统计
*/
@GetMapping("/statistics")
public Result<Map<String, Object>> getStatistics(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
return Result.success(orderService.getStatistics(startDate, endDate));
}
}

View File

@@ -0,0 +1,110 @@
package com.example.building.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.common.Result;
import com.example.building.entity.Category;
import com.example.building.entity.Product;
import com.example.building.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 商品控制器
* 支持商品CRUD、分类管理
*/
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 获取分类列表
*/
@GetMapping("/categories")
public Result<List<Category>> getCategories() {
return Result.success(productService.getCategories());
}
/**
* 新增分类
*/
@PostMapping("/categories")
public Result<Category> createCategory(@RequestBody Category category) {
return Result.success(productService.createCategory(category));
}
/**
* 修改分类
*/
@PutMapping("/categories/{id}")
public Result<Category> updateCategory(@PathVariable String id, @RequestBody Category category) {
return Result.success(productService.updateCategory(id, category));
}
/**
* 删除分类
*/
@DeleteMapping("/categories/{id}")
public Result<Void> deleteCategory(@PathVariable String id) {
productService.deleteCategory(id);
return Result.success();
}
/**
* 获取商品列表
*/
@GetMapping
public Result<Page<Product>> getProducts(
@RequestParam(required = false) String categoryId,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
return Result.success(productService.getProducts(categoryId, keyword, page, pageSize));
}
/**
* 获取商品详情
*/
@GetMapping("/{id}")
public Result<Product> getProduct(@PathVariable String id) {
return Result.success(productService.getProduct(id));
}
/**
* 新增商品
*/
@PostMapping
public Result<Product> createProduct(@RequestBody Product product) {
return Result.success(productService.createProduct(product));
}
/**
* 修改商品
*/
@PutMapping("/{id}")
public Result<Product> updateProduct(@PathVariable String id, @RequestBody Product product) {
return Result.success(productService.updateProduct(id, product));
}
/**
* 删除商品
*/
@DeleteMapping("/{id}")
public Result<Void> deleteProduct(@PathVariable String id) {
productService.deleteProduct(id);
return Result.success();
}
/**
* 获取库存预警商品
*/
@GetMapping("/alerts")
public Result<List<Map<String, Object>>> getStockAlerts() {
return Result.success(productService.getStockAlerts());
}
}

View File

@@ -0,0 +1,74 @@
package com.example.building.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.common.Result;
import com.example.building.entity.Stock;
import com.example.building.entity.StockFlow;
import com.example.building.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 库存控制器
* 核心业务:入库、库存查询、库存流水
*/
@RestController
@RequestMapping("/api/v1/stock")
public class StockController {
@Autowired
private StockService stockService;
/**
* 入库
*/
@PostMapping("/in")
public Result<Stock> stockIn(@RequestParam String productId,
@RequestParam Integer quantity,
@RequestParam(required = false) String remark,
@RequestHeader("X-User-Id") String operatorId) {
return Result.success(stockService.stockIn(productId, quantity, remark, operatorId));
}
/**
* 库存查询
*/
@GetMapping
public Result<Page<Stock>> getStockList(
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
return Result.success(stockService.getStockList(keyword, page, pageSize));
}
/**
* 单商品库存
*/
@GetMapping("/{productId}")
public Result<Stock> getStock(@PathVariable String productId) {
return Result.success(stockService.getStock(productId));
}
/**
* 库存调整
*/
@PostMapping("/adjust")
public Result<Stock> adjustStock(@RequestParam String productId,
@RequestParam Integer quantity,
@RequestParam(required = false) String remark,
@RequestHeader("X-User-Id") String operatorId) {
return Result.success(stockService.adjustStock(productId, quantity, remark, operatorId));
}
/**
* 库存流水
*/
@GetMapping("/flow")
public Result<Page<StockFlow>> getStockFlow(
@RequestParam(required = false) String productId,
@RequestParam(required = false) Integer type,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer pageSize) {
return Result.success(stockService.getStockFlow(productId, type, page, pageSize));
}
}

View File

@@ -0,0 +1,68 @@
package com.example.building.dto;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.List;
/**
* 创建订单请求DTO
* 核心业务逻辑:
* 1. 计算订单原价(total_amount) = Σ(item.price × item.quantity)
* 2. 计算优惠金额(discount_amount) = total_amount × (100 - discount_rate) / 100
* 3. 计算实付金额(actual_amount) = total_amount - discount_amount
*/
@Data
public class CreateOrderRequest {
/**
* 客户ID
*/
private String customerId;
/**
* 订单明细
*/
@NotEmpty(message = "订单明细不能为空")
private List<OrderItemDTO> items;
/**
* 折扣率(百分比), 默认100
*/
private BigDecimal discountRate;
/**
* 备注
*/
private String remark;
/**
* 支付方式
*/
private String paymentMethod;
/**
* 订单明细项DTO
*/
@Data
public static class OrderItemDTO {
/**
* 商品ID
*/
@NotBlank(message = "商品ID不能为空")
private String productId;
/**
* 数量
*/
@NotNull(message = "数量不能为空")
private Integer quantity;
/**
* 销售单价(用户可自定义)
*/
private BigDecimal price;
}
}

View File

@@ -0,0 +1,38 @@
package com.example.building.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 登录请求DTO
*/
@Data
public class LoginRequest {
/**
* 手机号
*/
private String phone;
/**
* 验证码
*/
private String code;
/**
* 微信授权码
*/
private String wechatCode;
/**
* 支付宝授权码
*/
private String alipayCode;
/**
* 登录类型: phone-手机号, wechat-微信, alipay-支付宝
*/
@NotBlank(message = "登录类型不能为空")
private String loginType;
}

View File

@@ -0,0 +1,49 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 商品分类实体类
* 默认分类:五金建材、板材、木门、地板
*/
@Data
@TableName("categories")
public class Category {
@TableId(type = IdType.ASSIGN_UUID)
private String categoryId;
/**
* 分类名称
*/
private String name;
/**
* 父分类ID
*/
private String parentId;
/**
* 排序
*/
private Integer sortOrder;
/**
* 图标
*/
private String icon;
/**
* 状态: 1启用 0禁用
*/
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,59 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 客户实体类
*/
@Data
@TableName("customers")
public class Customer {
@TableId(type = IdType.ASSIGN_UUID)
private String customerId;
/**
* 客户名称
*/
private String name;
/**
* 联系电话
*/
private String phone;
/**
* 客户微信OpenID(用于推送)
*/
private String wechatOpenid;
/**
* 地址
*/
private String address;
/**
* 备注
*/
private String remark;
/**
* 累计消费金额
*/
private BigDecimal totalAmount;
/**
* 创建人ID
*/
private String createdBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,101 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
* 核心业务逻辑:
* - total_amount: 订单原价(商品标价×数量之和)
* - discount_amount: 优惠金额
* - actual_amount: 实付金额 = 原价 - 优惠金额
*/
@Data
@TableName("orders")
public class Order {
@TableId(type = IdType.ASSIGN_UUID)
private String orderId;
/**
* 订单编号(唯一)
*/
private String orderNo;
/**
* 客户ID
*/
private String customerId;
/**
* 客户名称(冗余)
*/
private String customerName;
/**
* 客户电话(冗余)
*/
private String customerPhone;
/**
* 客户微信OpenID
*/
private String customerWechat;
/**
* 订单原价(标价总和)
* 计算公式: Σ(item.price × item.quantity)
*/
private BigDecimal totalAmount;
/**
* 优惠金额
* 计算公式: totalAmount × (100 - discountRate) / 100
*/
private BigDecimal discountAmount;
/**
* 实收金额
* 计算公式: totalAmount - discountAmount
*/
private BigDecimal actualAmount;
/**
* 折扣率(百分比)
*/
private BigDecimal discountRate;
/**
* 状态: 1已完成 2已取消 3退款中 4已退款
*/
private Integer status;
/**
* 支付方式
*/
private String paymentMethod;
/**
* 备注
*/
private String remark;
/**
* 操作人ID
*/
private String operatorId;
/**
* 操作人姓名(冗余)
*/
private String operatorName;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,61 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单明细实体类
*/
@Data
@TableName("order_items")
public class OrderItem {
@TableId(type = IdType.ASSIGN_UUID)
private String itemId;
/**
* 订单ID
*/
private String orderId;
/**
* 商品ID
*/
private String productId;
/**
* 商品名称(冗余)
*/
private String productName;
/**
* 商品规格(冗余)
*/
private String productSpec;
/**
* 单位(冗余)
*/
private String unit;
/**
* 销售单价(当时标价)
*/
private BigDecimal price;
/**
* 数量
*/
private Integer quantity;
/**
* 小计(单价×数量)
*/
private BigDecimal subtotal;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,79 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 商品实体类
*/
@Data
@TableName("products")
public class Product {
@TableId(type = IdType.ASSIGN_UUID)
private String productId;
/**
* 分类ID
*/
private String categoryId;
/**
* 商品名称
*/
private String name;
/**
* 规格型号
*/
private String spec;
/**
* 单位
*/
private String unit;
/**
* 销售单价(原价)
*/
private BigDecimal price;
/**
* 成本价
*/
private BigDecimal costPrice;
/**
* 商品图片URL
*/
private String imageUrl;
/**
* 商品条码
*/
private String barcode;
/**
* 库存预警阈值
*/
private Integer stockAlert;
/**
* 商品描述
*/
private String description;
/**
* 状态: 1正常 0删除
*/
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,40 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 库存实体类
*/
@Data
@TableName("stock")
public class Stock {
@TableId(type = IdType.ASSIGN_UUID)
private String stockId;
/**
* 商品ID
*/
private String productId;
/**
* 仓库ID
*/
private String warehouseId;
/**
* 当前库存数量
*/
private Integer quantity;
/**
* 锁定数量(待发货)
*/
private Integer lockedQuantity;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,66 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 库存流水实体类
* 记录库存变动历史
*/
@Data
@TableName("stock_flow")
public class StockFlow {
@TableId(type = IdType.ASSIGN_UUID)
private String flowId;
/**
* 商品ID
*/
private String productId;
/**
* 类型: 1入库 2出库 3调整 4盘点
*/
private Integer type;
/**
* 变动数量(正数增加/负数减少)
*/
private Integer quantity;
/**
* 变动前数量
*/
private Integer beforeQuantity;
/**
* 变动后数量
*/
private Integer afterQuantity;
/**
* 关联单据ID
*/
private String relatedId;
/**
* 关联单据类型
*/
private String relatedType;
/**
* 操作人ID
*/
private String operatorId;
/**
* 备注
*/
private String remark;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,55 @@
package com.example.building.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 用户实体类
*/
@Data
@TableName("users")
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String userId;
private String username;
private String phone;
private String password;
/**
* 微信OpenID
*/
private String wechatOpenid;
/**
* 微信UnionID
*/
private String wechatUnionid;
/**
* 支付宝OpenID
*/
private String alipayOpenid;
/**
* 角色: admin-管理员, sales-销售员, warehouse-库管
*/
private String role;
/**
* 状态: 1正常 0禁用
*/
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.Category;
import org.apache.ibatis.annotations.Mapper;
/**
* 分类Mapper接口
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.Customer;
import org.apache.ibatis.annotations.Mapper;
/**
* 客户Mapper接口
*/
@Mapper
public interface CustomerMapper extends BaseMapper<Customer> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.OrderItem;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单明细Mapper接口
*/
@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.Order;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单Mapper接口
*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.Product;
import org.apache.ibatis.annotations.Mapper;
/**
* 商品Mapper接口
*/
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.StockFlow;
import org.apache.ibatis.annotations.Mapper;
/**
* 库存流水Mapper接口
*/
@Mapper
public interface StockFlowMapper extends BaseMapper<StockFlow> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.Stock;
import org.apache.ibatis.annotations.Mapper;
/**
* 库存Mapper接口
*/
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
}

View File

@@ -0,0 +1,12 @@
package com.example.building.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.building.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户Mapper接口
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@@ -0,0 +1,44 @@
package com.example.building.service;
import java.util.Map;
/**
* 认证服务接口
*/
public interface AuthService {
/**
* 发送验证码
*/
void sendCode(String phone);
/**
* 手机号验证码登录
*/
Map<String, Object> phoneLogin(String phone, String code);
/**
* 微信扫码登录
*/
Map<String, Object> wechatLogin(String code);
/**
* 支付宝扫码登录
*/
Map<String, Object> alipayLogin(String code);
/**
* 刷新Token
*/
Map<String, Object> refreshToken(String refreshToken);
/**
* 获取当前用户信息
*/
Map<String, Object> getCurrentUser(String userId);
/**
* 退出登录
*/
void logout(String token);
}

View File

@@ -0,0 +1,35 @@
package com.example.building.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.entity.Customer;
/**
* 客户服务接口
*/
public interface CustomerService {
/**
* 客户列表
*/
Page<Customer> getCustomers(String keyword, Integer page, Integer pageSize);
/**
* 客户详情
*/
Customer getCustomer(String id);
/**
* 新增客户
*/
Customer createCustomer(Customer customer);
/**
* 修改客户
*/
Customer updateCustomer(String id, Customer customer);
/**
* 删除客户
*/
void deleteCustomer(String id);
}

View File

@@ -0,0 +1,48 @@
package com.example.building.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.dto.CreateOrderRequest;
import com.example.building.entity.Order;
import java.util.Map;
/**
* 订单服务接口
* 核心业务:订单创建、价格计算(原价/优惠/实付)
*/
public interface OrderService {
/**
* 创建订单
* 核心逻辑:
* 1. 计算订单原价(total_amount) = Σ(item.price × item.quantity)
* 2. 计算优惠金额(discount_amount) = total_amount × (100 - discount_rate) / 100
* 3. 计算实付金额(actual_amount) = total_amount - discount_amount
*/
Order createOrder(CreateOrderRequest request, String operatorId, String operatorName);
/**
* 获取订单列表
*/
Page<Order> getOrders(String customerId, Integer status, String startDate, String endDate, Integer page, Integer pageSize);
/**
* 获取订单详情(含明细)
*/
Map<String, Object> getOrderDetail(String orderId);
/**
* 取消订单
*/
void cancelOrder(String orderId, String operatorId);
/**
* 退款
*/
void refundOrder(String orderId, String operatorId);
/**
* 订单统计
*/
Map<String, Object> getStatistics(String startDate, String endDate);
}

View File

@@ -0,0 +1,64 @@
package com.example.building.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.entity.Category;
import com.example.building.entity.Product;
import java.util.List;
import java.util.Map;
/**
* 商品服务接口
*/
public interface ProductService {
/**
* 获取分类列表
*/
List<Category> getCategories();
/**
* 新增分类
*/
Category createCategory(Category category);
/**
* 修改分类
*/
Category updateCategory(String id, Category category);
/**
* 删除分类
*/
void deleteCategory(String id);
/**
* 获取商品列表
*/
Page<Product> getProducts(String categoryId, String keyword, Integer page, Integer pageSize);
/**
* 获取商品详情
*/
Product getProduct(String id);
/**
* 新增商品
*/
Product createProduct(Product product);
/**
* 修改商品
*/
Product updateProduct(String id, Product product);
/**
* 删除商品
*/
void deleteProduct(String id);
/**
* 获取库存预警商品
*/
List<Map<String, Object>> getStockAlerts();
}

View File

@@ -0,0 +1,40 @@
package com.example.building.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.entity.Stock;
import com.example.building.entity.StockFlow;
import java.util.List;
import java.util.Map;
/**
* 库存服务接口
* 核心业务:入库、库存查询、库存流水
*/
public interface StockService {
/**
* 入库
*/
Stock stockIn(String productId, Integer quantity, String remark, String operatorId);
/**
* 库存查询
*/
Page<Stock> getStockList(String keyword, Integer page, Integer pageSize);
/**
* 单商品库存
*/
Stock getStock(String productId);
/**
* 库存调整
*/
Stock adjustStock(String productId, Integer quantity, String remark, String operatorId);
/**
* 库存流水
*/
Page<StockFlow> getStockFlow(String productId, Integer type, Integer page, Integer pageSize);
}

View File

@@ -0,0 +1,189 @@
package com.example.building.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.building.common.JwtUtil;
import com.example.building.entity.User;
import com.example.building.mapper.UserMapper;
import com.example.building.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 认证服务实现类
* 支持:手机号验证码登录、微信扫码登录、支付宝扫码登录
*/
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private UserMapper userMapper;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${jwt.expiration:7200000}")
private Long expiration;
/**
* 发送验证码
* 实际生产中应调用阿里云短信服务
*/
@Override
public void sendCode(String phone) {
// 生成6位随机验证码
String code = String.format("%06d", (int) (Math.random() * 1000000));
// 存入Redis5分钟有效
redisTemplate.opsForValue().set("sms:code:" + phone, code, 5, TimeUnit.MINUTES);
// TODO: 调用短信服务发送验证码
System.out.println("验证码已发送: " + phone + " - " + code);
}
/**
* 手机号验证码登录
*/
@Override
public Map<String, Object> phoneLogin(String phone, String code) {
// 验证验证码
String savedCode = (String) redisTemplate.opsForValue().get("sms:code:" + phone);
if (savedCode == null || !savedCode.equals(code)) {
throw new RuntimeException("验证码错误或已过期");
}
// 查询用户,不存在则创建
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getPhone, phone));
if (user == null) {
user = new User();
user.setUserId(UUID.randomUUID().toString());
user.setPhone(phone);
user.setUsername("用户" + phone.substring(7));
user.setRole("sales");
user.setStatus(1);
userMapper.insert(user);
}
// 生成Token
return generateTokens(user);
}
/**
* 微信扫码登录
* 实际生产中需要调用微信API获取openid
*/
@Override
public Map<String, Object> wechatLogin(String code) {
// TODO: 调用微信API获取openid
// String openid = wechatService.getOpenId(code);
String openid = "wechat_" + code;
// 查询用户,不存在则创建
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getWechatOpenid, openid));
if (user == null) {
user = new User();
user.setUserId(UUID.randomUUID().toString());
user.setWechatOpenid(openid);
user.setUsername("微信用户");
user.setRole("sales");
user.setStatus(1);
userMapper.insert(user);
}
return generateTokens(user);
}
/**
* 支付宝扫码登录
* 实际生产中需要调用支付宝API获取openid
*/
@Override
public Map<String, Object> alipayLogin(String code) {
// TODO: 调用支付宝API获取openid
// String openid = alipayService.getOpenId(code);
String openid = "alipay_" + code;
// 查询用户,不存在则创建
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getAlipayOpenid, openid));
if (user == null) {
user = new User();
user.setUserId(UUID.randomUUID().toString());
user.setAlipayOpenid(openid);
user.setUsername("支付宝用户");
user.setRole("sales");
user.setStatus(1);
userMapper.insert(user);
}
return generateTokens(user);
}
/**
* 刷新Token
*/
@Override
public Map<String, Object> refreshToken(String refreshToken) {
if (!jwtUtil.validateToken(refreshToken)) {
throw new RuntimeException("刷新Token无效");
}
String userId = jwtUtil.getUserId(refreshToken);
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
return generateTokens(user);
}
/**
* 获取当前用户信息
*/
@Override
public Map<String, Object> getCurrentUser(String userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
Map<String, Object> result = new HashMap<>();
result.put("userId", user.getUserId());
result.put("username", user.getUsername());
result.put("phone", user.getPhone());
result.put("role", user.getRole());
return result;
}
/**
* 退出登录
*/
@Override
public void logout(String token) {
// 将token加入黑名单
redisTemplate.opsForValue().set("blacklist:" + token, "1", 2, TimeUnit.HOURS);
}
/**
* 生成Token和RefreshToken
*/
private Map<String, Object> generateTokens(User user) {
String token = jwtUtil.generateToken(user.getUserId(), user.getUsername(), user.getRole());
String refreshToken = jwtUtil.generateRefreshToken(user.getUserId());
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("refreshToken", refreshToken);
result.put("userId", user.getUserId());
result.put("username", user.getUsername());
result.put("role", user.getRole());
return result;
}
}

View File

@@ -0,0 +1,84 @@
package com.example.building.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.entity.Customer;
import com.example.building.mapper.CustomerMapper;
import com.example.building.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.UUID;
/**
* 客户服务实现类
*/
@Service
public class CustomerServiceImpl implements CustomerService {
@Autowired
private CustomerMapper customerMapper;
/**
* 客户列表
*/
@Override
public Page<Customer> getCustomers(String keyword, Integer page, Integer pageSize) {
Page<Customer> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.like(Customer::getName, keyword)
.or()
.like(Customer::getPhone, keyword);
}
wrapper.orderByDesc(Customer::getCreatedAt);
return customerMapper.selectPage(pageParam, wrapper);
}
/**
* 客户详情
*/
@Override
public Customer getCustomer(String id) {
Customer customer = customerMapper.selectById(id);
if (customer == null) {
throw new RuntimeException("客户不存在");
}
return customer;
}
/**
* 新增客户
*/
@Override
public Customer createCustomer(Customer customer) {
customer.setCustomerId(UUID.randomUUID().toString());
customer.setTotalAmount(BigDecimal.ZERO);
customerMapper.insert(customer);
return customer;
}
/**
* 修改客户
*/
@Override
public Customer updateCustomer(String id, Customer customer) {
Customer existing = customerMapper.selectById(id);
if (existing == null) {
throw new RuntimeException("客户不存在");
}
customer.setCustomerId(id);
customerMapper.updateById(customer);
return customer;
}
/**
* 删除客户
*/
@Override
public void deleteCustomer(String id) {
customerMapper.deleteById(id);
}
}

View File

@@ -0,0 +1,349 @@
package com.example.building.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.dto.CreateOrderRequest;
import com.example.building.entity.*;
import com.example.building.mapper.*;
import com.example.building.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 订单服务实现类
* 核心业务逻辑:
* 1. 订单原价(total_amount) = Σ(item.price × item.quantity)
* 2. 优惠金额(discount_amount) = total_amount × (100 - discount_rate) / 100
* 3. 实付金额(actual_amount) = total_amount - discount_amount
*/
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private ProductMapper productMapper;
@Autowired
private StockMapper stockMapper;
@Autowired
private StockFlowMapper stockFlowMapper;
@Autowired
private CustomerMapper customerMapper;
/**
* 创建订单
* 核心:价格计算、库存扣减、客户累计消费更新
*/
@Override
@Transactional
public Order createOrder(CreateOrderRequest request, String operatorId, String operatorName) {
// 1. 准备订单数据
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString());
order.setOrderNo(generateOrderNo());
order.setOperatorId(operatorId);
order.setOperatorName(operatorName);
order.setDiscountRate(request.getDiscountRate() != null ? request.getDiscountRate() : new BigDecimal("100"));
order.setRemark(request.getRemark());
order.setPaymentMethod(request.getPaymentMethod());
order.setStatus(1); // 已完成
// 2. 查询客户信息(如果指定了客户)
if (request.getCustomerId() != null) {
Customer customer = customerMapper.selectById(request.getCustomerId());
if (customer != null) {
order.setCustomerId(customer.getCustomerId());
order.setCustomerName(customer.getName());
order.setCustomerPhone(customer.getPhone());
order.setCustomerWechat(customer.getWechatOpenid());
}
}
// 3. 计算订单金额
BigDecimal totalAmount = BigDecimal.ZERO; // 原价
List<OrderItem> orderItems = new ArrayList<>();
for (CreateOrderRequest.OrderItemDTO itemDTO : request.getItems()) {
// 查询商品信息
Product product = productMapper.selectById(itemDTO.getProductId());
if (product == null) {
throw new RuntimeException("商品不存在: " + itemDTO.getProductId());
}
// 使用用户指定价格或商品标价
BigDecimal price = itemDTO.getPrice() != null ? itemDTO.getPrice() : product.getPrice();
// 计算小计
BigDecimal subtotal = price.multiply(new BigDecimal(itemDTO.getQuantity()))
.setScale(2, RoundingMode.HALF_UP);
// 累加原价
totalAmount = totalAmount.add(subtotal);
// 构建订单明细
OrderItem item = new OrderItem();
item.setItemId(UUID.randomUUID().toString());
item.setOrderId(order.getOrderId());
item.setProductId(product.getProductId());
item.setProductName(product.getName());
item.setProductSpec(product.getSpec());
item.setUnit(product.getUnit());
item.setPrice(price);
item.setQuantity(itemDTO.getQuantity());
item.setSubtotal(subtotal);
orderItems.add(item);
// 4. 扣减库存
decreaseStock(product.getProductId(), itemDTO.getQuantity(), order.getOrderId(), operatorId);
}
// 设置订单原价
order.setTotalAmount(totalAmount);
// 5. 计算优惠金额和实付金额
BigDecimal discountRate = order.getDiscountRate();
BigDecimal discountAmount = totalAmount.multiply(new BigDecimal("100").subtract(discountRate))
.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
BigDecimal actualAmount = totalAmount.subtract(discountAmount);
order.setDiscountAmount(discountAmount);
order.setActualAmount(actualAmount);
// 6. 保存订单
orderMapper.insert(order);
// 7. 保存订单明细
for (OrderItem item : orderItems) {
orderItemMapper.insert(item);
}
// 8. 更新客户累计消费金额
if (request.getCustomerId() != null) {
Customer customer = customerMapper.selectById(request.getCustomerId());
if (customer != null) {
BigDecimal newTotal = customer.getTotalAmount().add(actualAmount);
customer.setTotalAmount(newTotal);
customerMapper.updateById(customer);
}
}
return order;
}
/**
* 获取订单列表
*/
@Override
public Page<Order> getOrders(String customerId, Integer status, String startDate, String endDate, Integer page, Integer pageSize) {
Page<Order> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
if (customerId != null) {
wrapper.eq(Order::getCustomerId, customerId);
}
if (status != null) {
wrapper.eq(Order::getStatus, status);
}
if (startDate != null) {
wrapper.ge(Order::getCreatedAt, startDate);
}
if (endDate != null) {
wrapper.le(Order::getCreatedAt, endDate);
}
wrapper.orderByDesc(Order::getCreatedAt);
return orderMapper.selectPage(pageParam, wrapper);
}
/**
* 获取订单详情(含明细)
*/
@Override
public Map<String, Object> getOrderDetail(String orderId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
List<OrderItem> items = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>()
.eq(OrderItem::getOrderId, orderId));
Map<String, Object> result = new HashMap<>();
result.put("order", order);
result.put("items", items);
return result;
}
/**
* 取消订单
*/
@Override
@Transactional
public void cancelOrder(String orderId, String operatorId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (order.getStatus() != 1) {
throw new RuntimeException("订单状态不允许取消");
}
// 恢复库存
List<OrderItem> items = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>()
.eq(OrderItem::getOrderId, orderId));
for (OrderItem item : items) {
increaseStock(item.getProductId(), item.getQuantity(), orderId, operatorId);
}
// 更新订单状态
order.setStatus(2); // 已取消
orderMapper.updateById(order);
}
/**
* 退款
*/
@Override
@Transactional
public void refundOrder(String orderId, String operatorId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new RuntimeException("订单不存在");
}
if (order.getStatus() != 1) {
throw new RuntimeException("订单状态不允许退款");
}
// 恢复库存
List<OrderItem> items = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>()
.eq(OrderItem::getOrderId, orderId));
for (OrderItem item : items) {
increaseStock(item.getProductId(), item.getQuantity(), orderId, operatorId);
}
// 更新订单状态
order.setStatus(4); // 已退款
orderMapper.updateById(order);
}
/**
* 订单统计
*/
@Override
public Map<String, Object> getStatistics(String startDate, String endDate) {
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getStatus, 1);
if (startDate != null) {
wrapper.ge(Order::getCreatedAt, startDate);
}
if (endDate != null) {
wrapper.le(Order::getCreatedAt, endDate);
}
List<Order> orders = orderMapper.selectList(wrapper);
BigDecimal totalAmount = BigDecimal.ZERO; // 原价合计
BigDecimal actualAmount = BigDecimal.ZERO; // 实付合计
for (Order order : orders) {
totalAmount = totalAmount.add(order.getTotalAmount());
actualAmount = actualAmount.add(order.getActualAmount());
}
Map<String, Object> result = new HashMap<>();
result.put("orderCount", orders.size());
result.put("totalAmount", totalAmount);
result.put("actualAmount", actualAmount);
result.put("discountAmount", totalAmount.subtract(actualAmount));
return result;
}
/**
* 生成订单编号
* 规则: ORD + 年月日 + 6位序号
*/
private String generateOrderNo() {
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
String key = "order:no:" + date;
// 这里简化处理实际应使用Redis自增
int seq = (int) (Math.random() * 1000000);
return "ORD" + date + String.format("%06d", seq);
}
/**
* 扣减库存
*/
private void decreaseStock(String productId, Integer quantity, String relatedId, String operatorId) {
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getProductId, productId));
if (stock == null) {
throw new RuntimeException("库存记录不存在");
}
if (stock.getQuantity() < quantity) {
throw new RuntimeException("库存不足");
}
int beforeQty = stock.getQuantity();
stock.setQuantity(beforeQty - quantity);
stockMapper.updateById(stock);
// 记录库存流水
saveStockFlow(productId, 2, -quantity, beforeQty, beforeQty - quantity, relatedId, "sale", operatorId);
}
/**
* 增加库存
*/
private void increaseStock(String productId, Integer quantity, String relatedId, String operatorId) {
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getProductId, productId));
if (stock == null) {
stock = new Stock();
stock.setStockId(UUID.randomUUID().toString());
stock.setProductId(productId);
stock.setQuantity(0);
stock.setLockedQuantity(0);
stockMapper.insert(stock);
}
int beforeQty = stock.getQuantity();
stock.setQuantity(beforeQty + quantity);
stockMapper.updateById(stock);
// 记录库存流水
saveStockFlow(productId, 1, quantity, beforeQty, beforeQty + quantity, relatedId, "cancel", operatorId);
}
/**
* 保存库存流水
*/
private void saveStockFlow(String productId, Integer type, Integer quantity,
Integer beforeQty, Integer afterQty, String relatedId,
String relatedType, String operatorId) {
StockFlow flow = new StockFlow();
flow.setFlowId(UUID.randomUUID().toString());
flow.setProductId(productId);
flow.setType(type);
flow.setQuantity(quantity);
flow.setBeforeQuantity(beforeQty);
flow.setAfterQuantity(afterQty);
flow.setRelatedId(relatedId);
flow.setRelatedType(relatedType);
flow.setOperatorId(operatorId);
stockFlowMapper.insert(flow);
}
}

View File

@@ -0,0 +1,169 @@
package com.example.building.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.entity.Category;
import com.example.building.entity.Product;
import com.example.building.entity.Stock;
import com.example.building.mapper.CategoryMapper;
import com.example.building.mapper.ProductMapper;
import com.example.building.mapper.StockMapper;
import com.example.building.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
/**
* 商品服务实现类
* 支持商品CRUD和分类管理
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private StockMapper stockMapper;
/**
* 获取分类列表
*/
@Override
public List<Category> getCategories() {
return categoryMapper.selectList(new LambdaQueryWrapper<Category>()
.eq(Category::getStatus, 1)
.orderByAsc(Category::getSortOrder));
}
/**
* 新增分类
*/
@Override
public Category createCategory(Category category) {
category.setCategoryId(UUID.randomUUID().toString());
category.setStatus(1);
categoryMapper.insert(category);
return category;
}
/**
* 修改分类
*/
@Override
public Category updateCategory(String id, Category category) {
Category existing = categoryMapper.selectById(id);
if (existing == null) {
throw new RuntimeException("分类不存在");
}
category.setCategoryId(id);
categoryMapper.updateById(category);
return category;
}
/**
* 删除分类
*/
@Override
public void deleteCategory(String id) {
categoryMapper.deleteById(id);
}
/**
* 获取商品列表
*/
@Override
public Page<Product> getProducts(String categoryId, String keyword, Integer page, Integer pageSize) {
Page<Product> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Product::getStatus, 1);
if (StringUtils.hasText(categoryId)) {
wrapper.eq(Product::getCategoryId, categoryId);
}
if (StringUtils.hasText(keyword)) {
wrapper.like(Product::getName, keyword);
}
wrapper.orderByDesc(Product::getCreatedAt);
return productMapper.selectPage(pageParam, wrapper);
}
/**
* 获取商品详情
*/
@Override
public Product getProduct(String id) {
Product product = productMapper.selectById(id);
if (product == null || product.getStatus() == 0) {
throw new RuntimeException("商品不存在");
}
return product;
}
/**
* 新增商品
*/
@Override
public Product createProduct(Product product) {
product.setProductId(UUID.randomUUID().toString());
product.setStatus(1);
productMapper.insert(product);
return product;
}
/**
* 修改商品
*/
@Override
public Product updateProduct(String id, Product product) {
Product existing = productMapper.selectById(id);
if (existing == null) {
throw new RuntimeException("商品不存在");
}
product.setProductId(id);
productMapper.updateById(product);
return product;
}
/**
* 删除商品(软删)
*/
@Override
public void deleteProduct(String id) {
Product product = new Product();
product.setProductId(id);
product.setStatus(0);
productMapper.updateById(product);
}
/**
* 获取库存预警商品
* 查询库存低于预警值的商品
*/
@Override
public List<Map<String, Object>> getStockAlerts() {
// 查询所有商品及其库存
List<Product> products = productMapper.selectList(new LambdaQueryWrapper<Product>()
.eq(Product::getStatus, 1));
List<Map<String, Object>> alerts = new ArrayList<>();
for (Product product : products) {
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getProductId, product.getProductId()));
int quantity = stock != null ? stock.getQuantity() : 0;
if (quantity < product.getStockAlert()) {
Map<String, Object> alert = new HashMap<>();
alert.put("productId", product.getProductId());
alert.put("productName", product.getName());
alert.put("stockAlert", product.getStockAlert());
alert.put("currentStock", quantity);
alerts.add(alert);
}
}
return alerts;
}
}

View File

@@ -0,0 +1,179 @@
package com.example.building.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.building.entity.Product;
import com.example.building.entity.Stock;
import com.example.building.entity.StockFlow;
import com.example.building.mapper.ProductMapper;
import com.example.building.mapper.StockFlowMapper;
import com.example.building.mapper.StockMapper;
import com.example.building.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.UUID;
/**
* 库存服务实现类
* 核心业务:入库、库存查询、库存流水
*/
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockMapper stockMapper;
@Autowired
private StockFlowMapper stockFlowMapper;
@Autowired
private ProductMapper productMapper;
/**
* 入库
*/
@Override
@Transactional
public Stock stockIn(String productId, Integer quantity, String remark, String operatorId) {
// 验证商品存在
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 查询或创建库存记录
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getProductId, productId));
int beforeQty = 0;
if (stock == null) {
stock = new Stock();
stock.setStockId(UUID.randomUUID().toString());
stock.setProductId(productId);
stock.setQuantity(0);
stock.setLockedQuantity(0);
stockMapper.insert(stock);
} else {
beforeQty = stock.getQuantity();
}
// 更新库存
stock.setQuantity(beforeQty + quantity);
stockMapper.updateById(stock);
// 记录库存流水 (类型1: 入库)
saveStockFlow(productId, 1, quantity, beforeQty, beforeQty + quantity,
null, "stock_in", operatorId, remark);
return stock;
}
/**
* 库存查询
*/
@Override
public Page<Stock> getStockList(String keyword, Integer page, Integer pageSize) {
Page<Stock> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<Stock> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.like(Stock::getProductId, keyword);
}
wrapper.orderByDesc(Stock::getUpdatedAt);
return stockMapper.selectPage(pageParam, wrapper);
}
/**
* 单商品库存
*/
@Override
public Stock getStock(String productId) {
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getProductId, productId));
if (stock == null) {
stock = new Stock();
stock.setProductId(productId);
stock.setQuantity(0);
stock.setLockedQuantity(0);
}
return stock;
}
/**
* 库存调整
*/
@Override
@Transactional
public Stock adjustStock(String productId, Integer quantity, String remark, String operatorId) {
// 验证商品存在
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 查询或创建库存记录
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>()
.eq(Stock::getProductId, productId));
int beforeQty = 0;
if (stock == null) {
stock = new Stock();
stock.setStockId(UUID.randomUUID().toString());
stock.setProductId(productId);
stock.setQuantity(quantity);
stock.setLockedQuantity(0);
stockMapper.insert(stock);
beforeQty = 0;
} else {
beforeQty = stock.getQuantity();
stock.setQuantity(quantity);
stockMapper.updateById(stock);
}
// 记录库存流水 (类型3: 调整)
int changeQty = quantity - beforeQty;
saveStockFlow(productId, 3, changeQty, beforeQty, quantity,
null, "adjust", operatorId, remark);
return stock;
}
/**
* 库存流水
*/
@Override
public Page<StockFlow> getStockFlow(String productId, Integer type, Integer page, Integer pageSize) {
Page<StockFlow> pageParam = new Page<>(page, pageSize);
LambdaQueryWrapper<StockFlow> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(productId)) {
wrapper.eq(StockFlow::getProductId, productId);
}
if (type != null) {
wrapper.eq(StockFlow::getType, type);
}
wrapper.orderByDesc(StockFlow::getCreatedAt);
return stockFlowMapper.selectPage(pageParam, wrapper);
}
/**
* 保存库存流水
*/
private void saveStockFlow(String productId, Integer type, Integer quantity,
Integer beforeQty, Integer afterQty, String relatedId,
String relatedType, String operatorId, String remark) {
StockFlow flow = new StockFlow();
flow.setFlowId(UUID.randomUUID().toString());
flow.setProductId(productId);
flow.setType(type);
flow.setQuantity(quantity);
flow.setBeforeQuantity(beforeQty);
flow.setAfterQuantity(afterQty);
flow.setRelatedId(relatedId);
flow.setRelatedType(relatedType);
flow.setOperatorId(operatorId);
flow.setRemark(remark);
stockFlowMapper.insert(flow);
}
}

View File

@@ -0,0 +1,51 @@
server:
port: 8080
spring:
application:
name: building
# PostgreSQL数据库配置
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/building_materials
username: postgres
password: postgres
# Redis配置
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# MyBatis-Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: assign_uuid
logic-delete-field: status
logic-delete-value: 0
logic-not-delete-value: 1
# JWT配置
jwt:
secret: building-materials-secret-key-2024
expiration: 7200000 # 2小时
refresh-expiration: 604800000 # 7天
# 日志配置
logging:
level:
com.example.building: debug
org.springframework: info