Post

05_进销存系统_功能实现

05_进销存系统_功能实现 本章节将逐一实现系统的核心功能模块。我们将按照 登录 -> 仪表盘 -> 分类 -> 商品 -> 客户 -> 订单 的顺序进行开发。 在实现过程中,我们会结合实际场景,适时讲解 DTO/VO、MyBatis-Plus 分页、全局时间格式化 以及 常用注解 等核心概念。 一

进销存项目 阅读 0 点赞 0 评论 0

05_进销存系统_功能实现

本章节将逐一实现系统的核心功能模块。我们将按照 登录 -> 仪表盘 -> 分类 -> 商品 -> 客户 -> 订单 的顺序进行开发。

在实现过程中,我们会结合实际场景,适时讲解 DTO/VOMyBatis-Plus 分页全局时间格式化 以及 常用注解 等核心概念。


一、登录模块

登录是系统的入口,主要功能是验证用户身份并创建会话。我们首先通过这个模块来熟悉 Controller 的基本写法和常用注解。

1.1 常用注解说明

在下面的代码中,你会看到许多“陌生”的注解。为了让你不再困惑,这里整理了最常用的几个:

1. Knife4j (接口文档) 注解

用于生成在线接口文档 (http://localhost:8080/doc.html)。

  • @Tag(name = "xxx"): 用在 Controller 类上,表示这个类是关于“xxx”模块的(例如:登录模块)。
  • @Operation(summary = "xxx"): 用在 方法上,说明这个接口是干什么的(例如:用户登录)。
  • @Schema(name = "SysUser",description="系统用户信息"): 用在 实体类上,说明这个类是干什么的。
  • @Schema(name = "id",title = "主键",description = "自动生成用户唯一标识"): 用在 实体类属性上,说明这个属性是干什么的。

2. Spring MVC 注解

  • @RestController: 标记这是一个控制器,并且所有方法返回 JSON 数据。
  • @PostMapping / @GetMapping: 定义 HTTP 请求方式。
  • @RequestBody: 接收前端传递的 JSON 参数。

1.2 全局配置 (逻辑删除 & 时间格式)

为了简化开发,我们在 application.yml 中进行全局配置,解决两个常见问题:

  1. 逻辑删除:MyBatis-Plus 自动处理 is_deleted 字段,查询时自动过滤,删除时自动更新。
  2. 时间格式化:Jackson 自动将 LocalDateTime 格式化为 yyyy-MM-dd HH:mm:ss,无需在每个字段上加注解。

配置文件 (application.yml)

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDeleted  # 全局逻辑删除字段名
      logic-delete-value: 1          # 删除状态的值
      logic-not-delete-value: 0      # 未删除状态的值

1.3 接口实现 (LoginController)

@RestController
@RequestMapping("/api")
@Tag(name = "登录模块") // Knife4j: 模块名称
public class LoginController {

    @Autowired
    private SysUserMapper sysUserMapper;

    @PostMapping("/login")
    @Operation(summary = "用户登录") // Knife4j: 接口描述
    public Result<SysUser> login(@RequestBody SysUser loginUser, HttpSession session) {
        // 1. 构建查询条件:WHERE username = ?
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", loginUser.getUsername());
        
        // 2. 查询数据库
        SysUser user = sysUserMapper.selectOne(queryWrapper);
        
        // 3. 校验密码
        if (user != null && user.getPassword().equals(loginUser.getPassword())) {
            // 登录成功,将用户信息存入 Session
            session.setAttribute("user", user);
            return Result.success("登录成功", user);
        }
        return Result.fail(401, "登录失败:用户名或密码错误");
    }
    
    @GetMapping("/logout")
    @Operation(summary = "用户登出")
    public Result<Object> logout(HttpSession session) {
        session.invalidate(); // 销毁 Session
        return Result.success("退出成功", null);
    }
}

1.4 登录拦截器

为了防止未登录用户访问后台接口,我们需要配置拦截器。

拦截器实现 (LoginInterceptor)

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 放行 OPTIONS 请求 (跨域预检)
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }

        HttpSession session = request.getSession();
        SysUser user = (SysUser) session.getAttribute("user");
        if (user == null) {
            response.setStatus(401);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("{\"code\":401, \"message\":\"未登录,请先登录\"}");
            return false;
        }
        return true;
    }
}

