JavaWeb MyBatisPlus添加一个能够批量插入的Mapper通用方法

JavaWeb MyBatisPlus添加一个能够批量插入的Mapper通用方法

众所周知,mybatisplus提供的BaseMapper里只有单条插入的方法,没有批量插入的方法,
而在Service层的批量插入并不是真的批量插入,实际上是遍历insert,但也不是一次insert就一次IO,而是到一定数量才会去IO一次,性能不是很差,但也不够好。

怎么才能实现真正的批量插入呢?

这里是mybatisplus官方的演示仓库,可以先去了解一下。

一、注册自定义通用方法流程

  1. 把自定义方法写到BaseMapper,因为没法改BaseMapper,所以继承一下它
language-java
1
2
3
4
5
public interface MyBaseMapper<T> extends BaseMapper<T> {


int batchInsert(@Param("list") List<T> entityList);
}

MyBaseMapper扩展了原有的BaseMapper,所以你之后的Mapper层都继承自MyBaseMapper而不是BaseMapper即可。

  1. 把通用方法注册到mybatisplus
language-java
1
2
3
4
5
6
7
8
9
10
11
@Component
public class MySqlInjector extends DefaultSqlInjector {

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {

List<AbstractMethod> defaultMethodList = super.getMethodList(mapperClass, tableInfo);
defaultMethodList.add(new BatchInsert("batchInsert"));
return defaultMethodList;
}
}

关键的一句在于defaultMethodList.add(new BatchInsert("batchInsert"));,意为注册一个新的方法叫batchInsert,具体实现在BatchInsert类。

  1. 实现BatchInsert类
