初始化

This commit is contained in:
zweiandlen 2025-04-23 11:03:01 +08:00
commit 89c2e6c5c4
Signed by: zweiandlen
GPG Key ID: A6A3880FDDD83652
148 changed files with 11513 additions and 0 deletions

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# Spring Boot 3 项目模版
## 本模版项目使用方法
1. 安装 [cookiecutter](https://cookiecutter.readthedocs.io/en/stable/) 命令
2. cookiecutter 本模版的文件夹
3. 根据命令行提示,生成项目
## 项目模版
1. Spring Boot 3 + mybatis plus + mapstruct + mysql / postgis + redis 包含 RABC 权限模型
```
cookiecutter https://www.llvy.ltd/llvy.ltd/sample-project.git --checkout main
```

15
cookiecutter.json Normal file
View File

@ -0,0 +1,15 @@
{
"project_hans": "项目名称",
"project_name": "Sample Project",
"project_slug": "{{ cookiecutter.project_name | lower | replace(' ', '_') | replace('-', '_') }}",
"mvn_group_id": "com.sample",
"mvn_artifact_id": "{{ cookiecutter.project_name | lower | replace(' ', '_') | replace('-', '_') }}",
"__mvn_package": "{{ cookiecutter.mvn_group_id }}.{{ cookiecutter.mvn_artifact_id }}",
"__package_path": "{{ cookiecutter.__mvn_package | replace('.', '/') }}",
"platform": ["mysql", "postgis"],
"author": "zweiandlen",
"email": "zweiandlen@outlook.com",
"_copy_without_render": [
"*.vue"
]
}

View File

@ -0,0 +1,50 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### Vue.js ###
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
# Editor directories and files
.vscode/
.mvn/
/src/main/resources/static/
/files/
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@ -0,0 +1,12 @@
[
{
"title": "disable",
"type": "BOOLEAN",
"selectValue": ""
},
{
"title": "support",
"type": "SELECT",
"selectValue": "add,edit,query,del,ui"
}
]

View File

@ -0,0 +1,4 @@
##自动导入包(仅导入实体属性需要的包,通常用于实体类)
#foreach($import in $importList)
import $!import;
#end

View File

@ -0,0 +1,39 @@
##Velocity宏定义
##定义设置表名后缀的宏定义,调用方式:#setTableSuffix("Test")
#macro(setTableSuffix $suffix)
#set($tableName = $!tool.append($tableInfo.name, $suffix))
#end
##定义设置包名后缀的宏定义,调用方式:#setPackageSuffix("Test")
#macro(setPackageSuffix $suffix)
#if($suffix!="")package #end#if($tableInfo.savePackageName!="")$!{tableInfo.savePackageName}.#{end}$!suffix;
#end
##定义直接保存路径与文件名简化的宏定义,调用方式:#save("/entity", ".java")
#macro(save $path $fileName)
$!callback.setSavePath($tool.append($tableInfo.savePath, $path))
$!callback.setFileName($tool.append($tableInfo.name, $fileName))
#end
##定义表注释的宏定义,调用方式:#tableComment("注释信息")
#macro(tableComment $desc)
/**
* $!{tableInfo.comment}($!{tableInfo.name})$desc
*
* @author $!author
* @since $!time.currTime()
*/
#end
##定义GETSET方法的宏定义调用方式#getSetMethod($column)
#macro(getSetMethod $column)
public $!{tool.getClsNameByFullName($column.type)} get$!{tool.firstUpperCase($column.name)}() {
return $!{column.name};
}
public void set$!{tool.firstUpperCase($column.name)}($!{tool.getClsNameByFullName($column.type)} $!{column.name}) {
this.$!{column.name} = $!{column.name};
}
#end

View File

@ -0,0 +1,41 @@
##初始化区域
##去掉表的t_前缀
$!tableInfo.setName($tool.getClassName($tableInfo.obj.name.replaceFirst("t_","")))
##参考阿里巴巴开发手册POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误
## when use actual columns name, should set this.
###foreach($column in $tableInfo.fullColumn)
## $!column.setName($column.obj.name)
###end
#foreach($column in $tableInfo.fullColumn)
#if($column.name.startsWith("is") && $column.type.equals("java.lang.Boolean"))
$!column.setName($tool.firstLowerCase($column.name.substring(2)))
#end
#end
##实现动态排除列
#set($temp = $tool.newHashSet("testCreateTime", "otherColumn"))
#foreach($item in $temp)
#set($newList = $tool.newArrayList())
#foreach($column in $tableInfo.fullColumn)
#if($column.name!=$item)
##带有反回值的方法调用时使用$tool.call来消除返回值
$tool.call($newList.add($column))
#end
#end
##重新保存
$tableInfo.setFullColumn($newList)
#end
##对importList进行篡改
#set($temp = $tool.newHashSet())
#foreach($column in $tableInfo.fullColumn)
#if(!$column.type.startsWith("java.lang."))
##带有反回值的方法调用时使用$tool.call来消除返回值
$tool.call($temp.add($column.type))
#end
#end
##覆盖
#set($importList = $temp)

View File

@ -0,0 +1,27 @@
##following code can be generated use MybatisCodeHelperPro plugin mybatis generator mingrate to template generate.
##copy group for different project.
#set($javamodelSrcFolder="${projectPath}/src/main/java")
#set($modelPackageName="{{ cookiecutter.__mvn_package }}.rest.entity")
#set($mapperSrcFolder="${projectPath}/src/main/java")
#set($mapperPackageName="{{ cookiecutter.__mvn_package }}.rest.dao")
#set($mapperXmlFolder="${projectPath}/src/main/resources")
#set($mapperXmlPackage="mapper")
#set($serviceSrcFolder="${projectPath}/src/main/java")
#set($servicePackageName="{{ cookiecutter.__mvn_package }}.rest.service")
#set($serviceImplSrcFolder="${projectPath}/src/main/java")
#set($serviceImplPackageName="{{ cookiecutter.__mvn_package }}.rest.service.impl")
#set($controllerSrcFolder="${projectPath}/src/main/java")
#set($controllerPackageName="{{ cookiecutter.__mvn_package }}.rest.controller")
#set($useLombok=true)
#set($useSwagger=false)
#set($useOpenApi=false)
#set($addSchemaName=false)
#set($mapperSuffix="Mapper")
#set($daoSuffix="Dao")
#set($useActualColumName=false)
#if($useActualColumName)
#foreach($column in $tableInfo.fullColumn)
$!column.setName($column.obj.name)
#end
#end

View File

@ -0,0 +1,31 @@
##针对Mybatis 进行支持主要用于生成xml文件
#foreach($column in $tableInfo.fullColumn)
##储存列类型
$tool.call($column.ext.put("sqlType", $tool.getField($column.obj.dataType, "typeName")))
#if($tool.newHashSet("java.lang.String").contains($column.type))
#set($jdbcType="VARCHAR")
#elseif($tool.newHashSet("java.lang.Boolean", "boolean").contains($column.type))
#set($jdbcType="BOOLEAN")
#elseif($tool.newHashSet("java.lang.Byte", "byte").contains($column.type))
#set($jdbcType="BYTE")
#elseif($tool.newHashSet("java.lang.Integer", "int", "java.lang.Short", "short").contains($column.type))
#set($jdbcType="INTEGER")
#elseif($tool.newHashSet("java.lang.Long", "long").contains($column.type))
#set($jdbcType="BIGINT")
#elseif($tool.newHashSet("java.lang.Float", "float", "java.lang.Double", "double").contains($column.type))
#set($jdbcType="NUMERIC")
#elseif($tool.newHashSet(
"java.util.Date", "java.sql.Timestamp", "java.time.Instant", "java.time.LocalDateTime",
"java.time.OffsetDateTime", "java.time.ZonedDateTime").contains($column.type))
#set($jdbcType="TIMESTAMP")
#elseif($tool.newHashSet("java.sql.Date", "java.time.LocalDate", "java.time.LocalTime").contains($column.type))
#set($jdbcType="TIMESTAMP")
#else
##其他类型
#set($jdbcType="VARCHAR")
#end
$tool.call($column.ext.put("jdbcType", $jdbcType))
#end
##定义宏,查询所有列
#macro(allSqlColumn)#foreach($column in $tableInfo.fullColumn)$column.obj.name#if($velocityHasNext), #end#end#end

View File

