【ts + java】古玩系统开发总结
src别名的配置
开发中文件和文件的关系会比较复杂,我们需要给src文件夹一个别名@吧
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias: {'@': path.resolve("./src") // 相对路径别名配置,使用@代替src}}
})
typescript配置:
//tsconfig.app.json
{"compilerOptions":{"baseUrl":"./" // 解析非相对模块的基地址,默认是当前目录"paths":{ // 路径映射,相对于baseUrl"@/*":["src/*"]}}
}
环境变量的配置
开发会经历开发,测试,生产环境三个阶段,不同阶段请求的状态不同,于是环境变量的配置需求就有了。只要做简单的配置,这样就可以将环境切换的配置交给代码。(一般一个环境对应一台服务器)
在项目根目录下添加生产,开发,测试环境文件:
.env.deveoplment
.env.production
.env.test
文件内容如下:变量必须以VITE_为前缀
# 变量必须以VITE_为前缀才能暴露给外部读取
# 开发阶段可以获取到的开发环境变量
NODE_ENV = 'development'
VITE_APP_TITLE = '古玩售卖后台系统'
VITE_APP_BASE_API = '/dev-api'
VITE_serve = 'http://xxx.com'NODE_ENV = 'production'
VITE_APP_TITLE = '古玩售卖后台系统'
VITE_APP_BASE_API = '/prod-api'
VITE_serve = 'http://yyy.com'NODE_ENV = 'test'
VITE_APP_TITLE = '古玩售卖后台系统'
VITE_APP_BASE_API = '/test-api'
VITE_serve = 'http://zzz.com'
配置运行命令:package.json
"scripts": {"dev": "vite","build:test":"vue-tsc && vite build --mode test","build:pro":"vue-tsc && vite build -- mode production"}
通过import.meta.env获取环境变量。例如在main.js中打印:可以发现在配置文件中配置的开发环境存储变量显示如下,可以在开发阶段使用。

SVG图标配置
安装:
pnpm install vite-plugin-svg-icons -D
cnpm i fast-glob -D
vite.config.ts中配置:
// 引入svg插件
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";export default defineConfig({plugins: [vue(),createSvgIconsPlugin({// 将svg矢量图标放在项目的src/assets/icons目录下iconDirs:[path.resolve(process.cwd(),'src/assets/icons')],symbolId:'icon-[dir]-[name]'})]
})
在main.ts 中添加:
// svg插件需要的配置代码
import 'virtual:svg-icons-register'
按照配置文件在src/assets/icons下保存svg图片的代码,常用网站iconfont-阿里巴巴矢量图标库

