qq_26950675头像
关注

第48课-业务模块开发

第 48 课:业务模块开发

本课目标:掌握通用CRUD服务封装、文件上传、分页查询、DTO/VO转换等常见业务开发技巧。


一、概念讲解

1.1 通用CRUD封装

传统方式:每个实体都写一套CRUD代码
User:create, findById, findAll, update, delete
Product:create, findById, findAll, update, delete
Order:create, findById, findAll, update, delete
→ 大量重复代码

通用封装:
BaseService<T>:create, findById, findAll, update, delete
User extends BaseService<User>
Product extends BaseService<Product>
→ 代码复用,减少重复

1.2 文件上传流程

前端上传 ──> 后端接收 ──> 文件校验 ──> 生成文件名 ──> 保存文件 ──> 返回URL

校验内容:
1. 文件大小限制
2. 文件类型限制
3. 文件名安全检查
4. 防止文件名冲突(UUID重命名)

1.3 分页查询原理

前端传递:page=1, size=10, keyword=手机

后端处理:
1. 构建Pageable对象
2. 执行分页查询(SELECT COUNT + SELECT 数据)
3. 封装返回结果

返回数据:
{
  "records": [...],    // 当前页数据
  "total": 100,        // 总记录数
  "page": 1,           // 当前页
  "size": 10,          // 每页大小
  "totalPages": 10     // 总页数
}

二、语法格式

2.1 泛型Service基本结构

public interface BaseService<T> {
    T save(T entity);
    T findById(Long id);
    List<T> findAll();
    PageResult<T> findPage(int page, int size);
    T update(Long id, T entity);
    void delete(Long id);
}

2.2 文件上传API

@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
    // 1. 校验文件
    // 2. 保存文件
    // 3. 返回URL
}

三、代码案例

3.1 通用Service接口

package com.example.demo.common.service;

import com.example.demo.common.result.PageResult;
import java.util.List;

public interface BaseService<T> {

    T save(T entity);

    T findById(Long id);

    List<T> findAll();

    PageResult<T> findPage(int page, int size);

    T update(Long id, T entity);

    void delete(Long id);
}

3.2 通用Service实现

package com.example.demo.common.service;

import com.example.demo.common.result.PageResult;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public abstract class BaseServiceImpl<T, R extends JpaRepository<T, Long>>
        implements BaseService<T> {

    protected final R repository;

    protected BaseServiceImpl(R repository) {
        this.repository = repository;
    }

    @Override
    public T save(T entity) {
        return repository.save(entity);
    }

    @Override
    public T findById(Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new RuntimeException("数据不存在"));
    }

    @Override
    public List<T> findAll() {
        return repository.findAll();
    }

    @Override
    public PageResult<T> findPage(int page, int size) {
        Pageable pageable = PageRequest.of(page - 1, size,
            Sort.by(Sort.Direction.DESC, "id"));
        Page<T> pageResult = repository.findAll(pageable);
        return new PageResult<>(
            pageResult.getContent(),
            pageResult.getTotalElements(),
            page,
            size
        );
    }

    @Override
    public T update(Long id, T entity) {
        if (!repository.existsById(id)) {
            throw new RuntimeException("数据不存在");
        }
        return repository.save(entity);
    }

    @Override
    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new RuntimeException("数据不存在");
        }
        repository.deleteById(id);
    }
}

3.3 商品模块实现

实体类
package com.example.demo.module.product.entity;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "sys_product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false, precision = 10, scale = 2)
    private BigDecimal price;

    @Column(columnDefinition = "INT DEFAULT 0")
    private int stock;

    private Long categoryId;

    @Column(columnDefinition = "INT DEFAULT 1")
    private int status;

    private String description;

    private String imageUrl;

    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    @PrePersist
    public void prePersist() {
        this.createTime = LocalDateTime.now();
        this.updateTime = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        this.updateTime = LocalDateTime.now();
    }

    public Product() {}

    public Product(String name, BigDecimal price, int stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
        this.status = 1;
    }

    // getter/setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public int getStock() { return stock; }
    public void setStock(int stock) { this.stock = stock; }
    public Long getCategoryId() { return categoryId; }
    public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
    public int getStatus() { return status; }
    public void setStatus(int status) { this.status = status; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public String getImageUrl() { return imageUrl; }
    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
    public LocalDateTime getCreateTime() { return createTime; }
    public LocalDateTime getUpdateTime() { return updateTime; }
}
Repository
package com.example.demo.module.product.mapper;

