From e7c7f3b174f65ecaad6f6f8bf7d11cb95255ce91 Mon Sep 17 00:00:00 2001 From: Agent Date: Fri, 20 Mar 2026 04:59:00 +0000 Subject: [PATCH] Initial commit: backend code --- Dockerfile | 19 + pom.xml | 122 ++++++ .../example/building/BuildingApplication.java | 17 + .../com/example/building/common/JwtUtil.java | 133 +++++++ .../com/example/building/common/Result.java | 60 +++ .../example/building/config/CorsConfig.java | 23 ++ .../building/config/MybatisPlusConfig.java | 48 +++ .../example/building/config/RedisConfig.java | 44 +++ .../building/controller/AuthController.java | 84 +++++ .../controller/CustomerController.java | 65 ++++ .../building/controller/OrderController.java | 92 +++++ .../controller/ProductController.java | 110 ++++++ .../building/controller/StockController.java | 74 ++++ .../building/dto/CreateOrderRequest.java | 68 ++++ .../example/building/dto/LoginRequest.java | 38 ++ .../com/example/building/entity/Category.java | 49 +++ .../com/example/building/entity/Customer.java | 59 +++ .../com/example/building/entity/Order.java | 101 +++++ .../example/building/entity/OrderItem.java | 61 +++ .../com/example/building/entity/Product.java | 79 ++++ .../com/example/building/entity/Stock.java | 40 ++ .../example/building/entity/StockFlow.java | 66 ++++ .../com/example/building/entity/User.java | 55 +++ .../building/mapper/CategoryMapper.java | 12 + .../building/mapper/CustomerMapper.java | 12 + .../building/mapper/OrderItemMapper.java | 12 + .../example/building/mapper/OrderMapper.java | 12 + .../building/mapper/ProductMapper.java | 12 + .../building/mapper/StockFlowMapper.java | 12 + .../example/building/mapper/StockMapper.java | 12 + .../example/building/mapper/UserMapper.java | 12 + .../example/building/service/AuthService.java | 44 +++ .../building/service/CustomerService.java | 35 ++ .../building/service/OrderService.java | 48 +++ .../building/service/ProductService.java | 64 ++++ .../building/service/StockService.java | 40 ++ .../service/impl/AuthServiceImpl.java | 189 ++++++++++ .../service/impl/CustomerServiceImpl.java | 84 +++++ .../service/impl/OrderServiceImpl.java | 349 ++++++++++++++++++ .../service/impl/ProductServiceImpl.java | 169 +++++++++ .../service/impl/StockServiceImpl.java | 179 +++++++++ src/main/resources/application.yml | 51 +++ 42 files changed, 2855 insertions(+) create mode 100644 Dockerfile create mode 100644 pom.xml create mode 100644 src/main/java/com/example/building/BuildingApplication.java create mode 100644 src/main/java/com/example/building/common/JwtUtil.java create mode 100644 src/main/java/com/example/building/common/Result.java create mode 100644 src/main/java/com/example/building/config/CorsConfig.java create mode 100644 src/main/java/com/example/building/config/MybatisPlusConfig.java create mode 100644 src/main/java/com/example/building/config/RedisConfig.java create mode 100644 src/main/java/com/example/building/controller/AuthController.java create mode 100644 src/main/java/com/example/building/controller/CustomerController.java create mode 100644 src/main/java/com/example/building/controller/OrderController.java create mode 100644 src/main/java/com/example/building/controller/ProductController.java create mode 100644 src/main/java/com/example/building/controller/StockController.java create mode 100644 src/main/java/com/example/building/dto/CreateOrderRequest.java create mode 100644 src/main/java/com/example/building/dto/LoginRequest.java create mode 100644 src/main/java/com/example/building/entity/Category.java create mode 100644 src/main/java/com/example/building/entity/Customer.java create mode 100644 src/main/java/com/example/building/entity/Order.java create mode 100644 src/main/java/com/example/building/entity/OrderItem.java create mode 100644 src/main/java/com/example/building/entity/Product.java create mode 100644 src/main/java/com/example/building/entity/Stock.java create mode 100644 src/main/java/com/example/building/entity/StockFlow.java create mode 100644 src/main/java/com/example/building/entity/User.java create mode 100644 src/main/java/com/example/building/mapper/CategoryMapper.java create mode 100644 src/main/java/com/example/building/mapper/CustomerMapper.java create mode 100644 src/main/java/com/example/building/mapper/OrderItemMapper.java create mode 100644 src/main/java/com/example/building/mapper/OrderMapper.java create mode 100644 src/main/java/com/example/building/mapper/ProductMapper.java create mode 100644 src/main/java/com/example/building/mapper/StockFlowMapper.java create mode 100644 src/main/java/com/example/building/mapper/StockMapper.java create mode 100644 src/main/java/com/example/building/mapper/UserMapper.java create mode 100644 src/main/java/com/example/building/service/AuthService.java create mode 100644 src/main/java/com/example/building/service/CustomerService.java create mode 100644 src/main/java/com/example/building/service/OrderService.java create mode 100644 src/main/java/com/example/building/service/ProductService.java create mode 100644 src/main/java/com/example/building/service/StockService.java create mode 100644 src/main/java/com/example/building/service/impl/AuthServiceImpl.java create mode 100644 src/main/java/com/example/building/service/impl/CustomerServiceImpl.java create mode 100644 src/main/java/com/example/building/service/impl/OrderServiceImpl.java create mode 100644 src/main/java/com/example/building/service/impl/ProductServiceImpl.java create mode 100644 src/main/java/com/example/building/service/impl/StockServiceImpl.java create mode 100644 src/main/resources/application.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d6b708 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..042f490 --- /dev/null +++ b/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.example + building + 1.0.0 + building + 建材销售管家后端服务 + + + 17 + 3.5.3.1 + 0.11.5 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.postgresql + postgresql + runtime + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + + io.jsonwebtoken + jjwt-api + ${jwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jwt.version} + runtime + + + + + org.projectlombok + lombok + true + + + + + org.apache.commons + commons-lang3 + + + + + com.alibaba + fastjson + 2.0.43 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/example/building/BuildingApplication.java b/src/main/java/com/example/building/BuildingApplication.java new file mode 100644 index 0000000..b5d8007 --- /dev/null +++ b/src/main/java/com/example/building/BuildingApplication.java @@ -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); + } +} diff --git a/src/main/java/com/example/building/common/JwtUtil.java b/src/main/java/com/example/building/common/JwtUtil.java new file mode 100644 index 0000000..812c93e --- /dev/null +++ b/src/main/java/com/example/building/common/JwtUtil.java @@ -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 claims = new HashMap<>(); + claims.put("userId", userId); + claims.put("username", username); + claims.put("role", role); + return createToken(claims, userId); + } + + /** + * 创建Token + */ + private String createToken(Map 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; + } + } +} diff --git a/src/main/java/com/example/building/common/Result.java b/src/main/java/com/example/building/common/Result.java new file mode 100644 index 0000000..e2f295d --- /dev/null +++ b/src/main/java/com/example/building/common/Result.java @@ -0,0 +1,60 @@ +package com.example.building.common; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 统一响应结果类 + */ +@Data +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 状态码: 0成功, 非0失败 + */ + private Integer code; + + /** + * 提示信息 + */ + private String message; + + /** + * 业务数据 + */ + private T data; + + public static Result success() { + return success(null); + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(0); + result.setMessage("成功"); + result.setData(data); + return result; + } + + public static Result success(T data, String message) { + Result result = new Result<>(); + result.setCode(0); + result.setMessage(message); + result.setData(data); + return result; + } + + public static Result error(String message) { + return error(3000, message); + } + + public static Result error(Integer code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } +} diff --git a/src/main/java/com/example/building/config/CorsConfig.java b/src/main/java/com/example/building/config/CorsConfig.java new file mode 100644 index 0000000..da309b5 --- /dev/null +++ b/src/main/java/com/example/building/config/CorsConfig.java @@ -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); + } +} diff --git a/src/main/java/com/example/building/config/MybatisPlusConfig.java b/src/main/java/com/example/building/config/MybatisPlusConfig.java new file mode 100644 index 0000000..a8b18c0 --- /dev/null +++ b/src/main/java/com/example/building/config/MybatisPlusConfig.java @@ -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()); + } + }; + } +} diff --git a/src/main/java/com/example/building/config/RedisConfig.java b/src/main/java/com/example/building/config/RedisConfig.java new file mode 100644 index 0000000..ae46dc6 --- /dev/null +++ b/src/main/java/com/example/building/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate 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 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; + } +} diff --git a/src/main/java/com/example/building/controller/AuthController.java b/src/main/java/com/example/building/controller/AuthController.java new file mode 100644 index 0000000..0c1a9b2 --- /dev/null +++ b/src/main/java/com/example/building/controller/AuthController.java @@ -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 sendCode(@RequestParam String phone) { + authService.sendCode(phone); + return Result.success(); + } + + /** + * 手机号验证码登录 + */ + @PostMapping("/phone-login") + public Result> phoneLogin(@RequestParam String phone, @RequestParam String code) { + Map result = authService.phoneLogin(phone, code); + return Result.success(result); + } + + /** + * 微信登录 + */ + @PostMapping("/wechat") + public Result> wechatLogin(@RequestParam String code) { + Map result = authService.wechatLogin(code); + return Result.success(result); + } + + /** + * 支付宝登录 + */ + @PostMapping("/alipay") + public Result> alipayLogin(@RequestParam String code) { + Map result = authService.alipayLogin(code); + return Result.success(result); + } + + /** + * 刷新Token + */ + @PostMapping("/refresh") + public Result> refresh(@RequestParam String refreshToken) { + Map result = authService.refreshToken(refreshToken); + return Result.success(result); + } + + /** + * 获取当前用户 + */ + @GetMapping("/me") + public Result> me(@RequestHeader("X-User-Id") String userId) { + Map result = authService.getCurrentUser(userId); + return Result.success(result); + } + + /** + * 退出登录 + */ + @PostMapping("/logout") + public Result logout(@RequestHeader("Authorization") String token) { + String jwtToken = token.replace("Bearer ", ""); + authService.logout(jwtToken); + return Result.success(); + } +} diff --git a/src/main/java/com/example/building/controller/CustomerController.java b/src/main/java/com/example/building/controller/CustomerController.java new file mode 100644 index 0000000..a14d9e6 --- /dev/null +++ b/src/main/java/com/example/building/controller/CustomerController.java @@ -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> 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 getCustomer(@PathVariable String id) { + return Result.success(customerService.getCustomer(id)); + } + + /** + * 新增客户 + */ + @PostMapping + public Result createCustomer(@RequestBody Customer customer, + @RequestHeader("X-User-Id") String userId) { + customer.setCreatedBy(userId); + return Result.success(customerService.createCustomer(customer)); + } + + /** + * 修改客户 + */ + @PutMapping("/{id}") + public Result updateCustomer(@PathVariable String id, @RequestBody Customer customer) { + return Result.success(customerService.updateCustomer(id, customer)); + } + + /** + * 删除客户 + */ + @DeleteMapping("/{id}") + public Result deleteCustomer(@PathVariable String id) { + customerService.deleteCustomer(id); + return Result.success(); + } +} diff --git a/src/main/java/com/example/building/controller/OrderController.java b/src/main/java/com/example/building/controller/OrderController.java new file mode 100644 index 0000000..0147ac1 --- /dev/null +++ b/src/main/java/com/example/building/controller/OrderController.java @@ -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 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> 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> getOrderDetail(@PathVariable String id) { + return Result.success(orderService.getOrderDetail(id)); + } + + /** + * 取消订单 + */ + @PutMapping("/{id}/cancel") + public Result cancelOrder(@PathVariable String id, + @RequestHeader("X-User-Id") String operatorId) { + orderService.cancelOrder(id, operatorId); + return Result.success(); + } + + /** + * 退款 + */ + @PutMapping("/{id}/refund") + public Result refundOrder(@PathVariable String id, + @RequestHeader("X-User-Id") String operatorId) { + orderService.refundOrder(id, operatorId); + return Result.success(); + } + + /** + * 订单统计 + */ + @GetMapping("/statistics") + public Result> getStatistics( + @RequestParam(required = false) String startDate, + @RequestParam(required = false) String endDate) { + return Result.success(orderService.getStatistics(startDate, endDate)); + } +} diff --git a/src/main/java/com/example/building/controller/ProductController.java b/src/main/java/com/example/building/controller/ProductController.java new file mode 100644 index 0000000..7ce163b --- /dev/null +++ b/src/main/java/com/example/building/controller/ProductController.java @@ -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> getCategories() { + return Result.success(productService.getCategories()); + } + + /** + * 新增分类 + */ + @PostMapping("/categories") + public Result createCategory(@RequestBody Category category) { + return Result.success(productService.createCategory(category)); + } + + /** + * 修改分类 + */ + @PutMapping("/categories/{id}") + public Result updateCategory(@PathVariable String id, @RequestBody Category category) { + return Result.success(productService.updateCategory(id, category)); + } + + /** + * 删除分类 + */ + @DeleteMapping("/categories/{id}") + public Result deleteCategory(@PathVariable String id) { + productService.deleteCategory(id); + return Result.success(); + } + + /** + * 获取商品列表 + */ + @GetMapping + public Result> 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 getProduct(@PathVariable String id) { + return Result.success(productService.getProduct(id)); + } + + /** + * 新增商品 + */ + @PostMapping + public Result createProduct(@RequestBody Product product) { + return Result.success(productService.createProduct(product)); + } + + /** + * 修改商品 + */ + @PutMapping("/{id}") + public Result updateProduct(@PathVariable String id, @RequestBody Product product) { + return Result.success(productService.updateProduct(id, product)); + } + + /** + * 删除商品 + */ + @DeleteMapping("/{id}") + public Result deleteProduct(@PathVariable String id) { + productService.deleteProduct(id); + return Result.success(); + } + + /** + * 获取库存预警商品 + */ + @GetMapping("/alerts") + public Result>> getStockAlerts() { + return Result.success(productService.getStockAlerts()); + } +} diff --git a/src/main/java/com/example/building/controller/StockController.java b/src/main/java/com/example/building/controller/StockController.java new file mode 100644 index 0000000..3afe03d --- /dev/null +++ b/src/main/java/com/example/building/controller/StockController.java @@ -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 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> 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 getStock(@PathVariable String productId) { + return Result.success(stockService.getStock(productId)); + } + + /** + * 库存调整 + */ + @PostMapping("/adjust") + public Result 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> 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)); + } +} diff --git a/src/main/java/com/example/building/dto/CreateOrderRequest.java b/src/main/java/com/example/building/dto/CreateOrderRequest.java new file mode 100644 index 0000000..a32e8f2 --- /dev/null +++ b/src/main/java/com/example/building/dto/CreateOrderRequest.java @@ -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 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; + } +} diff --git a/src/main/java/com/example/building/dto/LoginRequest.java b/src/main/java/com/example/building/dto/LoginRequest.java new file mode 100644 index 0000000..c7688ed --- /dev/null +++ b/src/main/java/com/example/building/dto/LoginRequest.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/Category.java b/src/main/java/com/example/building/entity/Category.java new file mode 100644 index 0000000..f854074 --- /dev/null +++ b/src/main/java/com/example/building/entity/Category.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/Customer.java b/src/main/java/com/example/building/entity/Customer.java new file mode 100644 index 0000000..3f4de89 --- /dev/null +++ b/src/main/java/com/example/building/entity/Customer.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/Order.java b/src/main/java/com/example/building/entity/Order.java new file mode 100644 index 0000000..97e9065 --- /dev/null +++ b/src/main/java/com/example/building/entity/Order.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/OrderItem.java b/src/main/java/com/example/building/entity/OrderItem.java new file mode 100644 index 0000000..317c015 --- /dev/null +++ b/src/main/java/com/example/building/entity/OrderItem.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/Product.java b/src/main/java/com/example/building/entity/Product.java new file mode 100644 index 0000000..e7588df --- /dev/null +++ b/src/main/java/com/example/building/entity/Product.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/Stock.java b/src/main/java/com/example/building/entity/Stock.java new file mode 100644 index 0000000..b46f9f8 --- /dev/null +++ b/src/main/java/com/example/building/entity/Stock.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/StockFlow.java b/src/main/java/com/example/building/entity/StockFlow.java new file mode 100644 index 0000000..964af8d --- /dev/null +++ b/src/main/java/com/example/building/entity/StockFlow.java @@ -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; +} diff --git a/src/main/java/com/example/building/entity/User.java b/src/main/java/com/example/building/entity/User.java new file mode 100644 index 0000000..329f05a --- /dev/null +++ b/src/main/java/com/example/building/entity/User.java @@ -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; +} diff --git a/src/main/java/com/example/building/mapper/CategoryMapper.java b/src/main/java/com/example/building/mapper/CategoryMapper.java new file mode 100644 index 0000000..254e3df --- /dev/null +++ b/src/main/java/com/example/building/mapper/CategoryMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/CustomerMapper.java b/src/main/java/com/example/building/mapper/CustomerMapper.java new file mode 100644 index 0000000..cf3b39c --- /dev/null +++ b/src/main/java/com/example/building/mapper/CustomerMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/OrderItemMapper.java b/src/main/java/com/example/building/mapper/OrderItemMapper.java new file mode 100644 index 0000000..a13b59f --- /dev/null +++ b/src/main/java/com/example/building/mapper/OrderItemMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/OrderMapper.java b/src/main/java/com/example/building/mapper/OrderMapper.java new file mode 100644 index 0000000..4b05371 --- /dev/null +++ b/src/main/java/com/example/building/mapper/OrderMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/ProductMapper.java b/src/main/java/com/example/building/mapper/ProductMapper.java new file mode 100644 index 0000000..1611b7b --- /dev/null +++ b/src/main/java/com/example/building/mapper/ProductMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/StockFlowMapper.java b/src/main/java/com/example/building/mapper/StockFlowMapper.java new file mode 100644 index 0000000..497fb8f --- /dev/null +++ b/src/main/java/com/example/building/mapper/StockFlowMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/StockMapper.java b/src/main/java/com/example/building/mapper/StockMapper.java new file mode 100644 index 0000000..eb59550 --- /dev/null +++ b/src/main/java/com/example/building/mapper/StockMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/mapper/UserMapper.java b/src/main/java/com/example/building/mapper/UserMapper.java new file mode 100644 index 0000000..d62baa8 --- /dev/null +++ b/src/main/java/com/example/building/mapper/UserMapper.java @@ -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 { +} diff --git a/src/main/java/com/example/building/service/AuthService.java b/src/main/java/com/example/building/service/AuthService.java new file mode 100644 index 0000000..f74b6ef --- /dev/null +++ b/src/main/java/com/example/building/service/AuthService.java @@ -0,0 +1,44 @@ +package com.example.building.service; + +import java.util.Map; + +/** + * 认证服务接口 + */ +public interface AuthService { + + /** + * 发送验证码 + */ + void sendCode(String phone); + + /** + * 手机号验证码登录 + */ + Map phoneLogin(String phone, String code); + + /** + * 微信扫码登录 + */ + Map wechatLogin(String code); + + /** + * 支付宝扫码登录 + */ + Map alipayLogin(String code); + + /** + * 刷新Token + */ + Map refreshToken(String refreshToken); + + /** + * 获取当前用户信息 + */ + Map getCurrentUser(String userId); + + /** + * 退出登录 + */ + void logout(String token); +} diff --git a/src/main/java/com/example/building/service/CustomerService.java b/src/main/java/com/example/building/service/CustomerService.java new file mode 100644 index 0000000..4641d20 --- /dev/null +++ b/src/main/java/com/example/building/service/CustomerService.java @@ -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 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); +} diff --git a/src/main/java/com/example/building/service/OrderService.java b/src/main/java/com/example/building/service/OrderService.java new file mode 100644 index 0000000..aaf5bd6 --- /dev/null +++ b/src/main/java/com/example/building/service/OrderService.java @@ -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 getOrders(String customerId, Integer status, String startDate, String endDate, Integer page, Integer pageSize); + + /** + * 获取订单详情(含明细) + */ + Map getOrderDetail(String orderId); + + /** + * 取消订单 + */ + void cancelOrder(String orderId, String operatorId); + + /** + * 退款 + */ + void refundOrder(String orderId, String operatorId); + + /** + * 订单统计 + */ + Map getStatistics(String startDate, String endDate); +} diff --git a/src/main/java/com/example/building/service/ProductService.java b/src/main/java/com/example/building/service/ProductService.java new file mode 100644 index 0000000..60bf9c9 --- /dev/null +++ b/src/main/java/com/example/building/service/ProductService.java @@ -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 getCategories(); + + /** + * 新增分类 + */ + Category createCategory(Category category); + + /** + * 修改分类 + */ + Category updateCategory(String id, Category category); + + /** + * 删除分类 + */ + void deleteCategory(String id); + + /** + * 获取商品列表 + */ + Page 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> getStockAlerts(); +} diff --git a/src/main/java/com/example/building/service/StockService.java b/src/main/java/com/example/building/service/StockService.java new file mode 100644 index 0000000..dbb5d9b --- /dev/null +++ b/src/main/java/com/example/building/service/StockService.java @@ -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 getStockList(String keyword, Integer page, Integer pageSize); + + /** + * 单商品库存 + */ + Stock getStock(String productId); + + /** + * 库存调整 + */ + Stock adjustStock(String productId, Integer quantity, String remark, String operatorId); + + /** + * 库存流水 + */ + Page getStockFlow(String productId, Integer type, Integer page, Integer pageSize); +} diff --git a/src/main/java/com/example/building/service/impl/AuthServiceImpl.java b/src/main/java/com/example/building/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..b01aa7a --- /dev/null +++ b/src/main/java/com/example/building/service/impl/AuthServiceImpl.java @@ -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 redisTemplate; + + @Value("${jwt.expiration:7200000}") + private Long expiration; + + /** + * 发送验证码 + * 实际生产中应调用阿里云短信服务 + */ + @Override + public void sendCode(String phone) { + // 生成6位随机验证码 + String code = String.format("%06d", (int) (Math.random() * 1000000)); + // 存入Redis,5分钟有效 + redisTemplate.opsForValue().set("sms:code:" + phone, code, 5, TimeUnit.MINUTES); + // TODO: 调用短信服务发送验证码 + System.out.println("验证码已发送: " + phone + " - " + code); + } + + /** + * 手机号验证码登录 + */ + @Override + public Map 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() + .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 wechatLogin(String code) { + // TODO: 调用微信API获取openid + // String openid = wechatService.getOpenId(code); + String openid = "wechat_" + code; + + // 查询用户,不存在则创建 + User user = userMapper.selectOne(new LambdaQueryWrapper() + .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 alipayLogin(String code) { + // TODO: 调用支付宝API获取openid + // String openid = alipayService.getOpenId(code); + String openid = "alipay_" + code; + + // 查询用户,不存在则创建 + User user = userMapper.selectOne(new LambdaQueryWrapper() + .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 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 getCurrentUser(String userId) { + User user = userMapper.selectById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + Map 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 generateTokens(User user) { + String token = jwtUtil.generateToken(user.getUserId(), user.getUsername(), user.getRole()); + String refreshToken = jwtUtil.generateRefreshToken(user.getUserId()); + + Map 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; + } +} diff --git a/src/main/java/com/example/building/service/impl/CustomerServiceImpl.java b/src/main/java/com/example/building/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..bbb38af --- /dev/null +++ b/src/main/java/com/example/building/service/impl/CustomerServiceImpl.java @@ -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 getCustomers(String keyword, Integer page, Integer pageSize) { + Page pageParam = new Page<>(page, pageSize); + LambdaQueryWrapper 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); + } +} diff --git a/src/main/java/com/example/building/service/impl/OrderServiceImpl.java b/src/main/java/com/example/building/service/impl/OrderServiceImpl.java new file mode 100644 index 0000000..e834cee --- /dev/null +++ b/src/main/java/com/example/building/service/impl/OrderServiceImpl.java @@ -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 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 getOrders(String customerId, Integer status, String startDate, String endDate, Integer page, Integer pageSize) { + Page pageParam = new Page<>(page, pageSize); + LambdaQueryWrapper 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 getOrderDetail(String orderId) { + Order order = orderMapper.selectById(orderId); + if (order == null) { + throw new RuntimeException("订单不存在"); + } + + List items = orderItemMapper.selectList(new LambdaQueryWrapper() + .eq(OrderItem::getOrderId, orderId)); + + Map 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 items = orderItemMapper.selectList(new LambdaQueryWrapper() + .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 items = orderItemMapper.selectList(new LambdaQueryWrapper() + .eq(OrderItem::getOrderId, orderId)); + for (OrderItem item : items) { + increaseStock(item.getProductId(), item.getQuantity(), orderId, operatorId); + } + + // 更新订单状态 + order.setStatus(4); // 已退款 + orderMapper.updateById(order); + } + + /** + * 订单统计 + */ + @Override + public Map getStatistics(String startDate, String endDate) { + LambdaQueryWrapper 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 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 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() + .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() + .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); + } +} diff --git a/src/main/java/com/example/building/service/impl/ProductServiceImpl.java b/src/main/java/com/example/building/service/impl/ProductServiceImpl.java new file mode 100644 index 0000000..1fbcdc5 --- /dev/null +++ b/src/main/java/com/example/building/service/impl/ProductServiceImpl.java @@ -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 getCategories() { + return categoryMapper.selectList(new LambdaQueryWrapper() + .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 getProducts(String categoryId, String keyword, Integer page, Integer pageSize) { + Page pageParam = new Page<>(page, pageSize); + LambdaQueryWrapper 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> getStockAlerts() { + // 查询所有商品及其库存 + List products = productMapper.selectList(new LambdaQueryWrapper() + .eq(Product::getStatus, 1)); + + List> alerts = new ArrayList<>(); + for (Product product : products) { + Stock stock = stockMapper.selectOne(new LambdaQueryWrapper() + .eq(Stock::getProductId, product.getProductId())); + int quantity = stock != null ? stock.getQuantity() : 0; + if (quantity < product.getStockAlert()) { + Map 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; + } +} diff --git a/src/main/java/com/example/building/service/impl/StockServiceImpl.java b/src/main/java/com/example/building/service/impl/StockServiceImpl.java new file mode 100644 index 0000000..c288c71 --- /dev/null +++ b/src/main/java/com/example/building/service/impl/StockServiceImpl.java @@ -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() + .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 getStockList(String keyword, Integer page, Integer pageSize) { + Page pageParam = new Page<>(page, pageSize); + LambdaQueryWrapper 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() + .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() + .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 getStockFlow(String productId, Integer type, Integer page, Integer pageSize) { + Page pageParam = new Page<>(page, pageSize); + LambdaQueryWrapper 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); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..ff44e15 --- /dev/null +++ b/src/main/resources/application.yml @@ -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