language-java
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
public class BatchInsert extends AbstractMethod {

public BatchInsert(String name) {

super(name);
}

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {

KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
String columnScript = getAllInsertSqlColumn(tableInfo.getFieldList());
String valuesScript = SqlScriptUtils.convertForeach(LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET,
LIST, null, "item", COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {

if (tableInfo.getIdType() == IdType.AUTO) {

/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
// 去除转义符
keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn());
} else if (null != tableInfo.getKeySequence()) {

keyGenerator = TableInfoHelper.genKeyGenerator(methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
}

/**
* 获取 insert 时所有列名组成的sql片段
* @param fieldList 表字段信息列表
* @return sql 脚本片段
*/
private String getAllInsertSqlColumn(List<TableFieldInfo> fieldList) {

return LEFT_BRACKET + fieldList.stream()
.map(TableFieldInfo::getColumn).filter(Objects::nonNull).collect(joining(COMMA + NEWLINE)) + RIGHT_BRACKET;
}

/**
* 获取 insert 时所有属性值组成的sql片段
* @param prefix 前缀
* @param fieldList 表字段信息列表
* @return sql 脚本片段
*/
private String getAllInsertSqlProperty(final String prefix, List<TableFieldInfo> fieldList) {

final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
.map(i -> i.getInsertSqlProperty(newPrefix).replace(",", ""))
.filter(Objects::nonNull)
.collect(joining(COMMA + NEWLINE));
}
}

二、BatchInsert具体实现逻辑解析

以如下简单的表user举例,

列名 描述 类型
id 主键,自增 bigint
user_name 用户名 varchar(64)
user_age 用户年龄 int

对于的entity大抵如下

language-java
1
2
3
4
5
6
7
8
9
10
11
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("user")
public class User{

@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String userName;
private int userAge;
}

那么对于batchInsert,我们希望传入List<User>并希望得到类似如下的mybtaisplus xml sql语句

language-xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<insert id="batchInsert" parameterType="java.util.List">
insert into user(
id,
user_name,
user_age
)values
<foreach collection="list" item="item" separator=",">
(
#{item.id},
#{item.userName},
#{item.userAge}
)
</foreach>
</insert>

但是我们并不自己写这个xml,不然这需要对每一个数据表都要写一个,就像不那么硬的硬代码一样,我们希望有段逻辑,只需要传入entity,就能自己解析其中列名和对应的属性名,生成这段xml实现批量插入的功能。

假设你的UserMapper已经继承自MyBaseMapper,如果调用UserMapper.bacthInsert(List<User> entityList),那么会进入这个函数

language-java
1
2
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo)

其中mapperClass是映射器类,modelClass是模型类,我们并不需要了解,最主要的是tableInfo,这是表信息,它包含了关于数据库表的各种信息,如表名、列名、主键等。这个参数提供了详细的表信息,这对于生成针对特定表的SQL语句是必要的。

然后执行如下

language-java
1
2
3
4
5
6
7
//如果你的表名没有主键,那么你需要指定keyGenerator 为NoKeyGenerator,
//因为重写injectMappedStatement最后需要返回return this.addInsertMappedStatement
//其中就需要KeyGenerator
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
//SqlMethod.INSERT_ONE就是"INSERT INTO %s %s VALUES %s"
//我们依据表的信息生成列名sql片段和属性名sql片段后填入%s就可以得到近似最后的xml sql
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;

然后执行如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//tableInfo.getFieldList()会得到一个包含数据表列信息(不包含主键)的TableFieldInfo类组成的List
String columnScript = getAllInsertSqlColumn(tableInfo.getFieldList());
//这行代码就是在调用这个函数
private String getAllInsertSqlColumn(List<TableFieldInfo> fieldList) {

return LEFT_BRACKET + fieldList.stream()
//从TableFieldInfo中只拿取列名
.map(TableFieldInfo::getColumn)
//过滤null
.filter(Objects::nonNull)
//在元素间以逗号和分行分割
.collect(joining(COMMA + NEWLINE)) + RIGHT_BRACKET;
}
//对于User表,这个函数返回以下String
/*
(user_name,
user_age)
*/

然后执行如下

language-java
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
//首先调用了getAllInsertSqlProperty
String valuesScript = SqlScriptUtils.convertForeach(
// 这也是个内置函数,可以直接去看看
LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET,
LIST,
null,
"item",
COMMA
);
//LEFT_BRACKET + getAllInsertSqlProperty("item.", tableInfo.getFieldList()) + RIGHT_BRACKET
//得到
/*
(#{userName},
#{userAge})
*/
//经过convertForeach函数后,得到如下字符串
/*
<foreach collection="list" item="item" separator=",">
(#{userName},
#{userAge})
</foreach>
*/

//getAllInsertSqlProperty函数如下
private String getAllInsertSqlProperty(final String prefix, List<TableFieldInfo> fieldList) {

//这里newPrefix 就是"item."
final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
//i.getInsertSqlProperty("item.")是内置函数,假设i现在遍历到了user_name列
//那么得到的就是"#{userName},"
//然后,被删了
//所以本来每个元素从TableFieldInfo变成了形如"#{userName}"的字符串
.map(i -> i.getInsertSqlProperty(newPrefix).replace(",", ""))
.filter(Objects::nonNull)
//在元素间插入逗号和分行
.collect(joining(COMMA + NEWLINE));
}
//对于User表,这个函数返回以下String
/*
#{userName},
#{userAge}
*/

然后执行如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
		//定义主键属性名
String keyProperty = null;
//定义主键列名
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {

if (tableInfo.getIdType() == IdType.AUTO) {

/* 自增主键 */
keyGenerator = Jdbc3KeyGenerator.INSTANCE;
keyProperty = tableInfo.getKeyProperty();
// 去除转义符
keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn());
} else if (null != tableInfo.getKeySequence()) {

keyGenerator = TableInfoHelper.genKeyGenerator(methodName, tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
//这段代码没什么好说的,就是根据不同情况,得到三个变量
//keyGenerator keyProperty keyColumn

然后执行如下

language-java
1
2
3
4
5
6
7
8
9
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
//就是把表名user,列名片段,属性名片段,填入%s中,得到如下
/*
INSERT INTO user (user_name,
user_age) VALUES <foreach collection="list" item="item" separator=",">
(#{userName},
#{userAge})
</foreach>
*/

然后执行如下

language-java
1
2
3
4
//这两句没有什么可说的,是重写injectMappedStatement函数的必要的操作
//自定义的内容就在于sql和主键
SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
基本正则表达式

基本正则表达式

正则表达式在线测试https://c.runoob.com/front-end/854/

一、基本规则

1. 基本匹配

正则表达式最基本的形式是一个由字母和数字组成的序列。这个序列可以精确匹配文本中的内容。

表达式 描述
abc 精确匹配 ‘abc’

2. 量词

量词定义了一个模式应该出现的次数。

表达式 描述
* 匹配前面的模式零次或多次
+ 匹配前面的模式一次或多次
? 匹配前面的模式零次或一次
{n} 匹配前面的模式 n 次
{n,} 匹配前面的模式至少 n 次
{n,m} 匹配前面的模式至少 n 次,但不超过 m 次

*+ 限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个 ? 就可以实现非贪婪或最小匹配。

3. 字符类

字符类允许我们定义一个字符集合,然后匹配其中任何一个字符。

表达式 描述
[abc] 匹配 ‘a’、’b’ 或 ‘c’
[a-z] 匹配任何小写字母
[A-Z] 匹配任何大写字母
[0-9] 匹配任何数字

4. 特殊字符

含有特殊含义的字符

表达式 描述
\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘\‘ 匹配 “” 而 “(“ 则匹配 “(“。
. 匹配任何单个字符(除了换行符\n)
\w 匹配任何单词字符(等同于 [a-zA-Z0-9_])
\W 匹配任何非单词字符
\d 匹配任何数字字符(等同于 [0-9])
\D 匹配任何非数字字符
\s 匹配任何空白字符
\S 匹配任何非空白字符
\n 匹配一个换行符
\r 匹配一个回车符
\t 匹配一个制表符
^ 匹配输入字符串开始的位置
$ 匹配输入字符串结尾的位置
\b 匹配一个单词边界,即字与空格间的位置
\B 匹配一个非单词边界

5. 分组

在正则表达式中,括号 () 用于创建一个分组。分组有两个主要的用途:
捕获分组 :括号中的表达式会作为一个单独的单元进行匹配,然后将匹配的内容保存起来以供后续使用。例如,表达式 (abc) 会匹配文本中的 ‘abc’,并将其保存为一个分组。
量词应用:括号还可以让我们对一组字符应用量词。例如,表达式 (abc){2} 会匹配 ‘abcabc’,因为 {2} 量词应用于整个 ‘abc’ 分组,表示这个分组需要连续匹配两次。

以下是一些使用括号的例子:
(abc):匹配并捕获 ‘abc’。
a(bc):匹配并捕获 ‘bc’,前面有 ‘a’。
(a|b)c:匹配 ‘ac’ 或 ‘bc’。

在正则表达式中,你可以通过反向引用来使用捕获到的内容。反向引用使用反斜杠 \ 加上一个数字来表示,例如 \1、\2 等。
这个数字表示的是捕获组的编号。捕获组的编号是根据左括号 ( 出现的顺序来确定的,从 1 开始计数。例如,在表达式 (a)(b)(c) 中,a 是第 1 组,b 是第 2 组,c 是第 3 组。

以下是一些使用反向引用的例子:
(a)\1:匹配 ‘aa’。\1 是对第一组的引用,表示和第一组匹配的相同内容,也就是 ‘a’。
(a)(b)\2\1:匹配 ‘abba’。\2 是对第二组的引用,表示 ‘b’,\1 是对第一组的引用,表示 ‘a’。

二、修饰符

标记也称为修饰符,正则表达式的标记用于指定额外的匹配策略。
标记不写在正则表达式里,标记位于表达式之外,格式如

language-bash
1
/pattern/flags
修饰符 含义 描述
i ignore - 不区分大小写 将匹配设置为不区分大小写,搜索时不区分大小写,如 A 和 a 没有区别。
g global - 全局匹配 查找所有的匹配项。
m multi line - 多行匹配 使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
s 特殊字符圆点 . 中包含换行符 \n 默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。

三、优先级

正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。相同优先级的从左到右进行运算,不同优先级的运算先高后低。

运算符 描述
\ 转义符
(), (?:), (?=), [] 圆括号和方括号
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, \任何元字符、任何字符 定位点和序列(即:位置和顺序)
| 替换,”或”操作 字符具有高于替换运算符的优先级,使得”m|food”匹配”m”或”food”。若要匹配”mood”或”food”,请使用括号创建子表达式,从而产生”(m|f)ood”。
JavaWeb若依分页插件使用

JavaWeb若依分页插件使用

后端只需要在你本身的contoller调用service取得数据那条命令前后加上2行代码,就可以使用mybatis的分页插件。

language-java
1
2
3
4
5
6
7
8
@GetMapping("move/search")
public AjaxResult moveSearch(StockMoveSearchDTO dto){

startPage();//第一句
List<BztStockMoveHeadExp> list = stockMgntService.moveSearch(dto);//这是自己本身的逻辑代码
TableDataInfo dataTable = getDataTable(list);//第二句
return success(dataTable);
}

整体看起来,startPage像是加载了某些配置,然后基于这些配置getDataTable对你本来的逻辑进行了修改,实现了分页排序效果。

1.原理解析

ctrl左键点击startPage(),两次追溯源码实现,可以看到以下代码

language-java
1
2
3
4
5
6
7
8
9
10
public static void startPage()
{

PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
Boolean reasonable = pageDomain.getReasonable();
PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
}

PageHelper.startPage(pageNum, pageSize, orderBy)myabtis的分页插件,

  • 第一个参数是当前页码
  • 第二个参数是每一页数量
  • 第三个参数是排序信息,包括排序列和排序顺序

PageHelper会拦截你的sql代码然后加上类似limit ,order by等语句实现所谓的分页排序查询,数据截取是在sql中完成的,很快,所以你不必担心这个插件是不是在数据选完之后再分割导致性能问题。

那么问题来了,pageNum, pageSize, orderBy这三个参数从哪里来?毋庸置疑,肯定是从前端来,但是却不需要你写在dto中,因为若依会从你传到后端的数据中截取这三个参数。ctrl左键点击buildPageRequest(),追溯源码实现,你会看到以下代码。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static PageDomain getPageDomain()
{

PageDomain pageDomain = new PageDomain();
pageDomain.setPageNum(Convert.toInt(ServletUtils.getParameter(PAGE_NUM), 1));
pageDomain.setPageSize(Convert.toInt(ServletUtils.getParameter(PAGE_SIZE), 10));
pageDomain.setOrderByColumn(ServletUtils.getParameter(ORDER_BY_COLUMN));
pageDomain.setIsAsc(ServletUtils.getParameter(IS_ASC));
pageDomain.setReasonable(ServletUtils.getParameterToBool(REASONABLE));
return pageDomain;
}

public static PageDomain buildPageRequest()
{

return getPageDomain();
}

大概意思就是会从你传到后端的数据从截取以下字段

  • pageNum
  • pageSize
  • orderByColumn
  • isAsc
    orderby由后两个字段组成。所以在你前端传到后端的数据中,添加这4个字段和值即可实现分页效果和排序效果。

2. 前端示例

language-html
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
<template>
<el-table :data="tableList" @sort-change="handleSortChange">
<el-table-column prop="formCreateTime"
label="录入日期"
sortable="custom"
:sort-orders="['ascending', 'descending']"
>
<template #default="scope">
{
{ new Date(scope.row.formCreateTime).toLocaleDateString() }}
</template>
</el-table-column>
</template>

<script setup>
const queryParams = reactive({

pageNum: undefined,
pageSize: undefined,
orderByColumn: 'formCreateTime',
isAsc: 'descending',
})
const tableList = ref([])

const handleSortChange=(column)=>{

queryParams.orderByColumn = column.prop;
queryParams.isAsc = column.order;
onSubmitQuiet();
}
const onSubmitQuiet = () => {

queryParams.pageNum = 1;
queryParams.pageSize = 10;
moveSearch(queryParams).then(rp => {

// console.log(rp)
tableList.value = rp.data.rows;
pageTotal.value = rp.data.total;
})
}
</script>

这是一个简单的表格,只包含一列,当点击这一列表头时,触发handleSortChange,更新orderByColumnisAsc 然后重新从后端查询一遍,注意用Get方式。pageNumpageSize 可以通过el-pagination组件更新,这里就不写了。后端返回值包含rows表示具体数据是一个list,total表示数据量。需要注意的时传给后端的orderByColumn所代表的字符串(这里是formCreateTime)应该和后端对应domain对应的变量名一致。

3. 后端示例

language-java
1
2
3
4
5
6
7
8
@GetMapping("move/search")
public AjaxResult moveSearch(StockMoveSearchDTO dto){

startPage();
List<BztStockMoveHeadExp> list = stockMgntService.moveSearch(dto);
TableDataInfo dataTable = getDataTable(list);
return success(dataTable);
}

其中List<BztStockMoveHeadExp> list = stockMgntService.moveSearch(dto);只需按照原来的逻辑写就行,不需要考虑分页或者排序。

JavaWeb 自动编号器Utils,用于各种自增长的编号,单号等

JavaWeb 自动编号器Utils,用于各种自增长的编号,单号等

使用以下技术栈

  • springboot
  • mybatisplus
  • mysql

一、首先设计一张表

表名假设为auto_numbering

名称 数据类型 备注 描述
id bigint 主键
name varchar(64) 编号名称
prefix varchar(8) 前缀
inti_value bigint 初始值
current_value bigint 当前值
length int 编号长度(不包含前缀)

假设有两条数据

id name prefix inti_value current_value length
1 stock_input SI 0 2 8
2 product_code PC 0 5 8

第一条表示入库单编号,编号的规则是,前缀为SI,当前编号数为2,长度为8,那么下一次使用入库单号时,对应的current_value加1变为3,得到的单号应该是字符串"SI00000003" 。第二条表示产品编号,同理,对应的current_value加1变为6,应该得到字符串"PC00000006"

那么如何设计这样一个自动编号工具类呢?这个工具类的方法最好是static,这样可以直接得到自动编号器的返回值,同时又要去让数据表对应的current_value自增。

二、创建对应的Domain类、Mapper接口、编号名称枚举类

domain

language-java
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
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("auto_numbering")
public class AutoNumbering{


private static final long serialVersionUID = 1L;

/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 编号名称
*/
private String name;

/**
* 前缀
*/
private String prefix;

/**
* 初始值
*/
private Long intiValue;

/**
* 当前值
*/
private Long currentValue;

/**
* 编号长度(不包含前缀)
*/
private Integer length;

}

mapper

language-java
1
2
3
4
5
6
7
8
9
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AutoNumberingMapper extends BaseMapper<AutoNumbering> {


}

enum

language-java
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
public enum AutoNumberingEnum {


STOCK_INPUT("stock_input", "入库编号"),
PRODUCT_CODE("product_code", "产品编号");

private final String name;
private final String remark;

AutoNumberingEnum(String name, String remark) {

this.name = name;
this.remark = remark;
}

/**
* 获取枚举名称
*
* @return name
*/
public String getName() {

return this.name;
}

/**
* 获取枚举描述
*
* @return remark
*/
public String getRemark() {

return this.remark;
}
}

三、写一个Service接口和实现类

service接口类

language-java
1
2
3
4
5
6
7
8
9
10
public interface AutoNumberingService {


/**
* 获取下一个编号
* @param param 自动编号枚举值
* @return 自动编号字符串
*/
String getNextNo(AutoNumberingEnum param);
}

serviceimpl

language-java
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
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class AutoNumberingServiceImpl implements AutoNumberingService {


@Autowired
private AutoNumberingMapper autoNumberingMapper ;

/**
* 自动编号方法
*
* @param param 自动编号枚举值
* @return 编号
*/
@Transactional
public String getNextNo(AutoNumberingEnum param) {


// 查找当前的采番配置信息
LambdaQueryWrapper<AutoNumbering> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AutoNumbering::getName, param.getName());
List<AutoNumbering> AutoNumberings = AutoNumberingMapper.selectList(wrapper);
// 未获取到配置信息
if (AutoNumberings.isEmpty()) {

return null;
} else {

// 规则获取
AutoNumbering autoNumbering = AutoNumberings.get(0);
// 前缀
String prefix = autoNumbering.getPrefix();
// 长度
Integer length = autoNumbering.getLength();
// 顺番
Long currentNo = autoNumbering.getCurrentValue();
// 更新原数据
currentNo++;
}
autoNumbering.setCurrentValue(currentNo);
AutoNumberingMapper.updateById(autoNumbering);

// 生成编号
String formatNo = StringUtils.leftPad(String.valueOf(currentNo), length, "0");
return String.format("%s%s", prefix, formatNo);
}
}

四、写一个Utils类静态获取编号

language-java
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
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NoGeneratorUtil {


private static AutoNumberingService autoNumberingService;

@Autowired
public void setAutoNumberingService(AutoNumberingService autoNumberingService) {

NoGeneratorUtil.autoNumberingService = autoNumberingService;
}

/**
* 获取最新的入库单编号
* @return 最新的入库单编号
*/
public static String getNextStockInputNo() {

return autoNumberingService.getNextNo(AutoNumberingEnum.STOCK_INPUT);
}

/**
* 获取最新的产品编号
* @return 最新的c'p 编号
*/
public static String getNextProductNo() {

return autoNumberingService.getNextNo(AutoNumberingEnum.PRODUCT_CODE);
}

这段代码是SpringBoot框架中的一种常见用法,用于将SpringBoot管理的bean注入到静态变量中。那么,autoNumberingService是如何被注入的呢?实际上,当SpringBoot启动时,它会扫描所有的类,并为带有@Component(或者@Service等)注解的类创建实例。在创建NoGeneratorUtil实例的过程中,SpringBoot会调用所有带有@Autowired注解的setter方法,包括setAutoNumberingService方法,从而将AutoNumberingService的实现类注入到autoNumberingService静态变量中。

五、使用

之后如下使用即可一行代码获取最新编号

language-java
1
2
String string1 = NoGeneratorUtil.getNextStockInputNo();
String string2 = NoGeneratorUtil.getNextProductNo();
JavaWeb 若依RuoYi-Vue3框架将Mybatis切换成MybatisPlus

JavaWeb 若依RuoYi-Vue3框架将Mybatis切换成MybatisPlus

这里是官方的做法

mybatisSqlSessionFactoryBean,而mybatis-plusMybatisSqlSessionFactoryBean,所以一般最好是项目中使用一个最好,当然想要共存也可以,mybatis-plus的版本最好要高。这里只讲如何切换成MyBatisPlus。

一、修改yml

application.yml中将mybatis配置注释并写上新的mybatisplus,如下所示

language-yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
## MyBatis配置
#mybatis:
# # 搜索指定包别名
# typeAliasesPackage: com.ruoyi.**.domain
# # 配置mapper的扫描,找到所有的mapper.xml映射文件
# mapperLocations: classpath*:mapper/**/*Mapper.xml
# # 加载全局的配置文件
# configLocation: classpath:mybatis/mybatis-config.xml
mybatis-plus:
# 配置要扫描的xml文件目录,classpath* 代表所有模块的resources目录 classpath 不加星号代表当前模块下的resources目录
mapper-locations: classpath*:mapper/**/*Mapper.xml
# 实体扫描,*通配符
typeAliasesPackage: com.ruoyi.**.domain

二、maven导入mybatisplus包,如下

language-xml
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>最新版本</version>
</dependency>

三、注释SqlSessionFactoryBean

找到com.ruoyi.framework.config.MyBatisConfig,注释public SqlSessionFactory sqlSessionFactory(DataSource dataSource)如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//    @Bean
// public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
// {

// String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
// String mapperLocations = env.getProperty("mybatis.mapperLocations");
// String configLocation = env.getProperty("mybatis.configLocation");
// typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
// VFS.addImplClass(SpringBootVFS.class);
//
// final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// sessionFactory.setDataSource(dataSource);
// sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
// sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
// sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
// return sessionFactory.getObject();
// }

到这里就完成了切换,快去试试 吧。

资料参考
若依框架集成mybatis换成mybatis-plus记录

JavaWeb el-upload图片上传SpringBoot保存并且前端使用img src直接访问(基于RuoYi-Vue3)

JavaWeb el-upload图片上传SpringBoot保存并且前端使用img src直接访问(基于RuoYi-Vue3)

一、Vue前端

language-html
1
2
3
4
5
6
7
8
9
10
11
12
<el-upload
v-model:file-list="createParams.fileList"
action="http://localhost:8080/DataMgt/uploadPic"
:headers="{ Authorization: 'Bearer ' + getToken() }"
list-type="picture-card"
:on-success="handleSuccess"
:before-upload="handlePicBeforeUpload"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
>
<el-icon><Plus /></el-icon>
</el-upload>

需要注意的是action填写后端api路径,同时header需要填写token不然会被拦截,其余的没有影响可以翻阅文档理解。

二、SpringBoot后端

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/DataMgt")
public class Home extends BaseController {

private static String picDir = "C:\\Users\\mumu\\Desktop\\images\\";

@RequestMapping("/uploadPic")
public AjaxResult uploadPic(MultipartFile file) throws IOException {

String filePath = picDir+ UUID.randomUUID().toString() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf('.'));
File saveF = new File(filePath);
logger.info("save pic:"+filePath);
file.transferTo(saveF);
return success(filePath);
}
}

picDir填写的是图片保存到哪个文件夹路径,无论你是windows开发环境还是linux生产环境,这个路径都应该是绝对路径。
现在图片保存到了这个路径,那么我们前端imgsrc又如何才能经由后端直接访问图片呢?

第一,需要通过SpringBoot的@Configuration配置一个映射,如下

language-java
1
2
3
4
5
6
7
8
9
10
@Configuration
public class StaticConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/images/**")
.addResourceLocations("file:C:\\Users\\mumu\\Desktop\\images\\");
}
}

意思是将C:\\Users\\mumu\\Desktop\\images\\路径映射到/images/,那么当你访问http://localhost:8080/images/1.png时其实就是在访问C:\\Users\\mumu\\Desktop\\images\\1.png

第二,需要在若依的com.ruoyi.framework.config.SecurityConfigconfigure函数中中配置访问权限,将/images/添加其中,对所有人开放,如下

language-java
1
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**", "/images/**").permitAll()

最后,在前端,像这样就可以经由后端访问图片啦。

language-html
1
<el-image src="http://localhost:8080/images/1.png" style="width: 200px;height: 200px"></el-image>

三、延伸

这是多图片上传组件,但其实每张都会和后端交互一次。

如果这个图片墙和文字搭配在一起成为一个图文动态发布页面,那么创建主副数据表,主表保存idtext表示id和文字,副表保存master_idname,表示主表id,图片名。
当新建动态时,在前端即刻生成当前时间作为图文动态的唯一标识。
当上传图片时,以唯一标识作为master_id并以保存的名称作为name插入副表。
当动态提交时,以唯一标识作为id并以文本内容作为text插入主表。
当取消时,以唯一标识为查询条件删除主副表相关数据和图片资源。

Vue3 setup路由进入以及变化问题

Vue3 setup路由进入以及变化问题

1. 起因

在Vue 3中,<script setup>语法糖提供了一种更加简洁和组合式的方式来定义组件。然而,由于<script setup>的特性,它不能直接使用beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave这些导航守卫。

但是vue-router中有两个的类似函数可以触发路由离开和变化,只需要import一下就可以。

language-js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import {
onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router";

onBeforeRouteUpdate((to, from, next) => {

console.log("onBeforeRouteUpdate",to,from, typeof next)
next();
})
onBeforeRouteLeave((to, from, next)=>{

console.log("beforeRouteLeave",to,from, typeof next)
next()
})
</script>

但是却没有beforeRouteEnter的替代品。

2. 浏览过的代替方法

https://github.com/vuejs/rfcs/discussions/302#discussioncomment-2794537
https://blog.richex.cn/vue3-how-to-use-beforerouteenter-in-script-setup-syntactic-sugar.html
https://blog.csdn.net/oafzzl/article/details/125045087

但是都是在<script setup>之上新加了第二个<script>,用第二个<script>来使用无setupbeforeRouteEnter。限制很多,尤其是两个script之间互动很麻烦,甚至无法实现。

3. 还是Watch

https://juejin.cn/post/7171489778230100004
https://blog.csdn.net/m0_55986526/article/details/122827829

下面是一个当路由从"/stock/move/moveDetail""/stock/move/moveSearch"触发函数的例子。同时第一次进入时也会触发watch,就相当于beforeRouteEnter了。

language-js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import {
watch} from "vue";
import {
useRouter} from "vue-router";
let router = useRouter()
// 监听当前路由变化
watch(() => router.currentRoute.value,(newPath, oldPath) => {

if (newPath != null && oldPath != null && "fullPath" in newPath && "fullPath" in oldPath && newPath["fullPath"] === "/stock/move/moveSearch" && oldPath["fullPath"] === "/stock/move/moveDetail"){

onSubmitQuiet()
}
}, {
immediate: true});
</script>
JavaWeb Springboot后端接收前端Date变量

JavaWeb Springboot后端接收前端Date变量

如果前端传输的日期符合ISO 8601格式,那么后端用@PostMapping + @RequestBody那么springboot就会自动解析。

如果传输的日期格式不符合ISO 8601格式或者传输用的是Get方法,那么在接收传输的实体类中加上@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss"),这里面的pattern对应传来的日期格式。

language-html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ISO 8601是一种国际标准的日期和时间表示方式。这种格式的主要特点包括:

- 时间日期按照年月日时分秒的顺序排列,大时间单位在小时间单位之前。
- 每个时间单位的位数固定,不足时于左补0。
- 提供两种方法来表示时间:其一为只有数字的基础格式;其二为添加分隔符的扩展格式,让人能更容易阅读。

具体来说,ISO 8601的日期和时间表示方式的格式为`YYYY-MM-DDTHH:mm:ss.sssZ`,其中:

- `YYYY`代表四位数年份。
- `MM`代表月份。
- `DD`代表天数。
- `T`作为日期和时间的分隔符。
- `HH`代表小时。
- `mm`代表分钟。
- `ss.sss`代表秒和毫秒。
- `Z`代表的是时区。

例如,

"2023年11月15日06时16分50秒"可以表示为"2023-11-15T06:16:50"。
"2023-11-15"是ISO 8601日期格式的一个子集。在ISO 8601中,日期可以表示为"YYYY-MM-DD"的格式。因此,"2023-11-15"是符合ISO 8601日期格式的。

JavaWeb后端解析前端传输的excel文件(SpringBoot+Vue3)

JavaWeb后端解析前端传输的excel文件(SpringBoot+Vue3)

一、前端

前端拿Vue3+ElementPlus做上传示例

language-html
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
<template>
<el-button type="primary" @click="updateData" size="small" plain>数据上传</el-button>
<el-dialog :title="upload.title" v-model="upload.open" width="400px" align-center append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload">
<upload-filled/>
</el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>仅允许导入xls、xlsx格式文件。</span>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>

const upload = reactive({

// 是否显示弹出层(用户导入)
open: false,
// 弹出层标题(用户导入)
title: "标题",
// 是否禁用上传
isUploading: false,
// 是否更新已经存在的用户数据
updateSupport: 0,
// 设置上传的请求头部
headers: {
Authorization: "Bearer " + getToken()},
// 上传的地址
url: import.meta.env.VITE_APP_BASE_API + "/yourControllerApi"
});

//数据上传
const updateData = () => {

upload.title = "数据上传";
upload.open = true;
}

//文件上传中处理
const handleFileUploadProgress = (event, file, fileList) => {

upload.isUploading = true;
console.log(upload.url)
};

//文件上传成功处理
const handleFileSuccess = (rp, file, fileList) => {

//这里的rp就是后端controller的响应数据
console.log(rp)
upload.open = false;
upload.isUploading = false;
proxy.$refs["uploadRef"].handleRemove(file);
};
</script>

二、后端

  1. 导入依赖
language-xml
1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
  1. controller接收file
language-java
1
2
3
4
5
@PostMapping("/yourControllerApi")
public AjaxResult importData(@RequestBody MultipartFile file){

return stockMgntService.importData(file);
}
  1. service具体实现
language-java
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
@Service
public class StockMgntServiceImpl implements StockMgntService {

@Override
public AjaxResult importData(MultipartFile file) {

try(InputStream is = file.getInputStream();) {

Workbook workbook = WorkbookFactory.create(is);
int numberOfSheets = workbook.getNumberOfSheets();
if (numberOfSheets != 1){

//要处理多个sheet,循环就可以
System.out.println("只允许有1个Sheet");
return null;
}
//取得第一个sheet
Sheet sheet = workbook.getSheetAt(0);
//获取最后一行的索引,但通常会比预想的要多出几行。
int lastRowNum = sheet.getLastRowNum();
//循环读取每一行
for (int rowNum = 0; rowNum < lastRowNum; rowNum++) {

Row rowData = sheet.getRow(rowNum);
if (rowData != null){

//可以使用rowData.getLastCellNum()获取最后一列的索引,这里只有6行,是假设现在的excel模板是固定的6行
for(int cellNum = 0; cellNum < 6; ++cellNum){

Cell cell = rowData.getCell(cellNum);
if (cell == null){
continue;}
cell.setCellType(CellType.STRING);
if (!cell.getStringCellValue().isEmpty()){

System.out.println(cell.getStringCellValue());
}
/*
利用这个方法可以更好的将所有数据以String获取到
Cell cell = productCodeRow.getCell(cellNum , Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
String cellValue = dataFormatter.formatCellValue(cell );
*/
}
}
}
}catch (Exception e) {

System.out.println(e.getMessage());
throw new RuntimeException();
}
}
}

