【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…...
机器学习周报-文献阅读
文章目录 摘要Abstract 1 相关知识1.1 WDN建模1.2 掩码操作(Masking Operation) 2 论文内容2.1 WDN信息的数据处理2.2 使用所收集的数据构造模型2.2.1 Gated graph neural network2.2.2 Masking operation2.2.3 Training loss2.2.4 Evaluation metrics 2…...
LabVIEW微位移平台位移控制系统
本文介绍了基于LabVIEW的微位移平台位移控制系统的研究。通过设计一个闭环控制系统,针对微位移平台的通信驱动问题进行了解决,并提出了一种LabVIEW的应用方案,用于监控和控制微位移平台的位移,从而提高系统的精度和稳定性。 项目背…...
fpga系列 HDL:XILINX Vivado ILA FPGA 在线逻辑分析
ILA为内置逻辑分析仪,通过JTAG与FPGA连接,程序在真实硬件中运行,功能类似Quaruts的SignalTap II 。 ip创建ila 使用ila ip核 timescale 1ns / 1ps module HLSLED(input wire clk ,input wire rst_n ,output wire led);// reg led_o_i 1…...
刷题记录 贪心算法-2:455. 分发饼干
题目:455. 分发饼干 难度:简单 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸&a…...
Android --- CameraX讲解
预备知识 surface surfaceView SurfaceHolder surface 是什么? 一句话来说: surface是一块用于填充图像数据的内存。 surfaceView 是什么? 它是一个显示surface 的View。 在app中仍在 ViewHierachy 中,但在wms 中可以理解为…...
ElasticSearch view
基础知识类 elasticsearch和数据库之间区别? elasticsearch:面向文档,数据以文档的形式存储,即JSON格式的对象。更强调数据的搜索、索引和分析。 数据库:更侧重于事务处理、数据的严格结构化和完整性,适用于…...
list的使用,及部分功能的模拟实现(C++)
目录(文章中"节点"和"结点"是同一个意思) 1. list的介绍及使用 1.1 list的介绍 1.2 list的使用 1.2.1 list的构造 1.2.2 list iterator的使用 1.2.3 list capacity 1.2.4 list element access 1.2.5 list modifiers 1.2.6 list…...
联想Y7000+RTX4060+i7+Ubuntu22.04运行DeepSeek开源多模态大模型Janus-Pro-1B+本地部署
直接上手搓了: conda create -n myenv python3.10 -ygit clone https://github.com/deepseek-ai/Janus.gitcd Januspip install -e .pip install webencodings beautifulsoup4 tinycss2pip install -e .[gradio]pip install pexpect>4.3python demo/app_januspr…...
[Spring] Gateway详解
🌸个人主页: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与…...
音叉模态分析
目录 0 序言 1 自由状态下模态求解 1.1 添加模态项目 1.2 生成网格 1.3 设置最大模态阶数 1.4 求解 1.5 结果查看 1.6 结果分析 2 音叉能否释放频率440Hz的音调 3 预应力模态求解 3.1 静态结构分析 3.1.1 添加静态结构项目 3.1.2生成网格 3.1.3添加边界条件 3.1…...
BW AO/工作簿权限配置
场景: 按事业部配置工作簿权限; 1、创建用户 事务码:SU01,用户主数据的维护,可以创建、修改、删除、锁定、解锁、修改密码等 用户设置详情页 2、创建权限角色 用户的权限菜单是通过权限角色分配来实现的 2.1、自定…...
C++ 字母大小写转换两种方法统计数字字符的个数
目录 题目: 代码1: 代码2: 题目描述输入一行字符,统计出其中数字字符的个数。 代码如下: 判断⼀个字符是否是数字字符有⼀个函数是 isdigit ,可以直接使⽤。 代码如下: 题目: 大家都知道…...
如何使用 ChatBox AI 简化本地模型对话操作
部署模型请看上一篇帖子:本地部署DeepSeek教程(Mac版本)-CSDN博客 使用 ChatBox AI 简化本地模型对话操作: 打开 ChatBox AI 官网:Chatbox AI官网:办公学习的AI好助手,全平台AI客户端…...
前端面试笔试题目(一)
以下模拟了大厂前端面试流程,并给出了涵盖HTML、CSS、JavaScript等基础和进阶知识的前端笔试题目,以帮助你更好地准备面试。 面试流程模拟 1. 自我介绍(5 - 10分钟):面试官会请你进行简单的自我介绍,包括…...
Docker Hello World
Docker Hello World 引言 Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及应用的依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。本文将带领您从零开始,学习如何使用 Docker 运行一个简单的 "Hello World"…...
UE 5.3 C++ 对垃圾回收的初步认识
一.UObject的创建 UObject 不支持构造参数。 所有的C UObject都会在引擎启动的时候初始化,然后引擎会调用其默认构造器。如果没有默认的构造器,那么 UObject 将不会编译。 有修改父类参数的需求,就使用指定带参构造 // Sets default value…...
ARM内核:嵌入式时代的核心引擎
引言 在当今智能设备无处不在的时代,ARM(Advanced RISC Machines)处理器凭借其高性能、低功耗的特性,成为智能手机、物联网设备、汽车电子等领域的核心引擎。作为精简指令集(RISC)的典范,ARM核…...
需求分析应该从哪些方面来着手做?
需求分析一般可从以下几个方面着手: 业务需求方面 - 与相关方沟通:与业务部门、客户等进行深入交流,通过访谈、问卷调查、会议讨论等方式,明确他们对项目的期望、目标和整体业务需求,了解项目要解决的业务问题及达成的…...
【Unity2D 2022:C#Script】DoTween插件的使用
一、插件介绍 DOTween 是一个快速、高效、完全类型安全的 Unity 面向对象的动画引擎,针对 C# 用户进行了优化,免费和开源,具有大量高级功能 二、插件的下载 1. DoTween官网:DOTween (HOTween v2) 2. DoTween下载: …...
无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
P3 QT项目----记事本(3.8)
3.8 记事本项目总结 项目源码 1.main.cpp #include "widget.h" #include <QApplication> int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 2.widget.cpp #include "widget.h" #include &q…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
Qt 事件处理中 return 的深入解析
Qt 事件处理中 return 的深入解析 在 Qt 事件处理中,return 语句的使用是另一个关键概念,它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别:不同层级的事件处理 方…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...
篇章二 论坛系统——系统设计
目录 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 1. 数据库设计 1.1 数据库名: forum db 1.2 表的设计 1.3 编写SQL 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 通过需求分析获得概念类并结合业务实现过程中的技术需要&#x…...
02.运算符
目录 什么是运算符 算术运算符 1.基本四则运算符 2.增量运算符 3.自增/自减运算符 关系运算符 逻辑运算符 &&:逻辑与 ||:逻辑或 !:逻辑非 短路求值 位运算符 按位与&: 按位或 | 按位取反~ …...