注册拦截器 (WebConfig)

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/api/**") // 拦截所有接口
                .excludePathPatterns("/api/login", "/api/logout"); // 放行登录登出
    }
}

二、仪表盘模块

仪表盘展示系统的关键指标:商品总数、今日订单数、总销售额。

2.1 核心概念:VO (View Object)

在开发中,我们通常不会直接返回数据库实体对象 (Entity) 给前端,而是定义专门的 VO (视图对象)

  • Entity: 对应数据库表结构(如 SaleOrder 表)。
  • VO: 对应前端页面展示需求(如 DashboardVO 聚合了多个表的数据)。

2.2 定义 VO (DashboardVO)

@Data
public class DashboardVO {
    @Schema(description = "商品总数") // Knife4j: 字段描述
    private Long productCount;
    
    @Schema(description = "今日订单数")
    private Long todayOrderCount;
    
    @Schema(description = "今日销售额")
    private BigDecimal totalSales;
}

2.3 业务逻辑 (DashboardServiceImpl)

Service 层负责处理复杂的业务逻辑,Controller 层只负责接收参数和返回结果。

@Service
public class DashboardServiceImpl implements DashboardService {
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private SaleOrderMapper saleOrderMapper;

    @Override
    public DashboardVO getStats() {
        DashboardVO vo = new DashboardVO();
        
        // 1. 统计商品总数 (MyBatis-Plus 会自动过滤 is_deleted=1 的数据)
        vo.setProductCount(productMapper.selectCount(null));
        
        // 2. 统计今日订单
        // 获取今天的开始和结束时间
        LocalDateTime start = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
        LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
        
        vo.setTodayOrderCount(saleOrderMapper.selectCount(new QueryWrapper<SaleOrder>()
                .between("create_time", start, end))); 
                
        // 3. 统计总销售额 (需要自定义SQL)
        vo.setTotalSales(saleOrderMapper.selectTotalSales());
        
        return vo;
    }
}

2.4 数据层实现 (SaleOrderMapper)

由于统计总销售额涉及到求和操作,这是 MyBatis-Plus 默认方法无法直接覆盖的,我们需要在 Mapper 接口中使用注解或 XML 编写 SQL。

@Mapper
public interface SaleOrderMapper extends BaseMapper<SaleOrder> {

    /**
     * 统计所有有效订单的总金额
     * 注意:使用 @Select 注解手写 SQL 时,MyBatis-Plus 的自动逻辑删除过滤不会生效,必须手动添加 WHERE is_deleted = 0
     * @return 总销售额
     */
    @Select("SELECT IFNULL(SUM(total_amount), 0) FROM sale_order WHERE is_deleted = 0 AND status != 2")
    BigDecimal selectTotalSales();
}

2.5 接口实现 (DashboardController)

@RestController
@RequestMapping("/api/dashboard")
@Tag(name = "仪表盘")
public class DashboardController {
    @Autowired
    private DashboardService dashboardService;

    @GetMapping("/stats")
    @Operation(summary = "获取统计数据")
    public Result<DashboardVO> getStats() {
        // 调用 Service 层方法
        return Result.success(dashboardService.getStats());
    }
}

三、分类管理 (逻辑删除)

3.1 实体类 (Category)

得益于我们在 1.2 节 配置的全局逻辑删除,我们只需要在实体类字段上加上 @TableLogic 注解即可。

  • 查询时:MP 自动追加 WHERE is_deleted = 0
  • 删除时:MP 自动执行 UPDATE ... SET is_deleted = 1
@Data
@TableName("category")
public class Category {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    priavte Integer sort;
    private String description;
    private Integer status;
    @Schema(name = "updateTime", title = "更新时间", description = "分类信息最后修改时间", readOnly = true)
    private LocalDateTime updateTime;
    
    
    @TableLogic  // 核心注解:标记这是逻辑删除字段
    @JsonIgnore  // 核心注解:前端不需要看到这个字段
    private Integer isDeleted;
    
}

3.2 接口实现 (CategoryController)

MyBatis-Plus 会自动处理逻辑删除,调用 removeById 实际上执行的是 UPDATE