三、结语

这样的方式只能处理简单的excel,对于结构更复杂的excel,需要其他工具utils结合。
以及可以结合SpringBoot有更好的poi使用方法。

JavaWeb后端将excel文件传输到前端浏览器下载(SpringBoot+Vue3)

JavaWeb后端将excel文件传输到前端浏览器下载(SpringBoot+Vue3)

一、后端

  1. 导入依赖
language-xml
1
2
3
4
5
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
  1. controller层
    其中@RequestParam String moveNo是前端传入的参数,而HttpServletResponse response不是,前者可以没有,后者一定要有。
language-java
1
2
3
4
5
6
7
@GetMapping("/yourControllerApi")
public AjaxResult exportData(@RequestParam String moveNo, HttpServletResponse response){

System.out.println(moveNo);
System.out.println(response);
return stockMgntService.exportData(moveNo, response);
}
  1. service层
language-java
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
@Service
public class StockMgntServiceImpl implements StockMgntService {

@Override
public AjaxResult exportData(String moveNo, HttpServletResponse response) {

//这里是从后台resources文件夹下读取一个excel模板
ClassPathResource resource = new ClassPathResource("template/模板.xlsx");
try(InputStream fis = resource.getInputStream();
Workbook workbook = WorkbookFactory.create(fis);
ServletOutputStream os = response.getOutputStream()
){

//这块写入你的数据
//往第一个sheet的第一行的第2列和第5列写入数据
Sheet sheet = workbook.getSheetAt(0);
sheet.getRow(0).getCell(1, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue("test");
sheet.getRow(0).getCell(5, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellValue("test");
/*......你的操作......*/
//这块开始配置传输
response.setCharacterEncoding("UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-disposition", "attachment;filename=template.xlsx");
response.flushBuffer();
workbook.write(os);
os.flush();
os.close();
}catch (IOException e) {

throw new RuntimeException(e);
}
return null;
}
}

二、前端

axios向对应api发送请求并携带参数,然后使用Blob来接收后端的OutputStream输出流并保存下载保存到本地浏览器。

language-html
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
<template>
<el-button type="primary" @click="downloadData" size="small" plain>模板下载</el-button>
</template>

<script setup>
const downloadData = () => {

console.log(queryParams.moveNo)
axios({

url: '/yourControllerApi',
method: 'get',
params: {
moveNo:'0000212132142'},
responseType: "blob"
}).then(rp=>{

console.log(rp)
const blob = new Blob([rp], {
type: 'application/vnd.ms-excel'});
let link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.setAttribute('download', '模板下载.xlsx');
link.click();
link = null;
});
}
</script>