商品甄选

开发环境

若依框架: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

商品规格

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

开发日志

day…?

项目初始化 1.基于ruoyi框架的初始化项目,更改一些名称信息,以及结构 2.前端使用基于ruoyi框架的更改后的项目,包含了后续功能所需。 3.初始化数据库:使用提供的sql文件初始化数据库 4.前端ui菜单管理添加对应菜单与路由映射,权限映射
商品管理-品牌管理 基于数据库表和若依后端结构,完成增删改,以及分页查询,按名称查询
基础数据-单位管理 基于数据库表和若依后端结构,完成增删改,以及分页查询,按名称查询
基础数据-商品分类 基于数据库表和若依后端结构,完成商品类别查询,子类别查询
基础数据-分类品牌 基于数据库表和若依后端结构,完成增删,分页查询(多表)
基础数据-商品规格 基于数据库表和若依后端结构,完成增删,分页查询(多表),以及新增时的重复校验

day+1

Controller