@ -0,0 +1,182 @@
##导入宏定义
$!{define.vm}
$!{mybatisCodehelper.vm}
##设置表后缀(宏定义)
#set($controllerName = $tool.append($tableInfo.name, "Controller"))
##设置回调
#set($controllerSavePath = $tool.append(${controllerSrcFolder},"/",${controllerPackageName.replace(".","/")}))
$!callback.setSavePath($controllerSavePath)
$!callback.setFileName($tool.append($controllerName, ".java"))
##定义服务名
#set($serviceName = $!tool.append($!tool.firstLowerCase($!tableInfo.name), "Service"))
##定义实体对象名
#set($entityName = $!tool.firstLowerCase($!tableInfo.name))
package ${controllerPackageName};
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import $!{modelPackageName}.$!{tableInfo.name};
import ${servicePackageName}.$!{tableInfo.name}Service;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
##表注释(宏定义)
#tableComment("表控制层.")
###if(${tableInfo.comment})
##@Tag(name = "${tableInfo.comment}")
###end
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/$!tool.firstLowerCase($!tableInfo.name)")
public class $!{tableInfo.name}Controller extends ApiController {
/** 服务对象. */
private final $!{tableInfo.name}Service $!{serviceName};
public $!{tableInfo.name}Controller($!{tableInfo.name}Service $!{serviceName}) {
this.$!{serviceName} = $!{serviceName};
}
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param $!entityName 查询实体
* @return 分页数据
*/
@Operation(
summary = "${tableInfo.comment} - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "$!entityName", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(@Nullable PageDTO<$!{tableInfo.name}> page, @Nullable $!{tableInfo.name} $!entityName) {
return success(this.$!{serviceName}.page(page, new QueryWrapper<>($!entityName)));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "${tableInfo.comment} - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.$!{serviceName}.getById(id));
}
/**
* 新增数据.
*
* @param $!entityName 实体对象
* @return 新增结果
*/
@Operation(
summary = "${tableInfo.comment} - 新增数据",
parameters = {
@Parameter(name = "$!entityName", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody $!tableInfo.name $!entityName) {
return success(this.$!{serviceName}.save($!entityName));
}
/**
* 批量新增数据.
*
* @param $!{entityName}s 实体对象
* @return 新增结果
*/
@Operation(
summary = "${tableInfo.comment} - 批量新增数据",
parameters = {
@Parameter(name = "$!{entityName}s", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<$!tableInfo.name> $!{entityName}s) {
return success(this.$!{serviceName}.saveBatch($!{entityName}s));
}
/**
* 修改数据.
*
* @param $!entityName 实体对象
* @return 修改结果
*/
@Operation(
summary = "${tableInfo.comment} - 修改数据",
parameters = {@Parameter(name = "$!entityName", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody $!tableInfo.name $!entityName) {
return success(this.$!{serviceName}.saveOrUpdate($!entityName));
}
/**
* 批量修改数据.
*
* @param $!{entityName}s 实体对象
* @return 修改结果
*/
@Operation(
summary = "${tableInfo.comment} - 批量修改数据",
parameters = {@Parameter(name = "$!{entityName}s", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<$!tableInfo.name> $!{entityName}s) {
return success(this.$!{serviceName}.saveOrUpdateBatch($!{entityName}s));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "${tableInfo.comment} - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.$!{serviceName}.removeByIds(idList));
}
}

View File

@ -0,0 +1,21 @@
##导入宏定义
$!{define.vm}
$!{mybatisCodehelper.vm}
##设置表后缀(宏定义)
#set($daoName = $tool.append($tableInfo.name, ${daoSuffix}))
##设置回调
#set($daoSavePath = $tool.append(${mapperSrcFolder},"/",${mapperPackageName.replace(".","/")}))
$!callback.setSavePath($daoSavePath)
$!callback.setFileName($tool.append($daoName, ".java"))
package ${mapperPackageName};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import $!{modelPackageName}.$!{tableInfo.name};
##表注释(宏定义)
#tableComment("表数据库访问层.")
public interface $!{tableInfo.name}Dao extends BaseMapper<$!tableInfo.name> {
}

View File

@ -0,0 +1,105 @@
##导入宏定义
$!{define.vm}
$!{mybatisCodehelper.vm}
#set($entitySavePath = $tool.append(${javamodelSrcFolder},"/",${modelPackageName.replace(".","/")}))
$!callback.setSavePath($entitySavePath)
$!callback.setFileName($tool.append($tableInfo.name, ".java"))
##自动导入包(全局变量)
package ${modelPackageName};
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.Version;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import org.locationtech.jts.geom.Geometry;
import ltd.llvy.postgis.handler.GeometryDeserializer;
import ltd.llvy.postgis.handler.GeometrySerializer;
import java.io.Serializable;
$!autoImport
import java.time.OffsetDateTime;
import org.apache.ibatis.type.JdbcType;
import lombok.Data;
import lombok.EqualsAndHashCode;
##表注释(宏定义)
#tableComment("表实体类.")
#if(${tableInfo.comment})
@Schema(description = "${tableInfo.comment}")
#end
@EqualsAndHashCode(callSuper = true)
@Data
public class $!{tableInfo.name} extends Model<$!{tableInfo.name}> {
#foreach($column in $tableInfo.fullColumn)
#if(${column.comment})
//${column.comment}
#end
#if($!{column.name} == "id" || $!{column.name.indexOf("Id")} != -1)
@Schema(description = "${column.comment}", type = "string")
@JsonSerialize(using = ToStringSerializer.class)
#else
@Schema(description = "${column.comment}")
#end
#if("password" == $!{column.name})
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
#end
#if("version" == $!{column.name})
@Version
#end
#if("deleteTime" == $!{column.name})
@TableLogic
#end
#if("createTime" == $!{column.name})
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
{% if cookiecutter.platform == "postgis" -%}
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.OTHER)
{% else %}
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.DATETIMEOFFSET)
{% endif %}
#end
#if("updateTime" == $!{column.name})
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
{% if cookiecutter.platform == "postgis" -%}
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.OTHER)
{% else %}
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.DATETIMEOFFSET)
{% endif %}
#end
#if("createBy" == $!{column.name})
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@TableField(fill = FieldFill.INSERT)
#end
#if("updateBy" == $!{column.name})
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@TableField(fill = FieldFill.INSERT_UPDATE)
#end
#if("geom" == $!{column.name})
@JsonSerialize(using = GeometrySerializer.class)
@JsonDeserialize(using = GeometryDeserializer.class)
#end
private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end
#foreach($column in $tableInfo.pkColumn)
/**
* 获取主键值.
*
* @return 主键值
*/
@Override
public Serializable pkVal () {
return this.$!column.name;
}
#break
#end
}

View File

@ -0,0 +1,22 @@
##引入mybatis支持
$!{mybatisCodehelper.vm}
$!{mybatisSupport.vm}
##设置保存名称与保存位置
#set($XmlSavePath = $tool.append(${mapperXmlFolder},"/",${mapperXmlPackage.replace(".","/")}))
$!callback.setSavePath($XmlSavePath)
$!callback.setFileName($tool.append($!{tableInfo.name}, $!{mapperSuffix},".xml"))
#set($daoName = $tool.append($tableInfo.name, ${daoSuffix}))
##拿到主键
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="{{ cookiecutter.__mvn_package }}.rest.dao.$!{tableInfo.name}Dao">
<resultMap type="{{ cookiecutter.__mvn_package }}.rest.entity.$!{tableInfo.name}" id="$!{tableInfo.name}Map">
#foreach($column in $tableInfo.fullColumn)
<result property="$!column.name" column="$!column.obj.name" jdbcType="$!column.ext.jdbcType"/>
#end
</resultMap>
</mapper>

View File

@ -0,0 +1,24 @@
##导入宏定义
$!{define.vm}
##定义初始变量
$!{mybatisCodehelper.vm}
#set($serviceName = $tool.append($tableInfo.name, "Service"))
##设置回调
#set($serviceSavePath = $tool.append(${serviceSrcFolder},"/",${servicePackageName.replace(".","/")}))
$!callback.setSavePath($serviceSavePath)
$!callback.setFileName($tool.append($serviceName, ".java"))
package $!{servicePackageName};
import com.baomidou.mybatisplus.extension.service.IService;
import $!{modelPackageName}.$!{tableInfo.name};
##表注释(宏定义)
#tableComment("表服务接口.")
public interface $!{serviceName} extends IService
<$!tableInfo.name> {
}

View File

@ -0,0 +1,29 @@
##导入宏定义
$!{define.vm}
$!{mybatisCodehelper.vm}
#set($ServiceImplName = $tool.append($tableInfo.name, "ServiceImpl"))
##设置回调
##$!callback.setFileName($tool.append($ServiceImplName, ".java"))
##$!callback.setSavePath($tool.append($tableInfo.savePath, "/service/impl"))
#set($serviceImplSavePath = $tool.append(${serviceImplSrcFolder},"/",${serviceImplPackageName.replace(".","/")}))
$!callback.setSavePath($serviceImplSavePath)
$!callback.setFileName($tool.append($ServiceImplName, ".java"))
#set($daoName = $tool.append($tableInfo.name, ${daoSuffix}))
package $!{serviceImplPackageName};
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import ${mapperPackageName}.${daoName};
import $!{modelPackageName}.$!{tableInfo.name};
import ${servicePackageName}.$!{tableInfo.name}Service;
import org.springframework.stereotype.Service;
##表注释(宏定义)
#tableComment("表服务实现类.")
@Service("$!tool.firstLowerCase($tableInfo.name)Service")
public class $!{ServiceImplName} extends ServiceImpl<$!{daoName}, $!{tableInfo.name}> implements
$!{tableInfo.name}Service {
}

View File

@ -0,0 +1,97 @@
[
{
"matchType": "REGEX",
"columnType": "varchar(\\(\\d+\\))?",
"javaType": "java.lang.String"
},
{
"matchType": "REGEX",
"columnType": "char(\\(\\d+\\))?",
"javaType": "java.lang.String"
},
{
"matchType": "ORDINARY",
"columnType": "tinyint(1)",
"javaType": "java.lang.Boolean"
},
{
"matchType": "ORDINARY",
"columnType": "bit(1)",
"javaType": "java.lang.Boolean"
},
{
"matchType": "REGEX",
"columnType": "(tiny|medium|long)*text",
"javaType": "java.lang.String"
},
{
"matchType": "REGEX",
"columnType": "decimal(\\(\\d+,\\d+\\))?",
"javaType": "java.math.BigDecimal"
},
{
"matchType": "ORDINARY",
"columnType": "integer",
"javaType": "java.lang.Integer"
},
{
"matchType": "REGEX",
"columnType": "(tiny|small|medium)*int(\\(\\d+\\))?",
"javaType": "java.lang.Integer"
},
{
"matchType": "ORDINARY",
"columnType": "int4",
"javaType": "java.lang.Integer"
},
{
"matchType": "ORDINARY",
"columnType": "int8",
"javaType": "java.lang.Long"
},
{
"matchType": "REGEX",
"columnType": "bigint(\\(\\d+\\))?",
"javaType": "java.lang.Long"
},
{
"matchType": "ORDINARY",
"columnType": "double",
"javaType": "java.lang.Double"
},
{
"matchType": "ORDINARY",
"columnType": "date",
"javaType": "java.time.LocalDate"
},
{
"matchType": "ORDINARY",
"columnType": "datetime",
"javaType": "java.time.OffsetDateTime"
},
{
"matchType": "ORDINARY",
"columnType": "timestamp with time zone",
"javaType": "java.time.OffsetDateTime"
},
{
"matchType": "ORDINARY",
"columnType": "timestamp",
"javaType": "java.lang.Long"
},
{
"matchType": "ORDINARY",
"columnType": "time",
"javaType": "java.time.LocalTime"
},
{
"matchType": "ORDINARY",
"columnType": "boolean",
"javaType": "java.lang.Boolean"
},
{
"matchType": "ORDINARY",
"columnType": "geometry",
"javaType": "org.postgis.Geometry"
}
]

View File

@ -0,0 +1,72 @@
[
{
"matchType": "REGEX",
"columnType": "varchar(\\(\\d+\\))?",
"javaType": "java.lang.String"
},
{
"matchType": "REGEX",
"columnType": "char(\\(\\d+\\))?",
"javaType": "java.lang.String"
},
{
"matchType": "REGEX",
"columnType": "(tiny|medium|long)*text",
"javaType": "java.lang.String"
},
{
"matchType": "REGEX",
"columnType": "decimal(\\(\\d+,\\d+\\))?",
"javaType": "java.lang.Double"
},
{
"matchType": "ORDINARY",
"columnType": "integer",
"javaType": "java.lang.Integer"
},
{
"matchType": "REGEX",
"columnType": "(tiny|small|medium)*int(\\(\\d+\\))?",
"javaType": "java.lang.Integer"
},
{
"matchType": "ORDINARY",
"columnType": "int4",
"javaType": "java.lang.Integer"
},
{
"matchType": "ORDINARY",
"columnType": "int8",
"javaType": "java.lang.Long"
},
{
"matchType": "REGEX",
"columnType": "bigint(\\(\\d+\\))?",
"javaType": "java.lang.Long"
},
{
"matchType": "ORDINARY",
"columnType": "date",
"javaType": "java.time.LocalDate"
},
{
"matchType": "ORDINARY",
"columnType": "datetime",
"javaType": "java.time.LocalDate"
},
{
"matchType": "ORDINARY",
"columnType": "timestamp",
"javaType": "java.time.LocalDate"
},
{
"matchType": "ORDINARY",
"columnType": "time",
"javaType": "java.time.LocalTime"
},
{
"matchType": "ORDINARY",
"columnType": "boolean",
"javaType": "java.lang.Boolean"
}
]

View File

@ -0,0 +1,11 @@
[
{
"groupName": "MybatisPlus",
"templateName": "MybatisPlus",
"globalConfigName": "MybatisCodeHelperPro",
"columnConfigName": "Default.json",
"typeMapperName": "Default.json",
"tableNameRegex": ".*",
"schemaNameRegex": ".*"
}
]

View File

@ -0,0 +1,12 @@
#* @implicitly included *#
#* @vtlvariable name="author" type="java.lang.String" *#
#* @vtlvariable name="encode" type="java.lang.String" *#
#* @vtlvariable name="modulePath" type="java.lang.String" *#
#* @vtlvariable name="projectPath" type="java.lang.String" *#
#* @vtlvariable name="importList" type="java.util.List<java.lang.String>" *#
#* @vtlvariable name="callback" type="com.bruce.plugin.entity.Callback" *#
#* @vtlvariable name="tool" type="com.bruce.plugin.tool.GlobalTool" *#
#* @vtlvariable name="time" type="com.bruce.plugin.tool.TimeUtils" *#
#* @vtlvariable name="tableInfo" type="com.bruce.plugin.entity.TableInfo" *#
#* @vtlvariable name="tableInfoList" type="java.util.List<com.bruce.plugin.entity.TableInfo>" *#
#* @vtlvariable name="generateService" type="com.bruce.plugin.tool.ExtraCodeGenerateUtils" *#

View File

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

View File

@ -0,0 +1,2 @@
VITE_APP_TITLE=测试环境
VITE_BASE_URL=api

View File

@ -0,0 +1,2 @@
VITE_APP_TITLE={{ cookiecutter.project_hans }}
VITE_BASE_URL=/

View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

View File

@ -0,0 +1,36 @@
# manager
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and
disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -0,0 +1,28 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default defineConfig([
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
...pluginOxlint.configs['flat/recommended'],
skipFormatting,
])

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<link href="/favicon.ico" rel="icon">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title><%= VITE_APP_TITLE %></title>
</head>
<body>
<div id="app"></div>
<script src="/src/main.js" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"paths": {
"@/*": [
"./src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -0,0 +1,51 @@
{
"name": "manager-ui",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix",
"lint": "run-s lint:*",
"format": "prettier --write src/"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.4",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"qs": "^6.14.0",
"tree-lodash": "^0.4.0",
"vue": "^3.5.13",
"vue-request": "^2.0.4",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vue/eslint-config-prettier": "^10.2.0",
"eslint": "^9.25.1",
"eslint-plugin-oxlint": "^0.16.7",
"eslint-plugin-vue": "~10.0.0",
"globals": "^16.0.0",
"npm-run-all2": "^7.0.2",
"oxlint": "^0.16.7",
"prettier": "3.5.3",
"sass-embedded": "^1.87.0",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.3.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-vue-devtools": "^7.7.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,13 @@
<script setup>
import { onBeforeMount } from 'vue'
import { useSystemStore } from '@/stores/system.js'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
onBeforeMount(useSystemStore().init)
</script>
<template>
<a-config-provider :locale="zhCN" >
<RouterView />
</a-config-provider>
</template>

View File

@ -0,0 +1,8 @@
:root {
}
.dark {
}
.light {
}

View File

@ -0,0 +1,34 @@
@import 'base.css';
html,
body,
#app {
height: 100%;
width: 100%;
margin: 0;
}
/* 全局滚动条样式(内侧滚动条) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent; /* 滚动条轨道透明 */
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
.text-single {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,181 @@
<script setup>
import { reactive } from 'vue'
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
NotificationOutlined,
UserOutlined,
} from '@ant-design/icons-vue'
import LayoutMenu from '@/components/Layout/LayoutMenu.vue'
import { useSystemStore } from '@/stores/system.js'
import axios from '@/http/api.js'
import router from '@/router/index.js'
import { message } from 'ant-design-vue'
import { useRequest } from 'vue-request'
import qs from 'qs'
const systemStore = useSystemStore()
const state = reactive({
width: 256,
sider_width: 80,
sider: true,
drawer: false,
broken: false,
})
const breakpoint = (broken) => {
state.broken = broken
collapsed()
}
const collapsed = () => {
if (state.broken) {
state.sider_width = 0
state.sider = true
state.drawer = !state.drawer
} else {
state.sider_width = 80
state.drawer = false
state.sider = !state.sider
}
}
const logout = () => {
axios.post('/manager/logout').then(([err, res]) => {
if (err || res.code !== 0) {
message.error(err.msg || '注销失败')
} else {
message.success(res.msg || '注销成功')
useSystemStore().$reset()
}
})
router.push('/login')
}
const toMessage = () => router.push({ path: '/message' })
//
const fetchMessage = async (params) => {
let [err, res] = await axios.get('/message?' + qs.stringify(params, { allowDots: true }))
if (err || res.code !== 0) {
return message.warning('查询最新消息失败')
}
return res?.data?.records || []
}
const { data: messages } = useRequest(fetchMessage, {
pollingInterval: import.meta.env.DEV ? -1 : 1000,
defaultParams: [{ size: -1, status: 0 }],
})
</script>
<template>
<ALayout>
<ADrawer
v-model:open="state.drawer"
:bodyStyle="{ padding: '0px', backgroundColor: '#001529' }"
:closable="false"
:width="state.width"
placement="left"
>
<LayoutMenu />
</ADrawer>
<ALayoutSider
v-model:collapsed="state.sider"
:collapsed-width="state.sider_width"
:defaultCollapsed="true"
:trigger="null"
:width="state.width"
breakpoint="lg"
class="sider"
@breakpoint="breakpoint"
>
<LayoutMenu />
</ALayoutSider>
<ALayout>
<ALayoutHeader class="header">
<div class="btn-sider" @click="collapsed">
<MenuUnfoldOutlined v-if="state.sider || state.drawer" />
<MenuFoldOutlined v-else />
</div>
<ABadge :count="messages?.length || 0" class="btn-notify" @click="toMessage">
<NotificationOutlined />
</ABadge>
<ADropdown>
<div class="btn-avatar">
<AAvatar shape="square">
<template #icon>
<UserOutlined />
</template>
</AAvatar>
<span class="name">
<span v-if="systemStore.tenant">{{ systemStore.tenant }} / </span>
<span>{{ systemStore.realName || systemStore.name }}</span>
</span>
</div>
<template #overlay>
<AMenu>
<AMenuItem>系统设置</AMenuItem>
<AMenuItem @click="logout">注销登录</AMenuItem>
</AMenu>
</template>
</ADropdown>
</ALayoutHeader>
<ALayout>
<ALayoutContent class="content">
<RouterView />
</ALayoutContent>
</ALayout>
</ALayout>
</ALayout>
</template>
<style lang="scss" scoped>
.sider {
height: 100vh;
overflow-y: auto;
}
.header {
background-color: white;
padding: 0 10px;
display: flex;
.btn-sider {
height: 64px;
font-size: 20px;
padding: 0 10px;
cursor: pointer;
}
.btn-notify {
height: 40px;
font-size: 20px;
padding: 0 10px;
cursor: pointer;
line-height: 40px;
align-self: center;
margin-left: auto;
margin-right: 16px;
}
.btn-avatar {
cursor: pointer;
.name {
margin-left: 8px;
color: rgba(0, 0, 0, 0.65);
}
}
}
.content {
height: calc(100vh - 64px - 20px);
margin: 10px;
padding: 24px;
background-color: white;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,45 @@
<script setup>
import { AntDesignOutlined } from '@ant-design/icons-vue'
import router from '@/router/index.js'
import { useSystemStore } from '@/stores/system.js'
import { computed } from 'vue'
const title = computed(() => import.meta.env.VITE_APP_TITLE)
const systemStore = useSystemStore()
const selectMenu = ({ item, key }) => {
if (item.link) {
//
router.push({
name: 'WebView',
query: { link: item.link },
})
} else {
router.push(key)
}
}
const toHome = () => router.push({ path: '/' })
</script>
<template>
<div>
<div class="title" @click="toHome">
<AntDesignOutlined />
<span style="margin-left: 12px">{{ title }}</span>
</div>
<AMenu :items="systemStore.fmtMenus()" mode="inline" theme="dark" @select="selectMenu" />
</div>
</template>
<style lang="scss" scoped>
.title {
color: white;
height: 64px;
line-height: 64px;
padding-left: 26px;
font-size: 1.8rem;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,20 @@
import { useSystemStore } from '@/stores/system.js'
export default function (el, binding) {
const systemStore = useSystemStore()
const { value } = binding
if (systemStore.isSa) return true
if (value && value instanceof Array && value.length > 0) {
let has = systemStore.auths.some((item) => {
return value.includes(item)
})
if (!has) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('需要指定权限')
}
}

View File

@ -0,0 +1,9 @@
import hasAuth from '@/directive/hasAuth.js'
import isa from '@/directive/isa.js'
export default {
install(app) {
app.directive('hasAuth', hasAuth)
app.directive('isa', isa)
},
}

View File

@ -0,0 +1,11 @@
import { useSystemStore } from '@/stores/system.js'
export default function (el) {
const systemStore = useSystemStore()
if (systemStore.isSa) {
return true
} else {
el.parentNode && el.parentNode.removeChild(el)
}
}

View File

@ -0,0 +1,20 @@
import axios from 'axios'
import router from '@/router/index.js'
const http = axios.create({ baseURL: import.meta.env.VITE_BASE_URL })
http.interceptors.request.use(
(config) => config,
(error) => Promise.reject(error),
)
http.interceptors.response.use(
(response) => {
return [null, response.data]
},
(error) => {
let response = error.response
if (response.status === 401) return router.push('/login')
return [response.data, null]
},
)
export default http

View File

@ -0,0 +1,26 @@
import './assets/css/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
import directive from '@/directive/index.js'
const app = createApp(App)
dayjs.extend(localizedFormat)
dayjs.locale('zh-cn')
dayjs.extend(relativeTime)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(directive)
app.mount('#app')

View File

@ -0,0 +1,93 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import LayoutIndex from '@/components/Layout/LayoutIndex.vue'
import { useSystemStore } from '@/stores/system.js'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
component: () => import('@/views/LoginView.vue'),
},
{
path: '/',
component: LayoutIndex,
redirect: '/home',
children: [
{
path: '/home',
meta: { needLogin: true },
component: () => import('@/views/HomeView.vue'),
},
{
path: '/system',
children: [
{
path: '/system/account',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/AccountView.vue'),
},
{
path: '/system/role',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/RoleView.vue'),
},
{
path: '/system/authority',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/AuthorityView.vue'),
},
{
path: '/system/tenant',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/TenantView.vue'),
},
{
path: '/system/logger',
meta: { needLogin: true, needMenu: true },
component: () => import('@/views/LoggerView.vue'),
},
],
},
{
path: '/message',
name: 'Message',
component: () => import('@/views/MessageView.vue'),
},
{
path: '/webview',
name: 'WebView',
props: (route) => ({ link: route.query.link || '#' }),
component: () => import('@/views/WebView.vue'),
},
{
path: '403',
component: () => import('@/views/exception/403View.vue'),
},
{
path: '500',
component: () => import('@/views/exception/500View.vue'),
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/exception/404View.vue'),
},
],
},
],
})
router.beforeEach((to, from, next) => {
const systemStore = useSystemStore()
// 判断是否需要登录
if (to.meta.needLogin && !systemStore.isLogin) {
return next('/login')
}
// 判断是否拥有菜单权限
if (to.meta.needMenu && !systemStore.menus.some((item) => to.path.startsWith(item.path))) {
return next('/403')
}
return next()
})
export default router

View File

@ -0,0 +1,73 @@
import { defineStore } from 'pinia'
import { jwtDecode } from 'jwt-decode'
import Cookies from 'js-cookie'
import axios from '@/http/api.js'
import { fromArray, map } from 'tree-lodash'
import { toRaw } from 'vue'
const TokenKey = 'Sa-Token'
export const useSystemStore = defineStore('system', {
state: () => {
return {
token: '', // 令牌
name: '', // 用户名
realName: '', // 显示姓名
tenant: '', // 租户
auths: [], // 角色和权限
menus: [], // 菜单
}
},
getters: {
isSa(state) {
return !state.tenant
},
isLogin(state) {
return !!state.token
},
},
actions: {
// 解析令牌
parseToken() {
let token = Cookies.get(TokenKey)
if (token) {
this.token = token
var payload = jwtDecode(token)
this.name = payload['name']
this.realName = payload['real_name']
this.tenant = payload['tenant']
} else {
useSystemStore().$reset()
}
},
// 获得角色和权限
ownAuths() {
let token = Cookies.get(TokenKey)
if (token) {
axios.get('/manager/own/auths').then(([_, res]) => (this.auths = res.data || []))
}
},
// 获得菜单
ownMenus() {
let token = Cookies.get(TokenKey)
if (token) {
axios.get('/manager/own/menus').then(([_, res]) => (this.menus = res.data || []))
}
},
// 格式化菜单
fmtMenus() {
return map(fromArray(toRaw(this.menus), { parentKey: 'parentId' }), (item) => ({
key: item.path || item.value,
label: item.name,
link: item.link || '',
}))
},
// 初始化
init() {
this.parseToken()
this.ownAuths()
this.ownMenus()
},
},
persist: true,
})

View File

@ -0,0 +1,11 @@
import JSEncrypt from 'jsencrypt'
const publicKey = `
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDORIceR8iDNIdH366iHZ9LCrkq VF84SRgO0JsZO79vc/1hcsECcs7kQCtFD9kj5Bz4P4iMJQ+hZeaPBKmrfHl91DDr hjuACgA3Pk0Pr5TBdN3eemA0Ri50NyjhoGpJvE8dZe1sbn4lfQwtOsx+kmP+Ixb3 oa6wdPQb3gfnQJqxDQIDAQAB
`
export function encrypt(text) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(text) // 对数据进行加密
}

View File

@ -0,0 +1,578 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
const searchRef = ref()
const editRef = ref()
const pwdRef = ref()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
pwd: {
title: '设置密码',
show: false,
},
})
const columns = [
{
title: '账户',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '名称',
dataIndex: 'realName',
width: 120,
ellipsis: true,
},
{
title: '手机号',
dataIndex: 'mobile',
maxWidth: 150,
ellipsis: true,
},
{
title: '邮箱',
dataIndex: 'email',
maxWidth: 200,
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '可用' : '禁用'
},
filters: [
{ text: '可用', value: 0 },
{ text: '禁用', value: 1 },
],
filterMultiple: false,
},
{
title: '激活',
dataIndex: 'isActive',
width: 80,
customRender: ({ text: isActive }) => {
return isActive === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 180,
fixed: 'right',
},
]
const rules = {
name: [
{ required: true, message: '账户必填', trigger: 'blur' },
{ min: 2, max: 50, message: '最少2个字符最多50个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '密码必填', trigger: 'blur' },
{ min: 6, max: 100, message: '最少6个字符最多100个字符', trigger: 'blur' },
],
repeat: [
{ required: true, message: '重复密码必填', trigger: 'blur' },
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value !== state.edit.password) {
return Promise.reject('两次输入密码不同')
} else {
return Promise.resolve()
}
},
},
],
mobile: [
{
pattern: /^1[3-9]\d{9}$/,
message: '手机格式不正确',
trigger: 'blur',
},
],
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
tenant: [
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value || value === '') {
return Promise.resolve()
} else {
return Promise.reject('租户必选')
}
},
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onPwdChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.pwd.show = true
}
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({ label: item.name, value: item.name }))
opts.unshift({ label: '公共数据', value: '' })
return opts
}
}
const fetchRoleOpt = async (params) => {
let [err, res] = await axios.get('/db/tRole', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
return res.data.records.map((item) => ({ label: item.name, value: item.id }))
}
}
const fetchData = (params) => {
return axios
.get('/db/tAccount?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/db/tAccount', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tAccount', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tAccount', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const submitPwd = async () => {
pwdRef.value.validate().then(() => {
if (state.edit.id) {
let params = { id: state.edit.id, password: state.edit.password }
axios.put('/manager/password', params).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改密码失败')
} else {
state.pwd.show = false
return message.success('修改密码成功')
}
})
}
})
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data: roleOpts, run: runRoleOpt } = useRequest(fetchRoleOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
//
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
//
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tAccount/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
//
var blob = res
var filename = '凭据.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const roleOpt = computed({
get() {
if (state.edit?.roleIds) {
return state.edit.roleIds.split(',')
} else {
return []
}
},
set(val) {
state.edit.roleIds = val.join(',')
},
})
</script>
<template>
<div class="account">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="账户" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入账户" />
</a-form-item>
<a-form-item label="名称" name="realName">
<a-input v-model:value="state.search.realName" placeholder="请输入显示名称" />
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input v-model:value="state.search.mobile" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="state.search.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:account:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tAccount/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:account:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-button v-hasAuth="['system:account:put']" type="link" @click="onPwdChange(record)"
>密码修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:account:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-row>
<a-col :span="12">
<a-form-item label="账户" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入账户" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="名称" name="realName">
<a-input v-model:value="state.edit.realName" placeholder="请输入显示名称" />
</a-form-item>
</a-col>
</a-row>
<template v-if="!state.edit.id">
<a-form-item label="密码" name="password">
<a-input-password v-model:value="state.edit.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="重复密码" name="repeat">
<a-input-password v-model:value="state.edit.repeat" placeholder="请输入确认密码" />
</a-form-item>
</template>
<a-row>
<a-col :span="12">
<a-form-item label="手机号" name="mobile">
<a-input v-model:value="state.edit.mobile" placeholder="请输入手机号" />
</a-form-item>
</a-col>
<a-col :push="2" :span="12">
<a-form-item label="是否启用" name="status">
<a-switch v-model:checked="state.edit.status" :checkedValue="0" :unCheckedValue="1" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="邮箱" name="email">
<a-input v-model:value="state.edit.email" placeholder="请输入邮箱" />
</a-form-item>
</a-col>
<a-col :push="2" :span="12">
<a-form-item label="是否激活" name="isActive">
<a-switch
v-model:checked="state.edit.isActive"
:checkedValue="0"
:unCheckedValue="1"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="关联角色" name="roleIds">
<a-select
v-model:value="roleOpt"
:filterOption="false"
:options="roleOpts"
mode="multiple"
placeholder="请多选用户角色"
@search="(value) => runRoleOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-row>
<a-col :span="12">
<a-form-item label="关联员工" name="staffId">
<a-input v-model:value="state.edit.staffId" placeholder="请选择员工" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="关联会员" name="userId">
<a-input v-model:value="state.edit.userId" placeholder="请选择会员" />
</a-form-item>
</a-col>
</a-row>
<a-form-item v-isa label="租户" name="tenant">
<a-select
v-model:value="state.edit.tenant"
:filterOption="false"
:options="tenantOpts"
placeholder="请选择隶属租户"
show-search
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="state.pwd.show" :title="state.pwd.title" @ok="submitPwd">
<a-form ref="pwdRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="密码" name="password">
<a-input-password v-model:value="state.edit.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="重复密码" name="repeat">
<a-input-password v-model:value="state.edit.repeat" placeholder="请输入确认密码" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.account {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,548 @@
<script setup>
import _ from 'lodash'
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
})
const columns = [
{
title: '权限名称',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '英文值',
dataIndex: 'value',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '路径',
dataIndex: 'path',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '外部链接',
dataIndex: 'link',
maxWidth: 200,
ellipsis: true,
},
{
title: '类型',
dataIndex: 'type',
sorter: true,
width: 100,
customRender: ({ text: type }) => {
switch (type) {
case 0:
return '接口'
case 1:
return '菜单'
case 2:
return '按钮'
default:
return '其他'
}
},
filters: [
{ text: '接口', value: 0 },
{ text: '菜单', value: 1 },
{ text: '按钮', value: 2 },
],
filterMultiple: false,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
customRender: ({ text: status }) => {
return status === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 120,
fixed: 'right',
},
]
const rules = {
name: [
{
required: true,
message: '权限名称必填',
trigger: 'blur',
},
{
min: 2,
max: 100,
message: '最少2个字符最多100个字符',
trigger: 'blur',
},
],
value: [
{
required: true,
message: '英文值必填',
trigger: 'blur',
},
{
min: 4,
max: 100,
message: '最少4个字符最多100个字符',
trigger: 'blur',
},
],
parentId: [
{
required: true,
message: '父节点必选',
trigger: 'blur',
},
],
type: [
{
required: true,
message: '类型必选',
trigger: 'blur',
},
],
tenant: [
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value || value === '') {
return Promise.resolve()
} else {
return Promise.reject('租户必选')
}
},
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant?' + qs.stringify(params, { skipNulls: true }))
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({ label: item.name, value: item.name }))
opts.unshift({ label: '公共数据', value: '' })
return opts
}
}
const fetchAuthorityOpt = async (params) => {
let [err, res] = await axios.get('/db/tAuthority?' + qs.stringify(params, { skipNulls: true }))
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({
label: item.name + ' (' + (item.description || '无描述') + ')',
value: item.id,
}))
opts.unshift({ label: '根节点', value: '0' })
return opts
}
}
const fetchData = (params) => {
return axios
.get('/db/tAuthority?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
if (state.edit.id === state.edit.parentId) {
return message.warning('父节点不能是自身')
}
axios.put('/db/tAuthority', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tAuthority', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tAuthority', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data: authorityOpts, run: runAuthorityOpt } = useRequest(fetchAuthorityOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
//
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
//
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const onChangePath = (path) => {
if (path) {
state.edit.value = _(path).trim('/').replaceAll('/', ':')
} else {
state.edit.value = ''
}
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tAuthority/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
//
var blob = res
var filename = '权限.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
</script>
<template>
<div class="authority">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="权限名称" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="权限类型" name="type">
<a-select v-model:value="state.search.type" allowClear placeholder="请选择类型">
<a-select-option value="0">接口</a-select-option>
<a-select-option value="1">菜单</a-select-option>
<a-select-option value="2">按钮</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:authority:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tAuthority/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:authority:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:authority:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-row>
<a-col :span="12">
<a-form-item label="名称" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入权限名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="英文值" name="value">
<a-input v-model:value="state.edit.value" placeholder="请输入英文简写" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="父节点" name="parentId">
<a-select
v-model:value="state.edit.parentId"
:filterOption="false"
:options="authorityOpts"
placeholder="请选择父节点"
show-search
@search="(value) => runAuthorityOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-row>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="state.edit.type" placeholder="请选择类型">
<a-select-option :value="0">接口</a-select-option>
<a-select-option :value="1">菜单</a-select-option>
<a-select-option :value="2">按钮</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :push="2" :span="12">
<a-form-item label="是否启用" name="status">
<a-switch
v-model:checked="state.edit.status"
:checkedValue="0"
:unCheckedValue="1"
placeholder="请输入状态"
/>
</a-form-item>
</a-col>
</a-row>
<template v-if="state.edit.type === 1">
<a-form-item label="路径" name="path">
<a-input
v-model:value="state.edit.path"
placeholder="请输入以 / 开头的路径,将修改英文值"
@change="onChangePath(state.edit.path)"
/>
</a-form-item>
<a-form-item label="外部链接" name="link">
<a-input v-model:value="state.edit.link" placeholder="请输入外部链接" />
</a-form-item>
</template>
<a-form-item v-isa label="租户" name="tenant">
<a-select
v-model:value="state.edit.tenant"
:filterOption="false"
:options="tenantOpts"
placeholder="请选择隶属租户"
show-search
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.authority {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,9 @@
<script setup></script>
<template>
<div>
<h1>首页</h1>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,268 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { usePagination } from 'vue-request'
const searchRef = ref()
const state = reactive({
search: {},
edit: {},
})
const columns = [
{
title: '类型',
dataIndex: 'type',
sorter: true,
width: 200,
ellipsis: true,
customRender: ({ text }) => {
switch (text) {
case 0:
return '数据变更自动日志'
}
},
},
{
title: '操作',
dataIndex: 'operation',
sorter: true,
width: 80,
customRender: ({ text }) => {
switch (text) {
case 'insert':
return '新增'
case 'update':
return '修改'
case 'delete':
return '删除'
default:
return '其他'
}
},
},
{
title: '表名称',
dataIndex: 'tableName',
width: 100,
ellipsis: true,
},
{
title: '日志',
dataIndex: 'recordStaus',
width: 80,
align: 'center',
customRender: ({ text }) => (text ? '是' : '否'),
},
{
title: '变动内容',
dataIndex: 'changed',
maxWidth: 250,
ellipsis: true,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 80,
fixed: 'right',
},
]
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const fetchData = (params) => {
return axios
.get('/db/tLogger?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tLogger', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
//
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
//
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tLogger/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
//
var blob = res
var filename = '日志.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
</script>
<template>
<div class="tenant">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="表名称" name="tableName">
<a-input v-model:value="state.search.tableName" placeholder="请输入表名称" />
</a-form-item>
<a-form-item label="操作类型" name="operation">
<a-select v-model:value="state.search.operation" allowClear placeholder="请选择类型">
<a-select-option value="insert">增加</a-select-option>
<a-select-option value="update">修改</a-select-option>
<a-select-option value="delete">删除</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['export:excel']" type="primary" @click="excelExport()">导出</a-button>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:logger:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</div>
</template>
<style lang="scss" scoped>
.tenant {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,232 @@
<script setup>
import api from '@/http/api.js'
import { computed, onBeforeMount, ref } from 'vue'
import { LockOutlined, SafetyOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { encrypt } from '@/utils/crypto.js'
import { useSystemStore } from '@/stores/system.js'
import router from '@/router/index.js'
const title = computed(() => import.meta.env.VITE_APP_TITLE)
const formRef = ref()
const systemStore = useSystemStore()
const userForm = ref({
name: '',
password: '',
captcha: '',
captchaId: '',
captchaImg: '',
})
const login = async () => {
await formRef.value.validate()
let [err, res] = await api.post('/manager/login', {
name: encrypt(userForm.value.name),
password: encrypt(userForm.value.password),
captcha: userForm.value.captcha,
captchaId: userForm.value.captchaId,
})
if (err || res.code !== 0) {
formRef.value.resetFields()
getCaptcha()
return message.error(err.msg || '登录失败')
} else {
systemStore.init()
router.push('/')
return message.success(res.data || '登录成功')
}
}
const getCaptcha = async () => {
let [err, res] = await api.get('/pub/captcha/img')
if (err || res.code !== 0) {
return message.error(err.msg || '获取验证码失败')
}
userForm.value.captchaImg = res.data.captcha
userForm.value.captchaId = res.data.objectId
}
onBeforeMount(getCaptcha)
</script>
<template>
<div class="login">
<div class="login-form">
<h3 class="title">{{ title }}</h3>
<a-form ref="formRef" :model="userForm" :wrapper-col="{ span: 18 }" name="basic">
<a-form-item
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
name="name"
>
<a-input v-model:value="userForm.name" placeholder="请输入用户名">
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
name="password"
>
<a-input-password v-model:value="userForm.password" placeholder="请输入密码">
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item
:rules="[{ required: true, message: '验证码不能为空', trigger: 'blur' }]"
name="captcha"
>
<a-input v-model:value="userForm.captcha" placeholder="请输入验证码">
<template #prefix>
<SafetyOutlined />
</template>
</a-input>
<a-image
:preview="false"
:src="userForm.captchaImg"
class="captcha"
@click="getCaptcha"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="login">登陆</a-button>
</a-form-item>
</a-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.login {
width: 100%;
height: 100%;
background-image: url(@/assets/images/login.webp);
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
@media screen and (min-width: 1025px) {
align-items: flex-start;
padding-left: 15%;
:deep(.login-form) {
width: 450px;
}
}
@media screen and (min-width: 769px) and (max-width: 1024px) {
align-items: center;
:deep(.login-form) {
width: 450px;
}
}
@media screen and (max-width: 768px) {
align-items: center;
:deep(.login-form) {
width: calc(100% - 40px);
}
}
.login-form {
background-color: rgb(104 121 165 / 30%);
border-radius: 20px;
border: 1px solid #fff;
.title {
text-align: center;
font-size: 20px;
color: #b3dbfc;
}
:deep(.ant-form) {
.ant-form-item {
margin-bottom: 30px;
.ant-form-item-row {
display: flex;
justify-content: center;
}
}
.ant-form-item-control-input-content {
display: flex;
}
.ant-input-affix-wrapper {
position: relative;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 20px;
padding: 0;
height: 40px;
.ant-input-prefix {
position: absolute;
left: 15px;
top: 50%;
z-index: 10;
transform: translateY(-50%);
margin-inline-end: 0;
font-size: 18px;
color: #b3dbfc;
}
.ant-input-suffix {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
.anticon {
color: #b3dbfc;
}
}
.ant-input {
padding-left: 40px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0);
color: #b3dbfc;
&::placeholder {
color: #b3dbfc;
}
&:-webkit-autofill {
background-color: rgba(0, 0, 0, 0);
}
}
}
.ant-form-item-explain-error {
margin-top: 5px;
}
.captcha {
width: auto;
height: 100%;
margin-left: 8px;
cursor: pointer;
}
.ant-btn {
border-radius: 20px;
width: 100%;
height: 40px;
background-image: linear-gradient(to right, #2b72ff, #0055fb);
}
}
}
}
</style>

View File

@ -0,0 +1,371 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
import { useSystemStore } from '@/stores/system.js'
const searchRef = ref()
const editRef = ref()
const systemStore = useSystemStore()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
})
const columns = [
{
title: '发送人',
dataIndex: 'sender',
sorter: true,
maxWidth: 120,
ellipsis: true,
},
{
title: '接收人',
dataIndex: 'recipient',
maxWidth: 180,
ellipsis: true,
},
{
title: '过期时间',
dataIndex: 'expiry',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '未读' : '已读'
},
filters: [
{ text: '未读', value: 0 },
{ text: '已读', value: 1 },
],
filterMultiple: false,
},
{
title: '内容',
dataIndex: 'description',
width: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 160,
fixed: 'right',
},
]
const rules = {
recipient: [
{
required: true,
message: '接收人必填',
trigger: 'blur',
},
{
min: 2,
max: 100,
message: '最少2个字符最多100个字符',
trigger: 'blur',
},
],
description: [
{
required: true,
message: '内容必填',
trigger: 'blur',
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onReadChange = async (record) => {
let [err, res] = await axios.put('/message/read/' + record.id)
if (err || res.code !== 0) {
return message.error(err.msg || '已读失败')
} else {
onTableChange()
}
}
const fetchRecipientOpt = async (params) => {
let res = await Promise.all([
axios.get('/db/tTenant', { params }),
axios.get('/db/tAccount', { params }),
])
if (res[0][0] || res[0][1].code !== 0 || res[1][0] || res[1][1].code !== 0) {
return message.error('加载数据失败')
}
res[0][1].data.records.forEach((record) => {
record.isExpiry = true
})
return [...res[0][1].data.records, ...res[1][1].data.records]
}
const fetchData = (params) => {
return axios
.get('/message?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
state.edit.sender = systemStore.realName || systemStore.name
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/message', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/message', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/message', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data: recipient, run: runRecipient } = useRequest(fetchRecipientOpt, {
manual: true,
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
//
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
//
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const recipientOpts = computed(() => {
if (recipient.value) {
let items = recipient.value.map((item) => ({
value: item.realName || item.name,
label: item.realName || item.name,
isExpiry: item.isExpiry || false,
}))
items.unshift({ value: '所有人', label: '所有人', isExpiry: true })
return items
} else {
return [{ value: '所有人', label: '所有人', isExpiry: true }]
}
})
const onSearchRecipient = (value) => {
if (value) runRecipient({ size: -1, mention: value })
}
const onSelectRecipient = (option) => {
if (option.isExpiry) state.edit.isExpiry = true
}
</script>
<template>
<div class="tenant">
<a-form ref="searchRef" :labelCol="{ flex: '40px' }" :model="state.search" layout="inline">
<a-form-item label="内容" name="description">
<a-input v-model:value="state.search.description" placeholder="请输入内容" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="state.search.status" placeholder="请选择类型">
<a-select-option value="">全部</a-select-option>
<a-select-option value="0">未读</a-select-option>
<a-select-option value="1">已读</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['message:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['message:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-button type="link" @click="onReadChange(record)">已读</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['message:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="接收人" name="recipient">
<a-mentions
v-model:value="state.edit.recipient"
:filterOption="false"
:options="recipientOpts"
placeholder="@接收人,可选所有人、租户、账户"
@search="onSearchRecipient"
@select="onSelectRecipient"
>
</a-mentions>
</a-form-item>
<a-form-item label="内容" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.tenant {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,527 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import _ from 'lodash'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
import { fromArray } from 'tree-lodash'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
edit: {},
compare: {
id: 0,
tenant: '',
list: [],
before: [],
beforeHalf: [],
later: [],
laterHalf: [],
},
checked: [],
halfChecked: [],
modal: {
title: '',
show: false,
},
auth: {
title: '设置权限',
show: false,
},
})
const columns = [
{
title: '角色名称',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '英文值',
dataIndex: 'value',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 180,
fixed: 'right',
},
]
const rules = {
name: [
{ required: true, message: '角色名称必填', trigger: 'blur' },
{ min: 2, max: 100, message: '最少2个字符最多100个字符', trigger: 'blur' },
],
value: [
{ required: true, message: '英文值必填', trigger: 'blur' },
{ min: 4, max: 100, message: '最少4个字符最多100个字符', trigger: 'blur' },
],
tenant: [
{
trigger: 'blur',
validator: async (_rule, value) => {
if (value || value === '') {
return Promise.resolve()
} else {
return Promise.reject('租户必选')
}
},
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const onAuthChange = (record) => {
state.compare.id = record.id
state.compare.tenant = record.tenant
fetchRoleAuthorityOpt({
size: -1,
roleId: state.compare.id,
})
state.auth.show = true
}
const onAuthCheck = (checkedKeys, { halfCheckedKeys }) => {
state.halfChecked = halfCheckedKeys
//
state.compare.later = checkedKeys
state.compare.laterHalf = halfCheckedKeys
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
let opts = res.data.records.map((item) => ({ label: item.name, value: item.name }))
opts.unshift({ label: '公共数据', value: '' })
return opts
}
}
const fetchAuthorityOpt = async (params) => {
let [err, res] = await axios.get('/db/tAuthority?' + qs.stringify(params, { skipNulls: true }))
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
return res.data.records || []
}
}
const fetchRoleAuthorityOpt = async (params) => {
let [err, res] = await axios.get(
'/db/tRoleAuthority?' + qs.stringify(params, { skipNulls: true }),
)
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
} else {
const data = res.data.records || []
state.compare.list = data
//
state.checked = data.filter((item) => !item.half).map((item) => item.authorityId)
state.halfChecked = data.filter((item) => item.half).map((item) => item.authorityId)
state.compare.before = data.filter((item) => !item.half).map((item) => item.authorityId)
state.compare.beforeHalf = data.filter((item) => item.half).map((item) => item.authorityId)
}
}
const fetchData = (params) => {
return axios
.get('/db/tRole?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/db/tRole', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tRole', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tRole', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const submitCheck = () => {
//
let deletes = _.difference(state.compare.before, state.compare.later)
let deletesHalf = _.difference(state.compare.beforeHalf, state.compare.laterHalf)
let creates = _.difference(state.compare.later, state.compare.before)
let createsHalf = _.difference(state.compare.laterHalf, state.compare.beforeHalf)
console.log(deletes, deletesHalf, creates, createsHalf)
//
let deleteIds = state.compare.list
.filter(
(item) => _.includes(deletes, item.authorityId) || _.includes(deletesHalf, item.authorityId),
)
.map((item) => item.id)
if (deleteIds && deleteIds.length > 0) {
let params = { idList: deleteIds.join(',') }
axios.delete('/db/tRoleAuthority', { params })
}
//
if (createsHalf && createsHalf.length > 0) {
axios.post(
'/db/tRoleAuthority/s',
createsHalf.map((item) => ({
tenant: state.compare.tenant,
roleId: state.compare.id,
authorityId: item,
half: true,
})),
)
}
if (creates && creates.length > 0) {
axios.post(
'/db/tRoleAuthority/s',
creates.map((item) => ({
tenant: state.compare.tenant,
roleId: state.compare.id,
authorityId: item,
half: false,
})),
)
}
state.auth.show = false
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data: authorityOpts } = useRequest(fetchAuthorityOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
//
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
//
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tRole/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
//
var blob = res
var filename = '角色.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const treeAuth = computed(() => fromArray(authorityOpts.value, { parentKey: 'parentId' }))
</script>
<template>
<div class="role">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="角色名称" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:role:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tRole/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:role:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-button v-hasAuth="['system:role:put']" type="link" @click="onAuthChange(record)"
>分配权限
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:role:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="名称" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入角色名称" />
</a-form-item>
<a-form-item label="英文值" name="value">
<a-input v-model:value="state.edit.value" placeholder="请输入英文值" />
</a-form-item>
<a-form-item label="是否启用" name="status">
<a-switch
v-model:checked="state.edit.status"
:checkedValue="0"
:unCheckedValue="1"
placeholder="请输入状态"
/>
</a-form-item>
<a-form-item v-isa label="租户" name="tenant">
<a-select
v-model:value="state.edit.tenant"
:filterOption="false"
:options="tenantOpts"
placeholder="请选择隶属租户"
show-search
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
<a-modal v-model:open="state.auth.show" :title="state.auth.title" @ok="submitCheck">
<a-tree
v-model:checked-keys="state.checked"
:field-names="{ key: 'id' }"
:tree-data="treeAuth"
checkable
@check="onAuthCheck"
>
<template #title="{ name, description }">
{{ name + '' }}
<span style="color: darkgray">{{ description }}</span>
</template>
</a-tree>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.role {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,432 @@
<script setup>
import qs from 'qs'
import dayjs from 'dayjs'
import axios from '@/http/api.js'
import { computed, reactive, ref } from 'vue'
import { DownOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { usePagination, useRequest } from 'vue-request'
const searchRef = ref()
const editRef = ref()
const state = reactive({
search: {},
edit: {},
modal: {
title: '',
show: false,
},
})
const columns = [
{
title: '租户名称',
dataIndex: 'name',
sorter: true,
width: 120,
ellipsis: true,
},
{
title: '共享给...',
dataIndex: 'shared',
width: 180,
ellipsis: true,
},
{
title: '租户类型',
dataIndex: 'type',
sorter: true,
width: 120,
customRender: ({ text: type }) => {
return type === 0 ? '企业' : '个人'
},
filters: [
{ text: '企业', value: 0 },
{ text: '个人', value: 1 },
],
filterMultiple: false,
},
{
title: '状态',
dataIndex: 'status',
width: 80,
customRender: ({ text: status }) => {
return status === 0 ? '正常' : '冻结'
},
filters: [
{ text: '正常', value: 0 },
{ text: '冻结', value: 1 },
],
filterMultiple: false,
},
{
title: '描述',
dataIndex: 'description',
maxWidth: 200,
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '修改时间',
dataIndex: 'updateTime',
maxWidth: 150,
ellipsis: true,
customRender: ({ text }) => {
return text ? dayjs(text).format('LLL') : ''
},
},
{
title: '操作',
dataIndex: 'action',
align: 'center',
width: 120,
fixed: 'right',
},
]
const rules = {
name: [
{
required: true,
message: '租户名称必填',
trigger: 'blur',
},
{
min: 2,
max: 100,
message: '最少2个字符最多100个字符',
trigger: 'blur',
},
],
}
const onSearchRest = () => {
searchRef.value.resetFields()
onTableChange()
}
const onEditChange = (record) => {
if (record) {
Object.assign(state.edit, record)
state.modal.title = '修改'
} else {
state.edit = {}
state.modal.title = '新增'
}
state.modal.show = true
}
const fetchTenantOpt = async (params) => {
let [err, res] = await axios.get('/db/tTenant', { params })
if (err || res.code !== 0) {
message.error(err.msg || '获取数据失败')
return []
} else {
return res.data.records.map((item) => ({ label: item.name, value: item.name }))
}
}
const fetchData = (params) => {
return axios
.get('/db/tTenant?' + qs.stringify(params, { allowDots: true }))
.then(([, res]) => res.data)
}
const submitData = async () => {
editRef.value
.validate()
.then(() => {
if (state.edit.id) {
axios.put('/db/tTenant', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '修改数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('修改数据成功')
}
})
} else {
axios.post('/db/tTenant', state.edit).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '保存数据失败')
} else {
state.modal.show = false
onTableChange()
return message.success('保存数据成功')
}
})
}
})
.catch(() => {
message.warning('表单校验错误')
})
}
const deleteData = (record) => {
let params = { idList: record.id }
axios.delete('/db/tTenant', { params }).then(([err, res]) => {
if (err || res.code !== 0) {
return message.error(err.msg || '删除数据失败')
} else {
onTableChange()
return message.success('删除数据成功')
}
})
}
const { data: tenantOpts, run: runTenantOpt } = useRequest(fetchTenantOpt, {
debounceInterval: 300,
defaultParams: [{ size: -1 }],
})
const { data, run, loading, current, pageSize, total } = usePagination(fetchData, {
pagination: {
currentKey: 'current',
pageSizeKey: 'size',
listKey: 'records',
totalKey: 'total',
},
defaultParams: [
{
current: 1,
size: 10,
},
],
})
const onTableChange = (pagination, filters, sorter) => {
//
let s = {}
if (sorter) {
s.orders = [{ column: sorter?.field, asc: sorter?.order === 'ascend' }]
}
//
let f = {}
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
if (value) {
f[key] = value.join(',')
} else {
f[key] = ''
}
})
}
run({
current: pagination?.current || current,
size: pagination?.pageSize || pageSize,
...state.search,
...s,
...f,
})
}
const excelExport = async () => {
let params = { ...state.search }
let [err, res] = await axios.get('/db/tTenant/export', { params, responseType: 'blob' })
if (err) {
return message.error(err.msg || '获取数据失败')
}
//
var blob = res
var filename = '租户.xls'
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename)
} else {
var blobURL =
window.URL && window.URL.createObjectURL
? window.URL.createObjectURL(blob)
: window.webkitURL.createObjectURL(blob)
var tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
setTimeout(function () {
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}, 200)
}
}
const excelImport = ({ file }) => {
switch (file.status) {
case 'error':
message.error(file?.response?.msg || '上传失败')
break
case 'done':
message.success('导入成功')
break
}
}
const pagination = computed(() => ({
current: current.value,
pageSize: pageSize.value,
total: total.value,
}))
const tenantOpt = computed({
get() {
if (state.edit?.shared) {
return state.edit.shared.split(',')
} else {
return []
}
},
set(val) {
state.edit.shared = val.join(',')
},
})
</script>
<template>
<div class="tenant">
<a-form ref="searchRef" :labelCol="{ flex: '80px' }" :model="state.search" layout="inline">
<a-form-item label="租户名称" name="name">
<a-input v-model:value="state.search.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="租户类型" name="type">
<a-select v-model:value="state.search.type" placeholder="请选择类型">
<a-select-option value="">全部</a-select-option>
<a-select-option value="0">企业</a-select-option>
<a-select-option value="1">个人</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" @click="onTableChange()">查询</a-button>
<a-button html-type="reset" style="margin-left: 10px" @click="onSearchRest">重置</a-button>
</a-form-item>
<a-form-item>
<a-button v-hasAuth="['system:tenant:post']" type="primary" @click="onEditChange()"
>新增
</a-button>
<a-dropdown>
<template #overlay>
<a-menu>
<div v-hasAuth="['import:excel']">
<a-menu-item key="import">
<a-upload
:showUploadList="false"
accept=".xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
action="/api/db/tTenant/import"
@change="excelImport"
>
导入 Excel
</a-upload>
</a-menu-item>
</div>
<div v-hasAuth="['export:excel']">
<a-menu-item key="export" @click="excelExport">导出 Excel</a-menu-item>
</div>
</a-menu>
</template>
<a-button style="margin-left: 10px">
更多
<DownOutlined />
</a-button>
</a-dropdown>
</a-form-item>
</a-form>
<a-table
:columns="columns"
:data-source="data?.records"
:loading="loading"
:pagination="pagination"
:scroll="{ x: '100%' }"
@change="onTableChange"
>
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-button v-hasAuth="['system:tenant:put']" type="link" @click="onEditChange(record)"
>修改
</a-button>
<a-popconfirm title="确定要删除这条记录吗?" @confirm="deleteData(record)">
<a-button v-hasAuth="['system:tenant:del']" danger type="link">删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 弹出框 -->
<a-modal v-model:open="state.modal.show" :title="state.modal.title" @ok="submitData">
<a-form ref="editRef" :labelCol="{ flex: '80px' }" :model="state.edit" :rules="rules">
<a-form-item label="名称" name="name">
<a-input v-model:value="state.edit.name" placeholder="请输入名称" />
</a-form-item>
<a-row>
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="state.edit.type" placeholder="请选择类型">
<a-select-option :value="0">企业</a-select-option>
<a-select-option :value="1">个人</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :push="2" :span="10">
<a-form-item label="是否启用" name="status">
<a-switch
v-model:checked="state.edit.status"
:checkedValue="0"
:unCheckedValue="1"
placeholder="请输入状态"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="共享数据给" name="status">
<a-select
v-model:value="tenantOpt"
:filterOption="false"
:options="tenantOpts"
mode="multiple"
placeholder="请多选共享数据的其他租户"
@search="(value) => runTenantOpt({ size: -1, name: value || null })"
/>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="state.edit.description" placeholder="请输入描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style lang="scss" scoped>
.tenant {
.ant-form {
.ant-form-item:nth-last-child(2) {
margin-right: 0;
}
.ant-form-item:last-child {
margin-left: auto;
margin-right: 0;
}
}
.ant-btn-link {
padding: 0 4px;
}
:deep(.ant-form-item) {
margin-bottom: 10px;
.ant-row {
flex-flow: nowrap;
}
}
}
</style>

View File

@ -0,0 +1,31 @@
<script setup>
import { LoadingOutlined } from '@ant-design/icons-vue'
import { ref } from 'vue'
defineProps({
link: String,
})
const loading = ref(true)
const iframeLoaded = () => {
loading.value = false
}
</script>
<template>
<a-result v-if="loading" subTitle="应用加载中,请耐心等待。" title="加载中...">
<template #icon>
<LoadingOutlined />
</template>
</a-result>
<iframe :onload="iframeLoaded" :src="link" allow="microphone" />
</template>
<style lang="scss" scoped>
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>

View File

@ -0,0 +1,16 @@
<script setup>
import router from '@/router/index.js'
const toHome = () => router.push({ path: '/' })
const toBack = () => router.back()
</script>
<template>
<a-result status="403" sub-title="抱歉您无权访问此页面" title="403">
<template #extra>
<a-button type="primary" @click="toHome">回到首页</a-button>
<a-button type="default" @click="toBack">上一页</a-button>
</template>
</a-result>
</template>

View File

@ -0,0 +1,16 @@
<script setup>
import router from '@/router/index.js'
const toHome = () => router.push({ path: '/' })
const toBack = () => router.back()
</script>
<template>
<a-result status="404" sub-title="抱歉您访问的页面不存在" title="404">
<template #extra>
<a-button type="primary" @click="toHome">回到首页</a-button>
<a-button type="default" @click="toBack">上一页</a-button>
</template>
</a-result>
</template>

View File

@ -0,0 +1,18 @@
<script setup>
import router from '@/router/index.js'
const toHome = () => router.push({ path: '/' })
const toBack = () => router.back()
</script>
<template>
<a-result status="500" sub-title="对不起服务器出错" title="500">
<template #extra>
<a-button type="primary" @click="toHome">回到首页</a-button>
<a-button type="default" @click="toBack">上一页</a-button>
</template>
</a-result>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,89 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import { createHtmlPlugin } from 'vite-plugin-html'
import viteCompression from 'vite-plugin-compression'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// eslint-disable-next-line no-undef
const env = loadEnv(mode, process.cwd())
return {
base: './',
build: {
emptyOutDir: true,
manifest: true,
outDir: '../src/main/resources/static/manager-ui',
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('ant-design-vue')) {
return 'ant-design-vue'
} else if (id.includes('dayjs')) {
return 'dayjs'
} else if (id.includes('lodash')) {
return 'lodash'
} else if (id.includes('tree-lodash')) {
return 'tree-lodash'
} else if (id.includes('vue-request')) {
return 'vue-request'
} else if (id.includes('js-cookie')) {
return 'js-cookie'
} else if (id.includes('jsencrypt')) {
return 'jsencrypt'
} else if (id.includes('jwt-decode')) {
return 'jwt-decode'
} else {
return 'vendor'
}
} else {
return undefined
}
},
},
},
},
plugins: [
vue(),
vueJsx(),
viteCompression({ threshold: 1024 * 300 }),
vueDevTools(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
createHtmlPlugin({
inject: {
data: {
VITE_APP_TITLE: env.VITE_APP_TITLE,
},
},
}),
],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
}
})