import com.example.demo.module.product.entity.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    // 按名称搜索(分页)
    Page<Product> findByNameContaining(String name, Pageable pageable);

    // 按分类查询
    List<Product> findByCategoryId(Long categoryId);

    // 按价格范围查询
    List<Product> findByPriceBetween(java.math.BigDecimal min, java.math.BigDecimal max);

    // 按状态查询
    Page<Product> findByStatus(int status, Pageable pageable);

    // 自定义查询:搜索商品(名称或描述包含关键字)
    @Query("SELECT p FROM Product p WHERE " +
           "(:keyword IS NULL OR p.name LIKE %:keyword%) AND " +
           "(:categoryId IS NULL OR p.categoryId = :categoryId)")
    Page<Product> searchProducts(
        @Param("keyword") String keyword,
        @Param("categoryId") Long categoryId,
        Pageable pageable);

    // 库存扣减
    @Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity")
    int deductStock(@Param("id") Long id, @Param("quantity") int quantity);
}
DTO
package com.example.demo.module.product.dto;

import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;

@Data
public class ProductCreateDTO {

    @NotBlank(message = "商品名称不能为空")
    @Size(max = 100, message = "商品名称不超过100个字符")
    private String name;

    @NotNull(message = "价格不能为空")
    @DecimalMin(value = "0.01", message = "价格必须大于0")
    private BigDecimal price;

    @Min(value = 0, message = "库存不能为负数")
    private int stock;

    private Long categoryId;

    @Size(max = 500, message = "描述不超过500个字符")
    private String description;

    private String imageUrl;
}
Service
package com.example.demo.module.product.service;

import com.example.demo.common.result.PageResult;
import com.example.demo.common.service.BaseServiceImpl;
import com.example.demo.module.product.dto.ProductCreateDTO;
import com.example.demo.module.product.entity.Product;
import com.example.demo.module.product.mapper.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ProductService
        extends BaseServiceImpl<Product, ProductRepository> {

    public ProductService(ProductRepository repository) {
        super(repository);
    }

    // 创建商品
    public Product create(ProductCreateDTO dto) {
        Product product = new Product();
        product.setName(dto.getName());
        product.setPrice(dto.getPrice());
        product.setStock(dto.getStock());
        product.setCategoryId(dto.getCategoryId());
        product.setDescription(dto.getDescription());
        product.setImageUrl(dto.getImageUrl());
        product.setStatus(1);
        return repository.save(product);
    }

    // 搜索商品(分页)
    @Transactional(readOnly = true)
    public PageResult<Product> search(String keyword, Long categoryId,
                                       int page, int size) {
        Pageable pageable = PageRequest.of(page - 1, size,
            Sort.by(Sort.Direction.DESC, "createTime"));

        Page<Product> result = repository.searchProducts(
            keyword, categoryId, pageable);

        return new PageResult<>(
            result.getContent(),
            result.getTotalElements(),
            page,
            size
        );
    }

    // 扣减库存
    public void deductStock(Long productId, int quantity) {
        int affected = repository.deductStock(productId, quantity);
        if (affected == 0) {
            throw new RuntimeException("库存不足");
        }
    }
}
Controller
package com.example.demo.module.product.controller;

import com.example.demo.common.result.PageResult;
import com.example.demo.common.result.Result;
import com.example.demo.module.product.dto.ProductCreateDTO;
import com.example.demo.module.product.entity.Product;
import com.example.demo.module.product.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @PostMapping
    public Result<Product> create(@Valid @RequestBody ProductCreateDTO dto) {
        return Result.success("创建成功", productService.create(dto));
    }

    @GetMapping("/{id}")
    public Result<Product> getById(@PathVariable Long id) {
        return Result.success(productService.findById(id));
    }

    @GetMapping
    public Result<PageResult<Product>> list(
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) Long categoryId,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size) {
        return Result.success(productService.search(keyword, categoryId, page, size));
    }

    @PutMapping("/{id}")
    public Result<Product> update(@PathVariable Long id,
                                   @Valid @RequestBody Product product) {
        return Result.success("更新成功", productService.update(id, product));
    }

    @DeleteMapping("/{id}")
    public Result<Void> delete(@PathVariable Long id) {
        productService.delete(id);
        return Result.success("删除成功", null);
    }

    @PostMapping("/{id}/deduct")
    public Result<Void> deductStock(@PathVariable Long id,
                                     @RequestParam int quantity) {
        productService.deductStock(id, quantity);
        return Result.success("库存扣减成功", null);
    }
}

3.4 文件上传Controller

package com.example.demo.common.controller;

import com.example.demo.common.result.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api/file")
public class FileController {

    @Value("${file.upload-dir:./uploads}")
    private String uploadDir;

    @Value("${file.allowed-types:image/jpeg,image/png,image/gif,application/pdf}")
    private String allowedTypes;

    @Value("${file.max-size:10485760}") // 10MB
    private long maxSize;