@RestController
@RequestMapping("/api/categories")
@Tag(name = "分类管理")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    @GetMapping
    @Operation(summary = "获取所有分类")
    public Result<List<Category>> list() {
        // 自动追加 WHERE is_deleted = 0
        return Result.success(categoryService.list());
    }

    @PostMapping
    @Operation(summary = "添加分类")
    public Result<Object> add(@RequestBody Category category) {
        return categoryService.save(category) ? Result.success(null) : Result.fail("添加失败");
    }

    @PutMapping
    @Operation(summary = "更新分类")
    public Result<Object> update(@RequestBody Category category) {
        return categoryService.updateById(category) ? Result.success(null) : Result.fail("更新失败");
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "删除分类")
    public Result<Object> delete(@PathVariable Long id) {
        // 自动执行 UPDATE category SET is_deleted=1 WHERE id=? AND is_deleted=0
        return categoryService.removeById(id) ? Result.success(null) : Result.fail("删除失败");
    }
}

四、商品管理 (分页插件与关联查询)

商品列表需要分页展示,并且需要显示“分类名称”(数据库只有 category_id),这是学习 分页配置关联查询 的好机会。

4.1 核心概念:MyBatis-Plus 分页配置

如果不配置分页插件,调用分页接口时 total(总记录数)将始终为 0,分页功能失效。

创建 config/MybatisPlusConfig.java

@Configuration // 标记为配置类
public class MybatisPlusConfig {

    /**
     * 添加分页拦截器
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 指定数据库类型为 MySQL,开启分页功能
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

4.2 定义 ProductVO

我们需要在商品列表中展示分类名称。

@Data
public class ProductVO {
    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stock;
    
    @Schema(description = "分类名称 (数据库中无此字段)")
    private String categoryName; 
    private Integer status; //状态
    // Jackson注解:格式化时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private LocalDateTime createTime;
}

4.3 数据层实现 (ProductMapper)

关联查询也需要我们自定义 SQL。

1. Mapper 接口 (ProductMapper.java)

@Mapper
public interface ProductMapper extends BaseMapper<Product> {
    
    /**
     * 分页查询商品列表(包含分类名称)
     * @param page 分页参数
     * @param name 商品名称 (可选)
     * @param categoryId 分类ID (可选)
     * @return 分页结果
     */
    IPage<ProductVO> selectProductList(Page<ProductVO> page, 
                                     @Param("name") String name, 
                                     @Param("categoryId") Long categoryId);
}

2. Mapper XML (ProductMapper.xml)
编写 SQL 语句,通过 LEFT JOIN 关联分类表。

<select id="selectProductList" resultType="com.atguigu.liteims.vo.ProductVO">
    SELECT p.*, c.name AS category_name
    FROM product p
    LEFT JOIN category c ON p.category_id = c.id
    WHERE p.is_deleted = 0
    <if test="name != null"> AND p.name LIKE CONCAT('%', #{name}, '%') </if>
    <if test="categoryId != null"> AND p.category_id = #{categoryId} </if>
    ORDER BY p.create_time DESC
</select>

4.3 业务逻辑层 (ProductService)

虽然 MyBatis-Plus 提供了基础的 CRUD,但对于自定义的分页查询,我们建议封装在 Service 层,保持 Controller 的整洁。

1. Service 接口 (ProductService.java)

public interface ProductService extends IService<Product> {
    /**
     * 分页查询商品(包含分类名称)
     */
    IPage<ProductVO> getProductList(Page<ProductVO> page, String name, Long categoryId);
}

2. Service 实现类 (ProductServiceImpl.java)

@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public IPage<ProductVO> getProductList(Page<ProductVO> page, String name, Long categoryId) {
        // 直接调用 Mapper 自定义方法
        return productMapper.selectProductList(page, name, categoryId);
    }
}

4.4 接口实现 (ProductController)

@RestController
@RequestMapping("/api/products")
@Tag(name = "商品管理")
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping
    @Operation(summary = "分页查询商品")
    public Result<IPage<ProductVO>> list(@RequestParam(defaultValue = "1") Integer page,
                   @RequestParam(defaultValue = "10") Integer size,
                   String name, Long categoryId) {
        Page<ProductVO> pageParam = new Page<>(page, size);
        // 调用 Service 层方法
        IPage<ProductVO> result = productService.getProductList(pageParam, name, categoryId);
        return Result.success(result);
    }

    @PostMapping
    @Operation(summary = "添加商品")
    public Result<Object> add(@RequestBody Product product) {
        return productService.save(product) ? Result.success(null) : Result.fail("添加失败");
    }

    @PutMapping
    @Operation(summary = "更新商品")
    public Result<Object> update(@RequestBody Product product) {
        return productService.updateById(product) ? Result.success(null) : Result.fail("更新失败");
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "删除商品")
    public Result<Object> delete(@PathVariable Long id) {
        return productService.removeById(id) ? Result.success(null) : Result.fail("删除失败");
    }
}

