project 商品甄选 神奇海螺 2026-01-23 2026-03-02 开发环境 若依框架:https://doc.ruoyi.vip/
技术栈 后端 SpringBoot 简化Spring应用配置与开发 SpringCloud: SpringCloud Alibaba Nacos 注册中心与配置中心 SpringCloud Alibaba Sentinel 服务熔断与降级(限流) SpringCloud Alibaba Task 定时任务 SpringCloud GateWay 网关(访问服务要经过网关) SpringCloud OpenFeign 微服务通信组件 SpringCloud LoadBalancer 负载均衡器 Mybatis-plus 持久层框架,依赖Mybatis Redssion 操作redis的框架 ThreadPoolExecutor 线程池 Lombok 自动生成方法 Knife4J 接口可视化文档 Mysql 数据库 Redis 缓存 RabbitMq 消息队列 MinIO 分布式文件存储 Docker 容器化技术
前端 ECMAScript 6:也称为ECMAScript2015,是JavaScript的一个版本,引入了许多新的语法和功能。 Vue.js:一个用于构建用户界面的渐进式JavaScript框架。 Vuex:Vue.js应用程序的状态管理模式和库。 Vue Router:Vue.is官方的路由管理器,用于构建单页面应用(SPA)。 Vue CLI:Vue.jis开发的标准工具,提供了一个丰富的交互式项目脚手架。 Axiox:一个基于Promise的 HTTP客户端,用于浏览器和Node.js,常用于发送网络请求。 Element Plus:一套为 Vue.js3.0开发的桌面端u组件库,提供丰富的组件以帮助快速构建界面。
docker环境搭建 1.下载minio、mysql、nacos、rabbitmq、redis、sentinel镜像 2.通过命令或compose.yml文件启动容器
基础环境搭建 对Ruoyi-Cloud进行了修改,将Cloud版本改为了3.6.3,将jdk版本改为了17,还有一些名称的修改
项目结构图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 com.spzx ├── spzx-gateway // 网关模块 [8080] ├── spzx-auth // 认证中心 [9200] ├── spzx-api // 接口模块 │ └── spzx-api-system // 系统接口 ├── spzx-common // 通用模块 │ └── spzx-common-core // 核心模块 │ └── spzx-common-datascope // 权限范围 │ └── spzx-common-datasource // 多数据源 │ └── spzx-common-log // 日志记录 │ └── spzx-common-redis // 缓存服务 │ └── spzx-common-seata // 分布式事务 │ └── spzx-common-security // 安全模块 ├── spzx-modules // 业务模块 │ └── spzx-system // 系统模块 [9201] │ └── spzx-gen // 代码生成 [9202] │ └── spzx-file // 文件服务 [9300] ├── spzx-visual // 图形化管理模块 │ └── spzx-monitor // 监控中心 [9100] ├──pom.xml // 公共依赖
spzx-monitor 使用spring boot admin健康监测组件实现
前端项目搭建 使用若依配套的前端框架,进行一些名称上的修改
业务编写 创建商品模块 创建pom.xml 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.spzx</groupId> <artifactId>spzx-modules</artifactId> <version>3.6.3</version> </parent> <artifactId>spzx-product</artifactId> <description> spzx-product系统模块 </description> <dependencies> <!-- SpringCloud Alibaba Nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- SpringCloud Alibaba Nacos Config --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!-- SpringCloud Alibaba Sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <!-- SpringBoot Actuator --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- Mysql Connector --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!-- RuoYi Common DataScope --> <dependency> <groupId>com.spzx</groupId> <artifactId>spzx-common-datascope</artifactId> </dependency> <!-- RuoYi Common Log --> <dependency> <groupId>com.spzx</groupId> <artifactId>spzx-common-log</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>
创建bootstrap.yml 配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # Tomcat server: port: 9205 # Spring spring: application: # 应用名称 name: spzx-product profiles: # 环境配置 active: dev main: allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册 cloud: nacos: discovery: # 服务注册地址 server-addr: 192.168.6.104:8848 config: # 配置中心地址 server-addr: 192.168.6.104:8848 # 配置文件格式 file-extension: yml # 共享配置 shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
创建logback.xml 日志配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 <?xml version="1.0" encoding="UTF-8"?> <configuration scan="true" scanPeriod="60 seconds" debug="false"> <!-- 日志存放路径 --> <property name="log.path" value="logs/spzx-product" /> <!-- 日志输出格式 --> <property name="log.pattern" value="%yellow(%d{HH:mm:ss.SSS}) [%thread] %green(%-5level) %cyan(%logger{20}) - [%method,%line] - %msg%n" /> <!-- 控制台输出 --> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${log.pattern}</pattern> </encoder> </appender> <!-- 系统日志输出 --> <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/info.log</file> <!-- 循环政策:基于时间创建日志文件 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件名格式 --> <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 日志最大的历史 60天 --> <maxHistory>60</maxHistory> </rollingPolicy> <encoder> <pattern>${log.pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 过滤的级别 --> <level>INFO</level> <!-- 匹配时的操作:接收(记录) --> <onMatch>ACCEPT</onMatch> <!-- 不匹配时的操作:拒绝(不记录) --> <onMismatch>DENY</onMismatch> </filter> </appender> <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/error.log</file> <!-- 循环政策:基于时间创建日志文件 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 日志文件名格式 --> <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 日志最大的历史 60天 --> <maxHistory>60</maxHistory> </rollingPolicy> <encoder> <pattern>${log.pattern}</pattern> </encoder> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <!-- 过滤的级别 --> <level>ERROR</level> <!-- 匹配时的操作:接收(记录) --> <onMatch>ACCEPT</onMatch> <!-- 不匹配时的操作:拒绝(不记录) --> <onMismatch>DENY</onMismatch> </filter> </appender> <!-- 系统模块日志级别控制 --> <logger name="com.spzx" level="info" /> <!-- Spring日志级别控制 --> <logger name="org.springframework" level="warn" /> <root level="info"> <appender-ref ref="console" /> </root> <!--系统操作日志--> <root level="info"> <appender-ref ref="file_info" /> <appender-ref ref="file_error" /> </root> </configuration>
创建banner.txt 启动日志信息配置 1 2 3 4 5 6 7 8 9 10 Spring Boot Version: ${spring-boot.version} Spring Application Name: ${spring.application.name} _ _ (_) | | _ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___ | '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ | | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | | |_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_| __/ | __/ | |___/ |___/
创建nacos配置中心配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 mybatis-plus: mapper-locations: classpath*:mapper/**/*Mapper.xml type-aliases-package: com.spzx.**.domain configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志 global-config: db-config: logic-delete-field: del_flag # 全局逻辑删除的实体字段名 logic-delete-value: 2 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) # spring配置 spring: data: redis: host: 192.168.6.104 port: 6379 password: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.6.104:3306/spzx-product?characterEncoding=utf-8&useSSL=false username: root password: root hikari: connection-test-query: SELECT 1 connection-timeout: 60000 idle-timeout: 500000 max-lifetime: 540000 maximum-pool-size: 10 minimum-idle: 5 pool-name: GuliHikariPool
创建启动类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.spzx; import com.spzx.common.security.annotation.EnableCustomConfig; import com.spzx.common.security.annotation.EnableRyFeignClients; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableCustomConfig @EnableRyFeignClients @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } }
使用Mybatis-plus代码生成 在根模块下锁定版本 1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency>
在使用的项目中添加依赖 1 2 3 4 5 6 7 8 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency>
创建代码生成器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.spzx.product; public class GenMP { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://192.168.200.10:3306/spzx-product?characterEncoding=utf-8&useSSL=false", "root", "root") .globalConfig(builder -> builder .author("atguigu") .outputDir("D:/spzx/product") .dateType(DateType.ONLY_DATE) .disableOpenDir() ) .packageConfig(builder -> builder .parent("com.spzx.product") .entity("domain") .mapper("mapper") .xml("mapper.product") .service("service") .serviceImpl("service.impl") .controller("controller") ) .strategyConfig(builder -> builder .addInclude( "brand", "category", "category_brand", "product", "product_details", "product_sku", "product_spec", "product_unit", "sku_stock") // 设置需要生成的表名 .entityBuilder() .enableLombok() .superClass(BaseEntity.class) .addSuperEntityColumns( "id", "create_by", "create_time", "update_by", "update_time", "remark", "del_flag") //.addIgnoreColumns .enableFileOverride() .serviceBuilder() //.formatServiceFileName("I%sService") .enableFileOverride() .controllerBuilder() .superClass(BaseController.class) .enableRestStyle() .enableFileOverride() ) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } }
minio文件上传 若依框架自带了文件上传接口,直接使用即可
minio存储桶权限配置 1 2 3 4 5 6 7 8 9 { "Statement" : [ { "Action" : "s3:GetObject", "Effect" : "Allow", "Principal" : "*", "Resource" : "arn:aws:s3:::spzx/*" } ], "Version" : "2012-10-17" }
品牌分页查询 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RestController @RequestMapping("/brand") public class BrandController extends BaseController { @Autowired private IBrandService brandService; @Operation(summary = "查询品牌列表") @GetMapping("/list") public TableDataInfo list(@Parameter(description="") String name) { startPage(); List<Brand> list = brandService.selectBrandList(name); return getDataTable(list); } }
Service 接口 1 2 3 4 public interface IBrandService extends IService<Brand> { List<Brand> selectBrandList(String name); }
实现类 1 2 3 4 5 6 7 8 9 10 @Service public class BrandServiceImpl extends ServiceImpl<BrandMapper, Brand> implements IBrandService { @Override public List<Brand> selectBrandList(String name) { LambdaQueryWrapper<Brand> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.like(StringUtils.isNoneBlank(name),Brand::getName,name); return list(lambdaQueryWrapper); } }
品牌增删改查 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 package com.spzx.product.controller; import com.spzx.common.core.web.domain.AjaxResult; import com.spzx.common.core.web.page.TableDataInfo; import com.spzx.product.domain.Brand; import com.spzx.product.service.IBrandService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.spzx.common.core.web.controller.BaseController; import java.util.List; /** * <p> * 分类品牌 前端控制器 * </p> * * @author atguigu * @since 2026-02-01 */ @RestController @RequestMapping("/brand") public class BrandController extends BaseController { @Autowired private IBrandService brandService; @Operation(summary = "查询品牌列表") @GetMapping("/list") public TableDataInfo list(@Parameter(description="名称") String name) { startPage(); List<Brand> list = brandService.selectBrandList(name); return getDataTable(list); } @Operation(summary = "查询指定品牌") @GetMapping("/{id}") public AjaxResult byId(@PathVariable Long id) { Brand brand = brandService.getById(id); return AjaxResult.success(brand); } @Operation(summary = "新增品牌") @PostMapping("/insert") public AjaxResult insert(@Parameter(description = "新增") @RequestBody Brand brand) { brandService.InsertBrand(brand); return AjaxResult.success(); } @Operation(summary = "修改品牌") @PutMapping("/update") public AjaxResult update(@Parameter(description = "修改") @RequestBody Brand brand) { brandService.UpdateBrand(brand); return AjaxResult.success(); } @Operation(summary = "删除品牌") @DeleteMapping("/{id}") public AjaxResult delete(@PathVariable List<Long> ids) { brandService.removeBatchByIds(ids); return AjaxResult.success(); } }
Service 接口 1 2 3 4 5 6 7 8 public interface IBrandService extends IService<Brand> { List<Brand> selectBrandList(String name); void InsertBrand(Brand brand); void UpdateBrand(Brand brand); }
实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.spzx.product.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.spzx.common.core.exception.ServiceException; import com.spzx.common.security.utils.SecurityUtils; import com.spzx.product.domain.Brand; import com.spzx.product.mapper.BrandMapper; import com.spzx.product.service.IBrandService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * <p> * 分类品牌 服务实现类 * </p> * * @author atguigu * @since 2026-02-01 */ @Service public class BrandServiceImpl extends ServiceImpl<BrandMapper, Brand> implements IBrandService { @Autowired private BrandMapper brandMapper; @Override public List<Brand> selectBrandList(String name) { LambdaQueryWrapper<Brand> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.like(StringUtils.isNoneBlank(name),Brand::getName,name); return list(lambdaQueryWrapper); } @Override public void InsertBrand(Brand brand) { //获取创建者 brand.setCreateBy(SecurityUtils.getUsername()); //检查品牌是否存在 long count = count(new LambdaQueryWrapper<Brand>().eq(Brand::getName, brand.getName())); if(count>0){ throw new ServiceException("品牌已存在"); } brandMapper.insert(brand); } @Override public void UpdateBrand(Brand brand) { LambdaUpdateWrapper<Brand> lambdaUpdateWrapper = new LambdaUpdateWrapper<>(); lambdaUpdateWrapper.eq(Brand::getId,brand.getId()); brandMapper.update(brand,lambdaUpdateWrapper); } }
前端整合 配置nocos 1 2 3 4 5 6 7 # 商品服务 - id: spzx-product uri: lb://spzx-product predicates: - Path=/product/** filters: - StripPrefix=1
用户登录与权限 Ruoyi遵循RBAC模型开发的权限控制,基于角色的权限控制 登录: 用户登录->网关(校验信息并路由到对应的服务)->认证服务->系统服务->登录成功返回token 权限: 用户请求->网关(验证令牌是否有效,并将用户信息及token存到请求头中)->系统服务(拦截器拦截请求,获取请求头信息,存储到ThreadLocal中)->处理请求,响应结果
日志 采用AOP在操作前获取操作信息并记录到数据库/本地/文件存储服务器
商品管理-商品单位
Controller
package com.spzx.product.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.spzx.common.core.web.domain.AjaxResult;
import com.spzx.common.core.web.page.TableDataInfo;
import com.spzx.common.log.annotation.Log;
import com.spzx.common.log.enums.OperatorType;
import com.spzx.common.security.annotation.RequiresPermissions;
import com.spzx.product.domain.Brand;
import com.spzx.product.domain.ProductUnit;
import com.spzx.product.service.IBrandService;
import com.spzx.product.service.IProductUnitService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.spzx.common.core.web.controller.BaseController;
import java.util.List;
/**
*
* 商品单位 前端控制器
*
*
* @author atguigu
* @since 2026-02-01
*/
@RestController
@RequestMapping("/productUnit")
public class ProductUnitController extends BaseController {
@Autowired
private IProductUnitService productUnitService;
@Operation(summary = "查询单位列表")
@Log(title="查询操作",operatorType = OperatorType.OTHER)
@RequiresPermissions("product:unit:list")
@GetMapping("/list")
public TableDataInfo list(@Parameter(description="名称") String name)
{
startPage();
List list = productUnitService.selectProductUnitList(name);
return getDataTable(list);
}
@Operation(summary = "根据id查询指定单位")
@GetMapping("/{id}")
public AjaxResult byId(@PathVariable Long id)
{
ProductUnit productUnit = productUnitService.getById(id);
return AjaxResult.success(productUnit);
}
@Operation(summary = "新增单位")
@PostMapping
public AjaxResult insert(@Parameter(description = "新增") @RequestBody ProductUnit productUnit)
{
productUnitService.save(productUnit);
return AjaxResult.success();
}
@Operation(summary = "修改单位")
@PutMapping
public AjaxResult update(@Parameter(description = "修改") @RequestBody ProductUnit productUnit)
{
productUnitService.updateProductUnit(productUnit);
return AjaxResult.success();
}
@Operation(summary = "删除单位")
@DeleteMapping("/{ids}")
public AjaxResult delete(@PathVariable List ids)
{
productUnitService.removeBatchByIds(ids);
return AjaxResult.success();
}
}
Service interface
public interface IProductUnitService extends IService {
List selectProductUnitList(String name);
void updateProductUnit(ProductUnit productUnit);
}
Service implement
@Service
public class ProductUnitServiceImpl extends ServiceImpl implements IProductUnitService {
@Override
public List selectProductUnitList(String name) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name),ProductUnit::getName, name);
return list(wrapper);
}
@Override
public void updateProductUnit(ProductUnit productUnit) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ProductUnit::getId, productUnit.getId());
update(productUnit, wrapper);
}
}
商品分类
Controller
@RestController
@RequestMapping("/category")
public class CategoryController extends BaseController {
@Autowired
private ICategoryService categoryService;
@GetMapping("/treeSelect/{parent_id}")
public AjaxResult getCategoryList(@PathVariable Long parent_id){
List list=categoryService.getList(parent_id);
return AjaxResult.success(list);
}
}
Service Interface
public interface ICategoryService extends IService {
List getList(Long parentId);
}
Service Implement
@Service
public class CategoryServiceImpl extends ServiceImpl implements ICategoryService {
@Override
public List getList(Long parentId) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Category::getParentId,parentId);
queryWrapper.select(Category::getId,Category::getName,Category::getParentId,Category::getStatus,Category::getImageUrl,Category::getOrderNum);
List list = this.list(queryWrapper);
if(!ObjectUtils.isEmpty(list)){
for (Category category : list) {
Long count=this.count(new LambdaQueryWrapper().eq(Category::getParentId,category.getId()));
category.setHasChildren(count>0);
}
}
return list;
}
}
分类品牌
Controller
package com.spzx.product.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.core.web.domain.AjaxResult;
import com.spzx.common.core.web.page.TableDataInfo;
import com.spzx.common.log.annotation.Log;
import com.spzx.common.log.enums.OperatorType;
import com.spzx.common.security.annotation.RequiresPermissions;
import com.spzx.product.domain.CategoryBrand;
import com.spzx.product.service.ICategoryBrandService;
import com.spzx.product.service.IProductService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.spzx.common.core.web.controller.BaseController;
import java.util.List;
/**
*
* 分类品牌 前端控制器
*
*
* @author atguigu
* @since 2026-02-01
*/
@RestController
@RequestMapping("/categoryBrand")
public class CategoryBrandController extends BaseController {
@Autowired
private ICategoryBrandService categoryBrandService;
@Operation(summary = "查询分类品牌列表")
@Log(title="查询操作",operatorType = OperatorType.OTHER)
@RequiresPermissions("product:categoryBrand:list")
@GetMapping("/list")
public TableDataInfo list(){
startPage();
List list = categoryBrandService.getCategoryBrandList();
return getDataTable(list);
}
@Operation(summary = "新增分类品牌")
@Log(title="新增操作",operatorType = OperatorType.OTHER)
@PostMapping
public AjaxResult insertCategoryBrand(@RequestBody CategoryBrand categoryBrand){
categoryBrandService.insertCategoryBrand(categoryBrand);
return AjaxResult.success();
}
@Operation(summary = "删除分类品牌")
@Log(title="删除操作",operatorType = OperatorType.OTHER)
@DeleteMapping("{ids}")
public AjaxResult deleteCategoryBrands(@PathVariable List ids){
categoryBrandService.removeByIds(ids);
return AjaxResult.success();
}
}
Service Interface
public interface ICategoryBrandService extends IService {
List getCategoryBrandList();
void insertCategoryBrand(CategoryBrand categoryBrand);
}
Service Implement
package com.spzx.product.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.security.utils.SecurityUtils;
import com.spzx.product.domain.Brand;
import com.spzx.product.domain.Category;
import com.spzx.product.domain.CategoryBrand;
import com.spzx.product.mapper.CategoryBrandMapper;
import com.spzx.product.service.IBrandService;
import com.spzx.product.service.ICategoryBrandService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spzx.product.service.ICategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
* 分类品牌 服务实现类
*
*
* @author atguigu
* @since 2026-02-01
*/
@Service
public class CategoryBrandServiceImpl extends ServiceImpl implements ICategoryBrandService {
@Autowired
private CategoryBrandMapper categoryBrandMapper;
@Autowired
private IBrandService brandService;
@Override
public List getCategoryBrandList() {
return categoryBrandMapper.selectCategoryBrandList();
}
@Override
public void insertCategoryBrand(CategoryBrand categoryBrand) {
long count = count(new LambdaQueryWrapper()
.eq(CategoryBrand::getCategoryId, categoryBrand.getCategoryId())
.eq(CategoryBrand::getBrandId, categoryBrand.getBrandId())
);
if(count>0){
throw new ServiceException("该分类已有此品牌");
}
categoryBrand.setCreateBy(SecurityUtils.getUsername());
categoryBrand.setUpdateBy(SecurityUtils.getUsername());
Brand brand = brandService.getById(categoryBrand.getBrandId());
categoryBrand.setLogo(brand.getLogo());
categoryBrandMapper.insert(categoryBrand);
}
}
Mapper Interface
public interface CategoryBrandMapper extends BaseMapper {
List selectCategoryBrandList();
}
Mapper XML
select cb.id,c.name categoryName,b.name brandName,b.logo logo,cb.update_time
from category c inner join category_brand cb on c.id=cb.category_id
inner join brand b on b.id=cb.brand_id
where c.del_flag=0 and b.del_flag=0 and cb.del_flag=0
商品规格
Controller
package com.spzx.product.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.core.web.domain.AjaxResult;
import com.spzx.common.core.web.page.TableDataInfo;
import com.spzx.common.log.annotation.Log;
import com.spzx.common.log.enums.OperatorType;
import com.spzx.common.security.annotation.RequiresPermissions;
import com.spzx.common.security.utils.SecurityUtils;
import com.spzx.product.domain.Brand;
import com.spzx.product.domain.ProductSpec;
import com.spzx.product.mapper.ProductSpecMapper;
import com.spzx.product.service.IProductSpecService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.spzx.common.core.web.controller.BaseController;
import java.util.List;
/**
*
* 商品规格 前端控制器
*
*
* @author atguigu
* @since 2026-02-01
*/
@RestController
@RequestMapping("/productSpec")
public class ProductSpecController extends BaseController {
@Autowired
private IProductSpecService productSpecService;
@Operation(summary = "查询商品规格列表")
@Log(title="查询操作",operatorType = OperatorType.OTHER)
@RequiresPermissions("product:productSpec:list")
@GetMapping("/list")
public TableDataInfo list(){
startPage();
List list = productSpecService.selectProductSpecList();
return getDataTable(list);
}
@Operation(summary = "添加商品规格")
@Log(title="添加操作",operatorType = OperatorType.OTHER)
@PostMapping
public AjaxResult insertProductSpec(@RequestBody ProductSpec productSpec) throws JsonProcessingException {
productSpecService.saveProductSpec(productSpec);
return AjaxResult.success();
}
@Operation(summary = "删除商品规格")
@Log(title="删除操作",operatorType = OperatorType.OTHER)
@DeleteMapping("{ids}")
public AjaxResult deleteProductSpec(@PathVariable List ids){
productSpecService.removeByIds(ids);
return AjaxResult.success();
}
}
Service Interface
public interface IProductSpecService extends IService {
List selectProductSpecList();
void saveProductSpec(ProductSpec productSpec) throws JsonProcessingException;
}
Service Implement
package com.spzx.product.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.security.utils.SecurityUtils;
import com.spzx.product.domain.ProductSpec;
import com.spzx.product.mapper.ProductSpecMapper;
import com.spzx.product.service.IProductSpecService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
* 商品规格 服务实现类
*
*
* @author atguigu
* @since 2026-02-01
*/
@Service
public class ProductSpecServiceImpl extends ServiceImpl implements IProductSpecService {
@Autowired
private ProductSpecMapper productSpecMapper;
@Override
public List selectProductSpecList() {
return productSpecMapper.selectAll();
}
@Override
public void saveProductSpec(ProductSpec productSpec){
String specValue = productSpec.getSpecValue();
validateSpecJson(specValue);
productSpec.setCreateBy(SecurityUtils.getUsername());
productSpec.setUpdateBy(SecurityUtils.getUsername());
this.save(productSpec);
}
private static final ObjectMapper MAPPER = new ObjectMapper();
public static void validateSpecJson(String specValueJson) {
try {
JsonNode root = MAPPER.readTree(specValueJson);
if (root == null || !root.isArray()) {
throw new ServiceException("规格格式错误:应为 JSON 数组");
}
// 校验 key 全局唯一
Set keySet = new HashSet<>();
for (JsonNode item : root) {
if (item == null || !item.isObject()) {
throw new ServiceException("规格格式错误:数组元素必须是对象");
}
String key = item.path("key").asText("").trim();
if (key.isEmpty()) {
throw new ServiceException("规格名称为空");
}
if (!keySet.add(key)) {
throw new ServiceException("规格名称重复:" + key);
}
JsonNode valueListNode = item.path("valueList");
if (!valueListNode.isArray() || valueListNode.size() == 0) {
throw new ServiceException("规格值为空:" + key);
}
// 校验当前 key 的 valueList 唯一
Set valueSet = new HashSet<>();
for (JsonNode v : valueListNode) {
String val = v.asText("").trim();
if (val.isEmpty()) {
throw new ServiceException("规格值为空:" + key);
}
if (!valueSet.add(val)) {
throw new ServiceException("规格值重复:" + key + " = " + val);
}
}
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
// JSON 解析失败等
throw new ServiceException("规格 JSON 解析失败:" + e.getMessage());
}
}
}
Mapper Interface
public interface ProductSpecMapper extends BaseMapper {
List selectAll();
}
Mapper XML
select ps.id,ps.spec_name,ps.spec_value,ps.create_time,c.name categoryName
from category c inner join product_spec ps
on c.id=ps.category_id
where c.del_flag=0 and ps.del_flag=0
开发日志 day…?
项目初始化
1.基于ruoyi框架的初始化项目,更改一些名称信息,以及结构
2.前端使用基于ruoyi框架的更改后的项目,包含了后续功能所需。
3.初始化数据库:使用提供的sql文件初始化数据库
4.前端ui菜单管理添加对应菜单与路由映射,权限映射
商品管理-品牌管理
基于数据库表和若依后端结构,完成增删改,以及分页查询,按名称查询
基础数据-单位管理
基于数据库表和若依后端结构,完成增删改,以及分页查询,按名称查询
基础数据-商品分类
基于数据库表和若依后端结构,完成商品类别查询,子类别查询
基础数据-分类品牌
基于数据库表和若依后端结构,完成增删,分页查询(多表)
基础数据-商品规格
基于数据库表和若依后端结构,完成增删,分页查询(多表),以及新增时的重复校验
day+1 …
Controller