随后可以在项目中使用该目录下svg图片,使用方式如下,注意需要将svg标签和use标签结合使用,xlink:href指定需要使用哪个图标,其命名方式遵循配置文件#icon-svg文件名,fill属性设置图标的颜色。
<template><div><!-- svg是图标外层的容器节点,内部需要和use标签结合使用 --><svg><!-- xlink:href指定需要使用哪个图标 --><use xlink:href="#icon-phone" fill="red"></use></svg></div>
</template>
由于在项目中会经常使用到svg图标,可以将其封装成一个组件(src/conpoments/SvgIcons/index.vue)
template><svg :style="{width,height}"><!-- xlink:href指定需要使用哪个图标 --><use :xlink:href="prefix + name" :fill="color"></use></svg>
</template><script setup lang="ts">defineProps({// xlink:href属性的前缀prefix:{type:String,default:'#icon-'},// 提供使用图标的名字name:String,color:{type:String,default:''},width:{type:String,defaule:'16px'},height:{type:String,defaule:'16px'}})
</script>
这样就可以在其他组件中使用,但使用时需要import引入,还可以将其注册为全局组件。Vue3中使用app.component()注册全局组件。main.js
import SvgIcon from '@/components/SvgIcon/index.vue'
app.component('SvgIcon',SvgIcon);
但项目中会存在许多组件都需要注册为全局组件,在main.ts中一一引入太过于繁琐。可以创建自定义插件对象,在其中注册整个项目的全局组件。在component目录下创建index.ts:
import SvgIcon from './SvgIcon/index.vue'
// 将所有要注册的全局组件放在一个对象中
const allGlobalComponent: { [key: string]: any } = {SvgIcon}// 对外暴露一个插件对象
export default {// 务必叫做install方法,会将app应用实例传递给我们install(app:any) {// 注册项目所有的全局组件Object.keys(allGlobalComponent).forEach(key => {// 注册为全局组件app.component(key,allGlobalComponent[key])})}
}
插件对象必须有install方法。将所有注册的全局组件放在allGlobalComponent对象中,{ [key: string]: any } 表示该对象可以使用任何字符串作为索引,并且可以返回任何类型的属性值。不这样写会报错。
main.ts:安装自定义插件
// 引入自定义插件对象,注册整个项目的全局组件
import globalComponent from '@/components'
// 安装自定义插件,此时会触发install方法
app.use(globalComponent);
这样在项目中就可以直接引入SvgIcon组件了
集成sass
项目中使用sass,需要在对应的style标签上添加属性lang="scss"
我们需要给项目添加一些全局样式。在src/style目录下创建一个index.scss(存放所有的全局样式),在里面引入reset.scss(清除默认样式),代码可以在scss-reset - npm上获取。
在main.ts中引入
// 引入模板的全局样式
import '@/styles/index.scss'
但是index.scss中没有办法使用$变量,需要给项目引入全局变量$(sass中变量使用$符开头),在style/variable.scss创建一个variable.scss文件(存放项目的scss全局变量),并在vite.config.ts文件配置如下:
export default defineConfig({// scss样式全局变量的配置css:{preprocessorOptions:{scss:{javascriptEnabled:true,additionalData:'@import "./src/styles/variable.scss";'}}}
})
在variable.scss中定义scss全局变量
$color:blue;
这样在项目中就可以使用例如:h1{ color:$color }
mock接口
vite安装:cnpm i vite-plugin-mock mockjs -D
在vite.config.ts中配置文件启用插件
export default defineConfig(({command}) => {return {plugins: [viteMockServe ({localEnabled:command === 'serve', // 保证开发阶段能使用mock接口})],} })
在项目的根目录下创建mock文件夹,里面存放接口,例如,user.ts:
function createUserList () {return [{userId:1,avator:'',username:'admin',password:'11111',desc:'平台管理员',roles:['平台管理员'],buttons:['user.detail'],routes:['home'],token:'Admin1 Token'},{userId:2,avator:'',username:'system',password:'11111',desc:'平台管理员',roles:['平台管理员'],buttons:['user.detail','cuser.user'],routes:['home'],token:'Admin2 Token'}]
}export default [// 用户登录接口{url:'/api/user/login',method:'post',response:({body}) => {const { username, password} = body;const checkUser = createUserList().find((item) => item.username === username && item.password === password )if(!checkUser) {return {code:201,data:{message:'账号或密码不正确'}}}const {token} = checkUser;return { code:200,data:{token}}}},// 获取用户信息{url:'/api/user/info',method:'get',response:(request) => {// 获取请求头携带的tokenconst token = request.herder.token;const checkUser = createUserList().find((item) => item.token === token)if(!checkUser) {return {code:201,data:{message:'获取用户信息失败'}}}return {code:200,data:checkUser}}}]
vite脚手架跨域设置
vite提供一个方法loadEnv加载对应环境下的变量,loadEnv()方法一执行就会返回当前开发环境对象,包含项目.env.xxx文件下配置的变量。
此外defineConfig函数的回调会注入一个mode变量,默认是开发环境
还需要获取环境文件的位置,即传入项目根目录,用process.cwd()获取
即 let env = loadEnv(mode,process.cwd()) 表示要加载哪个环境对应的哪个文件,随后就可使用文件中配置的变量
import { defineConfig,loadEnv } from 'vite'// command获取当前的运行环境
export default defineConfig(({command,mode}) => {// 获取各种环境下对应的变量let env = loadEnv(mode,process.cwd())return {// 代理跨域server:{proxy:{// 获取对应环境代理跨域需要带的关键字 例如开发环境需要配置/api才能从指定服务器拿数据[env.VITE_APP_BASE_API]: {// 获取数据服务器的地址target:env.VITE_serve,// 是否代理跨域changeOrigin:true,// 路径重写 真实的接口路径前面是没有/api的,需要将'/api'其替换为''rewrite:(path) => path.replace(/^\/api/,''),}}}} })
Java实体类在项目中的分类
1)封装请求参数的实体类:定义的时候会携带到dto,如:数据传输对象 Data Transfer Object,会定义在dto包中
2)与数据库对应的实体类:和数据库表名一致,定义在domain,entity,pojo包中
3)封装响应结果的实体类:定义的时候会携带到vo(视图对象)字样,定义在vo包中

统一结果实体类
让项目中所有的后端接口返回相同的数据格式。项目中所有的controller接口返回的都是Result格式的数据,code是状态码,message是响应信息,data是响应的数据
@Data
@Schema(description = "响应结果实体类")
public class Result<T> {//返回码@Schema(description = "业务状态码")private Integer code;//返回消息@Schema(description = "响应消息")private String message;//返回数据@Schema(description = "业务数据")private T data;// 私有化构造private Result() {}// 通过它返回统一的数据public static <T> Result<T> build(T body, Integer code, String message) {Result<T> result = new Result<>();result.setData(body);result.setCode(code);result.setMessage(message);return result;}// 通过枚举构造Result对象public static <T> Result build(T body , ResultCodeEnum resultCodeEnum) {return build(body , resultCodeEnum.getCode() , resultCodeEnum.getMessage()) ;}}
枚举类中配置好所有的响应状态码对应的响应信息,可以直接调用 public static <T> Result build(T body , ResultCodeEnum resultCodeEnum) 方法
@Getter // 提供获取属性值的getter方法
public enum ResultCodeEnum {SUCCESS(200 , "操作成功") ,LOGIN_ERROR(201 , "用户名或者密码错误"),VALIDATECODE_ERROR(202 , "验证码错误") ,LOGIN_AUTH(208 , "用户未登录"),USER_NAME_IS_EXISTS(209 , "用户名已经存在"),SYSTEM_ERROR(9999 , "您的网络有问题请稍后重试"),NODE_ERROR( 217, "该节点下有子节点,不可以删除"),DATA_ERROR(204, "数据异常"),ACCOUNT_STOP( 216, "账号已停用"),STOCK_LESS( 219, "库存不足"),;private Integer code ; // 业务状态码private String message ; // 响应消息private ResultCodeEnum(Integer code , String message) {this.code = code ;this.message = message ;}}
如下图,项目中所有的controller接口返回的数据都是Result类型的,调用Result.build方法获取一个Result对象返回前端:

后端项目统一的异常处理
注解一:@ControllerAdvice Controller增强器,给controller增加统一的操作和处理
注解二:@ExceptionHandler 捕获controller抛出的指定类型异常
1)创建一个统一异常处理类;
2)在这个类上添加注解@ControllerAdvice;
3)在该类上创建异常处理的方法,方法上加@ExceptionHandler注解,指定异常类型,当出现异常该方法就会执行;
4)在异常方法中返回统一结果实体类Result格式;
以下的代码就是给controller增加一个对于异常的额外处理操作,当出现ExceptionHandle参数中配置的Exception类的异常时候,就会执行error方法,返回一个统一异常返回格式,即Result.build方法的返回值。
@ControllerAdvice
public class GlobalExceptionHandler {// 全局异常处理@ExceptionHandler(Exception.class)@ResponseBodypublic Result error() {return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);}// 自定义异常处理@ExceptionHandler(FrankException.class)@ResponseBodypublic Result error(FrankException e) {return Result.build(null,e.getResultCodeEnum());}
}
此外还可以自定义异常,自定义异常类需要继承RuntimeException类,在类中加上需要的属性,并在上述统一异常处理类中定义,注意此时ExceptionHandle的参数类名就为自定义异常类。
自定义异常需要手动抛出 throw new FrankException (ResultCodeEnum.Xxx)
@Data
public class FrankException extends RuntimeException {private Integer code;private String message;private ResultCodeEnum resultCodeEnum;public FrankException (ResultCodeEnum resultCodeEnum) {this.resultCodeEnum = resultCodeEnum;this.code = resultCodeEnum.getCode();this.message = resultCodeEnum.getMessage();}
}
后端解决跨域
使用如下配置类,在项目目录下创建config包,其下创建如下java类
@Component
public class WebMvcConfiguration implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域访问registry.addMapping("/**").allowCredentials(true) // 是否在允许跨域的情况下传递Cookie.allowedOriginPatterns("*").allowedMethods("*").allowedHeaders("*");}
}
后端图片验证码实现思路
后端需要实现两件事:
1)进入登录页面后,要生成图片验证码,将其存储在redis中,设置验证码的有效时间
2)用户提交登录表单后,需要验证验证码的正确性
推荐通过工具hutool生成验证码。
需要返回给前端的:1)验证码的key(使用UUID生成的随机字符串,作为key存入redis)2)验证码的base64编码,需要加上前缀 data:image/png;base64, ,这样前端才能将其显示。
放如redis中的:1)验证码的key(由UUID随机生成)和对应的验证码的值
hutool常用的方法:
class CaptchaUtil
1)public static CircleCaptcha createCircleCaptcha(验证码的宽度, 高度, 验证码的位数, 干扰线的数量)
class CircleCaptcha
2) public String getCode() 获取验证码的值
3)public String getImageBase64() 获取验证码base64图片
public ValidateCodeVo generateValidateCode() {// 通过工具hutool生成图片验证码// int width, int height : 图片验证码的宽度和高度// int codeCount 图片验证码的位数// int circleCount 干扰线的数量CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);String code = circleCaptcha.getCode();// 获取验证码的值String imageBase64 = circleCaptcha.getImageBase64();// 验证码图片 做了base64编码// 把验证码存储在redis中,设置key:UUID value:验证码的值String key = UUID.randomUUID().toString().replaceAll("-","");redisTemplate.opsForValue().set("user:validate" + key,code,5, TimeUnit.MINUTES);// 返回ValidateCodeVo对象ValidateCodeVo validateCodeVo = new ValidateCodeVo();validateCodeVo.setCodeKey(key); // redis中存储数据的keyvalidateCodeVo.setCodeValue("data:image/png;base64," + imageBase64);return validateCodeVo;}
}
用户登录校验(后端)
使用拦截器。项目下除了经过登录接口和图片验证码接口之外的所有接口都需要经过拦截器拦截。判断当前接口是否需要校验登录,拦截器中具体做法如下:
0) 判断请求的类型,将option请求直接放行
1)从请求头中获取token,根据token查询redis,获取用户信息
2)没有登录信息,直接返回提示信息
3)有登录信息,获取用户信息存储到Threadlocal中。这是jdk提供的线程工具类(线程变量),Threadlocal充当的变量属于当前线程,对于其他线程是隔离的(线程私有的),可以实现在同一个线程进行数据的共享。即在当前这次请求中可以方便的获取其中的数据。
4)更新redis数据过期时间,增加xx分钟。防止当前请求时数据即将过期,即经过一个请求和响应的时间后数据过期了,用户又要重新登录。
创建工具类AuthContextUtil,实现对ThreadLocal的操作:
public class AuthContextUtil {// 创建Threadlocal对象private static final ThreadLocal<SysUser> threadlocal = new ThreadLocal<>();// 添加数据public static void set(SysUser sysUser) {threadlocal.set(sysUser);}// 获取数据public static SysUser get() {return threadlocal.get();}// 删除数据public static void remove() {threadlocal.remove();}
}
创建拦截器类,实现拦截的核心步骤:
@Component
public class LoginAuthInterceptor implements HandlerInterceptor {@Autowiredprivate RedisTemplate<String,String> redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求方法// 如果请求方法是option(预检请求)比如查看一个当前是否支持跨域,支持再发正式请求 ,直接放行String method = request.getMethod();if ("OPTIONS".equals(method)) {return true;}// 请求头获取tokenString token = request.getHeader("token");// token为空返回错误信息if(StrUtil.isEmpty(token)) {responseNoLoginInfo(response);return false;}// 如果token不为空,拿着token查询redisString userInfoString = redisTemplate.opsForValue().get("user:login" + token);// redis查不到数据,返回错误提示if(StrUtil.isEmpty(userInfoString)) {responseNoLoginInfo(response);return false;}// redis查到错误信息,把用户的信息放到Threadlocal中SysUser sysUser = JSON.parseObject(userInfoString, SysUser.class);AuthContextUtil.set(sysUser);// 把redis用户信息数据更新过期时间redisTemplate.expire("user:login" + token,30, TimeUnit.MINUTES);// 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 删除ThreadLocal中的数据AuthContextUtil.remove();}// 响应208状态码给前端private void responseNoLoginInfo(HttpServletResponse response) {Result<Object> result = Result.build(null, ResultCodeEnum.LOGIN_AUTH);PrintWriter writer = null;response.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=utf-8");try {writer = response.getWriter();writer.println(JSON.toJSONString(result));} catch(IOException e) {e.printStackTrace();} finally {if(writer != null) writer.close();}}
}
在配置类中注册拦截器:
@Component
public class WebMvcConfiguration implements WebMvcConfigurer {@Autowiredprivate LoginAuthInterceptor loginAuthInterceptor;@Autowiredprivate UserProperties userProperties;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginAuthInterceptor).excludePathPatterns(userProperties.getNoAuthUrls())
// .excludePathPatterns("/admin/system/index/generateValidateCode","/admin/system/index/login") // 除去登录和验证码接口不拦截.addPathPatterns("/**"); // 所有路径}
}
excludePathPatterns中配置不需要拦截的路径,可以使用参数的形式传递这些接口路径,但路径一多不方便维护,需要将这些路径配置在application.yml中:其中的属性名可以自定义
antique:auth:noAuthUrls:- /admin/system/index/generateValidateCode- /admin/system/index/login
随后需要添加一个Properties去读取配置文件中的路径,在项目src目录下创建一个文件夹为properties,创建类UserProperties,添加@ConfigurationProperties 获取配置文件中的属性前缀,类中添加属性存放配置文件中的路径,属性名必须和配置文件中的名字保持一致
@Data
@ConfigurationProperties(prefix = "antique.auth")
public class UserProperties {// 变量名一定要和配置文件中的属性名一致private List<String> noAuthUrls;
}
随后在启动类上添加注解,让UserProperties类生效
@EnableConfigurationProperties(value = {UserProperties.class})
这样就可以在 WebMvcConfiguration 中使用properties获取配置文件中的路径名了
权限管理表结构设计
权限管理需求基本都遵循以下一套方案:
1)存在三个实体:用户,角色,菜单
2)用户表和角色表,角色表和菜单表都是多对多的关系。即一个用户拥有多个角色,一个角色包含多个用户。
3)为了表示这三者之间的多对多的关系,需要创建一张关系表,来表示角色和用户的关系,角色和菜单的关系。这张表至少包括两个字段,分别指向两个表的主键id。