五、客户管理 (标准CRUD)

客户管理是标准的单表增删改查,用于巩固前面的知识。

5.1 接口实现 (CustomerController)

@RestController
@RequestMapping("/api/customers")
@Tag(name = "客户管理")
public class CustomerController {

    @Autowired
    private CustomerService customerService;

    @GetMapping
    @Operation(summary = "获取客户列表")
    public Result<List<Customer>> list() {
        return Result.success(customerService.list());
    }

    @PostMapping
    @Operation(summary = "添加客户")
    public Result<Object> add(@RequestBody Customer customer) {
        return customerService.save(customer) ? Result.success(null) : Result.fail("添加失败");
    }

    @PutMapping
    @Operation(summary = "更新客户")
    public Result<Object> update(@RequestBody Customer customer) {
        return customerService.updateById(customer) ? Result.success(null) : Result.fail("更新失败");
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "删除客户")
    public Result<Object> delete(@PathVariable Long id) {
        return customerService.removeById(id) ? Result.success(null) : Result.fail("删除失败");
    }
}

六、订单管理 (DTO与事务)

订单模块最为复杂,涉及 DTO 接收复杂参数事务控制

6.1 核心概念:DTO (Data Transfer Object)

为什么需要 DTO?

在开发“创建订单”功能时,我们会发现一个问题:前端传递的 JSON 参数结构,与数据库中的任何一张表都无法直接对应。

  • 前端参数结构:是一个复杂的嵌套对象,包含“谁买的” (customerId) 和 “买了什么” (items 数组)。
  • 数据库表结构:订单数据被拆分到了 sale_order (主表) 和 sale_order_item (明细表) 两张表中。

如果强行使用 SaleOrder 实体类来接收参数,就无法接收 items 列表数据。因此,我们需要定义一个专门用于数据传输的对象 —— DTO (Data Transfer Object)

DTO 的作用就是根据前端接口的参数需求,量身定制一个 Java 类,用于接收和封装数据,然后再由 Service 层将其拆解并保存到对应的数据库表中。

OrderDTO 定义

@Data
@Schema(description = "创建订单参数")
public class OrderDTO {
    @Schema(description = "客户ID")
    private Long customerId;
    
    @Schema(description = "系统用户ID")
    private Long userId;
    
    @Schema(description = "商品明细列表")
    private List<OrderItemDTO> items; 

    @Data
    public static class OrderItemDTO {
        @Schema(description = "商品ID")
        private Long productId;
        @Schema(description = "购买数量")
        private Integer quantity;
    }
}

6.2 订单列表查询 (关联查询)

订单列表需要显示客户名称,这也是一个典型的关联查询场景。

1. 定义 VO (SaleOrderVO)

@Data
public class SaleOrderVO {
    private Long id;
    private String orderNo;
    private BigDecimal totalAmount;
    private Integer status; // 0-待处理, 1-已完成, 2-已取消
    private Long customerId; //客户ID
    @Schema(description = "客户名称")
    private String customerName; 
    private Long userId; //系统用户ID
    private String userName; //系统用户名称
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
    private LocalDateTime createTime;
}

2. Mapper 接口 (SaleOrderMapper.java)
在之前的基础上,添加分页查询方法。

/**
 * 分页查询订单列表
 */
IPage<SaleOrderVO> findPage(IPage<SaleOrderVO> pageParam, String orderNo);
/**
 * 根据订单id查询订单项列表
 * @param orderId 订单id
 * @return 订单项列表
 */
List<SaleOrderItem> selectItemByOrderId(Long orderId);

