商品甄选

开发环境

若依框架: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<ProductUnit> selectProductUnitList(String name);

void updateProductUnit(ProductUnit productUnit);

}

Service implement @Service public class ProductUnitServiceImpl extends ServiceImpl implements IProductUnitService {
@Override
public List<ProductUnit> selectProductUnitList(String name) {
    LambdaQueryWrapper<ProductUnit> wrapper = new LambdaQueryWrapper<>();
    wrapper.like(StringUtils.isNotBlank(name),ProductUnit::getName, name);
    return list(wrapper);
}

@Override
public void updateProductUnit(ProductUnit productUnit) {
    LambdaQueryWrapper<ProductUnit> 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<Category> list=categoryService.getList(parent_id);
    return AjaxResult.success(list);
}

}

Service Interface public interface ICategoryService extends IService {
List<Category> getList(Long parentId);

}

Service Implement @Service public class CategoryServiceImpl extends ServiceImpl implements ICategoryService {
@Override
public List<Category> getList(Long parentId) {
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Category::getParentId,parentId);
    queryWrapper.select(Category::getId,Category::getName,Category::getParentId,Category::getStatus,Category::getImageUrl,Category::getOrderNum);
    List<Category> list = this.list(queryWrapper);
    if(!ObjectUtils.isEmpty(list)){
        for (Category category : list) {
            Long count=this.count(new LambdaQueryWrapper<Category>().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<CategoryBrand> 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<CategoryBrandMapper, CategoryBrand> 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<CategoryBrand>()
             .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<CategoryBrand> 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<ProductSpec> 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<ProductSpecMapper, ProductSpec> 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<ProductSpec> selectAll();

}

Mapper XML

商品列表

Controller @RestController @RequestMapping("/product") public class ProductController extends BaseController {
@Autowired
private IProductService productService;

@Operation(summary = "查询商品列表")
@Log(title="查询操作",operatorType = OperatorType.OTHER)
@RequiresPermissions("product:product:list")
@GetMapping("/list")
public TableDataInfo list(){
    //开启分页
    startPage();
    //查询结果
    List<Product> list = productService.selectProductList();
    //返回数据
    return getDataTable(list);
}

}

Service Interface public interface IProductService extends IService {
List<Product> selectProductList();

}

Service Implement @Service public class ProductServiceImpl extends ServiceImpl implements IProductService {
@Autowired
private ProductMapper productMapper;


@Override
public List<Product> selectProductList() {

    return productMapper.selectProductAll();
}

}

Mapper Interface public interface ProductSpecMapper extends BaseMapper {
List<ProductSpec> selectAll();

}

Mapper XML
<select id="selectProductAll" resultType="com.spzx.product.domain.Product">
    select
        p.id id,
        p.slider_urls sliderUrls,
        p.name name,
        b.name brandName,
        c1.name category1Name,
        c2.name category2Name,
        c3.name category3Name,
        p.unit_name unitName,
        p.status status
    from product p
             inner join brand b on p.brand_id=b.id and b.del_flag=0
             inner join category c1 on p.category1_id=c1.id and b.del_flag=0
             inner join category c2 on p.category2_id=c2.id and b.del_flag=0
             inner join category c3 on p.category3_id=c3.id and b.del_flag=0
    where p.del_flag=0
</select>

开发日志

day…?

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

day+1

完成商品列表的分页查询(多表)

day+2

Controller