View File

@ -0,0 +1,113 @@
# {{ cookiecutter.project_hans }}
## 后端快速开始
1. 创建数据库
```sql
# mysql
CREATE DATABASE `{{ cookiecutter.mvn_artifact_id }}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
```
```sql
# postgis 启用 gis 扩展
CREATE DATABASE {{ cookiecutter.mvn_artifact_id }} WITH ENCODING = 'utf8';
CREATE EXTENSION postgis;
```
2. 项目运行
```shell
mvn spring-boot:run
```
3. 项目发布,修改 yml 配置文件选择发布配置
```shell
mvn package
```
4. 项目运行
```shell
java -jar {{ cookiecutter.mvn_artifact_id }}-0.0.1-SNAPSHOT.jar
```
## 管理后台快速开始
1. 安装项目依赖
```shell
npm i
```
2. 运行
```shell
npm run dev
```
3. 发布后,会打包至后端的资源文件夹 manager-ui 下
```shell
npm run build:prod
```
## 访问地址
1. 接口文档路径 [swagger-ui](http://localhost:8080/swagger-ui.html)
2. 后台管理路径 [manager-ui](http://localhost:8080/manager-ui.html)
## 注意事项
1. 修改项目常量 `{{ cookiecutter.__mvn_package }}.common.Constants`
2. 前后端 RSA 密钥替换 `{{ cookiecutter.__mvn_package }}.utils.CryptoUtil`
## 代码生成
1. Idea 安装插件 [EasyCode-MybatisCodeHelper](https://plugins.jetbrains.com/plugin/13847-easycode-mybatiscodehelper)
2. Idea 安装插件 [CheckStyle-IDEA](https://plugins.jetbrains.com/plugin/1065-checkstyle-idea)
3. Idea 开发工具连接数据库 {{ cookiecutter.mvn_artifact_id }}
4. 数据库表名右键即可生成代码
## 文件夹说明
```
── {{cookiecutter.project_slug}} {{ cookiecutter.project_hans }} 项目
├── CheckStyle 代码样式检查的配置
├── ManagerUI Vue.js 的后台项目
├── files 接口上传文件
├── EasyCode 代码生成工具模版
└── src
├── main
│ ├── java
│ │ └── {{cookiecutter.__package_path}}
│ │ ├── common 常量
│ │ ├── config 配置
│ │ ├── enums 枚举
│ │ ├── mapper 对象转换
│ │ ├── module 业务模块
│ │ │ ├── dao 自定义 SQL 语句
│ │ │ ├── manager 后台管理接口
│ │ │ │ └── request 自定义请求体
│ │ │ └── schedule 定时任务
│ │ ├── rest 代码生成 Restful 接口
│ │ │ ├── api
│ │ │ ├── controller
│ │ │ ├── dao
│ │ │ ├── entity
│ │ │ ├── enums
│ │ │ ├── exceptions
│ │ │ └── service
│ │ │ └── impl
│ │ ├── service 自定义服务
│ │ │ └── impl
│ │ ├── utils 常用工具
│ │ └── web Web 配置
│ └── resources
| ├── application.yml 配置文件
| ├── schema.sql 数据库结构,项目启动时运行
│ ├── mapper 代码生成
│ └── static 静态资源
| └── manager-ui 后端管理网页
└── test 单元测试
```

View File

@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>{{ cookiecutter.mvn_group_id }}</groupId>
<artifactId>{{ cookiecutter.mvn_artifact_id }}</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>{{ cookiecutter.mvn_group_id }}</name>
<description>{{ cookiecutter.project_name }}</description>
<properties>
<java.version>17</java.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.30</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot3-starter -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.dev33/sa-token-jwt -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.39.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.dev33/sa-token-redis-jackson -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.39.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.12.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.dromara.hutool/hutool-all -->
<dependency>
<groupId>org.dromara.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>6.0.0-M13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.github.biezhi/TinyPinyin -->
<dependency>
<groupId>io.github.biezhi</groupId>
<artifactId>TinyPinyin</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
{% if cookiecutter.platform == "postgis" -%}
<!-- 私有包 mybatis 的 GIS 对象处理 -->
<dependency>
<groupId>ltd.llvy</groupId>
<artifactId>handler</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
{% elif cookiecutter.platform == "mysql" -%}
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
{% endif %}
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>central</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
{% if cookiecutter.platform == "postgis" -%}
<repository>
<id>gitea</id>
<url>https://www.llvy.ltd/api/packages/llvy.ltd/maven</url>
</repository>
{% endif %}
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
{% if cookiecutter.platform == "postgis" -%}
<distributionManagement>
<repository>
<id>gitea</id>
<url>https://www.llvy.ltd/api/packages/llvy.ltd/maven</url>
</repository>
<snapshotRepository>
<id>gitea</id>
<url>https://www.llvy.ltd/api/packages/llvy.ltd/maven</url>
</snapshotRepository>
</distributionManagement>
{% endif %}
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>
-Amapstruct.defaultComponentModel=spring
</arg>
<arg>
-Amapstruct.defaultInjectionStrategy=constructor
</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,29 @@
package {{ cookiecutter.__mvn_package }};
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/** {{ cookiecutter.project_hans }}后端. */
@OpenAPIDefinition(
info =
@Info(
title = "{{ cookiecutter.project_hans }}后端",
version = "0.0.1",
description = "{{ cookiecutter.project_hans }}的接口文档",
contact = @Contact(name = "{{ cookiecutter.author }}", email = "{{ cookiecutter.email }}")))
@MapperScan({"{{ cookiecutter.__mvn_package }}.rest.dao", "{{ cookiecutter.__mvn_package }}.module.dao"})
@EnableAsync(proxyTargetClass = true)
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@ -0,0 +1,24 @@
package {{ cookiecutter.__mvn_package }}.common;
/**
* (Constants) 常量.
*
* @author zweiandlen
* @since 2024/7/3 上午10:52
*/
public class Constants {
// 令牌名称
public static final String TOKEN = "Sa-Token";
// 多租户字段
public static final String TENANT = "tenant";
// 登录名称
public static final String NAME = "name";
// 显示名称
public static final String REAL_NAME = "real_name";
// Jwt 密钥
public static final String JWT_KEY = "pHdSZVNUG71OY2IF";
// 临时签名有效时间5分钟
public static final Integer SIGN_EXPIRE = 5 * 60;
// 超级管理员名称
public static final String SUPER_MANAGER = "sa";
}

View File

@ -0,0 +1,65 @@
package {{ cookiecutter.__mvn_package }}.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.AllArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* (CaffeineConfig) 本地缓存.
*
* @author zweiandlen
* @since 2024/6/13 下午3:02
*/
@Configuration
@EnableCaching
public class CacheConfig {
// 验证码有效时常默认10分钟
public static final int CAPTCHA_OVER_TIME = 600;
// 数据库缓存时间默认1小时
public static final int DATA_OVER_TIME = 3600;
/**
* 缓存管理器包含不同的缓存配置.
*
* @return 缓存管理
*/
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches =
List.of(
new CaffeineCache(
CacheEnum.CAPTCHA.name(),
Caffeine.newBuilder()
.expireAfterWrite(CacheEnum.CAPTCHA.ttl, TimeUnit.SECONDS)
.maximumSize(CacheEnum.CAPTCHA.max)
.build()),
new CaffeineCache(
CacheEnum.DATA.name(),
Caffeine.newBuilder()
.expireAfterWrite(CacheEnum.DATA.ttl, TimeUnit.SECONDS)
.maximumSize(CacheEnum.DATA.max)
.build()));
cacheManager.setCaches(caches);
return cacheManager;
}
/** 缓存配置枚举. */
@AllArgsConstructor
public enum CacheEnum {
CAPTCHA(CAPTCHA_OVER_TIME, 100),
DATA(DATA_OVER_TIME, 1000);
private final long ttl;
private final int max;
}
}

View File

@ -0,0 +1,84 @@
package {{ cookiecutter.__mvn_package }}.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import {{ cookiecutter.__mvn_package }}.common.Constants;
import {{ cookiecutter.__mvn_package }}.utils.ThreadLocalUtil;
import {{ cookiecutter.__mvn_package }}.web.DataChangeInnerInterceptor;
import {{ cookiecutter.__mvn_package }}.web.OrgLineInnerInterceptor;
import java.time.OffsetDateTime;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* (MybatisConfig) Mybatis配置.
*
* @author zweiandlen
* @since 2024/3/28 10:13
*/
@Slf4j
@Component
public class MybatisConfig implements MetaObjectHandler {
private OrgLineInnerInterceptor orgLineInnerInterceptor;
public MybatisConfig(OrgLineInnerInterceptor orgLineInnerInterceptor) {
this.orgLineInnerInterceptor = orgLineInnerInterceptor;
}
@Override
public void insertFill(MetaObject metaObject) {
this.fillStrategy(metaObject, "createTime", OffsetDateTime.now());
this.fillStrategy(
metaObject,
"createBy",
ThreadLocalUtil.getByKey(
Constants.REAL_NAME, ThreadLocalUtil.getByKey(Constants.NAME, "匿名")));
}
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", OffsetDateTime.now());
metaObject.setValue(
"updateBy",
ThreadLocalUtil.getByKey(
Constants.REAL_NAME, ThreadLocalUtil.getByKey(Constants.NAME, "匿名")));
}
/**
* Mybatis 插件配置.
*
* @return 拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户插件
interceptor.addInnerInterceptor(orgLineInnerInterceptor);
// 分页插件
{% if cookiecutter.platform == "mysql" -%}
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
{% elif cookiecutter.platform == "postgis" -%}
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
{% endif %}
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor(true));
// 防止全表更新删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 数据变动记录插件自定义
DataChangeInnerInterceptor dataChangeInnerInterceptor = new DataChangeInnerInterceptor();
Properties dataChangeProperties = new Properties();
dataChangeProperties.setProperty(
"ignoredTableColumns", "logger.*;*.version,create_time,update_time");
dataChangeInnerInterceptor.setProperties(dataChangeProperties);
interceptor.addInnerInterceptor(dataChangeInnerInterceptor);
return interceptor;
}
}

View File

@ -0,0 +1,46 @@
package {{ cookiecutter.__mvn_package }}.config;
import cn.dev33.satoken.jwt.StpLogicJwtForMixin;
import cn.dev33.satoken.stp.StpLogic;
import {{ cookiecutter.__mvn_package }}.common.Constants;
import {{ cookiecutter.__mvn_package }}.web.SuperManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* (SaTokenConfigure) SaToken 配置.
*
* @author zweiandlen
* @since 2024/7/3 上午11:15
*/
@Configuration
public class SaTokenConfig {
/**
* 组件配置.
*
* @param config 配置
*/
@Autowired
public void configSaToken(cn.dev33.satoken.config.SaTokenConfig config) {
// Jwt 令牌
config.setTokenName(Constants.TOKEN);
config.setJwtSecretKey(Constants.JWT_KEY);
// 允许同一账号同一登录
config.setIsConcurrent(true);
// 同时登录时用一块令牌
config.setIsShare(true);
}
@Bean
public StpLogic getStpLogicJwt() {
// Jwt 混入业务字段
return new StpLogicJwtForMixin();
}
@Bean(initMethod = "randomPassword")
public SuperManager getSuperUser() {
return new SuperManager();
}
}

View File

@ -0,0 +1,152 @@
package {{ cookiecutter.__mvn_package }}.config;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import {{ cookiecutter.__mvn_package }}.common.Constants;
import {{ cookiecutter.__mvn_package }}.utils.ThreadLocalUtil;
import {{ cookiecutter.__mvn_package }}.web.SuperManager;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.map.MapBuilder;
import org.dromara.hutool.core.map.MapUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.extra.management.ManagementUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* (WebConfig) Mvc 配置.
*
* @author zweiandlen
* @since 2024/3/26 09:12
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource SuperManager superManager;
@Value("${file.path:}")
private String path;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 创建文件夹
if (StrUtil.isBlank(this.path)) {
// 默认在jar包所在位置创建 files 文件夹
String currentDir = ManagementUtil.getUserInfo().getCurrentDir();
File files = FileUtil.file(currentDir, "files");
this.path = StrUtil.replace(files.getAbsolutePath(), StrUtil.BACKSLASH, StrUtil.SLASH);
this.path = StrUtil.appendIfMissing(this.path, StrUtil.SLASH);
FileUtil.mkdir(files);
} else {
// 在配置文件位置创建 files 文件夹
this.path = StrUtil.replace(this.path, StrUtil.BACKSLASH, StrUtil.SLASH);
this.path = StrUtil.appendIfMissing(this.path, StrUtil.SLASH);
FileUtil.mkdir(this.path);
}
// 作为资源路径
registry.addResourceHandler("/files/**").addResourceLocations("file:" + this.path);
WebMvcConfigurer.super.addResourceHandlers(registry);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(
new AsyncHandlerInterceptor() {
// 请求开始时的拦截策略
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 请求时设置共享值
if (StpUtil.isLogin()) {
MapBuilder<String, Object> builder =
MapUtil.<String, Object>builder()
.put(
Constants.NAME,
StrUtil.defaultIfBlank(
String.valueOf(StpUtil.getExtra(Constants.NAME)), StrUtil.EMPTY))
.put(
Constants.REAL_NAME,
StrUtil.defaultIfBlank(
String.valueOf(StpUtil.getExtra(Constants.REAL_NAME)), StrUtil.EMPTY))
.put(
Constants.TENANT,
StrUtil.defaultIfBlank(
String.valueOf(StpUtil.getExtra(Constants.TENANT)), StrUtil.EMPTY));
ThreadLocalUtil.setMap(builder.build());
}
// 超级管理员时忽略租户
if (superManager.isReal()) {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
}
return AsyncHandlerInterceptor.super.preHandle(request, response, handler);
}
// 请求结束时启用所有的拦截策略清空上下文
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex)
throws Exception {
// 清除上下文
ThreadLocalUtil.clear();
// 启用mybatisplus的拦截策略
InterceptorIgnoreHelper.clearIgnoreStrategy();
AsyncHandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
});
// 路由拦截
registry
.addInterceptor(
new SaInterceptor(
handle -> {
// 校验登录
SaRouter.match("/**")
.notMatch(
"/error/**",
"/files/**",
"/index.html",
"/pub/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/manager-ui/**",
"/manager-ui.html",
"/manager/login",
"/manager/register")
.check(r -> StpUtil.checkLogin());
// 校验权限
SaRouter.match("/db/**", r -> StpUtil.checkPermission("db:rest"));
}))
.addPathPatterns("/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// WebMvcConfigurer.super.addCorsMappings(registry);
// registry
// .addMapping("/**")
// .allowedOriginPatterns("*")
// .allowCredentials(true)
// .allowedHeaders("*")
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .maxAge(3600);
// }
}

View File

@ -0,0 +1,55 @@
package {{ cookiecutter.__mvn_package }}.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* (EAuthorityType) 权限类型.
*
* @author zweiandlen
* @since 2024/4/10 上午11:40
*/
@Schema(
enumAsRef = true,
description =
"""
权限表分类(type)
0 - 后台接口
1 - 前台菜单
2 - 前台按钮
""")
@Getter
@AllArgsConstructor
public enum AuthorityType {
INTERFACE(0, "接口"),
MENU(1, "菜单"),
BUTTON(2, "按钮");
@EnumValue private final int field;
private final String description;
/**
* 创建枚举.
*
* @param field
* @return 枚举
*/
@JsonCreator
public static AuthorityType valueOf(int field) {
return Arrays.stream(AuthorityType.values())
.filter(i -> i.field == field)
.findFirst()
.orElse(null);
}
@JsonValue
public int getField() {
return field;
}
}

View File

@ -0,0 +1,51 @@
package {{ cookiecutter.__mvn_package }}.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* (ELoggerType) 日志类型.
*
* @author zweiandlen
* @since 2024/4/28 下午4:02
*/
@Schema(
enumAsRef = true,
description =
"""
日志表分类(type)
0 - 数据变更
""")
@Getter
@AllArgsConstructor
public enum LoggerType {
DB_AUTO_CHANGE(0, "数据变更");
@EnumValue private final int field;
private final String description;
/**
* 创建枚举.
*
* @param field
* @return 枚举
*/
@JsonCreator
public static LoggerType valueOf(int field) {
return Arrays.stream(LoggerType.values())
.filter(e -> e.field == field)
.findFirst()
.orElse(null);
}
@JsonValue
public int getField() {
return field;
}
}

View File

@ -0,0 +1,53 @@
package {{ cookiecutter.__mvn_package }}.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* (EStatus) 启用标识.
*
* @author zweiandlen
* @since 2024/4/10 上午11:31
*/
@Schema(
enumAsRef = true,
description =
"""
状态(status)
0 - 启用
1 - 禁用
""")
@Getter
@AllArgsConstructor
public enum Status {
ENABLE(0, "启用"),
DISABLE(1, "禁用");
@EnumValue private final int field;
private final String description;
/**
* 创建枚举.
*
* @param field
* @return 枚举
*/
@JsonCreator
public static Status valueOf(int field) {
return Arrays.stream(Status.values())
.filter(i -> i.getField() == field)
.findFirst()
.orElse(null);
}
@JsonValue
public int getField() {
return field;
}
}

View File

@ -0,0 +1,88 @@
package {{ cookiecutter.__mvn_package }}.mapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.AccountExcel;
import {{ cookiecutter.__mvn_package }}.module.manager.request.AccountPassword;
import {{ cookiecutter.__mvn_package }}.module.manager.request.AccountRegister;
import {{ cookiecutter.__mvn_package }}.rest.entity.Account;
import java.util.List;
import org.dromara.hutool.core.text.StrUtil;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
/**
* 凭据表(AccountMapper)表数据库访问层.
*
* @author zweiandlen
* @since 2024/7/5 上午9:48
*/
@Mapper(uses = {ConvertWorker.class})
public interface AccountMapper {
Account toAccount(AccountRegister account);
Account toAccount(AccountPassword account);
@Mapping(target = "isActive", source = "isActive", qualifiedByName = "toActiveStr")
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusStr")
AccountExcel toExcel(Account account);
List<AccountExcel> toExcel(List<Account> accounts);
@Mapping(
target = "password",
source = "password",
defaultValue = "$2a$10$t/cCJoxEWFbVRJy3MAfQhe4/7EeB2sCxE0BcIL6Pz/A28D/Xw0A9K")
@Mapping(target = "isActive", source = "isActive", qualifiedByName = "toActiveId")
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusId")
@Mapping(target = "version", ignore = true)
@Mapping(target = "deleteTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createBy", ignore = true)
@Mapping(target = "updateBy", ignore = true)
@Mapping(target = "tenant", source = "tenant", defaultValue = "")
Account toEntity(AccountExcel excel);
List<Account> toEntity(List<AccountExcel> excels);
/** 空字符串转化为null. */
default Long pkEmptyStr(String pk) {
return StrUtil.isEmptyIfStr(pk) ? null : Long.parseLong(pk);
}
@Named("toActiveId")
default Integer toActiveId(String active) {
return switch (active) {
case "激活" -> 0;
case "未激活" -> 1;
default -> 0;
};
}
@Named("toActiveStr")
default String toActiveStr(Integer active) {
return switch (active) {
case 0 -> "激活";
case 1 -> "未激活";
default -> "其他";
};
}
@Named("toStatusId")
default Integer toStatusId(String status) {
return switch (status) {
case "可用" -> 0;
case "禁用" -> 1;
default -> 0;
};
}
@Named("toStatusStr")
default String toStatusStr(Integer status) {
return switch (status) {
case 0 -> "可用";
case 1 -> "禁用";
default -> "其他";
};
}
}

View File

@ -0,0 +1,81 @@
package {{ cookiecutter.__mvn_package }}.mapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.AuthorityExcel;
import {{ cookiecutter.__mvn_package }}.rest.entity.Authority;
import java.util.List;
import org.dromara.hutool.core.text.StrUtil;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
/**
* (AuthorityMapper)
*
* @author zweiandlen
* @since 2025/4/22 09:12
*/
@Mapper
public interface AuthorityMapper {
@Mapping(target = "type", source = "type", qualifiedByName = "toTypeStr")
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusStr")
AuthorityExcel toExcel(Authority authority);
List<AuthorityExcel> toExcel(List<Authority> authorities);
@Mapping(target = "type", source = "type", qualifiedByName = "toTypeId")
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusId")
@Mapping(target = "version", ignore = true)
@Mapping(target = "deleteTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createBy", ignore = true)
@Mapping(target = "updateBy", ignore = true)
@Mapping(target = "tenant", source = "tenant", defaultValue = "")
Authority toEntiry(AuthorityExcel authority);
List<Authority> toEntity(List<AuthorityExcel> authorities);
/** 空字符串转化为null */
default Long mapEmptyStr(String pk) {
return StrUtil.isEmptyIfStr(pk) ? null : Long.parseLong(pk);
}
@Named("toTypeId")
default Integer toTypeId(String type) {
return switch (type) {
case "接口" -> 0;
case "菜单" -> 1;
case "按钮" -> 2;
default -> 0;
};
}
@Named("toTypeStr")
default String toTypeStr(Integer type) {
return switch (type) {
case 0 -> "接口";
case 1 -> "菜单";
case 2 -> "按钮";
default -> "其他";
};
}
@Named("toStatusId")
default Integer toStatusId(String status) {
return switch (status) {
case "可用" -> 0;
case "禁用" -> 1;
default -> 0;
};
}
@Named("toStatusStr")
default String toStatusStr(Integer status) {
return switch (status) {
case 0 -> "可用";
case 1 -> "禁用";
default -> "其他";
};
}
}

View File

@ -0,0 +1,12 @@
package {{ cookiecutter.__mvn_package }}.mapper;
import org.springframework.stereotype.Component;
/**
* (ConvertWorker) 类型转换.
*
* @author zweiandlen
* @since 2024/7/5 上午9:50
*/
@Component
public class ConvertWorker {}

View File

@ -0,0 +1,63 @@
package {{ cookiecutter.__mvn_package }}.mapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.LoggerExcel;
import {{ cookiecutter.__mvn_package }}.rest.entity.Logger;
import java.time.OffsetDateTime;
import java.util.List;
import org.dromara.hutool.core.date.DatePattern;
import org.dromara.hutool.core.date.DateUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
/**
* (LoggerMapper)
*
* @author zweiandlen
* @since 2025/4/15 14:30
*/
@Mapper
public interface LoggerMapper {
@Mapping(target = "type", source = "type", qualifiedByName = "toTypeStr")
LoggerExcel toExcel(Logger logger);
List<LoggerExcel> toExcel(List<Logger> loggers);
@Mapping(target = "type", source = "type", qualifiedByName = "toTypeId")
@Mapping(target = "deleteTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createBy", ignore = true)
@Mapping(target = "updateBy", ignore = true)
Logger toEntity(LoggerExcel loggerExcel);
List<Logger> toEntity(List<LoggerExcel> loggerExcels);
/** 空字符串转化为null */
default Long mapEmptyStr(String pk) {
return StrUtil.isEmptyIfStr(pk) ? null : Long.parseLong(pk);
}
/** 时间格式化 */
default String mapTimer(OffsetDateTime time) {
return DateUtil.format(time.toLocalDateTime(), DatePattern.CHINESE_DATE_TIME_PATTERN);
}
@Named("toTypeId")
default Integer toTypeId(String type) {
return switch (type) {
case "数据变更自动日志" -> 0;
default -> null;
};
}
@Named("toTypeStr")
default String toTypeStr(Integer type) {
return switch (type) {
case 0 -> "数据变更自动日志";
default -> null;
};
}
}

View File

@ -0,0 +1,59 @@
package {{ cookiecutter.__mvn_package }}.mapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.RoleExcel;
import {{ cookiecutter.__mvn_package }}.rest.entity.Role;
import java.util.List;
import org.dromara.hutool.core.text.StrUtil;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
/**
* (RoleMapper)
*
* @author zweiandlen
* @since 2025/4/22 09:29
*/
@Mapper
public interface RoleMapper {
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusStr")
RoleExcel toExcel(Role role);
List<RoleExcel> toExcel(List<Role> roles);
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusId")
@Mapping(target = "version", ignore = true)
@Mapping(target = "deleteTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createBy", ignore = true)
@Mapping(target = "updateBy", ignore = true)
@Mapping(target = "tenant", source = "tenant", defaultValue = "")
Role toEntity(RoleExcel excel);
List<Role> toEntity(List<RoleExcel> excels);
/** 空字符串转化为null */
default Long mapEmptyStr(String pk) {
return StrUtil.isEmptyIfStr(pk) ? null : Long.parseLong(pk);
}
@Named("toStatusId")
default Integer toStatusId(String status) {
return switch (status) {
case "可用" -> 0;
case "禁用" -> 1;
default -> 0;
};
}
@Named("toStatusStr")
default String toStatusStr(Integer status) {
return switch (status) {
case 0 -> "可用";
case 1 -> "禁用";
default -> "其他";
};
}
}

View File

@ -0,0 +1,78 @@
package {{ cookiecutter.__mvn_package }}.mapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.TenantExcel;
import {{ cookiecutter.__mvn_package }}.rest.entity.Tenant;
import java.util.List;
import org.dromara.hutool.core.text.StrUtil;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
/**
* (TenantMapper)
*
* @author zweiandlen
* @since 2025/3/31 10:59
*/
@Mapper
public interface TenantMapper {
@Mapping(target = "type", source = "type", qualifiedByName = "toTypeStr")
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusStr")
TenantExcel toExcel(Tenant tenant);
List<TenantExcel> toExcel(List<Tenant> tenants);
@Mapping(target = "type", source = "type", qualifiedByName = "toTypeId")
@Mapping(target = "status", source = "status", qualifiedByName = "toStatusId")
@Mapping(target = "version", ignore = true)
@Mapping(target = "deleteTime", ignore = true)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "updateTime", ignore = true)
@Mapping(target = "createBy", ignore = true)
@Mapping(target = "updateBy", ignore = true)
Tenant toEntity(TenantExcel excel);
List<Tenant> toEntity(List<TenantExcel> excels);
/** 空字符串转化为null */
default Long mapEmptyStr(String pk) {
return StrUtil.isEmptyIfStr(pk) ? null : Long.parseLong(pk);
}
@Named("toTypeId")
default Integer toTypeId(String type) {
return switch (type) {
case "企业" -> 0;
case "个人" -> 1;
default -> 0;
};
}
@Named("toTypeStr")
default String toTypeStr(Integer type) {
return switch (type) {
case 0 -> "企业";
case 1 -> "个人";
default -> "其他";
};
}
@Named("toStatusId")
default Integer toStatusId(String status) {
return switch (status) {
case "可用" -> 0;
case "禁用" -> 1;
default -> 0;
};
}
@Named("toStatusStr")
default String toStatusStr(Integer status) {
return switch (status) {
case 0 -> "可用";
case 1 -> "禁用";
default -> "其他";
};
}
}

View File

@ -0,0 +1,25 @@
package {{ cookiecutter.__mvn_package }}.mapper.entity;
import lombok.Data;
/**
* (AccountExcel)
*
* @author zweiandlen
* @since 2025/4/22 09:38
*/
@Data
public class AccountExcel {
private String id;
private String name;
private String realName;
private String password;
private String mobile;
private String email;
private String isActive;
private String status;
private String description;
private String createBy;
private String tenant;
}

View File

@ -0,0 +1,25 @@
package {{ cookiecutter.__mvn_package }}.mapper.entity;
import lombok.Data;
/**
* (AuthorityExcel)
*
* @author zweiandlen
* @since 2025/4/22 09:12
*/
@Data
public class AuthorityExcel {
private String id;
private String parentId;
private String name;
private String value;
private String path;
private String link;
private String type;
private String status;
private String description;
private String createBy;
private String tenant;
}

View File

@ -0,0 +1,23 @@
package {{ cookiecutter.__mvn_package }}.mapper.entity;
import lombok.Data;
/**
* (LoggerExcel)
*
* @author zweiandlen
* @since 2025/4/15 14:28
*/
@Data
public class LoggerExcel {
private String id;
private String type;
private String operation;
private String tableName;
private String recordStatus;
private String changed;
private String description;
private String createTime;
private String createBy;
}

View File

@ -0,0 +1,21 @@
package {{ cookiecutter.__mvn_package }}.mapper.entity;
import lombok.Data;
/**
* (RoleExcel)
*
* @author zweiandlen
* @since 2025/4/22 09:29
*/
@Data
public class RoleExcel {
private String id;
private String name;
private String value;
private String status;
private String description;
private String createBy;
private String tenant;
}

View File

@ -0,0 +1,20 @@
package {{ cookiecutter.__mvn_package }}.mapper.entity;
import lombok.Data;
/**
* (TenantExcel)
*
* @author zweiandlen
* @since 2025/3/31 10:56
*/
@Data
public class TenantExcel {
private String id;
private String name;
private String shared;
private String type;
private String status;
private String description;
}

View File

@ -0,0 +1,24 @@
package {{ cookiecutter.__mvn_package }}.module;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* (IndexController).
*
* @author zweiandlen
* @since 2024/7/3 下午2:42
*/
@Controller
public class IndexController {
@GetMapping(value = {"/manager-ui", "/manager-ui.html"})
public String manager() {
return "redirect:/manager-ui/";
}
@GetMapping("/manager-ui/")
public String managerUi() {
return "/manager-ui/index.html";
}
}

View File

@ -0,0 +1,267 @@
package {{ cookiecutter.__mvn_package }}.module;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.temp.SaTempUtil;
import {{ cookiecutter.__mvn_package }}.common.Constants;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.service.CaptchaService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.compress.ZipUtil;
import org.dromara.hutool.core.data.id.IdUtil;
import org.dromara.hutool.core.date.DatePattern;
import org.dromara.hutool.core.date.DateTime;
import org.dromara.hutool.core.date.format.FastDateFormat;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.file.FileNameUtil;
import org.dromara.hutool.core.io.file.FileReader;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.lang.Validator;
import org.dromara.hutool.core.map.MapUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.core.text.split.SplitUtil;
import org.dromara.hutool.core.util.CharsetUtil;
import org.dromara.hutool.extra.management.ManagementUtil;
import org.dromara.hutool.extra.pinyin.PinyinUtil;
import org.dromara.hutool.http.meta.ContentType;
import org.dromara.hutool.http.meta.HeaderName;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* (PubController) 公共接口.
*
* @author zweiandlen
* @since 2024/7/3 下午2:43
*/
@Slf4j
@Tag(name = "公共接口", description = "常用工具,无需令牌,部分接口需临时签名[sign]")
@RestController
@RequestMapping("pub")
public class PubController {
@Resource CaptchaService captchaService;
@Value("${file.path:}")
private String path;
@Operation(summary = "服务器时间")
@GetMapping("/now")
public OffsetDateTime now() {
return OffsetDateTime.now();
}
/**
* 图形验证码.
*
* @return 图形与ID
*/
@Operation(summary = "图形验证码")
@GetMapping("/captcha/img")
public R getCaptchaImg() {
String objectId = IdUtil.objectId();
String captcha = captchaService.getCodeById(objectId);
Map<String, String> result =
MapUtil.builder(new HashMap<String, String>())
.put("objectId", objectId)
.put("captcha", captcha)
.build();
return R.ok(result);
}
/**
* 邮件验证码.
*
* @return 邮件ID
*/
@Operation(summary = "邮件验证码")
@GetMapping("/captcha/email")
public R getCaptchaEmail(String email) {
if (!Validator.isEmail(email)) {
throw new RuntimeException("邮箱地址格式错误");
}
String objectId = IdUtil.objectId();
objectId = captchaService.getCodeByEmail(objectId, email);
Map<String, String> result =
MapUtil.builder(new HashMap<String, String>()).put("objectId", objectId).build();
return R.ok(result);
}
/**
* 获得拼音首字母.
*
* @param text 文字
* @return 拼音首字母
*/
@Operation(summary = "获得拼音首字母")
@GetMapping("/pinyin/first")
public R getPinyinFirst(String text) {
if (StrUtil.isNotBlank(text)) {
return R.ok(PinyinUtil.getFirstLetter(text, StrUtil.EMPTY));
} else {
return R.failed("不能为空");
}
}
/**
* 获得完整拼音.
*
* @param text 文字
* @return 拼音
*/
@Operation(summary = "获得完整拼音")
@GetMapping("/pinyin/full")
public R getPinyinFull(String text) {
if (StrUtil.isNotBlank(text)) {
return R.ok(PinyinUtil.getPinyin(text));
} else {
return R.failed("不能为空");
}
}
/**
* 获得临时签名5分钟有效.
*
* @param desc 注释
* @return 签名
*/
@Operation(summary = "获得临时签名5分钟有效")
@PostMapping("/sign")
public R getTempToken(@RequestParam(defaultValue = "缺省", required = false) String desc) {
return R.ok(
SaTempUtil.createToken(
StrUtil.format("临时签名 / {} / {}", IdUtil.objectId(), desc), Constants.SIGN_EXPIRE));
}
/**
* 上传文件已登录无需签名未登录需要签名.
*
* @param files 多文件
* @param sign 签名
* @return 存储路径
*/
@Operation(summary = "上传文件,已登录无需签名,未登录需要签名")
@PostMapping(value = "/file/upload", consumes = "multipart/form-data")
public R upload(
@RequestPart("file") MultipartFile[] files,
@RequestParam(value = "sign", required = false) String sign) {
// 解析签名
if (StpUtil.isLogin()) {
String loginId = StpUtil.getLoginIdAsString();
log.info("登录凭据 {} 开始上传 {} 个文件", loginId, files.length);
} else {
String token = SaTempUtil.parseToken(sign, String.class);
if (StrUtil.isNotBlank(token)) {
log.info("临时签名 {} 开始上传 {} 个文件", token, files.length);
} else {
throw new RuntimeException("缺少签名参数");
}
}
// 保存文件
if (StrUtil.isBlank(this.path)) {
// 默认在jar包所在位置创建 files 文件夹
String currentDir = ManagementUtil.getUserInfo().getCurrentDir();
File dir = FileUtil.file(currentDir, "files");
this.path = dir.getAbsolutePath();
}
String[] filepath = new String[files.length];
for (int i = 0; i < files.length; i++) {
MultipartFile file = files[i];
String fullname = IdUtil.objectId();
String extName = FileNameUtil.extName(file.getOriginalFilename());
// 文件后缀黑名单
String[] blackList = {"jsp", "php", "exe", "dll", "vxd", "html", "htm", "js", "asp", "aspx"};
if (ArrayUtil.containsIgnoreCase(blackList, extName)) {
throw new RuntimeException("该文件不允许上传");
}
if (StrUtil.isNotBlank(extName)) {
fullname = StrUtil.join(StrUtil.DOT, fullname, extName);
}
String date =
FastDateFormat.getInstance(DatePattern.PURE_DATE_PATTERN).format(DateTime.now());
File dest = FileUtil.file(this.path, date, fullname);
FileUtil.mkdir(dest);
try {
file.transferTo(dest);
filepath[i] = FileUtil.subPath(this.path, dest);
} catch (IOException e) {
throw new RuntimeException("文件保存失败!");
}
}
String join = ArrayUtil.join(filepath, StrUtil.COMMA);
log.info("文件上传成功 {}", join);
return R.ok(join);
}
/**
* 打包下载 Zip 存档已登录无需签名未登录需要签名.
*
* @param urls 多存储路径
* @param sign 签名
* @throws IOException IO异常
*/
@Operation(summary = "打包下载 Zip 存档,已登录无需签名,未登录需要签名")
@GetMapping("/file/zip")
public void archive(
HttpServletResponse response,
String urls,
@RequestParam(value = "sign", required = false) String sign)
throws IOException {
if (StrUtil.isBlank(urls)) {
return;
}
// 解析签名
if (StpUtil.isLogin()) {
String loginId = StpUtil.getLoginIdAsString();
log.info("登录凭据 {} 开始打包文件 {}", loginId, urls);
} else {
String token = SaTempUtil.parseToken(sign, String.class);
if (StrUtil.isNotBlank(token)) {
log.info("临时签名 {} 开始打包文件 {}", token, urls);
} else {
throw new RuntimeException("缺少签名参数");
}
}
String path;
if (StrUtil.isBlank(this.path)) {
String currentDir = ManagementUtil.getUserInfo().getCurrentDir();
File files = FileUtil.file(currentDir, "files");
path = StrUtil.replace(files.getAbsolutePath(), StrUtil.BACKSLASH, StrUtil.SLASH);
path = StrUtil.appendIfMissing(path, StrUtil.SLASH);
} else {
path = this.path;
}
String finalPath = path;
String[] fileStr =
SplitUtil.split(urls, StrUtil.COMMA).stream()
.map(item -> (finalPath + item))
.toArray(String[]::new);
File[] files = new File[fileStr.length];
for (int i = 0; i < fileStr.length; i++) {
files[i] = FileUtil.file(fileStr[i]);
}
response.reset();
response.setHeader(HeaderName.CONTENT_TYPE.getValue(), ContentType.OCTET_STREAM.getValue());
response.setHeader(
HeaderName.CONTENT_DISPOSITION.getValue(),
"attachment; filename=" + URLEncoder.encode(IdUtil.objectId() + ".zip", CharsetUtil.UTF_8));
ServletOutputStream out = response.getOutputStream();
FileReader.of(ZipUtil.zip(FileUtil.createTempFile(), false, files)).writeToStream(out);
IoUtil.closeQuietly(out);
log.info("文件打包成功,共 {} 个文件", files.length);
}
}

View File

@ -0,0 +1,23 @@
package {{ cookiecutter.__mvn_package }}.module.dao;
import java.time.LocalDateTime;
import org.apache.ibatis.annotations.Param;
/**
* (CustomDao) 自定义语句.
*
* @author zweiandlen
* @since 2024/7/3 下午2:26
*/
public interface CustomDao {
/**
* 清空日志.
*
* @param time 指定时间
* @return 删除条数
*/
int clearLogger(@Param("time") LocalDateTime time);
int expiryMessage();
}

View File

@ -0,0 +1,80 @@
package {{ cookiecutter.__mvn_package }}.module.manager;
import cn.dev33.satoken.stp.StpUtil;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.service.OwnService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* (AuthController) 管理接口.
*
* @author zweiandlen
* @since 2024/7/5 上午11:32
*/
@Tag(name = "管理接口", description = "需要令牌")
@RestController
@RequestMapping("manager/own")
public class AuthController {
@Resource OwnService ownService;
/**
* 拥有的角色及权限.
*
* @return 结果数据
*/
@Operation(summary = "用户的角色及权限")
@GetMapping("auths")
public R ownAuths() {
return R.ok(ownService.getOwnAuths(StpUtil.getLoginIdAsString()));
}
/**
* 拥有的角色.
*
* @return 结果数据
*/
@Operation(summary = "用户的角色")
@GetMapping("roles")
public R ownRoles() {
return R.ok(ownService.getOwnRole(StpUtil.getLoginIdAsString()));
}
/**
* 拥有的权限.
*
* @return 结果数据
*/
@Operation(summary = "用户的权限")
@GetMapping("authorities")
public R ownAuthority() {
return R.ok(ownService.getOwnAuthority(StpUtil.getLoginIdAsString()));
}
/**
* 拥有的菜单.
*
* @return 结果数据
*/
@Operation(summary = "用户的菜单")
@GetMapping("menus")
public R ownMenu() {
return R.ok(ownService.getOwnMenus(StpUtil.getLoginIdAsString()));
}
/**
* 拥有的按钮.
*
* @return 结果数据
*/
@Operation(summary = "用户的按钮")
@GetMapping("buttons")
public R ownButton() {
return R.ok(ownService.getOwnButtons(StpUtil.getLoginIdAsString()));
}
}

View File

@ -0,0 +1,205 @@
package {{ cookiecutter.__mvn_package }}.module.manager;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import {{ cookiecutter.__mvn_package }}.common.Constants;
import {{ cookiecutter.__mvn_package }}.enums.Status;
import {{ cookiecutter.__mvn_package }}.mapper.AccountMapper;
import {{ cookiecutter.__mvn_package }}.module.manager.request.AccountLogin;
import {{ cookiecutter.__mvn_package }}.module.manager.request.AccountPassword;
import {{ cookiecutter.__mvn_package }}.module.manager.request.AccountRegister;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Account;
import {{ cookiecutter.__mvn_package }}.rest.entity.Tenant;
import {{ cookiecutter.__mvn_package }}.rest.service.AccountService;
import {{ cookiecutter.__mvn_package }}.rest.service.TenantService;
import {{ cookiecutter.__mvn_package }}.service.CaptchaService;
import {{ cookiecutter.__mvn_package }}.utils.CryptoUtil;
import {{ cookiecutter.__mvn_package }}.web.SuperManager;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
/**
* (LoginController).
*
* @author zweiandlen
* @since 2024/7/3 下午3:24
*/
@Tag(name = "管理接口", description = "需要令牌")
@RestController
@RequestMapping("manager")
public class LoginController {
@Resource SuperManager superManager;
@Resource AccountMapper accountMapper;
@Resource CaptchaService captchaService;
@Resource AccountService accountService;
@Resource TenantService tenantService;
/**
* 凭据登录.
*
* @param account 请求体
* @return 响应结果
*/
@Operation(summary = "凭据登录", description = "公钥:" + CryptoUtil.PUB)
@PostMapping("login")
public R doLogin(@Valid @RequestBody AccountLogin account) {
// 忽略租户
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
if (StrUtil.isNotBlank(account.getCaptcha()) && StrUtil.isNotBlank(account.getCaptchaId())) {
if (!captchaService.checkCodeById(account.getCaptchaId(), account.getCaptcha())) {
throw new RuntimeException("验证码错误");
}
} else {
throw new RuntimeException("验证码与回执不能为空");
}
// 判断是否超级管理员
if (StrUtil.isNotBlank(account.getName())) {
String decrypt = CryptoUtil.decrypt(account.getName());
if (StrUtil.equalsIgnoreCase(superManager.getUsername(), decrypt)) {
// 校验密码
if (!StrUtil.equals(
CryptoUtil.decrypt(account.getPassword()), superManager.getPassword())) {
throw new RuntimeException("密码错误");
}
// 写入登录参数
SaLoginModel model = new SaLoginModel();
model.setExtra(Constants.NAME, superManager.getUsername());
model.setExtra(Constants.REAL_NAME, superManager.getRealName());
model.setExtra(Constants.TENANT, superManager.getTenant());
StpUtil.login(superManager.getUsername(), model);
return R.ok("超级管理员登录成功");
}
}
// 查找已有凭据
Account find = null;
if (StrUtil.isNotBlank(account.getName())) {
String decrypt = CryptoUtil.decrypt(account.getName());
find =
accountService.getOne(Wrappers.lambdaQuery(Account.class).eq(Account::getName, decrypt));
} else if (StrUtil.isNotBlank(account.getMobile())) {
String decrypt = CryptoUtil.decrypt(account.getMobile());
find =
accountService.getOne(
Wrappers.lambdaQuery(Account.class).eq(Account::getMobile, decrypt));
account.setName(account.getMobile());
} else if (StrUtil.isNotBlank(account.getEmail())) {
String decrypt = CryptoUtil.decrypt(account.getEmail());
find =
accountService.getOne(Wrappers.lambdaQuery(Account.class).eq(Account::getEmail, decrypt));
account.setName(StrUtil.subBefore(account.getName(), "@", false));
} else {
throw new RuntimeException("凭据不存在");
}
if (find == null) {
throw new RuntimeException("凭据不存在");
}
// 校验凭据状态
if (find.getStatus() != Status.ENABLE.getField()) {
throw new RuntimeException("凭据已禁用");
}
// 校验密码
if (!BCrypt.checkpw(CryptoUtil.decrypt(account.getPassword()), find.getPassword())) {
throw new RuntimeException("密码错误");
}
// 写入登录参数
SaLoginModel model = new SaLoginModel();
model.setExtra(Constants.NAME, find.getName());
model.setExtra(Constants.REAL_NAME, find.getRealName());
model.setExtra(Constants.TENANT, find.getTenant());
StpUtil.login(String.valueOf(find.getId()), model);
return R.ok("登录成功");
}
/**
* 凭据注册.
*
* @param account 请求体
* @return 响应结果
*/
@Transactional
@Operation(summary = "凭据注册")
@PostMapping("register")
public R register(@Valid @RequestBody AccountRegister account) {
// 忽略租户
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
if (StrUtil.isNotBlank(account.getCaptcha()) && StrUtil.isNotBlank(account.getCaptchaId())) {
if (!captchaService.checkCodeById(account.getCaptchaId(), account.getCaptcha())) {
throw new RuntimeException("验证码错误");
}
} else {
throw new RuntimeException("验证码与回执不能为空");
}
// 查找已有凭据
Account find = null;
if (StrUtil.isNotBlank(account.getName())) {
find =
accountService.getOne(
Wrappers.lambdaQuery(Account.class).eq(Account::getName, account.getName()));
} else if (StrUtil.isNotBlank(account.getMobile())) {
find =
accountService.getOne(
Wrappers.lambdaQuery(Account.class).eq(Account::getMobile, account.getMobile()));
account.setName(account.getMobile());
} else if (StrUtil.isNotBlank(account.getEmail())) {
find =
accountService.getOne(
Wrappers.lambdaQuery(Account.class).eq(Account::getEmail, account.getEmail()));
account.setName(StrUtil.subBefore(account.getEmail(), "@", false));
}
if (find != null) {
throw new RuntimeException("该凭据已存在");
}
// 新建租户
Tenant findTenant =
tenantService.getOne(
Wrappers.lambdaQuery(Tenant.class).eq(Tenant::getName, account.getTenant()));
if (findTenant == null) {
Tenant tenant = new Tenant();
tenant.setName(account.getTenant());
tenant.setDescription("创建凭据时自动生成");
tenantService.save(tenant);
}
// 新建凭据
Account mapperAccount = accountMapper.toAccount(account);
mapperAccount.setPassword(BCrypt.hashpw(account.getPassword()));
return R.ok(accountService.save(mapperAccount));
}
/**
* 密码修改.
*
* @param account 请求体
* @return 响应结果
*/
@Operation(summary = "密码修改")
@PutMapping("password")
public R password(@Valid @RequestBody AccountPassword account) {
// 查找已有凭据
Account find = accountService.getById(account.getId());
if (find == null) {
throw new RuntimeException("凭据不存在");
}
// 修改密码
Account mapperAccount = accountMapper.toAccount(account);
mapperAccount.setPassword(BCrypt.hashpw(account.getPassword()));
return R.ok(accountService.updateById(mapperAccount));
}
@Operation(summary = "凭据注销")
@PostMapping("logout")
public R logout() {
StpUtil.logout();
return R.ok("注销成功");
}
}

View File

@ -0,0 +1,170 @@
package {{ cookiecutter.__mvn_package }}.module.manager;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.common.Constants;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Message;
import {{ cookiecutter.__mvn_package }}.rest.service.MessageService;
import {{ cookiecutter.__mvn_package }}.utils.ThreadLocalUtil;
import {{ cookiecutter.__mvn_package }}.web.SuperManager;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import org.dromara.hutool.core.date.DateField;
import org.dromara.hutool.core.date.DateTime;
import org.dromara.hutool.core.date.DateUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
/**
* (MessageController)
*
* @author zweiandlen
* @since 2025/4/21 09:32
*/
@Tag(name = "消息接口", description = "需要令牌")
@RestController
@RequestMapping("message")
public class MessageController extends ApiController {
/** 服务对象. */
private final SuperManager superManager;
private final MessageService messageService;
public MessageController(SuperManager superManager, MessageService messageService) {
this.superManager = superManager;
this.messageService = messageService;
}
@Operation(
summary = "获取自己的消息",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "message", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
public R message(@Nullable PageDTO<Message> page, @Nullable Message message) {
String name =
ThreadLocalUtil.getByKey(Constants.NAME, ThreadLocalUtil.getByKey(Constants.NAME, ""));
String realName = ThreadLocalUtil.getByKey(Constants.REAL_NAME, "");
String tenant =
ThreadLocalUtil.getByKey(Constants.TENANT, ThreadLocalUtil.getByKey(Constants.TENANT, ""));
QueryWrapper<Message> queryWrapper = new QueryWrapper<>(message);
if (!superManager.isReal()) {
queryWrapper.or().eq("sender", name).or().eq("sender", realName);
queryWrapper.or().like("recipient", "所有人");
if (StrUtil.isNotBlank(name)) {
queryWrapper.or().like("recipient", name);
}
if (StrUtil.isNotBlank(realName)) {
queryWrapper.or().like("recipient", realName);
}
if (StrUtil.isNotBlank(tenant)) {
queryWrapper.or().like("recipient", tenant);
}
}
if (message != null && StrUtil.isNotEmpty(message.getDescription())) {
queryWrapper.like("description", message.getDescription());
message.setDescription(null);
}
queryWrapper.orderByAsc("status").orderByDesc("create_time");
return success(messageService.page(page, queryWrapper));
}
/**
* 新增数据.
*
* @param message 实体对象
* @return 新增结果
*/
@Operation(
summary = "消息表 - 新增数据",
parameters = {
@Parameter(name = "message", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
public R insert(@RequestBody Message message) {
if (message.getIsExpiry() != null && message.getIsExpiry()) {
// 设置过期时间10天后
DateTime offset = DateUtil.offset(DateUtil.today(), DateField.DAY_OF_MONTH, 10);
message.setExpiry(OffsetDateTime.of(DateUtil.toLocalDateTime(offset), ZoneOffset.ofHours(8)));
}
return success(this.messageService.save(message));
}
/**
* 修改数据.
*
* @param message 实体对象
* @return 修改结果
*/
@Operation(
summary = "消息表 - 修改数据",
parameters = {@Parameter(name = "message", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
public R update(@RequestBody Message message) {
if (message.getIsExpiry() != null && message.getIsExpiry()) {
// 设置过期时间10天后
DateTime offset = DateUtil.offset(DateUtil.today(), DateField.DAY_OF_MONTH, 10);
message.setExpiry(OffsetDateTime.of(DateUtil.toLocalDateTime(offset), ZoneOffset.ofHours(8)));
}
return success(this.messageService.saveOrUpdate(message));
}
/**
* 标记为已读.
*
* @param id 主键
* @return 修改结果
*/
@Operation(
summary = "消息表 - 通过主键已读单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "修改结果"))
@PutMapping("read/{id}")
public R read(@PathVariable("id") Serializable id) {
Message message = this.messageService.getById(id);
if (message.getIsExpiry()) {
// 设置过期时间的消息不允许已读
throw new RuntimeException("消息自动已读,不可手动设置");
} else {
message.setStatus(1);
}
return success(this.messageService.updateById(message));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "消息表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.messageService.removeByIds(idList));
}
}

View File

@ -0,0 +1,40 @@
package {{ cookiecutter.__mvn_package }}.module.manager.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* (AccountLogin) 登录请求体.
*
* @author zweiandlen
* @since 2024/4/1 09:40
*/
@Data
public class AccountLogin {
// @Size(min = 3, max = 50, message = "登录名长度在3到50个字符之间")
@Schema(description = "登录名RSA加密")
private String name;
// @Size(min = 8, max = 15, message = "手机号长度在8到15个字符之间")
@Schema(description = "手机号RSA加密")
private String mobile;
// @Email(message = "邮箱格式错误")
@Schema(description = "邮箱RSA加密")
private String email;
@NotBlank(message = "登录密码不能为空")
// @Size(min = 6, max = 32, message = "登录密码长度在6到32个字符之间")
@Schema(description = "登录密码RSA加密")
private String password;
@NotBlank(message = "验证码不能为空")
@Schema(description = "图形验证码")
private String captcha;
@NotBlank(message = "验证码回执不能为空")
@Schema(description = "图形验证码回执")
private String captchaId;
}

View File

@ -0,0 +1,26 @@
package {{ cookiecutter.__mvn_package }}.module.manager.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* (AccountPassword) 修改密码请求体.
*
* @author zweiandlen
* @since 2024/4/1 09:17
*/
@Data
public class AccountPassword {
@NotNull(message = "ID不能为空")
@Schema(description = "ID")
private long id;
@NotBlank(message = "登录密码不能为空")
@Size(min = 6, max = 32, message = "登录密码长度在6到32个字符之间")
@Schema(description = "登录密码")
private String password;
}

View File

@ -0,0 +1,48 @@
package {{ cookiecutter.__mvn_package }}.module.manager.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* (RAccount) 注册请求体.
*
* @author zweiandlen
* @since 2024/3/28 17:12
*/
@Data
public class AccountRegister {
@NotBlank(message = "登录名不能为空")
@Size(min = 3, max = 50, message = "登录名长度在3到50个字符之间")
@Schema(description = "登录名")
private String name;
@Size(min = 8, max = 15, message = "手机号长度在8到15个字符之间")
@Schema(description = "手机号")
private String mobile;
@Email(message = "邮箱格式错误")
@Schema(description = "邮箱")
private String email;
@NotBlank(message = "登录密码不能为空")
@Size(min = 6, max = 32, message = "登录密码长度在6到32个字符之间")
@Schema(description = "登录密码")
private String password;
@NotBlank(message = "公司名称不能为空")
@Size(min = 4, max = 100, message = "公司名称长度在4到100个字符之间")
@Schema(description = "公司名称")
private String tenant;
@NotBlank(message = "验证码不能为空")
@Schema(description = "图形验证码")
private String captcha;
@NotBlank(message = "验证码回执不能为空")
@Schema(description = "图形验证码回执")
private String captchaId;
}

View File

@ -0,0 +1,71 @@
package {{ cookiecutter.__mvn_package }}.module.schedule;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import {{ cookiecutter.__mvn_package }}.module.dao.CustomDao;
import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hutool.core.date.StopWatch;
import org.dromara.hutool.core.date.TimeUtil;
import org.dromara.hutool.core.lang.ConsoleTable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* (ScheduledService) 定时任务.
*
* @author zweiandlen
* @since 2024/7/3 下午2:27
*/
@Slf4j
@Component
public class ScheduledService {
@Resource CustomDao customDao;
/** 每天早上两点清理一个月前的日志. */
@Scheduled(cron = "0 0 2 * * ?")
public void clearLogger() {
// 定时
StopWatch watchClearLogger = StopWatch.of("日志清理");
watchClearLogger.start();
// 执行
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
LocalDateTime offset = TimeUtil.offset(TimeUtil.of(TimeUtil.today()), -1, ChronoUnit.MONTHS);
int size = customDao.clearLogger(offset);
InterceptorIgnoreHelper.clearIgnoreStrategy();
watchClearLogger.stop();
// 统计
ConsoleTable.of()
.setSBCMode(true)
.addHeader("定时任务", "日志时间", "删除行数", "耗时(毫秒)")
.addBody(
"日志清理",
offset.toString(),
String.valueOf(size),
String.valueOf(watchClearLogger.getTotalTimeMillis()))
.print();
}
/** 每天早上两点半执行一次过期消息. */
@Scheduled(cron = "0 30 2 * * ?")
public void expiryMessage() {
// 定时
StopWatch watchClearLogger = StopWatch.of("过期消息");
watchClearLogger.start();
// 执行
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
int size = customDao.expiryMessage();
InterceptorIgnoreHelper.clearIgnoreStrategy();
watchClearLogger.stop();
// 统计
ConsoleTable.of()
.setSBCMode(true)
.addHeader("定时任务", "更新行数", "耗时(毫秒)")
.addBody(
"过期消息", String.valueOf(size), String.valueOf(watchClearLogger.getTotalTimeMillis()))
.print();
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2011-2021, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package {{ cookiecutter.__mvn_package }}.rest.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* REST API 通用控制器.
*
* @author hubin
* @since 2018-06-08
*/
public class ApiController {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 请求成功.
*
* @param data 数据内容
* @param <T> 对象泛型
* @return ignore
*/
protected <T> R<T> success(T data) {
return R.ok(data);
}
/**
* 请求失败.
*
* @param msg 提示内容
* @return ignore
*/
protected <T> R<T> failed(String msg) {
return R.failed(msg);
}
/**
* 请求失败.
*
* @param errorCode 请求错误码
* @return ignore
*/
protected <T> R<T> failed(ErrorCode errorCode) {
return R.failed(errorCode);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2011-2021, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package {{ cookiecutter.__mvn_package }}.rest.api;
/**
* REST API 错误码接口.
*
* @author hubin
* @since 2018-06-05
*/
public interface ErrorCode {
/** 错误编码 -1、失败 0、成功. */
long getCode();
/** 错误描述. */
String getMsg();
}

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) 2011-2021, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package {{ cookiecutter.__mvn_package }}.rest.api;
import {{ cookiecutter.__mvn_package }}.rest.enums.ApiErrorCode;
import {{ cookiecutter.__mvn_package }}.rest.exceptions.ApiException;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.Optional;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
/**
* REST API 返回结果.
*
* @author hubin
* @since 2018-06-05
*/
@Schema(description = "通用响应体")
@Data
@Slf4j
@Accessors(chain = true)
public class R<T> implements Serializable {
@Serial private static final long serialVersionUID = -1481239544950144592L;
/** 错误码. */
private long code;
/** 结果集. */
private T data;
/** 描述. */
private String msg;
public R() {}
/**
* 按枚举初始化.
*
* @param errorCode 枚举
*/
public R(ErrorCode errorCode) {
errorCode = Optional.ofNullable(errorCode).orElse(ApiErrorCode.FAILED);
this.code = errorCode.getCode();
this.msg = errorCode.getMsg();
}
/**
* 按内容构建响应结果.
*
* @param data 内容
* @param <T> 类型
* @return 响应结果
*/
public static <T> R<T> ok(T data) {
ApiErrorCode aec = ApiErrorCode.SUCCESS;
if (data instanceof Boolean && Boolean.FALSE.equals(data)) {
aec = ApiErrorCode.FAILED;
}
return restResult(data, aec);
}
public static <T> R<T> failed(String msg) {
return restResult(null, ApiErrorCode.FAILED.getCode(), msg);
}
public static <T> R<T> failed(ErrorCode errorCode) {
return restResult(null, errorCode);
}
public static <T> R<T> restResult(T data, ErrorCode errorCode) {
return restResult(data, errorCode.getCode(), errorCode.getMsg());
}
private static <T> R<T> restResult(T data, long code, String msg) {
R<T> apiResult = new R<>();
apiResult.setCode(code);
apiResult.setData(data);
apiResult.setMsg(msg);
return apiResult;
}
public boolean ok() {
return ApiErrorCode.SUCCESS.getCode() == this.code;
}
/** 服务间调用非业务正常,异常直接释放. */
public T serviceData() {
if (!this.ok()) {
throw new ApiException(this.msg);
}
return this.data;
}
}

View File

@ -0,0 +1,283 @@
package {{ cookiecutter.__mvn_package }}.rest.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.secure.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.mapper.AccountMapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.AccountExcel;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Account;
import {{ cookiecutter.__mvn_package }}.rest.service.AccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.poi.excel.ExcelReader;
import org.dromara.hutool.poi.excel.ExcelUtil;
import org.dromara.hutool.poi.excel.ExcelWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 凭据表(Account)表控制层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:36
*/
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/tAccount")
@RequiredArgsConstructor
public class AccountController extends ApiController {
/** 服务对象. */
private final AccountService accountService;
private final AccountMapper accountMapper;
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param account 查询实体
* @return 分页数据
*/
@Operation(
summary = "凭据表 - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "account", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(
@Nullable PageDTO<Account> page, @Nullable Account account, @Nullable String mention) {
QueryWrapper<Account> queryWrapper = new QueryWrapper<>(account);
if (account != null && StrUtil.isNotBlank(account.getName())) {
queryWrapper.like("name", account.getName());
account.setName(null);
}
if (account != null && StrUtil.isNotBlank(account.getRealName())) {
queryWrapper.like("real_name", account.getRealName());
account.setRealName(null);
}
if (StrUtil.isNotBlank(mention)) {
queryWrapper.like("name", mention).or().like("real_name", mention);
}
return success(this.accountService.page(page, queryWrapper));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "凭据表 - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.accountService.getById(id));
}
/**
* 新增数据.
*
* @param account 实体对象
* @return 新增结果
*/
@Operation(
summary = "凭据表 - 新增数据",
parameters = {
@Parameter(name = "account", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody Account account) {
if (StrUtil.isNotBlank(account.getPassword())) {
account.setPassword(BCrypt.hashpw(account.getPassword()));
}
return success(this.accountService.save(account));
}
/**
* 批量新增数据.
*
* @param accounts 实体对象
* @return 新增结果
*/
@Operation(
summary = "凭据表 - 批量新增数据",
parameters = {
@Parameter(name = "accounts", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<Account> accounts) {
accounts.forEach(
item -> {
if (StrUtil.isNotBlank(item.getPassword())) {
item.setPassword(BCrypt.hashpw(item.getPassword()));
}
});
return success(this.accountService.saveBatch(accounts));
}
/**
* 修改数据.
*
* @param account 实体对象
* @return 修改结果
*/
@Operation(
summary = "凭据表 - 修改数据",
parameters = {@Parameter(name = "account", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody Account account) {
if (StrUtil.isNotBlank(account.getPassword())) {
account.setPassword(BCrypt.hashpw(account.getPassword()));
}
return success(this.accountService.saveOrUpdate(account));
}
/**
* 批量修改数据.
*
* @param accounts 实体对象
* @return 修改结果
*/
@Operation(
summary = "凭据表 - 批量修改数据",
parameters = {@Parameter(name = "accounts", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<Account> accounts) {
accounts.forEach(
item -> {
if (StrUtil.isNotBlank(item.getPassword())) {
item.setPassword(BCrypt.hashpw(item.getPassword()));
}
});
return success(this.accountService.saveOrUpdateBatch(accounts));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "凭据表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.accountService.removeByIds(idList));
}
/**
* 导出符合条件的数据
*
* @param account 查询条件
* @param response 响应内容
*/
@GetMapping("export")
@SaCheckPermission("db:rest:export")
public void excelExport(@Nullable Account account, HttpServletResponse response)
throws IOException {
List<Account> list = this.accountService.list(new QueryWrapper<>(account));
List<AccountExcel> excels = this.accountMapper.toExcel(list);
ExcelWriter writer = ExcelUtil.getWriter();
// 表头
writer.addHeaderAlias("id", "唯一标识");
writer.addHeaderAlias("name", "账户");
writer.addHeaderAlias("realName", "名称");
writer.addHeaderAlias("mobile", "手机号");
writer.addHeaderAlias("email", "邮箱");
writer.addHeaderAlias("isActive", "激活");
writer.addHeaderAlias("status", "状态");
writer.addHeaderAlias("description", "描述");
writer.addHeaderAlias("createBy", "创建人");
writer.addHeaderAlias("tenant", "租户");
// 可选项
if (!excels.isEmpty()) {
writer.addSelect(new CellRangeAddressList(1, excels.size(), 5, 5), "激活", "未激活");
writer.addSelect(new CellRangeAddressList(1, excels.size(), 6, 6), "可用", "禁用");
} else {
writer.addSelect(new CellRangeAddressList(1, 1, 5, 5), "激活", "未激活");
writer.addSelect(new CellRangeAddressList(1, 1, 6, 6), "可用", "禁用");
}
// 默认的未添加alias的属性也会写出如果想只写出加了别名的字段可以调用此方法排除之
writer.setOnlyAlias(true);
// 写入数据
writer.write(excels, true);
writer.autoSizeColumnAll();
// 写到响应流
response.setContentType(writer.getContentType());
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION, writer.getDisposition("凭据", StandardCharsets.UTF_8));
ServletOutputStream out = response.getOutputStream();
writer.flush(out);
// 关闭流
IoUtil.closeQuietly(writer, out);
}
@PostMapping("import")
@SaCheckPermission("db:rest:import")
public R excelImport(@RequestParam("file") MultipartFile[] files) throws IOException {
if (files == null || files.length < 1) throw new RuntimeException("文件上传失败");
InputStream in = files[0].getInputStream();
ExcelReader reader = ExcelUtil.getReader(in);
// 表头
reader.addHeaderAlias("唯一标识", "id");
reader.addHeaderAlias("账户", "name");
reader.addHeaderAlias("名称", "realName");
reader.addHeaderAlias("手机号", "mobile");
reader.addHeaderAlias("邮箱", "email");
reader.addHeaderAlias("激活", "isActive");
reader.addHeaderAlias("状态", "status");
reader.addHeaderAlias("描述", "description");
reader.addHeaderAlias("创建人", "createBy");
reader.addHeaderAlias("租户", "tenant");
List<AccountExcel> excels = reader.readAll(AccountExcel.class);
List<Account> tenants = accountMapper.toEntity(excels);
// 关闭流
IoUtil.closeQuietly(reader, in);
return success(accountService.saveOrUpdateBatch(tenants));
}
}

View File

@ -0,0 +1,257 @@
package {{ cookiecutter.__mvn_package }}.rest.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.mapper.AuthorityMapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.AuthorityExcel;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Authority;
import {{ cookiecutter.__mvn_package }}.rest.service.AuthorityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.poi.excel.ExcelReader;
import org.dromara.hutool.poi.excel.ExcelUtil;
import org.dromara.hutool.poi.excel.ExcelWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 权限表(Authority)表控制层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:34
*/
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/tAuthority")
@RequiredArgsConstructor
public class AuthorityController extends ApiController {
/** 服务对象. */
private final AuthorityService authorityService;
private final AuthorityMapper authorityMapper;
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param authority 查询实体
* @return 分页数据
*/
@Operation(
summary = "权限表 - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "authority", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(@Nullable PageDTO<Authority> page, @Nullable Authority authority) {
QueryWrapper<Authority> queryWrapper = new QueryWrapper<>(authority);
if (authority != null && StrUtil.isNotBlank(authority.getName())) {
queryWrapper.like("name", authority.getName());
authority.setName(null);
}
return success(this.authorityService.page(page, queryWrapper));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "权限表 - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.authorityService.getById(id));
}
/**
* 新增数据.
*
* @param authority 实体对象
* @return 新增结果
*/
@Operation(
summary = "权限表 - 新增数据",
parameters = {
@Parameter(name = "authority", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody Authority authority) {
return success(this.authorityService.save(authority));
}
/**
* 批量新增数据.
*
* @param authorities 实体对象
* @return 新增结果
*/
@Operation(
summary = "权限表 - 批量新增数据",
parameters = {
@Parameter(name = "authorities", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<Authority> authorities) {
return success(this.authorityService.saveBatch(authorities));
}
/**
* 修改数据.
*
* @param authority 实体对象
* @return 修改结果
*/
@Operation(
summary = "权限表 - 修改数据",
parameters = {@Parameter(name = "authority", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody Authority authority) {
return success(this.authorityService.saveOrUpdate(authority));
}
/**
* 批量修改数据.
*
* @param authorities 实体对象
* @return 修改结果
*/
@Operation(
summary = "权限表 - 批量修改数据",
parameters = {@Parameter(name = "authorities", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<Authority> authorities) {
return success(this.authorityService.saveOrUpdateBatch(authorities));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "权限表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.authorityService.removeByIds(idList));
}
/**
* 导出符合条件的数据
*
* @param authority 查询条件
* @param response 响应内容
*/
@GetMapping("export")
@SaCheckPermission("db:rest:export")
public void excelExport(@Nullable Authority authority, HttpServletResponse response)
throws IOException {
List<Authority> list = this.authorityService.list(new QueryWrapper<>(authority));
List<AuthorityExcel> excels = this.authorityMapper.toExcel(list);
ExcelWriter writer = ExcelUtil.getWriter();
// 表头
writer.addHeaderAlias("id", "唯一标识");
writer.addHeaderAlias("parentId", "父标识");
writer.addHeaderAlias("name", "名称");
writer.addHeaderAlias("value", "英文值");
writer.addHeaderAlias("path", "路径");
writer.addHeaderAlias("link", "外部链接");
writer.addHeaderAlias("type", "类型");
writer.addHeaderAlias("status", "状态");
writer.addHeaderAlias("description", "描述");
writer.addHeaderAlias("createBy", "创建人");
writer.addHeaderAlias("tenant", "租户");
// 可选项
if (!excels.isEmpty()) {
writer.addSelect(new CellRangeAddressList(1, excels.size(), 6, 6), "接口", "菜单", "按钮");
writer.addSelect(new CellRangeAddressList(1, excels.size(), 7, 7), "可用", "禁用");
} else {
writer.addSelect(new CellRangeAddressList(1, 1, 6, 6), "接口", "菜单", "按钮");
writer.addSelect(new CellRangeAddressList(1, 1, 7, 7), "可用", "禁用");
}
// 默认的未添加alias的属性也会写出如果想只写出加了别名的字段可以调用此方法排除之
writer.setOnlyAlias(true);
// 写入数据
writer.write(excels, true);
writer.autoSizeColumnAll();
// 写到响应流
response.setContentType(writer.getContentType());
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION, writer.getDisposition("权限", StandardCharsets.UTF_8));
ServletOutputStream out = response.getOutputStream();
writer.flush(out);
// 关闭流
IoUtil.closeQuietly(writer, out);
}
@PostMapping("import")
@SaCheckPermission("db:rest:import")
public R excelImport(@RequestParam("file") MultipartFile[] files) throws IOException {
if (files == null || files.length < 1) throw new RuntimeException("文件上传失败");
InputStream in = files[0].getInputStream();
ExcelReader reader = ExcelUtil.getReader(in);
// 表头
reader.addHeaderAlias("唯一标识", "id");
reader.addHeaderAlias("父标识", "parentId");
reader.addHeaderAlias("名称", "name");
reader.addHeaderAlias("英文值", "value");
reader.addHeaderAlias("路径", "path");
reader.addHeaderAlias("外部链接", "link");
reader.addHeaderAlias("类型", "type");
reader.addHeaderAlias("状态", "status");
reader.addHeaderAlias("描述", "description");
reader.addHeaderAlias("创建人", "createBy");
reader.addHeaderAlias("租户", "tenant");
List<AuthorityExcel> excels = reader.readAll(AuthorityExcel.class);
List<Authority> tenants = authorityMapper.toEntity(excels);
// 关闭流
IoUtil.closeQuietly(reader, in);
return success(authorityService.saveOrUpdateBatch(tenants));
}
}

View File

@ -0,0 +1,219 @@
package {{ cookiecutter.__mvn_package }}.rest.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.mapper.LoggerMapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.LoggerExcel;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Logger;
import {{ cookiecutter.__mvn_package }}.rest.service.LoggerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.poi.excel.ExcelUtil;
import org.dromara.hutool.poi.excel.ExcelWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
/**
* 日志表(Logger)表控制层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:33
*/
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/tLogger")
@RequiredArgsConstructor
public class LoggerController extends ApiController {
/** 服务对象. */
private final LoggerService loggerService;
private final LoggerMapper loggerMapper;
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param logger 查询实体
* @return 分页数据
*/
@Operation(
summary = "日志表 - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "logger", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(@Nullable PageDTO<Logger> page, @Nullable Logger logger) {
return success(this.loggerService.page(page, new QueryWrapper<>(logger)));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "日志表 - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.loggerService.getById(id));
}
/**
* 新增数据.
*
* @param logger 实体对象
* @return 新增结果
*/
@Operation(
summary = "日志表 - 新增数据",
parameters = {
@Parameter(name = "logger", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody Logger logger) {
return success(this.loggerService.save(logger));
}
/**
* 批量新增数据.
*
* @param loggers 实体对象
* @return 新增结果
*/
@Operation(
summary = "日志表 - 批量新增数据",
parameters = {
@Parameter(name = "loggers", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<Logger> loggers) {
return success(this.loggerService.saveBatch(loggers));
}
/**
* 修改数据.
*
* @param logger 实体对象
* @return 修改结果
*/
@Operation(
summary = "日志表 - 修改数据",
parameters = {@Parameter(name = "logger", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody Logger logger) {
return success(this.loggerService.saveOrUpdate(logger));
}
/**
* 批量修改数据.
*
* @param loggers 实体对象
* @return 修改结果
*/
@Operation(
summary = "日志表 - 批量修改数据",
parameters = {@Parameter(name = "loggers", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<Logger> loggers) {
return success(this.loggerService.saveOrUpdateBatch(loggers));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "日志表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.loggerService.removeByIds(idList));
}
/**
* 导出符合条件的数据
*
* @param logger 查询条件
* @param response 响应内容
*/
@GetMapping("export")
@SaCheckPermission("db:rest:export")
public void excelExport(@Nullable Logger logger, HttpServletResponse response)
throws IOException {
List<Logger> list = this.loggerService.list(new QueryWrapper<>(logger));
List<LoggerExcel> excels = this.loggerMapper.toExcel(list);
ExcelWriter writer = ExcelUtil.getWriter();
// 表头
writer.addHeaderAlias("id", "唯一标识");
writer.addHeaderAlias("type", "类型");
writer.addHeaderAlias("operate", "操作");
writer.addHeaderAlias("tableName", "表名称");
writer.addHeaderAlias("recordStatus", "打印日志");
writer.addHeaderAlias("changed", "变动内容");
writer.addHeaderAlias("description", "描述");
writer.addHeaderAlias("createTime", "创建时间");
writer.addHeaderAlias("createBy", "创建人");
// 可选项
if (!excels.isEmpty()) {
writer.addSelect(new CellRangeAddressList(1, excels.size(), 1, 1), "数据变更自动日志");
} else {
writer.addSelect(new CellRangeAddressList(1, 1, 1, 1), "数据变更自动日志");
}
// 默认的未添加alias的属性也会写出如果想只写出加了别名的字段可以调用此方法排除之
writer.setOnlyAlias(true);
// 写入数据
writer.write(excels, true);
writer.autoSizeColumnAll();
// 写到响应流
response.setContentType(writer.getContentType());
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION, writer.getDisposition("日志", StandardCharsets.UTF_8));
ServletOutputStream out = response.getOutputStream();
writer.flush(out);
// 关闭流
IoUtil.closeQuietly(writer, out);
}
}

View File

@ -0,0 +1,164 @@
package {{ cookiecutter.__mvn_package }}.rest.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.RoleAuthority;
import {{ cookiecutter.__mvn_package }}.rest.service.RoleAuthorityService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.util.List;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
/**
* 角色权限关联表(RoleAuthority)表控制层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:30
*/
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/tRoleAuthority")
public class RoleAuthorityController extends ApiController {
/** 服务对象. */
private final RoleAuthorityService roleAuthorityService;
public RoleAuthorityController(RoleAuthorityService roleAuthorityService) {
this.roleAuthorityService = roleAuthorityService;
}
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param roleAuthority 查询实体
* @return 分页数据
*/
@Operation(
summary = "角色权限关联表 - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "roleAuthority", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(@Nullable PageDTO<RoleAuthority> page, @Nullable RoleAuthority roleAuthority) {
return success(this.roleAuthorityService.page(page, new QueryWrapper<>(roleAuthority)));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "角色权限关联表 - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.roleAuthorityService.getById(id));
}
/**
* 新增数据.
*
* @param roleAuthority 实体对象
* @return 新增结果
*/
@Operation(
summary = "角色权限关联表 - 新增数据",
parameters = {
@Parameter(name = "roleAuthority", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody RoleAuthority roleAuthority) {
return success(this.roleAuthorityService.save(roleAuthority));
}
/**
* 批量新增数据.
*
* @param roleAuthorities 实体对象
* @return 新增结果
*/
@Operation(
summary = "角色权限关联表 - 批量新增数据",
parameters = {
@Parameter(name = "roleAuthorities", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<RoleAuthority> roleAuthorities) {
return success(this.roleAuthorityService.saveBatch(roleAuthorities));
}
/**
* 修改数据.
*
* @param roleAuthority 实体对象
* @return 修改结果
*/
@Operation(
summary = "角色权限关联表 - 修改数据",
parameters = {@Parameter(name = "roleAuthority", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody RoleAuthority roleAuthority) {
return success(this.roleAuthorityService.saveOrUpdate(roleAuthority));
}
/**
* 批量修改数据.
*
* @param roleAuthorities 实体对象
* @return 修改结果
*/
@Operation(
summary = "角色权限关联表 - 批量修改数据",
parameters = {@Parameter(name = "roleAuthorities", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<RoleAuthority> roleAuthorities) {
return success(this.roleAuthorityService.saveOrUpdateBatch(roleAuthorities));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "角色权限关联表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.roleAuthorityService.removeByIds(idList));
}
}

View File

@ -0,0 +1,246 @@
package {{ cookiecutter.__mvn_package }}.rest.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.mapper.RoleMapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.RoleExcel;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Role;
import {{ cookiecutter.__mvn_package }}.rest.service.RoleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.poi.excel.ExcelReader;
import org.dromara.hutool.poi.excel.ExcelUtil;
import org.dromara.hutool.poi.excel.ExcelWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 角色表(Role)表控制层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:32
*/
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/tRole")
@RequiredArgsConstructor
public class RoleController extends ApiController {
/** 服务对象. */
private final RoleService roleService;
private final RoleMapper roleMapper;
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param role 查询实体
* @return 分页数据
*/
@Operation(
summary = "角色表 - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "role", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(@Nullable PageDTO<Role> page, @Nullable Role role) {
QueryWrapper<Role> queryWrapper = new QueryWrapper<>(role);
if (role != null && StrUtil.isNotBlank(role.getName())) {
queryWrapper.like("name", role.getName());
role.setName(null);
}
return success(this.roleService.page(page, queryWrapper));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "角色表 - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.roleService.getById(id));
}
/**
* 新增数据.
*
* @param role 实体对象
* @return 新增结果
*/
@Operation(
summary = "角色表 - 新增数据",
parameters = {
@Parameter(name = "role", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody Role role) {
return success(this.roleService.save(role));
}
/**
* 批量新增数据.
*
* @param roles 实体对象
* @return 新增结果
*/
@Operation(
summary = "角色表 - 批量新增数据",
parameters = {
@Parameter(name = "roles", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<Role> roles) {
return success(this.roleService.saveBatch(roles));
}
/**
* 修改数据.
*
* @param role 实体对象
* @return 修改结果
*/
@Operation(
summary = "角色表 - 修改数据",
parameters = {@Parameter(name = "role", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody Role role) {
return success(this.roleService.saveOrUpdate(role));
}
/**
* 批量修改数据.
*
* @param roles 实体对象
* @return 修改结果
*/
@Operation(
summary = "角色表 - 批量修改数据",
parameters = {@Parameter(name = "roles", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<Role> roles) {
return success(this.roleService.saveOrUpdateBatch(roles));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "角色表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.roleService.removeByIds(idList));
}
/**
* 导出符合条件的数据
*
* @param role 查询条件
* @param response 响应内容
*/
@GetMapping("export")
@SaCheckPermission("db:rest:export")
public void excelExport(@Nullable Role role, HttpServletResponse response) throws IOException {
List<Role> list = this.roleService.list(new QueryWrapper<>(role));
List<RoleExcel> excels = this.roleMapper.toExcel(list);
ExcelWriter writer = ExcelUtil.getWriter();
// 表头
writer.addHeaderAlias("id", "唯一标识");
writer.addHeaderAlias("name", "名称");
writer.addHeaderAlias("value", "英文值");
writer.addHeaderAlias("status", "状态");
writer.addHeaderAlias("description", "描述");
writer.addHeaderAlias("createBy", "创建人");
writer.addHeaderAlias("tenant", "租户");
// 可选项
if (!excels.isEmpty()) {
writer.addSelect(new CellRangeAddressList(1, excels.size(), 3, 3), "可用", "禁用");
} else {
writer.addSelect(new CellRangeAddressList(1, 1, 3, 3), "可用", "禁用");
}
// 默认的未添加alias的属性也会写出如果想只写出加了别名的字段可以调用此方法排除之
writer.setOnlyAlias(true);
// 写入数据
writer.write(excels, true);
writer.autoSizeColumnAll();
// 写到响应流
response.setContentType(writer.getContentType());
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION, writer.getDisposition("租户租户", StandardCharsets.UTF_8));
ServletOutputStream out = response.getOutputStream();
writer.flush(out);
// 关闭流
IoUtil.closeQuietly(writer, out);
}
@PostMapping("import")
@SaCheckPermission("db:rest:import")
public R excelImport(@RequestParam("file") MultipartFile[] files) throws IOException {
if (files == null || files.length < 1) throw new RuntimeException("文件上传失败");
InputStream in = files[0].getInputStream();
ExcelReader reader = ExcelUtil.getReader(in);
// 表头
reader.addHeaderAlias("唯一标识", "id");
reader.addHeaderAlias("名称", "name");
reader.addHeaderAlias("英文值", "value");
reader.addHeaderAlias("状态", "status");
reader.addHeaderAlias("描述", "description");
reader.addHeaderAlias("创建人", "createBy");
reader.addHeaderAlias("租户", "tenant");
List<RoleExcel> excels = reader.readAll(RoleExcel.class);
List<Role> tenants = roleMapper.toEntity(excels);
// 关闭流
IoUtil.closeQuietly(reader, in);
return success(roleService.saveOrUpdateBatch(tenants));
}
}

View File

@ -0,0 +1,253 @@
package {{ cookiecutter.__mvn_package }}.rest.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import {{ cookiecutter.__mvn_package }}.mapper.TenantMapper;
import {{ cookiecutter.__mvn_package }}.mapper.entity.TenantExcel;
import {{ cookiecutter.__mvn_package }}.rest.api.ApiController;
import {{ cookiecutter.__mvn_package }}.rest.api.R;
import {{ cookiecutter.__mvn_package }}.rest.entity.Tenant;
import {{ cookiecutter.__mvn_package }}.rest.service.TenantService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.poi.excel.ExcelReader;
import org.dromara.hutool.poi.excel.ExcelUtil;
import org.dromara.hutool.poi.excel.ExcelWriter;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 租户表(Tenant)表控制层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:29
*/
@Tag(name = "标准接口", description = "由表结构自动生成")
@RestController
@RequestMapping("db/tTenant")
@RequiredArgsConstructor
public class TenantController extends ApiController {
/** 服务对象. */
private final TenantService tenantService;
private final TenantMapper tenantMapper;
/**
* 分页查询所有数据.
*
* @param page 分页对象
* @param tenant 查询实体
* @return 分页数据
*/
@Operation(
summary = "租户表 - 分页查询所有数据",
parameters = {
@Parameter(name = "page", description = "分页对象"),
@Parameter(name = "tenant", description = "查询实体"),
},
responses = @ApiResponse(description = "分页数据"),
description = "size=-1时查询所有数据orders配合asc排序")
@GetMapping
@SaCheckPermission("db:rest:get")
public R selectAll(
@Nullable PageDTO<Tenant> page, @Nullable Tenant tenant, @Nullable String mention) {
QueryWrapper<Tenant> queryWrapper = new QueryWrapper<>(tenant);
if (tenant != null && StrUtil.isNotBlank(tenant.getName())) {
queryWrapper.like("name", tenant.getName());
tenant.setName(null);
}
if (StrUtil.isNotBlank(mention)) {
queryWrapper.like("name", mention);
}
return success(this.tenantService.page(page, queryWrapper));
}
/**
* 通过主键查询单条数据.
*
* @param id 主键
* @return 单条数据
*/
@Operation(
summary = "租户表 - 通过主键查询单条数据",
parameters = {
@Parameter(name = "id", description = "主键", schema = @Schema(type = "string")),
},
responses = @ApiResponse(description = "单条数据"))
@GetMapping("{id}")
@SaCheckPermission("db:rest:get")
public R selectOne(@PathVariable Serializable id) {
return success(this.tenantService.getById(id));
}
/**
* 新增数据.
*
* @param tenant 实体对象
* @return 新增结果
*/
@Operation(
summary = "租户表 - 新增数据",
parameters = {
@Parameter(name = "tenant", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping
@SaCheckPermission("db:rest:post")
public R insert(@RequestBody Tenant tenant) {
return success(this.tenantService.save(tenant));
}
/**
* 批量新增数据.
*
* @param tenants 实体对象
* @return 新增结果
*/
@Operation(
summary = "租户表 - 批量新增数据",
parameters = {
@Parameter(name = "tenants", description = "实体对象"),
},
responses = @ApiResponse(description = "新增结果"))
@PostMapping("s")
@SaCheckPermission("db:rest:post")
public R inserts(@RequestBody List<Tenant> tenants) {
return success(this.tenantService.saveBatch(tenants));
}
/**
* 修改数据.
*
* @param tenant 实体对象
* @return 修改结果
*/
@Operation(
summary = "租户表 - 修改数据",
parameters = {@Parameter(name = "tenant", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping
@SaCheckPermission("db:rest:put")
public R update(@RequestBody Tenant tenant) {
return success(this.tenantService.saveOrUpdate(tenant));
}
/**
* 批量修改数据.
*
* @param tenants 实体对象
* @return 修改结果
*/
@Operation(
summary = "租户表 - 批量修改数据",
parameters = {@Parameter(name = "tenants", description = "实体对象")},
responses = @ApiResponse(description = "修改结果"),
description = "不存在的对象会新增")
@PutMapping("s")
@SaCheckPermission("db:rest:put")
public R updates(@RequestBody List<Tenant> tenants) {
return success(this.tenantService.saveOrUpdateBatch(tenants));
}
/**
* 删除数据.
*
* @param idList 主键结合
* @return 删除结果
*/
@Operation(
summary = "租户表 - 删除数据",
parameters = {
@Parameter(name = "idList", description = "主键结合", schema = @Schema(type = "string"))
},
responses = @ApiResponse(description = "删除结果"),
description = "主键用逗号拼接")
@DeleteMapping
@SaCheckPermission("db:rest:del")
public R delete(@RequestParam("idList") List<Long> idList) {
return success(this.tenantService.removeByIds(idList));
}
/**
* 导出符合条件的数据
*
* @param tenant 查询条件
* @param response 响应内容
*/
@GetMapping("export")
@SaCheckPermission("db:rest:export")
public void excelExport(@Nullable Tenant tenant, HttpServletResponse response)
throws IOException {
List<Tenant> list = this.tenantService.list(new QueryWrapper<>(tenant));
List<TenantExcel> excels = this.tenantMapper.toExcel(list);
ExcelWriter writer = ExcelUtil.getWriter();
// 表头
writer.addHeaderAlias("id", "唯一标识");
writer.addHeaderAlias("name", "名称");
writer.addHeaderAlias("shared", "共享租户");
writer.addHeaderAlias("type", "类型");
writer.addHeaderAlias("status", "状态");
writer.addHeaderAlias("description", "描述");
writer.addHeaderAlias("createBy", "创建人");
// 可选项
if (!excels.isEmpty()) {
writer.addSelect(new CellRangeAddressList(1, excels.size(), 3, 3), "企业", "个人");
writer.addSelect(new CellRangeAddressList(1, excels.size(), 4, 4), "可用", "禁用");
} else {
writer.addSelect(new CellRangeAddressList(1, 1, 3, 3), "企业", "个人");
writer.addSelect(new CellRangeAddressList(1, 1, 4, 4), "可用", "禁用");
}
// 默认的未添加alias的属性也会写出如果想只写出加了别名的字段可以调用此方法排除之
writer.setOnlyAlias(true);
// 写入数据
writer.write(excels, true);
writer.autoSizeColumnAll();
// 写到响应流
response.setContentType(writer.getContentType());
response.setHeader(
HttpHeaders.CONTENT_DISPOSITION, writer.getDisposition("租户租户", StandardCharsets.UTF_8));
ServletOutputStream out = response.getOutputStream();
writer.flush(out);
// 关闭流
IoUtil.closeQuietly(writer, out);
}
@PostMapping("import")
@SaCheckPermission("db:rest:import")
public R excelImport(@RequestParam("file") MultipartFile[] files) throws IOException {
if (files == null || files.length < 1) throw new RuntimeException("文件上传失败");
InputStream in = files[0].getInputStream();
ExcelReader reader = ExcelUtil.getReader(in);
// 表头
reader.addHeaderAlias("唯一标识", "id");
reader.addHeaderAlias("名称", "name");
reader.addHeaderAlias("共享租户", "shared");
reader.addHeaderAlias("类型", "type");
reader.addHeaderAlias("状态", "status");
reader.addHeaderAlias("描述", "description");
reader.addHeaderAlias("创建人", "createBy");
List<TenantExcel> excels = reader.readAll(TenantExcel.class);
List<Tenant> tenants = tenantMapper.toEntity(excels);
// 关闭流
IoUtil.closeQuietly(reader, in);
return success(tenantService.saveOrUpdateBatch(tenants));
}
}

View File

@ -0,0 +1,15 @@
package {{ cookiecutter.__mvn_package }}.rest.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import {{ cookiecutter.__mvn_package }}.rest.entity.Account;
import {{ cookiecutter.__mvn_package }}.web.MybatisCache;
import org.apache.ibatis.annotations.CacheNamespace;
/**
* 凭据表(Account)表数据库访问层.
*
* @author zweiandlen
* @since 2024-07-03 17:49:35
*/
@CacheNamespace(implementation = MybatisCache.class, eviction = MybatisCache.class)
public interface AccountDao extends BaseMapper<Account> {}

Some files were not shown because too many files have changed in this diff Show More