3. Mapper XML (SaleOrderMapper.xml)

    <resultMap id="SaleOrderVoResultMap" type="com.atguigu.liteims.vo.SaleOrderVO">
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>
        <result property="customerId" column="customer_id"/>
        <result property="customerName" column="customer_name"/>
        <result property="userId" column="user_id"/>
        <result property="userName" column="username"/>
        <result property="totalAmount" column="total_amount"/>
        <result property="status" column="status"/>
        <result property="createTime" column="create_time"/>
        <collection property="items" column="id" select="selectItemByOrderId"/>
    </resultMap>
    <select id="findPage" resultMap="SaleOrderVoResultMap">
        SELECT o.*, c.name AS customer_name,u.username,u.id as userId
        FROM sale_order o
        LEFT JOIN customer c ON o.customer_id = c.id
        LEFT JOIN sys_user u ON o.user_id = u.id
        WHERE o.is_deleted = 0
        <if test="orderNo != null and orderNo != ''">
            AND o.order_no LIKE CONCAT('%', #{orderNo}, '%')
        </if>
        ORDER BY o.create_time DESC
    </select>
    <select id="selectItemByOrderId" resultType="SaleOrderItem">
        select * from sale_order_item where order_id=#{orderId}
    </select>

6.4 业务逻辑 (SaleOrderServiceImpl)

订单创建需要保证数据的一致性:主表插入、明细表插入、库存扣减必须同时成功同时失败。这就需要用到 事务 (@Transactional)

@Service
public class SaleOrderServiceImpl extends ServiceImpl<SaleOrderMapper, SaleOrder> implements SaleOrderService {

    @Autowired
    private ProductMapper productMapper;
    
    @Autowired
    private SaleOrderItemMapper saleOrderItemMapper;
    
    @Autowired
    SaleOrderMapper saleOrderMapper;
    
    @Override
    public IPage<SaleOrderVO> findPage(Integer page, Integer size, String orderNo) {
        Page<SaleOrderVO> pageParam = new Page<>(page, size);
        return saleOrderMapper.findPage(pageParam, orderNo);
    }

    @Override
    @Transactional(rollbackFor = Exception.class) // 开启事务,任何异常都回滚
    public void createOrder(OrderDTO orderDTO) {
        // 1. 创建订单主表对象
    SaleOrder order = new SaleOrder();
    order.setOrderNo(UUID.randomUUID().toString().substring(0, 16)); // 生成随机订单号
    order.setCustomerId(orderDTO.getCustomerId());
    order.setUserId(orderDTo.getUserId());
    order.setTotalAmount(BigDecimal.ZERO); // 先初始化为 0,防止数据库非空校验失败
    order.setCreateTime(LocalDateTime.now());
    order.setStatus(0);
    
    // 插入主表 (MyBatis-Plus 会自动将生成的 ID 回填到 order 对象中)
    baseMapper.insert(order); 

    BigDecimal totalAmount = BigDecimal.ZERO;

    // 2. 遍历商品列表
    for (OrderDTO.OrderItemDTO itemDTO : orderDTO.getItems()) {
        // 2.1 查询商品并检查库存
        Product product = productMapper.selectById(itemDTO.getProductId());
        if (product.getStock() < itemDTO.getQuantity()) {
            throw new RuntimeException("库存不足: " + product.getName()); // 抛出异常,触发事务回滚
        }

        // 2.2 扣减库存
        product.setStock(product.getStock() - itemDTO.getQuantity());
        productMapper.updateById(product);

        // 2.3 保存订单明细
        SaleOrderItem orderItem = new SaleOrderItem();
        orderItem.setOrderId(order.getId()); // 使用主表回填的 ID
        orderItem.setProductId(product.getId());
        orderItem.setPrice(product.getPrice());
        orderItem.setQuantity(itemDTO.getQuantity());
        // 计算小计
        orderItem.setAmount(product.getPrice().multiply(new BigDecimal(itemDTO.getQuantity())));
        saleOrderItemMapper.insert(orderItem);

        // 累加总金额
        totalAmount = totalAmount.add(orderItem.getAmount());
    }

    // 3. 更新订单主表的总金额
    order.setTotalAmount(totalAmount);
    baseMapper.updateById(order);
    }

    @Override
    public void updateStatus(Long id, Integer status) {
        SaleOrder order = new SaleOrder();
        order.setId(id);
        order.setStatus(status);
        baseMapper.updateById(order);
    }
}

6.5 接口实现 (SaleOrderController)

@RestController
@RequestMapping("/api/orders")
@Tag(name = "订单管理")
public class SaleOrderController {
    @Autowired
    private SaleOrderService saleOrderService;

    @GetMapping
    @Operation(summary = "获取订单列表")
    public Result<IPage<SaleOrderVO>> findPage(Integer page, Integer size,String orderNo){
        return Result.success(saleOrderService.findPage(page, size, orderNo));
    }

    @PostMapping
    @Operation(summary = "创建订单")
    public Result<Object> createOrder(@RequestBody OrderDTO orderDTO,HttpSession session){
        SysUser sysUser = (SysUser)session.getAttribute("user");
        orderDTO.setUserId(sysUser.getId());
        saleOrderService.createOrder(orderDTO);
        return Result.success("订单创建成功", null);
    }
    
    @PutMapping("/{id}/status")
    @Operation(summary = "更新订单状态")
    public Result<Object> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
        saleOrderService.updateStatus(id, status);
        return Result.success(null);
    }
}