    @PostMapping("/upload")
    public Result<Map<String, String>> upload(
            @RequestParam("file") MultipartFile file) {

        // 1. 校验文件是否为空
        if (file.isEmpty()) {
            return Result.error(400, "请选择要上传的文件");
        }

        // 2. 校验文件大小
        if (file.getSize() > maxSize) {
            return Result.error(400, "文件大小不能超过10MB");
        }

        // 3. 校验文件类型
        String contentType = file.getContentType();
        if (contentType == null || !allowedTypes.contains(contentType)) {
            return Result.error(400, "不支持的文件类型");
        }

        // 4. 生成唯一文件名
        String originalFilename = file.getOriginalFilename();
        String extension = "";
        if (originalFilename != null && originalFilename.contains(".")) {
            extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        }
        String newFilename = UUID.randomUUID().toString() + extension;

        // 5. 保存文件
        try {
            File dir = new File(uploadDir);
            if (!dir.exists()) {
                dir.mkdirs();
            }

            File dest = new File(dir, newFilename);
            file.transferTo(dest);

            // 6. 返回文件访问路径
            Map<String, String> result = new HashMap<>();
            result.put("filename", newFilename);
            result.put("originalFilename", originalFilename);
            result.put("url", "/uploads/" + newFilename);
            result.put("size", String.valueOf(file.getSize()));

            return Result.success("上传成功", result);
        } catch (IOException e) {
            return Result.error(500, "文件上传失败:" + e.getMessage());
        }
    }

    @PostMapping("/upload/multiple")
    public Result<Map<String, Object>> uploadMultiple(
            @RequestParam("files") MultipartFile[] files) {

        int successCount = 0;
        int failCount = 0;

        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                successCount++;
            } else {
                failCount++;
            }
        }

        Map<String, Object> result = new HashMap<>();
        result.put("total", files.length);
        result.put("success", successCount);
        result.put("failed", failCount);

        return Result.success("上传完成", result);
    }
}

3.5 分页查询封装

package com.example.demo.common.query;

import lombok.Data;

@Data
public class PageQuery {
    private int page = 1;
    private int size = 10;
    private String sortField = "id";
    private String sortOrder = "desc";

    // 构建Pageable
    public org.springframework.data.domain.Pageable toPageable() {
        org.springframework.data.domain.Sort sort =
            "asc".equalsIgnoreCase(sortOrder)
                ? org.springframework.data.domain.Sort.by(sortField).ascending()
                : org.springframework.data.domain.Sort.by(sortField).descending();

        return org.springframework.data.domain.PageRequest.of(page - 1, size, sort);
    }
}

3.6 分页查询示例

package com.example.demo.module.order.controller;

import com.example.demo.common.query.PageQuery;
import com.example.demo.common.result.PageResult;
import com.example.demo.common.result.Result;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    // 使用通用分页查询
    @GetMapping
    public Result<PageResult<Map<String, Object>>> list(PageQuery query) {
        Pageable pageable = query.toPageable();

        // 模拟查询
        // Page<Order> orders = orderRepository.findAll(pageable);

        return Result.success(new PageResult<>(
            new java.util.ArrayList<>(),
            0,
            query.getPage(),
            query.getSize()
        ));
    }
}

3.7 统一异常处理

package com.example.demo.common.exception;

import com.example.demo.common.result.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleRuntimeException(RuntimeException ex) {
        return Result.error(400, ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Map<String, String>> handleValidationException(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        return Result.error(400, "参数校验失败");
    }

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<Void> handleMaxUploadSize(MaxUploadSizeExceededException ex) {
        return Result.error(400, "文件大小超过限制");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<Void> handleException(Exception ex) {
        return Result.error(500, "服务器内部错误");
    }
}

四、常见错误

4.1 泛型继承后方法不生效

原因:子类没有正确传递Repository。

解决:确保子类构造函数调用super(repository)。

4.2 文件上传中文名乱码

原因:浏览器编码问题。

解决:使用UUID重命名文件,或配置server.tomcat.uri-encoding=UTF-8。

4.3 分页参数越界

现象:请求第0页或负数页。

解决:在toPageable()方法中处理边界情况。

4.4 @Transactional在私有方法上不生效

原因:Spring AOP只能拦截public方法。

解决:确保事务方法是public的。


五、课后练习

  1. 基于通用Service,实现一个文章(Article)模块的完整CRUD
  2. 实现文件上传接口,支持图片压缩和缩略图生成
  3. 封装一个通用的搜索接口,支持多条件组合查询
  4. 编写分页查询的单元测试,验证各种边界情况

六、本课小结

知识点说明
通用Service泛型封装CRUD,减少重复代码
文件上传MultipartFile接收文件,校验+重命名+保存
分页查询Pageable封装分页参数,PageResult封装返回结果
DTO/VODTO接收前端数据,VO返回给前端,Entity映射数据库
多条件搜索@Query自定义查询,支持关键字+分类+状态组合
库存扣减@Query UPDATE操作,乐观锁控制并发

本课程持续更新中,欢迎关注!

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_26950675/article/details/166601547

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--