常见的需求的sql语句:
1)根据用户id查询用户具有的角色数据
select sys_role.* from
sys_role INNER JOIN sys_user_role
ON sys_role.id = sys_user_role.role_id
WHERE sys_user_role.user_id = 11
需要查询的是 角色用户关系表 和 角色表 将角色用户关系表的角色id字段和角色表的角色id字段做关联,连接两表
角色表sys_role:

角色用户表sys_user_role:

查询结果:

根据用户id查询用户具有的菜单数据
select distinct sys_menu.* from sys_menu
inner join sys_role_menu on sys_menu.id = sys_role_menu.menu_id
inner join sys_user_role on sys_role_menu.role_id = sys_user_role.role_id
where sys_user_role.user_id = 11
需要查询的是角色用户关系表,角色菜单关系表,菜单表。前两表之间用角色id做关联,后面两张表之间用菜单id做关联。有时一个用户具有多个角色,二这些角色具有的权限往往都会重复,所以需要对查询结果做去重处理使用distinct关键字
菜单表 sys_menu:

角色菜单表sys_role_menu:

用户角色表 sys_user_role:

Minio的使用
下载地址:https://dl.min.io/server/minio/release/windows-amd64/ 随后点击其中的minio.exe下载。下载成功后在任意目录下新建一个minio的文件夹,将该启动文件放下其下,并创建一个data文件夹用于存放数据。
随后在minio文件下使用cmd进行启动,在命令行中输入:minio.exe server 数据文件的路径 D:\Program Files\minio\data
出现如下图片即启动成功:

注意:尽量不要放在C盘programme file目录下,命令行启动后可能没有权限访问data目录;也不要下载最新版本的minio.exe会无法启动,并报错。
访问minio.io。命令行启动后会看到有两个端口号,一个是将资源上传的端口号,另一个是管理控制台的端口号。
使用系统默认的用户名和密码进行登录控制台,随后需要创建bucket才能使用minio存储文件

创建bucket
随后将access policy设置bucket的访问策略为public

访问官网:https://min.io/docs/minio/linux/developers/java/minio-java.html查看具体使用。
随后创建接口。接口的方法fileupload需要接收传输的文件对象,类型为MultipartFile,可以使用注解@RequestParam("file") ,也可以不使用如下。前端Element-plus默认的name属性的value就是file,即:<input type="file" name="file">。故注解参数file不能随便写。
@PostMapping("/fileupload")public Result fileupload(MultipartFile file) {// 获取上传的文件// 调用service方法上传,返回minio路径String url = fileUploadService.upload(file);return Result.build(url, ResultCodeEnum.SUCCESS);}
创建FileUploadServiceImpl,在官网首页中就有示例代码,
@Service
public class FileUploadServiceImpl implements FileUploadService {@Autowiredprivate MinioProperties minioProperties;@Overridepublic String upload(MultipartFile file) {try {// 创建MinioClient对象MinioClient minioClient =MinioClient.builder().endpoint(minioProperties.getEndpointUrl()).credentials(minioProperties.getAccessKey(), minioProperties.getSecreKey()).build();// 创建bucketboolean found =minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioProperties.getBucketName()).build());if (!found) {// bucket不存在则进行创建minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioProperties.getBucketName()).build());} else {System.out.println("Bucket 'antique-bucket' already exists.");}// 每个上传文件名称唯一 根据当前日期对上传的文件进行分组String dateToday = DateUtil.format(new Date(), "yyyyMMdd");String uuid = UUID.randomUUID().toString().replaceAll("-","");// 获取上传的文件名称String filename = dateToday + "/" + uuid + file.getOriginalFilename();// 文件上传操作minioClient.putObject(PutObjectArgs.builder().bucket(minioProperties.getBucketName()).object(filename) // 文件的名称.stream(file.getInputStream(),file.getSize(),-1).build());String url = minioProperties.getEndpointUrl() + "/" + minioProperties.getBucketName() + "/" + filename;return url;} catch (Exception e) {e.printStackTrace();throw new FrankException(ResultCodeEnum.SYSTEM_ERROR);}}
}
上述代码中首先创建一个MinioClient对象,需要在endpoint和credentials中添加连接的服务器地址和用户名及密码。
随后创建bucket,随后进行文件的上传,但官网中使用的是对象的方式上传。需要在官方文档中查找流的方式上传的方法
在里面找到MinioClient,需要在里面找用流的方式上传文件的方法putObject

使用的是知道传输文件大小的代码
此外,为了保证上传的文件不重名,我们使用UUID.randomUUID随机数为文件名加前缀,再给上传的文件按上传日期存在不同的文件中。即date/filename.xxx的形式做为文件名,传递给PutObjectArgs.object()方法,那么minio就会将/前面的部分为文件名创建文件夹,/后面的部分为文件名存入创建的文件夹中。
String dateToday = DateUtil.format(new Date(), "yyyyMMdd");
String uuid = UUID.randomUUID().toString().replaceAll("-","");
// 获取上传的文件名称
String filename = dateToday + "/" + uuid + file.getOriginalFilename();
前端使用element-plus上传文件,需要给出文件的上传地址action。文件上传不是使用ajax,使用普通方式进行的,所以请求过程中不会自动携带token,需要在headers中手动传递token.
文件上传后执行on-success的方法,后端会返回上传文件在服务器的存储路径,将上传的地址赋值给sysUser对象,在提交表单信息给服务器时将地址存放到数据库中。
<el-upload class="avatar-uploader" action="http://localhost:8501/admin/system/fileupload" :show-file-list="false":on-success="handleAvatarSuccess":headers="headers"
><img :src="sysUser.avatar" v-if="sysUser.avatar" class="avatar"><el-icon v-else class="avatar-uploader-icon"><Plus></Plus></el-icon>
</el-upload>....// 文件上传
const headers = ref({token:useApp().authorization.token
})const handleAvatarSuccess = (response,uploadFile) => {console.log(response)if(response.code === 200) {sysUser.value.avatar = response.data} else {ElMessage.error(`文件上传失败,${response.message}`)}
}
后台菜单管理
需要以这样的形式显示加载后台所有的菜单,进行管理。但菜单是多级嵌套的结构,需要以这样的方式显示,要在后端进行一些处理。
前端使用element-plus的树形结构表格展示

需要使用el-table显示树形结构的表格,需要准备的数据类型如下:

若是子菜单,则以children属性表示。数据库为每一个菜单分配一个唯一的id,并给每一个菜单一个parent_id字段,为0代表没有上级菜单,非0即表示上级菜单的id值。

所以要在后端使用递归的方式将数据库中查询得到的菜单List集合转换为前端需要的格式。

在entity sysMenu中设置好children属性,用来对应前端存放子菜单节点。
现在数据库中查询好所有的菜单,存放在List集合中,传入buildTree方法,先找到一级菜单即parentId为0的菜单。随后使用递归查找对应一级菜单的子菜单,即parentId为当前一级菜单的id的菜单,依此类推。
public class MenuHelper {// 递归实现封装的过程public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {// 创建一个list集合封装最终的数据List<SysMenu> trees = new ArrayList<>();for (SysMenu sysMenu:sysMenuList) {// 找到递归操作的入口 找到第一层菜单 parent_id = 0if (sysMenu.getParentId().longValue() == 0) {// 根据第一层找下层数据,使用递归 (第一层菜单,所有菜单集合)trees.add(findChildren(sysMenu,sysMenuList));}}return trees;}// 递归查找下层菜单public static SysMenu findChildren(SysMenu sysMenu, List<SysMenu> sysMenuList) {sysMenu.setChildren(new ArrayList<>());// 找下一层数据for(SysMenu it : sysMenuList) {// 判断id和parentId值是否相同if(sysMenu.getId().longValue() == it.getParentId().longValue()) {sysMenu.getChildren().add(findChildren(it,sysMenuList));}}return sysMenu;}
}
serviceImpl:
public List<SysMenu> findNodes() {// 查询所有菜单 返回List集合List<SysMenu> sysMenuList = sysMenuMapper.findAll();// 调用工具类中的方法,把返回list集合封装要求数据格式if (CollectionUtils.isEmpty(sysMenuList)) {return null;}List<SysMenu> treeList = MenuHelper.buildTree(sysMenuList);return treeList;}
EasyExcel导入导出数据
导出数据:导出数据,即文件的下载。需要查询数据库得到需要写入的内容,再把内容写到excel表格中返回给前端。
controller部分:不需要写返回值。文件下载需要使用HttpServletResponse对象
// 导出分类@GetMapping(value="/exportData")public void exportData(HttpServletResponse response) {categoryService.exportData(response);}
service部分:
设置响应头信息Content-disposition让文件以下载方式打开,若不设置则文件不能下载,只有这个内容是不可或缺的。
response.setHeader("Content-disposition","attachment;filename=" + filename + ".xlsx");
最终浏览器查看到的响应头信息如下:

随后查询数据库将要写入到excel中的内容查出来,得到categoryList。随后使用EasyExcel的write(OutputStream输出流,类型Class)方法实现数据的写入。
EasyExcel.write(response.getOutputStream(), CategoryExcelVo.class).sheet("分类数据").doWrite(categoryExcelVoList);
其中sheet('')传入的参数表示工作表的名称,最终下载得到的内容显示如下:

其中doWrite()中传入的是和CategoryExcelVo.class类型一致的数据集合,即需要写入到excel中的内容。但mapper返回的list集合元素是Category类型的,需要将结果转化为CategoryExcelVo类型。
这里使用foreach循环遍历集合List<Category>,转化为List<CategoryExcelVo>类型。转化的过程哦就是将两个集合类型共有的属性先get后set到新的CategoryExcelVo中去,一个个转换比较繁琐,可以使用org.springframework.beans.BeanUtils下的copyProperties(from,to)方法直接拷贝
@Service
public class CategoryServiceImpl implements CategoryService {@Autowiredprivate CategoryMapper categoryMapper;@Overridepublic void exportData(HttpServletResponse response) {try {// 设置响应的头信息和其他信息response.setContentType("application/vnd,ms-excel"); // 设置内容的类型 微软的excel表格response.setCharacterEncoding("utf-8"); //// 防止中文乱码String filename = URLEncoder.encode("分类数据", "UTF-8");// 设置响应头信息 Content-disposition 作用是将文件以下载的方式打开response.setHeader("Content-disposition","attachment;filename=" + filename + ".xlsx");// 调用mapper方法查询所有的分类 返回list集合List<Category> categoryList = categoryMapper.findAll();// List<category> => List<CategoryExcelVo>List<CategoryExcelVo> categoryExcelVoList = new ArrayList<>();for(Category category : categoryList) {CategoryExcelVo categoryExcelVo = new CategoryExcelVo();// 把category值获取出来,设置到categoryExcelVo中去// categoryExcelVo.setId(category.getId());BeanUtils.copyProperties(category,categoryExcelVo); // 把一个对象值复制到另一个里面去categoryExcelVoList.add(categoryExcelVo);}// 调用EasyExecl的write方法完成写操作EasyExcel.write(response.getOutputStream(), CategoryExcelVo.class).sheet("分类数据").doWrite(categoryExcelVoList);} catch (Exception e) {e.printStackTrace();throw new FrankException(ResultCodeEnum.DATA_ERROR);}}
}
其中传入的实体类类型CategoryExcelVo是与excel表格的列对应的java实体类

该实体类的属性上需要添加@ExcelProperty(value='',index=) 注解,其中value表示表头,index表示该属性在excel表中是第几列,从0开始。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryExcelVo {@ExcelProperty(value = "id" ,index = 0)private Long id;@ExcelProperty(value = "名称" ,index = 1)private String name;@ExcelProperty(value = "图片url" ,index = 2)private String imageUrl ;@ExcelProperty(value = "上级id" ,index = 3)private Long parentId;@ExcelProperty(value = "状态" ,index = 4)private Integer status;@ExcelProperty(value = "排序" ,index = 5)private Integer orderNum;}
mapper部分:
<mapper namespace="com.frank.spzx.manager.mapper.CategoryMapper"><resultMap id="categoryMap" type="com.atguigu.spzx.model.entity.product.Category" autoMapping="true"></resultMap><sql id="columns">id,name,image_url,parent_id,status,order_num,create_time,update_time,is_deleted</sql> <select id="findAll" resultMap="categoryMap">select <include refid="columns"/>from category where is_deleted=0 order by id</select>
</mapper>
前端部分:使用bolb类型作为响应类型,使用 URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL,这里传递的就是从后端请求得到的需要下载的excel文件的二进制数据,最后创建一个超链接标签,绑定其连接路径,自动执行其click事件完成自动下载。
category.js:
// 指定响应类型为bolb类型,即二进制数据类型用于表示大量的二进制数据
export const exportCategoryData = () =>
request.get(`${api_name}/exportData`,{responseType:'blob'})
// 导出数据const exportData = () => {exportCategoryData().then(res => {const bolb = new Blob([res])const link = document.createElement('a')// 创建a标签后将bolb对象转换为url// URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. link.href = window.URL.createObjectURL(bolb)// 设置下载文件的名称link.download = "分类数据.xlsx"// 模拟点击下载链接link.click()})}
导入数据:
将外部的excel文件导入系统,需要满足excel中的列名和和对应的Vo实体类的属性要一一对应。
controller:获取上传的文件需要使用MultipartFile对象
// 导入分类@PostMapping(value="/importData")public Result importData(MultipartFile file){// 获取上传的文件categoryService.importData(file);return Result.build(null,ResultCodeEnum.SUCCESS);}
service:使用EasyExcel获取上传的excel中的数据,需要监听器 + EasyExcel.read(InputStream文件输入流,类型Class,监听器).sheet().doRead()结合使用.
public void importData(MultipartFile file) {try {// 每次读取都是新创建的对象,避免了并发问题ExcelListener<CategoryExcelVo> excelListener = new ExcelListener(categoryMapper);EasyExcel.read(file.getInputStream(), CategoryExcelVo.class,excelListener).sheet().doRead();} catch (IOException e) {e.printStackTrace();throw new FrankException(ResultCodeEnum.DATA_ERROR);}}
在项目的Listener包下创建监听器文件:

ExcelListener:该类需要实现ReadListener接口,该接口中有两个方法是必须重写的,一个是invoke方法,该方法会从表格的第二行开始读取将每行的读取内容封装到T的对象中去,excel一共有几行数据该方法就是执行几次。
public void invoke(T data, AnalysisContext context) {}
另一个是doAfterAllAnalysed()方法,它是在excel读取所有的操作结束后执行。
public void doAfterAllAnalysed(AnalysisContext context) {}
ExcelListener这个类不能加上@Component注解,不能交给Spring管理!因为一旦给Spring管理,该类会变成单例,如果多个人同时读取文件会调用同一个Listener,无法区分是哪个文件读取出来的数据,造成并发问题。若需要在监听器中操作mapper,官网给出的解决方案是构造器传递mapper,在调用的时候需要手动进行new操作。
当读取的excel文件中的行数过多时,不能读一行添加一次数据库,会造成资源的消耗,产生OOM,官网给出的方案是批量添加数据,创建一个常量BATCH _COUNT用来存放一次读取的条数,再创建List集合cachedDataList用来存放这些内容。最后在invoke方法中批量添加。
若一次需要添加的数据没有超过BATCH _COUNT的值,则需要执行doAfterAllAnalysed方法,完成数据库的添加操作。
// 这个类不能交给spring管理,否则会变成单例,多个人同时读取文件时就调用同一个Listener 无法区分是哪个文件读取出来的(造成并发的问题)
public class ExcelListener<T> implements ReadListener<T> {// 创建List集合用于缓存数据 行数过多每读取一行加数据库会造成资源的消耗,容易OOM// 创建一个集合定义一个常量,为了做批量操作,每一百条数据加一次数据库private static final int BATCH_COUNT = 100;private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);// 使用构造传递mapper,操作数据库,因为不能交给spring管理private CategoryMapper categoryMapper;public ExcelListener(CategoryMapper categoryMapper) {this.categoryMapper = categoryMapper;}@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {ReadListener.super.onException(exception, context);}@Overridepublic void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {ReadListener.super.invokeHead(headMap, context);}// 从表格的第二行开始读取将每行的读取内容封装到T的对象中去,将每行的数据加到cachedDataList集合中, 当集合达到100 调用方法把数据加到数据库中 加完后将集合清理为初始状态@Overridepublic void invoke(T data, AnalysisContext context) {// 把每行数据的对象t放到cachedDataList的集合中去cachedDataList.add(data);if(cachedDataList.size() >= BATCH_COUNT) {// 调用方法批量添加到数据库中saveData();// 清理list集合 即重新初始化cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);}}private void saveData() {categoryMapper.catchInsert((List<CategoryExcelVo>)cachedDataList);}@Overridepublic void extra(CellExtra extra, AnalysisContext context) {ReadListener.super.extra(extra, context);}// 所有操作都完成后执行@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 保存数据 如果数据的行数没有达到100行 就执行下面的代码将数据加到数据库中saveData();}@Overridepublic boolean hasNext(AnalysisContext context) {return ReadListener.super.hasNext(context);}// 使用构造方法传递
}
Mapper:
@Mapper
public interface CategoryMapper {void catchInsert(List<CategoryExcelVo> cachedDataList);
}
<mapper namespace="com.frank.spzx.manager.mapper.CategoryMapper"><!-- 批量保存分类的方法--><insert id="catchInsert" useGeneratedKeys="true" keyProperty="id">insert into category(id,name,image_url,parent_id,status,order_num,create_time,update_time,is_deleted) values<foreach collection="cachedDataList" item="item" separator=",">(#{item.id},#{item.name},#{item.imageUrl},#{item.parentId},#{item.status},#{item.orderNum},now(),now(),0)</foreach></insert>
</mapper>
前端:
<el-dialog v-model="dialogImportVisible" title="导入" width="30%"><el-form label-width="120px"><el-form-item label="分类文件"><el-upload class="upload-demo"action="http://localhost:8501/admin/product/category/importData":on-success="onUploadSuccess":headers="headers"><el-button type="primary">上传</el-button></el-upload></el-form-item></el-form>
</el-dialog>
let dialogImportVisible = ref(false)
let headers = {token:useApp().authorization.token
}
// 打开导入数据
const importData = () => {dialogImportVisible.value = true
}
//分类数据文件excel上传
const onUploadSuccess = async (response,file) => {ElMessage.success('操作成功')dialogImportVisible.value = falselet result = await findCategoryByParentId(0)list.value = result.data
}
SpringTask定时任务
现在需要统计每天的订单总交易金额,我们可以使用如下的sql语句实现:
但是如果数据量很大,每次查询交易总金额都需要执行一次这样的sql语句会很消耗性能。常用的方法是开启定时任务,设定定时任务程序每天两点执行一次,在订单表中查询前一天的交易总金额数据,然后将结果写入统计结果表。
SpringTask就是spring中的一个模块,
在项目下创建一个task文件夹存放定时任务的类:

随后创建定时任务类:
在类上添加@Component注解将其交给Spring管理。需要执行定时任务需要在类的方法上加上@Scheduled + cron表达式,其中表达式的内容就是需要定时任务执行的要求
@Component
public class OrderStatisticsTask {// 测试定时任务 让该方法每五秒执行一次// 注解@Scheduled + cron 表达式// 其中cron表达式设定执行规则@Scheduled(cron="0/5 * * * * ?")public void test() {System.out.println(new Date().toInstant());}
}
在百度直接搜索cron表达式在线工具,自动生成表达式。这里的需求是间隔五秒打印一下当前时间。最后还要在启动类上加上@EnableScheduling开启定时任务。
随后启动项目控制台就会间隔五秒输出当前的时间。

注意:cron表达式这里只能由六位组成,即(秒 分 时 日 月 周 年) 中的年是不能包括在里面,因为表达式是不能跨年的只能使用前面六位
相关文章:
【ts + java】古玩系统开发总结
src别名的配置 开发中文件和文件的关系会比较复杂,我们需要给src文件夹一个别名吧 vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path// https://vitejs.dev/config/ export default defineConfig({pl…...
【自学笔记】Web前端的重点知识点-持续更新
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 Web前端知识点一、HTML基础二、CSS样式三、JavaScript基础四、前端框架与库五、前端工具与构建六、前端性能优化七、响应式设计与适配八、前端安全 总结 Web前端知识…...
【Docker】快速部署 Nacos 注册中心
【Docker】快速部署 Nacos 注册中心 引言 Nacos 注册中心是一个用于服务发现和配置管理的开源项目。提供了动态服务发现、服务健康检查、动态配置管理和服务管理等功能,帮助开发者更轻松地构建微服务架构。 仓库地址 https://github.com/alibaba/nacos 步骤 拉取…...
SpringCloud篇 微服务架构
1. 工程架构介绍 1.1 两种工程架构模型的特征 1.1.1 单体架构 上面这张图展示了单体架构(Monolithic Architecture)的基本组成和工作原理。单体架构是一种传统的软件架构模式,其中所有的功能都被打包在一个单一的、紧密耦合的应用程序中。 …...
tf.Keras (tf-1.15)使用记录4-model.fit方法及其callbacks参数
model.fit() 方法是 TensorFlow Keras 中用于训练模型的核心方法。 其中里面的callbacks参数是实现模型保存、监控、以及和tensorboard联动的重要API 1 model.fit() 方法的参数及使用 必需参数 x: 训练数据的输入。可以是 NumPy 数组、TensorFlow tf.data.Dataset、Python 生…...
Easy系列PLC尺寸测量功能块ST代码(激光微距仪应用)
激光微距仪可以测量短距离内的产品尺寸,产品规格书的测量 精度可以到0.001mm。具体需要看不同的型号。 1、激光微距仪 2、尺寸测量应用 下面我们以测量高度为例子,设计一个高度测量功能块,同时给出测量数据和合格不合格指标。 3、高度测量功能块 4、复位完成信号 5、功能…...
996引擎 -地图-添加安全区
996引擎 -地图-添加安全区 文件位置配置 cfg_startpoint.xls特效效果1345参考资料文件位置 文件位置服务端D:\996M2-lua\MirServer-lua\Mir200客户端D:\996M2-lua\996M2_debug\dev配置 cfg_startpoint.xls 服务端\Mir200\Envir\DATA\cfg_startpoint.xls 填歪了也有可能只画一…...
Node.js 全局对象
Node.js 全局对象 引言 在Node.js中,全局对象是JavaScript环境中的一部分,它提供了对Node.js运行时环境的访问。全局对象在Node.js中扮演着重要的角色,它使得开发者能够访问和操作Node.js的许多核心功能。本文将详细介绍Node.js的全局对象,包括其特点、常用方法和应用场景…...
[Collection与数据结构] B树与B+树
🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏: 🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 🍕 Collection与…...
redex快速体验
第一步: 2.回调函数在每次state发生变化时候自动执行...
【VM】VirtualBox安装CentOS8虚拟机
阅读本文前,请先根据 VirtualBox软件安装教程 安装VirtualBox虚拟机软件。 1. 下载centos8系统iso镜像 可以去两个地方下载,推荐跟随本文的操作用阿里云的镜像 centos官网:https://www.centos.org/download/阿里云镜像:http://…...
电子电气架构 --- 汽车电子拓扑架构的演进过程
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活…...
自动驾驶---苏箐对智驾产品的思考
1 前言 对于更高级别的自动驾驶,很多人都有不同的思考,方案也好,产品也罢。最近在圈内一位知名的自动驾驶专家苏箐发表了他自己对于自动驾驶未来的思考。 苏箐是地平线的副总裁兼首席架构师,同时也是高阶智能驾驶解决方案SuperDri…...
手写call函数、手写apply函数、手写bind函数
文章目录 1 手写call函数2 手写apply函数3 手写bind函数 1 手写call函数 call函数的实现步骤: 判断调用对象是否为函数。判断传入上下文对象是否存在,如果不存在,则设置为window。处理传入的参数,截取第一个参数后的所有参数。将…...
Python | Pytorch | Tensor知识点总结
如是我闻: Tensor 是我们接触Pytorch了解到的第一个概念,这里是一个关于 PyTorch Tensor 主题的知识点总结,涵盖了 Tensor 的基本概念、创建方式、运算操作、梯度计算和 GPU 加速等内容。 1. Tensor 基本概念 Tensor 是 PyTorch 的核心数据结…...
90,【6】攻防世界 WEB Web_php_unserialize
进入靶场 进入靶场 <?php // 定义一个名为 Demo 的类 class Demo { // 定义一个私有属性 $file,默认值为 index.phpprivate $file index.php;// 构造函数,当创建类的实例时会自动调用// 接收一个参数 $file,用于初始化对象的 $file 属…...
快速提升网站收录:如何设置网站标签?
本文转自:百万收录网 原文链接:https://www.baiwanshoulu.com/45.html 为了快速提升网站的收录,合理设置网站标签是至关重要的。网站标签主要包括标题标签(TitleTag)、描述标签(DescriptionTag)…...
【数据分析】案例04:豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask)
豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask) 豆瓣电影Top250官网:https://movie.douban.com/top250写在前面 实验目的:实现豆瓣电影Top250详情的数据分析与Web网页可视化。电脑系统:Windows使用软件:PyCharm、NavicatPython版本:Python 3.…...
Banana JS,一个严格子集 JavaScript 的解释器
项目地址:https://github.com/shajunxing/banana-js 特色 我的目标是剔除我在实践中总结的JavaScript语言的没用的和模棱两可的部分,只保留我喜欢和需要的,创建一个最小的语法解释器。只支持 JSON 兼容的数据类型和函数,函数是第…...
2025.2.1——四、php_rce RCE漏洞|PHP框架
题目来源:攻防世界 php_rce 目录 一、打开靶机,整理信息 二、解题思路 step 1:PHP框架漏洞以及RCE漏洞信息 1.PHP常用框架 2.RCE远程命令执行 step 2:根据靶机提示,寻找版本漏洞 step 3:进行攻击…...
从0开始使用面对对象C语言搭建一个基于OLED的图形显示框架(绘图设备封装)
目录 图像层的底层抽象——绘图设备抽象 如何抽象一个绘图设备? 桥接绘图设备,特化为OLED设备 题外话:设备的属性,与设计一个相似函数化简的通用办法 使用函数指针来操作设备 总结一下 图像层的底层抽象——绘图设备抽象 在…...
对比DeepSeek、ChatGPT和Kimi的学术写作撰写引言能力
引言 引言部分引入研究主题,明确研究背景、问题陈述,并提出研究的目的和重要性,最后,概述研究方法和论文结构。 下面我们使用DeepSeek、ChatGPT4以及Kimi辅助引言撰写。 提示词: 你现在是一名[计算机理论专家]&#…...
【C++篇】哈希表
目录 一,哈希概念 1.1,直接定址法 1.2,哈希冲突 1.3,负载因子 二,哈希函数 2.1,除法散列法 /除留余数法 2.2,乘法散列法 2.3,全域散列法 三,处理哈希冲突 3.1&…...
TVM调度原语完全指南:从入门到微架构级优化
调度原语 在TVM的抽象体系中,调度(Schedule)是对计算过程的时空重塑。每一个原语都是改变计算次序、数据流向或并行策略的手术刀。其核心作用可归纳为: 优化目标 max ( 计算密度 内存延迟 指令开销 ) \text{优化目标} \max…...
《解锁AI黑科技:数据分类聚类与可视化》
在当今数字化时代,数据如潮水般涌来,如何从海量数据中提取有价值的信息,成为了众多领域面临的关键挑战。人工智能(AI)技术的崛起,为解决这一难题提供了强大的工具。其中,能够实现数据分类与聚类…...
[MySQL]事务的隔离级别原理与底层实现
目录 1.为什么要有隔离性 2.事务的隔离级别 读未提交 读提交 可重复读 串行化 3.演示事务隔离级别的操作 查看与设置事务的隔离级别 演示读提交操作 演示可重复读操作 1.为什么要有隔离性 在真正的业务场景下,MySQL服务在同一时间一定会有大量的客户端进程…...
数据密码解锁之DeepSeek 和其他 AI 大模型对比的神秘面纱
本篇将揭露DeepSeek 和其他 AI 大模型差异所在。 目录 编辑 一本篇背景: 二性能对比: 2.1训练效率: 2.2推理速度: 三语言理解与生成能力对比: 3.1语言理解: 3.2语言生成: 四本篇小结…...
知识管理系统推动企业知识创新与人才培养的有效途径分析
内容概要 本文旨在深入探讨知识管理系统在现代企业中的应用及其对于知识创新与人才培养的重要性。通过分析知识管理系统的概念,企业可以认识到它不仅仅是信息管理的一种工具,更是提升整体创新能力的战略性资产。知识管理系统通过集成企业内部信息资源&a…...
【数据结构与算法】动态规划
目录 动态规划 1. 基本概念 2. 基本步骤 3. 经典应用场景 4. 优点和局限性 最长递增子序列(中等) 最大子数组和(中等) 动态规划 动态规划是一种用于解决多阶段决策问题的算法思想,它将复杂问题分解为一系列相对…...
ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务
目录 一、ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务 1. app.Services 2. GetRequiredService() 3. Init() 二、应用场景 三、依赖注入使用拓展 1、使用场景 2、使用步骤 1. 定义服务接口和实现类 2. 注册服务到依赖注入容器 3. 使用依赖注入获取并…...