七、系统日志 (AOP切面)

为了方便排查问题,我们希望记录每个接口的请求参数和响应耗时。使用 AOP (面向切面编程) 可以将这些通用逻辑与业务逻辑分离。

7.1 核心概念:AOP

  • 切面 (Aspect): 封装通用逻辑的类(如日志记录)。
  • 切点 (Pointcut): 定义在哪些地方执行切面逻辑(如 controller 包下的所有方法)。
  • 通知 (Advice): 具体的执行逻辑(如方法执行前后打印日志)。

7.2 日志切面实现 (LogAspect)

@Aspect
@Component
@Slf4j
public class LogAspect {

    // 定义切点:com.atguigu.liteims.controller 包下的所有方法
    @Pointcut("execution(* com.atguigu.liteims.controller..*.*(..))")
    public void controllerPointcut() {}

    // 环绕通知:在目标方法执行前后都执行
    @Around("controllerPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        // 1. 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            log.info("Request: {} {} - Args: {}", 
                    request.getMethod(), 
                    request.getRequestURI(), 
                    Arrays.toString(joinPoint.getArgs()));
        }

        // 2. 执行目标方法
        Object result = joinPoint.proceed();

        // 3. 计算耗时
        long timeTaken = System.currentTimeMillis() - startTime;
        log.info("Response: {} ms", timeTaken);
        
        return result;
    }
}

至此,我们的进销存系统后端核心功能已全部实现!

继续阅读

全部归档
07_前端开发基础_Vue3实战(前端工程化讲解)
07_前端开发基础_Vue3实战(前端工程化讲解)

本 绹 本篇章内容强调两大前端模块的构建:登录和商品管理。核心在于与后端接口的有效对接,确保交互流畅且准确。关键观点体现在使用 **Vue 3 来实现表单验证与数据交互、Element Plus 提供的组件以简化UI设格,以及通过`Axios" 增强的数据请求功能。。 登录功能涉及验证表单输入、发送 POST 请求至 `/api/login`,并实现成功后的页面路由跳转。商品管理模块则扩展了商品增删改查(CRUD) 的操作,从基本数据获取到包含分页、搜索、新增、编辑、和删除功能的完整实现。 具体而言,通过 Vue 3 的组合式 API 创建响应式数据及事件处理,通过 Element Plus 的表表组件渲染即时数据集,并利用 Element Plus 的对话框实现用户输入验证和数据提交。后代表现和交互处理的整体现示了前端应用的多余层次与动态响应能力。

06_进销存系统_进阶功能
06_进销存系统_进阶功能

该该简要摘要涵盖了 Excel 导出和定时任务的核心内容, .以及相关的关键结论和细节和技术收益: --- 在使用进销存系统中的改进功能。 弳主要有两项: . 一是 1 . . 1 . . . . . . . . . 开` . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 摘要: : 该系统进销存 . 中加入的两项改进功能: 包括使用 .Excel �导出功能和定时任务. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

04_进销存系统_项目搭建
04_进销存系统_项目搭建

从零开始搭建进销存系统的开发环境,涉及前端和后端两部分。前端仅需导入代码并启动,具体步骤包括导入前端项目 (位于 `frontend` 文件夹)、初始化依赖 (`npm install`) 与启动开发服务器 (`npm run dev`)。后端部分则更丰富,涵盖创建Spring Boot项目、引入特定依赖 (如Web、MySQL和MyBatis+Plus等)””以及配置文件 (`application.yml`)”。后端骨架生成后,需手动编写Controller层代码以实现具体的业务功能。总体而言,“本章节帮助开发者完成基础环境设置,为后续功能开发奠定基础。

评论