来自atguigu 视频链接
项目介绍
1.概述
尚医通即为网上预约挂号系统,旨在缓解看病难、挂号难的就医难题。随时随地轻松挂号!不用排长队!
2.技术点
核心技术
SpringBoot:简化新Spring应用的初始搭建以及开发过程
SpringCloud:基于Spring Boot实现的云原生应用开发工具,SpringCloud使用的技术:(SpringCloudGateway、Spring Cloud Alibaba Nacos、Spring Cloud Alibaba Sentinel、SpringCloud Task和SpringCloudFeign等)
MyBatis-Plus:持久层框架
Redis:内存缓存
RabbitMQ:消息中间件
HTTPClient: Http协议客户端
Swagger2:Api接口文档工具
Nginx:负载均衡
Lombok:简化实体类的开发
Mysql:关系型数据库
MongoDB:面向文档的NoSQL数据库
Vue.js:web 界面的渐进式框架
Node.js: JavaScript 运行环境
Axios:Axios 是一个基于 promise 的 HTTP 库
NPM:包管理器
Babel:转码器
Webpack:打包工具
Docker :容器技术
Git:代码管理工具
3.项目流程
4.架构
Mybatis-Plus
详情见另一篇文章,链接;
此项目大概涉及以下内容,可针对性学习。
-
主键策略 @TableId★
-
字段自动填充 @TableField★
-
乐观锁 悲观锁
-
基础CRUD
-
分页查询 ★
-
逻辑删除 ★.
条件构造器
springboot2.1之后内置mysql8.0驱动。
项目基础搭建
1.项目结构及搭建
可以选择新建springboot项目或maven项目两种方式搭建;父模块打包方式为pom,具体搭建看视频吧。
yyds-parent:根目录,管理子模块
common:公共模块父节点
common-util:工具类模块,所有模块都可以依赖于它
rabbit-util:rabbitmq业务封装
service-util:service服务的工具包,包含service服务的公共配置类,所有service模块依赖于它
hospital-manage:医院接口模拟端
model:实体类模块
server-gateway:服务网关
service:api接口服务父节点
service-cmn:字典api接口服务
service-hosp:医院api接口服务
service-order:订单api接口服务
service-oss:文件存储api接口服务
service-sms:短信api接口服务
service-statistics:统计api接口服务
service-task:定时任务服务
service-user:用户api接口服务
service-client:feign服务调用父节点
service-cmn-client:字典api接口
service-hosp-client:医院api接口
service-order-client:订单api接口
service-user-client:用户api接口
搭建模块
yygh_parent
common
common_util
service_util
model
service
service_hosp
service_user
2.集成Swagger2
一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务
常用注解
举例:
@EnableSwagger2 声明支持Swagger。声明在配置类“@Configuration”
@Api(description = "医院设置接口") 定义controller层接口说明
@ApiOperation(value = "医院设置列表") 定义方法说明
@ApiModelProperty(value = "姓名") 定义实体属性说明
@ApiParam(name = "id", value = "讲师ID", required = true) 定义参数说明
依赖及搭建步骤
因为swagger测试只有service模块里的服务用得到
在common模块service_util的config包中新建配置类Swagger2Config
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
//只显示api路径下的页面,包含/api才显示
.paths(Predicates.and(PathSelectors.regex("/api/.*")))
.build();
}
@Bean
public Docket adminApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("adminApi")
.apiInfo(adminApiInfo())
.select()
//只显示admin路径下的页面
.paths(Predicates.and(PathSelectors.regex("/admin/.*")))
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("网站-API文档")
.description("本文档描述了网站微服务接口定义")
.version("1.0")
.contact(new Contact("xiaoxin", "https://blog.csdn.net/m0_47498874?type=blog", "839623440@qq.com"))
.build();
}
private ApiInfo adminApiInfo(){
return new ApiInfoBuilder()
.title("后台管理系统-API文档")
.description("本文档描述了后台管理系统微服务接口定义")
.version("1.0")
.contact(new Contact("xiaoxin", "https://blog.csdn.net/m0_47498874?type=blog", "839623440@qq.com"))
.build();
}
}
依赖
common下的pom.xml
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
service 项目添加依赖
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>service_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
测试界面访问
需要在启动类上添加@ComponentScan(basePackages = “com.atguigu”)
否则扫描不到配置类
http://localhost:8201/swagger-ui.html
3.统一结果返回
common模块下创建子模块common_utils
1.状态码定义
2.统一结果类
@Data
@ApiModel(value = "全局统一返回结果")
public class Result<T> {
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private T data;
public Result(){}
//结果数据的添加,方便别的方法调用
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null)
result.setData(data);
return result;
}
//结果数据和状态码、信息的添加,其中resultCodeEnum可以是如ResultCodeEnum.SUCCESS/ResultCodeEnum.FAIL
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
//结果只返回状态码和信息,数据为空
public static <T> Result<T> build(Integer code, String message) {
Result<T> result = build(null);
result.setCode(code);
result.setMessage(message);
return result;
}
//成功的返回,只返回空数据及成功的状态
public static<T> Result<T> ok(){
return Result.ok(null);
}
/**
* 操作成功
* @param data
* @param <T>
* @return
*/
//成功的返回,也包括数据
public static<T> Result<T> ok(T data){
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}
//失败的返回,只返回信息、状态码
public static<T> Result<T> fail(){
return Result.fail(null);
}
/**
* 操作失败
* @param data
* @param <T>
* @return
*/
//失败的返回,也返回数据
public static<T> Result<T> fail(T data){
// Result<T> result = build(data);
return build(data, ResultCodeEnum.FAIL);
}
//可以链式调用(返回的Result对象),可重新set返回信息,不在单纯是fail的失败
public Result<T> message(String msg){
this.setMessage(msg);
return this;
}
4.统一异常处理
@ControllerAdvice spring AOP面向切面编程,对Controller进行切面环绕。
作用:全局异常处理、全局数据预处理、全局数据绑定
@ExceptionHandler (Exception.class) 异常拦截器(自定义异常处理器),需要结合@ControllerAdvice一起使用
common/common_util/exception包
系统异常
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e){
//e.printStackTrace();
return Result.fail(e.getmessage());
}
@ExceptionHandler(YyghException.class)
@ResponseBody
public Result error(YyghException e){
e.printStackTrace();
return Result.fail();
}
}
特殊异常
自定义异常
有些系统异常不能满足我们的需求
返回结果和自定义异常分开,这里自定义异常new了之后toString打印在了控制台;感觉这部分不是很好,后头借鉴一下谷粒学苑的吧
@Data
@ApiModel(value = "自定义全局异常类")
public class YyghException extends RuntimeException {
@ApiModelProperty(value = "异常状态码")
private Integer code;
/**
* 通过状态码和错误消息创建异常对象
* @param message
* @param code
*/
public YyghException(String message, Integer code) {
super(message);
this.code = code;
}
/**
* 接收枚举类型对象
* @param resultCodeEnum
*/
public YyghException(ResultCodeEnum resultCodeEnum) {
super(resultCodeEnum.getMessage());
this.code = resultCodeEnum.getCode();
}
@Override
public String toString() {
return "YyghException{" +
"code=" + code +
", message=" + this.getMessage() +
'}';
}
}
思考
对于枚举类状态码信息的操作;感觉在下面这种情况
if (resultMap == null) {//出错
return Result.fail().message(“支付出错”);
}
可以throw,new自定义异常类,返回错误信息(需要在enum中在新建一个对象)
也可以在返回结果类时重新set错误信息;
5.统一日志处理(了解)
spring boot内部使用Logback作为日志实现的框架。
配置日志级别
日志记录器(Logger)的行为是分等级的。如下表所示:
分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL
默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别
# 设置日志级别
logging.level.root=WARN
这种方式只能将日志打印在控制台上
sout 会产生io,需要删掉
日志配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="D:/JavaStudy/graduationDesign/yygh_log" />
<!-- 彩色日志 -->
<!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件-->
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
如果未设置此属性,那么当前logger将会继承上级的级别。
-->
<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
-->
<!--开发环境:打印控制台-->
<springProfile name="dev">
<!--可以输出项目中的debug日志,包括mybatis的sql日志-->
<logger name="com.guli" level="INFO" />
<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
可以包含零个或多个appender元素。
-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>
<!--生产环境:输出到文件-->
<springProfile name="pro">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="WARN_FILE" />
</root>
</springProfile>
</configuration>
医院设置做什么:不同医院和预约挂号平台建立链接;每个医院有自己的编号。
医院设置主要是用来保存开通医院的一些基本信息,每个医院一条信息,保存了医院编号(平台分配,全局唯一)和接口调用相关的签名key等信息,是整个流程的第一步,只有开通了医院设置信息,才可以上传医院相关信息。
医院被锁定后便不能再上传信息。
医院设置接口
数据模型
提示:使用mp,在service层调用basemapper不用再注入,因为继承的ServiceImpl已经注入了
service_hosp模块
1.带条件带分页查询接口
添加分页插件
@Configuration
@MapperScan("com.atguigu.yygh.hosp.mapper")
public class HospConfig {
/**
* 分页插件
*
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}
**HospitalSetController层接口**
@Api(tags = "医院设置管理")
@RestController
@RequestMapping("/admin/hosp/hospitalSet")
public class HospitalSetController
vo对象
HospitalSetQueryVo
包含医院名称和编号
/**
* 条件查询带分页
* 注意:@PostMapping 才能获取@RequestBody传来的json数据
* @responseBody底层是使用Jackson来完成对象到json的转换
* @RequestBody(required = false) 代表这个数据可以不传
* @param current:当前页
* @param limit:每页显示几条数据
* @param hospitalSetQueryVo:查询条件(医院编号、医院名称(模糊查询))
* @return
*/
@ApiOperation(value = "条件查询带分页")
@PostMapping("findPageHospSet/{current}/{limit}")
public Result findPageHospSet(@PathVariable("current") long current,
@PathVariable("limit") long limit,
@RequestBody(required = false) HospitalSetQueryVo hospitalSetQueryVo
){
//创建page对象,传递当前页,每页记录数
Page<HospitalSet> page = new Page<>(current,limit);
QueryWrapper<HospitalSet> wrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(hospitalSetQueryVo)) {
String hosname = hospitalSetQueryVo.getHosname();
String hoscode = hospitalSetQueryVo.getHoscode();
//构造条件
wrapper.like(!StringUtils.isEmpty(hosname),"hosname",hosname)
.eq(!StringUtils.isEmpty(hoscode),"hoscode", hoscode);
//调用方法实现分页的查询
Page<HospitalSet> hospitalSetPage = hospitalSetService.page(page, wrapper);
//返回结果
return Result.ok(hospitalSetPage);
}
page对象常用属性;(mybatis-plus封装的)
2.新增接口
添加MD5工具类
controller层接口
@ApiOperation(value = "添加医院设置")
@PostMapping("/saveHospitalSet")
public Result saveHospitalSet(@RequestBody HospitalSet hospitalSet) {
// 设置状态 1使用 0不能使用
hospitalSet.setStatus(1);
// 设置签名密钥
Random random = new Random();
hospitalSet.setSignKey(MD5.encrypt(System.currentTimeMillis() + "" + random.nextInt(1000)));
boolean save = hospitalSetService.save(hospitalSet);
return save?Result.ok():Result.fail();
}
数据库中设置了时间、状态的默认值,所以不用添加;但是还是可以做个mp的自动填充,修改时时间会自动改变等等。
3.修改接口
说明:此接口同时需要实现根据id查询,以实现修改时的数据回显。
/**
*根据id获取医院设置
* @param id 医院id
* @return 医院设置信息
*/
@GetMapping("getHospitalSetById/{id}")
public Result getHospitalSetById(@PathVariable("id") Long id){
HospitalSet hospitalSet = hospitalSetService.getById(id);
return Result.ok(hospitalSet);
}
/**
*修改医院设置
* @param hospitalSet 修改后医院设置信息
* @return 修改的结果
*/
@PostMapping("updateHospitalSet")
public Result updateHospitalSet(@RequestBody HospitalSet hospitalSet){
boolean flag = hospitalSetService.updateById(hospitalSet);
return flag?Result.ok():Result.fail();
}
4.批量删除接口
删除
/**
* 逻辑删除医院设置
* @return
*/
@ApiOperation(value = "逻辑删除医院设置")
@DeleteMapping("/{id}")
public Result removeHospSet(@PathVariable("id") long id) {
boolean flag = hospitalSetService.removeById(id);
if (flag) {
return Result.ok();
} else {
return Result.fail();
}
}
批量删除
/**
* 批量删除医院设置
* @param idList 要删除的id集合
* @return 返回批量删除的结果
*/
@DeleteMapping("batchRemove")
public Result batchRemoveHospitalSet(@RequestBody List<Long> idList){
boolean flag = hospitalSetService.removeByIds(idList);
return flag?Result.ok():Result.fail();
}
弹幕说集合没有实现序列化,分布式环境会接受不了数据?
5.锁定医院设置
/**
* 医院设置锁定和解锁的
* @param id 医院id
* @param status 该医院的状态
* @return
*/
@PutMapping("lockHospitalSet/{id}/{status}")
public Result lockHospitalSet(@PathVariable Long id,
@PathVariable Integer status){
//先根据id查询医院设置信息
HospitalSet hospitalSet = hospitalSetService.getById(id);
//设置状态
hospitalSet.setStatus(status);
boolean flag = hospitalSetService.updateById(hospitalSet);
return flag?Result.ok():Result.fail();
}
6.发送签名密钥
烂尾了。
@ApiOperation(value = "发送签名秘钥")
@PutMapping("/sendKey/{id}")
public Result sendKey(@PathVariable Long id) {
HospitalSet hospitalSet = hospitalSetService.getById(id);
String signKey = hospitalSet.getSignKey();
String hoscode = hospitalSet.getHoscode();
//TODO 发送短信
return Result.ok();
}
}
后台前端
vue-element-admin GitHub地址
vue-admin-template GitHub地址
项目在线预览:地址
elementui(基于vue2x) 地址
建议:你可以在 vue-admin-template 的基础上进行二次开发,把 vue-element-admin当做工具箱,想要什么功能或者组件就去 vue-element-admin 那里复制过来
//克隆工程 有关git可以看[这篇文章](http://t.csdn.cn/9qbIk)
git clone https://github.com/PanJiaChen/vue-admin-template.git
//下载相关依赖
npm install
//运行
npm run dev
如果下载以依赖出现node-sass类错,尝试先执行下面的代码
npm i -g node-sass --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
一.结构
vue-damin-temeplate
bulid:构建相关
config:全局配置
src:源代码
api:所有请求
assets:主题 字体等静态资源
components:全局公共组件
icons:项目所有svg icons
router:路由
store:全局store管理
styles:全局样式
utils:全局公用方法
views:视图
App.vue:入口页面
main.js:入口 加载组件 初始化等
permission.js:权限管理
static:静态资源
.babelrc:babel-loader配置
.eslintrc.js:eslint配置项
.gitignore:git忽略项
package.json:依赖管理
二.项目中的重要文件
package.js
npm项目的核心配置文件,包含项目信息,项目依赖,项目启动相关脚本
启动项目的命令: npm run dev
dev脚本:webpack-dev-server --inline --progress --config build/webpack.dev.conf.js
webpack-dev-server:一个小型的基于Node.js的http服务器,可以运行前端项目
–inline:一种启动模式
–progress:显示启动进度
–config build/webpack.dev.conf.js:指定webpack配置文件所在位置
** build/webpack.dev.conf.js**
webpack配置文件,包含项目在开发环境打包和运行的相关配置
webpack.dev.conf.js 中引用了 webpack.base.conf.js
webpack.base.conf.js 中定义了项目打包的入口文件
在HtmlWebpackPlugin配置html模板,生成的js就会自动插入到模板中,如下面的配置。
因此生成的js文件会被自动插入到名为index.html的页面中
** index.html**
项目默认的html页面
src/main.js
项目js入口文件,项目的所有前端功能都在这个文件中引入和定义,并初始化全局的Vue对象
config/dev.env.js
定义全局常量值
因此,在项目中的任意位置可以直接使用 process.env.BASE_API 常量表示后端接口的主机地址
src/utils/request.js
引入axios模块,定义全局的axios实例,并导出模块
src/api/login.js
引用request模块,调用远程api
注意
修改配置文件中的语法检查,如果不能接受一直报错;改为false
实现前面所写接口的前端页面
三.登录简易改造
前端
把登陆写死的,不用再请求。
actions: {
// 登录
Login({ commit }, userInfo) {
const data = {"token":"admin"}
setToken(data.token)
commit('SET_TOKEN', data.token)
// const username = userInfo.username.trim()
// return new Promise((resolve, reject) => {
// login(username, userInfo.password).then(response => {
// const data = response.data
// setToken(data.token)
// commit('SET_TOKEN', data.token)
// resolve()
// }).catch(error => {
// reject(error)
// })
// })
},
// 获取用户信息
GetInfo({ commit, state }) {
const data = {'roles':'admin','name':'admin','avatar':'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'}
if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', data.roles)
} else {
reject('getInfo: roles must be a non-null array !')
}
commit('SET_NAME', data.name)
commit('SET_AVATAR', data.avatar)
// return new Promise((resolve, reject) => {
// getInfo(state.token).then(response => {
// const data = response.data
// if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
// commit('SET_ROLES', data.roles)
// } else {
// reject('getInfo: roles must be a non-null array !')
// }
// commit('SET_NAME', data.name)
// commit('SET_AVATAR', data.avatar)
// resolve(response)
// }).catch(error => {
// reject(error)
// })
// })
},
// 登出
LogOut({ commit, state }) {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
// return new Promise((resolve, reject) => {
// logout(state.token).then(() => {
// commit('SET_TOKEN', '')
// commit('SET_ROLES', [])
// removeToken()
// resolve()
// }).catch(error => {
// reject(error)
// })
// })
},
// 前端 登出
FedLogOut({ commit }) {
// return new Promise(resolve => {
commit('SET_TOKEN', '')
removeToken()
resolve()
// })
}
}
前台开发步骤
修改请求路径
VUE_APP_BASE_API = 'http://localhost:8201/
修改api接口方法 请求路径
跨域问题解决方案
在controller层中添加注解@CrossOrigin
四.医院设置列表
1.添加路由,创建页面文件
{
path: '/yygh/hospset',
component: Layout,
redirect: '/yygh/hospset/list',
name: 'hosp',
meta: { title: '医院管理', icon: 'el-icon-s-help' },
children: [
{
path: 'list',
name: '医院设置列表',
component: () => import('@/views/yygh/hospset/list'),
meta: { title: '医院设置列表', icon: 'table' }
},
{
path: 'add',
name: '医院设置表单',
component: () => import('@/views/yygh/hospset/add'),
meta: { title: '添加医院设置', icon: 'tree' }
}
]
},
2.创建API
src\api\hosp.js
import request from '@/utils/request'
//提取请求路径
const api_name = '/admin/hosp/hospitalSet'
export default{
//分页条件查询医院设置
pageQuery(page,limit,searchObj){
return request({
url: `${api_name}/pageQuery/${page}/${limit}`,//插值表达式
method: 'post',
data:searchObj //使用json方式传递数据;如果写param,则前面不用加data
})
}
}
3.编写页面
添加分页元素
slot-scope代表整个表格;.代表每一行
prop属性
<template>
<div class="app-container">
医院设置列表
<el-table
v-loading="listLoading"
:data="list"
element-loading-text="数据加载中"
border
fit
highlight-current-row
>
<el-table-column label="序号" width="70" align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<!-- 字段名要与对象属性一致 -->
<el-table-column prop="hosname" label="医院名称" width="180" />
<el-table-column prop="hoscode" label="医院编号" width="160" />
<el-table-column prop="apiUrl" label="地址" width="200" />
<el-table-column prop="contactsName" label="联系人" />
<el-table-column prop="status" label="状态">
<template slot-scope="scope">
{{ scope.row.status === 1 ? "可用" : "不可用" }} <!-- === 比较类型也比较值 -->
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<router-link :to="'/yygh/hospset/edit/' + scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit"
>修改</el-button>
</router-link>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeDataById(scope.row.id)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
底部分页组件
<!-- 分页 -->
<el-pagination
:current-page="page"
:page-size="limit"
:total="total"
style="padding: 30px 0; text-align: center;"
layout="total, prev, pager, next, jumper"
//会自动传当前页,封装好的
@current-change="fetchData"
/>
分页查询表单
医院设置列表
<!--查询表单
inline一行显示-->
<el-form :inline="true" class="demo-form-inline">
<el-form-item>
<el-input v-model="searchObj.hosname" placeholder="医院名称" />
</el-form-item>
<el-form-item>
<el-input v-model="searchObj.hoscode" placeholder="医院编号" />
</el-form-item>
<el-button type="primary" icon="el-icon-search" @click="fetchData()"
>查询</el-button
>
<el-button type="default" @click="resetData()">清空</el-button>
</el-form>
页面逻辑js
//引入接口
imoort pageQuery from '@/api/hospset'
//------------------
data() {
return {
listLoading: true, //表格加载状态
list: [], //表格数据
page: 1, //当前页
limit: 10, //每页记录数
total:0,//总记录数
searchObj: {} //查询条件
};
},
created() {
this.fetchData();
},
methods: {
//分页条件查询 添加默认传参 es6新特性;
fetchData(page=1) {
this.page = page
//hospsetApi为引入的那个接口名,里面包含了那个文件里的所有接口方法
hospsetApi
.pageQuery(this.page, this.limit, this.searchObj)
.then(response => {
console.log(response);
this.list = response.data.pageModel.records;
this.total = response.data.pageModel.total
this.listLoading = false;
});
},
//清空
resetData() {
this.searchObj = {};
this.fetchData();
},
}
五.删除医院设置
1. 添加api接口
//医院设置删除
removeById(id){
return request({
url: `${api_name}/${id}`,//插值表达式
method: 'delete'
})
},
2.页面js
删除和修改按钮元素,在前面已经写了
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<router-link :to="'/yygh/hospset/edit/' + scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit"
>修改</el-button>
</router-link>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
<!--这里会传入要删除的id-->
@click="removeDataById(scope.row.id)"
>删除</el-button>
</template>
</el-table-column>
//删除
removeDataById(id) {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
//确定执行then的,取消执行catch的
}).then(() => {
//删除
hospsetApi.removeById(id).then((response) => {
//刷新页面;也可以用window.location.reload();
this.fetchData();//try传参current
});
this.$message({
type: "success",
message: "删除成功!",
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
六.新增医院设置
1. 实现接口API
//新增医院设置
save(hospitalSet){
return request({
url: `${api_name}/save`,//插值表达式
method: 'post',
data:hospitalSet
})
},
2.编写页面(add.vue)
新增与修改共用一个页面
表单元素
<template>
<div class="app-container">
医院设置表单
<el-form label-width="120px">
<el-form-item label="医院名称">
<el-input v-model="hospset.hosname"/>
</el-form-item>
<el-form-item label="医院编号">
<el-input v-model="hospset.hoscode"/>
</el-form-item>
<el-form-item label="api地址">
<el-input v-model="hospset.apiUrl"/>
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="hospset.contactsName"/>
</el-form-item>
<el-form-item label="电话">
<el-input v-model="hospset.contactsPhone"/>
</el-form-item>
<el-form-item>
<el-button :disabled="saveBtnDisabled" type="primary" @click="saveOrUpdate">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
页面js
页面跳转用 r o u t e r ,获取元素用 router,获取元素用 router,获取元素用route;注意区别
<script>
import hospsetApi from "@/api/yygh/hospset";
export default {
data() {
return {
hospset: {}, //表单对象
saveBtnDisabled: false //按钮是否不可操作
};
},
created() {},
methods: {
//保存
saveOrUpdate() {
//新增
this.saveHospset();
},
//新增
saveHospset() {
hospsetApi.save(this.hospset).then(response => {
this.$message({
type: "success",
message: "新增成功!"
});
//路由页面跳转
this.$router.push({path:'/yygh/hospset/list'})
});
}
}
};
</script>
七.医院设置修改
修改元素在前面已经写好
1.隐藏路由
什么是隐藏路由
{
//占位符设置什么名字,取值时就用什么
path: 'edit/:id',
name: 'HospSetEdit',
component: () => import('@/views/yygh/hospset/add'),
meta: { title: '编辑医院设置', noCache: 'tree' },
hidden: true
}
2.修改时的数据回显
1.添加api
//根据id进行查询
getById(id){
return request({
url: `${api_name}/getById/${id}`,//插值表达式
method: 'get'
})
},
2.页面js
<router-link :to="'/yygh/hospset/edit/' + scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit"
>修改</el-button>
</router-link>
created () {
//通过路由获取id(占位符),判断id数据是否存在(有参数且有id)
if(this.$route.params&&this.$route.params.id){
//注意,获取参数用route,不是router
//console.log(this.$route.params.id);
//数据回显
hospsetApi.getById(this.$route.params.id).then(response=>{
this.hospset = response.data.hospitalSet
})
}
},
3.实现修改保存
1.新增api
//修改医院设置
update(hospitalSet){
return request({
url: `${api_name}/update`,
method: 'post',
data: hospitalSet //用json
})
},
2.页面js
methods: {
//保存
saveOrUpdate(){
//新增与修改的区别:vu对象是否存在id
this.saveBtnDisabled = true
if (!this.hospset.id) {
//新增
this.saveHospset()
} else {
//编辑
this.updateHospset()
}
},
//修改
updateHospset(){
hospsetApi.update(this.hospset).then(response=>{
this.$message({
type: "success",
message: "修改成功!",
});
//页面跳转,用router
this.$router.push({path:'/yygh/hospset/list'})
});
},
思考:也可以不写两个方法(添加,修改);直接都传一个对象给后端,用mp的saveOrUpdate方法或者自己写一个。
八.医院设置批量删除
实现步骤
给表格添加复选框,可以拿到选择的对象
点击“批量删除”按钮,获取所有选择对象的id,存入集合
1.添加API
//批量删除医院设置
batchRemove(idList){
return request({
url: `${api_name}/batchRemove`,//插值表达式
method: 'delete',
data: idList
})
},
2.编写页面
只是添加了批量删除复选框
<!-- 工具条 -->
<div>
<el-button type="danger" size="mini" @click="removeRows()"
>批量删除</el-button>
</div>
<!-- alt + shift + f 格式化页面 -->
<!-- 复选框 批量修改 @selection-change="handleSelectionChange" -->
<el-table
v-loading="listLoading"
:data="list"
element-loading-text="数据加载中"
border
fit
highlight-current-row
@selection-change="handleSelectionChange">
<!--selection表示复选框-->
<el-table-column type="selection" width="55" />
3.页面js
复选框选择的method实现
data() {
return {
listLoading: true, //表格加载状态
list: [], //表格数据
page: 1, //当前页
limit: 5, //每页记录数
total: 0,
searchObj: {}, //查询条件
multipleSelection: [], // 批量选择中选择的记录列表(批量删除)
};
},
...
methods: {
....
//复选框选择方法;每次选择复选框会触发,传入对象(每行一个对象)
handleSelectionChange(selection) {
console.log(selection);
this.multipleSelection = selection;
},
批量删除method实现
记得判断一下如果没选择,提醒用户无法删除
//批量删除
removeRows() {
this.$confirm("此操作将永久删除该数据, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//1.创建参数对象idList
let idList = [];
//2.遍历multipleSelection对象,取出id
for (let i = 0; i < this.multipleSelection.length; i++) {
let hospset = this.multipleSelection[i];
let id = hospset.id;
//3.存入idList
idList.push(id);
}
console.log(idList);
//4.调接口进行批量删除
hospsetApi.batchRemove(idList).then((response) => {
this.fetchData();
this.$message({
type: "success",
message: "删除成功!",
});
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除",
});
});
},
九.锁定和取消锁定
1.新增api
//锁定和取消锁定
lockHospSet(id, status) {
return request({
url: `${api_name}/lockHospitalSet/${id}/${status}`,
method: 'put'
})
},
2.页面js
之前的页面元素(删除、保存、修改、锁定)
<el-table-column label="操作" width="280" align="center">
<template slot-scope="scope">
<el-button type="danger" size="mini"
icon="el-icon-delete" @click="removeDataById(scope.row.id)">删除</el-button>
<el-button v-if="scope.row.status==1" type="danger" size="mini"
icon="el-icon-delete" @click="lockHospSet(scope.row.id,0)">锁定</el-button>
<el-button v-if="scope.row.status==0" type="primary" size="mini"
icon="el-icon-delete" @click="lockHospSet(scope.row.id,1)">取消锁定</el-button>
<router-link :to="'/hospSet/edit/'+scope.row.id">
<el-button type="primary" size="mini" icon="el-icon-edit"></el-button>
</router-link>
</template>
</el-table-column>
//锁定和取消锁定
lockHospSet(id,status){
hospset.lockHospSet(id,status)
.then(response=>{
//刷新页面
this.getList()
})
},
数据字典
数据字典就是管理系统常用的分类数据或者一些固定数据,例如:省市区三级联动数据、民族数据、行业数据、学历数据等,由于该系统大量使用这种数据,所以我们要做一个数据管理方便管理系统数据,一般系统基本都会做数据管理。
数据字典就是系统中数据展示文字和编码的映射关系
一.表结构及分析
二.service_cmn模块搭建
三.数据字典列表
根据element组件要求,返回列表数据必须包含hasChildren字典;即是否包含子节点
在model模块查看Dict类
@ApiModelProperty(value = "是否包含子节点")
//表示在数据库中并不存在,不写下面的注解会报错的
@TableField(exist = false)
private boolean hasChildren;
后端实现
1.添加config配置类
@Configuration
//开启事务管理?
@EnableTransactionManagement
@MapperScan("com.atguigu.yygh.cmn.mapper")
public class CmnConfig {
}
2.实现接口
DictController
@Api(description = "数据字典接口")
@RestController
@RequestMapping("/admin/cmn/dict")
@CrossOrigin
public class DictController {
@Autowired
private DictService dictService;
//根据数据id查询子数据列表
@ApiOperation(value = "根据数据id查询子数据列表")
@GetMapping("findChildData/{id}")
public R findChildData(@PathVariable Long id){
List<Dict> list = dictService.findChildData(id);
return R.ok().data("list",list);
}
}
DictServiceImpl
查询:select * from dict where parent_id = id;
下面代码可以考虑用stream优化(for循环里有数据库操作)
@Service
public class DictServiceImpl
extends ServiceImpl<DictMapper, Dict>
implements DictService {
//根据数据id查询子数据列表
@Override
public List<Dict> findChildData(Long id) {
//1.根据父id查询子级别数据集合
QueryWrapper<Dict> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id",id);
List<Dict> dictList = baseMapper.selectList(wrapper);
//2.遍历查询是否有子数据
for (Dict dict : dictList) {
boolean hasChildren = this.isChildren(dict.getId());
dict.setHasChildren(hasChildren);
}
return dictList;
}
//查询是否有子数据,方法提取
private boolean isChildren(Long id) {
QueryWrapper<Dict> wrapper = new QueryWrapper<>();
wrapper.eq("parent_id",id);
Integer count = baseMapper.selectCount(wrapper);
return count > 0;
}
}
测试结果
前端实现
1.创建路由和页面文件
alwaysShow: true, //总是展示一级菜单
不加这个属性。当只有一个children,便只显示childen了
{
path: '/cmn',
component: Layout,
redirect: '/cmn/list',
name: '数据管理',
alwaysShow: true, //总是展示一级菜单
meta: { title: '数据管理', icon: 'example' },
children: [
{
path: 'list',
name: '数据字典',
component: () => import('@/views/yygh/dict/list'),
meta: { title: '数据字典', icon: 'table' }
}
]
},
2.创建API接口
api/dict.js
import request from '@/utils/request'
//提取请求路径
const api_name = '/admin/cmn/dict'
export default{
//根据数据id查询子数据列表
findChildData(id){
return request({
url: `${api_name}/findChildData/${id}`,//插值表达式
method: 'get'
})
},
}
3.添加页面元素
注意:若页面显示不对,可能是element-ui版本过低,修改版本后,删除node-moudle里的,再重新下载。
lazy属性为懒加载,只加载一级节点 ,通过 :load=“load” 方法加载子节点数据
<template>
<div class="app-container">
<!-- lazy 只加载一级节点 :load="load" -->
<el-table
:data="list"
style="width: 100%"
row-key="id"
border
lazy
<!--load方法,封装的,会递归查询。-->
:load="load"
<!--hasChildren用处在这-->
:tree-props="{children: 'children', hasChildren: 'hasChildren'}">
<el-table-column prop="name" label="名称" width="150">
</el-table-column>
<el-table-column prop="dictCode" label="编码" width="150">
</el-table-column>
<el-table-column prop="value" label="值" width="150">
</el-table-column>
<el-table-column prop="createTime" label="创建时间">
</el-table-column>
</el-table>
</div>
</template>
4.页面js
<script>
import dictApi from "@/api/yygh/dict"
export default{
data() {
return {
list: [] //字典数据集合
}
},
created() {
//全部分类id为1
this.getDate(1)
},
methods: {
//初始化查询方法
getDate(id){
dictApi.findChildData(id).then(response=>{
this.list = response.data.list
})
},
//树形节点加载方法,tree为节点对象,通过resolve()加载数据
load(tree, treeNode, resolve){
//传入结点id,继续查询子节点
dictApi.findChildData(tree.id).then(response=>{
//参考官方文档
resolve(response.data.list)
})
}
}
}
</script>
此时后端已经写了两个模块,对应不同端口,发请求的请求地址已经出现问题
解决方案:使用nginx作请求转发;统一设置请求路径为nginx的路径(VUE_APP_BASE_API = ‘http://localhost:9001’)
Nginx
1.nginx的下载、安装、启动
2.修改配置文件
所在位置为nginx/conf;一定要放在http块里
server {
#监听端口
listen 9001;
#nginx所在主机域名
server_name localhost;
location ~ /hosp/ {
#转发
proxy_pass http://localhost:8201;
}
location ~ /cmn/ {
#转发
proxy_pass http://localhost:8202;
}
}
四.EasyExcel操作
1.概述
Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内存。面对高访问高并发,一定会OOM或者JVM频繁的full gc (重jc)。
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行行读取数据,逐个解析。
github地址
2.测试demo
写操作
导入依赖
<!--EasyExcel-->
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
实体对象
@Data
public class Stu {
//设置表头名称
@ExcelProperty("学生编号")
private int sno;
//设置表头名称
@ExcelProperty("学生姓名")
private String sname;
}
测试类
public class WriteTest {
public static void main(String[] args) {
String fileName = "E:\\test.xlsx";
EasyExcel.write(fileName,Stu.class)//文件名/流,class类型
.sheet("学生信息")//设置sheet名
.doWrite(data());//传入要写的list集合data
}
//循环设置要添加的数据,最终封装到list集合中
private static List<Stu> data() {
List<Stu> list = new ArrayList<Stu>();
for (int i = 0; i < 10; i++) {
Stu data = new Stu();
data.setSno(i);
data.setSname("lucy"+i);
list.add(data);
}
return list;
}
}
读操作
改造实体类,指定对应关系
@Data
public class Stu {
//设置表头名称,指定映射关系(第0列);index即为索引
@ExcelProperty(value = "学生编号",index = 0)
private int sno;
//设置表头名称,指定映射关系(第1列)
@ExcelProperty(value = "学生姓名",index = 1)
private String sname;
}
创建监听器
public class ExcelListener extends AnalysisEventListener<Stu> {
//一行一行去读取excle内容,从第二行开始
@Override
public void invoke(Stu stu, AnalysisContext analysisContext) {
System.out.println("stu = " + stu);
}
//读取excel表头信息
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头信息:"+headMap);
}
//读取完成后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
测试
public class ReadTest {
public static void main(String[] args) {
String fileName = "E:\\test.xlsx";
//文件名,class类型,监听器
EasyExcel.read(fileName,Stu.class,new ExcelListener())
.sheet()
.doRead();
}
}
总结 :读操作需要配置监听器
五.数据字典导出、导入
1.导出
后端实现
添加依赖
在service_cmn里
<!--EasyExcel-->
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
返回前端显示对象DictEeVo
@Data
public class DictEeVo {
//对应excel表格的行开头信息
@ExcelProperty(value = "id" ,index = 0)
private Long id;
@ExcelProperty(value = "上级id" ,index = 1)
private Long parentId;
@ExcelProperty(value = "名称" ,index = 2)
private String name;
@ExcelProperty(value = "值" ,index = 3)
private String value;
@ExcelProperty(value = "编码" ,index = 4)
private String dictCode;
}
DictController
/**
* 导出数据字典接口
* @param response 响应输出流,将Excel返回;方便下载操作
* @return 导出结果
*/
@GetMapping("exportData")
public void exportDict(HttpServletResponse response){
dictService.exportDictData(response);
}
DictServiceImpl
//导出数据字典接口
@Override
public void exportDictData(HttpServletResponse response) {
//设置下载信息
response.setContentType("application/vnd.ms-excel");//文件类型
response.setCharacterEncoding("utf-8");//编码
//URLEncoder.encode(“哈哈”,“utf-8”);若是英文名,要这样指定编码格式
String fileName = "DataDict-"+ UUID.randomUUID();
//头信息意思:以下载方式打开
response.setHeader("Content-disposition", "attachment;filename="+ fileName + ".xlsx");
//查询数据库
List<Dict> dictList = baseMapper.selectList(null);
//Dict-->DictVo
List<DictEeVo> dictEeVoList = new ArrayList<>();
for (Dict dict : dictList) {
DictEeVo dictEeVo = new DictEeVo();
BeanUtils.copyProperties(dict,dictEeVo);
dictEeVoList.add(dictEeVo);
}
//调用方法进行写操作
try {
EasyExcel.write(response.getOutputStream(), DictEeVo.class).sheet("数据字典")
.doWrite(dictEeVoList);
} catch (IOException e) {
throw new YyghException("导出异常",500);
}
}
记得重温下javaweb写文件…
前端实现
添加页面元素
<div class="el-toolbar">
<div class="el-toolbar-body" style="justify-content: flex-start;">
<el-button type="text" @click="exportData"><i class="fa fa-plus"/> 导出</el-button>
</div>
</div>
页面js
因为不用返回数据,只是发请求,没必要再写在api接口那了。
//导出数据
exportData(){
//window.open("http://localhost:8202/admin/cmn/dict/exportData")
//照葫芦画瓢。从配置文件读取
window.open(`${process.env.VUE_APP_BASE_API}admin/cmn/dict/exportData`)
//window.location.href("http://localhost:8202/admin/cmn/dict/exportData")也可;还可以直接在页面写个a标签,直接在新的页面打开
}
2.导入
后端实现
controller层
@ApiOperation(value = "导入字典数据")
@PostMapping("importData")
public R importData(MultipartFile file){
dictService.importData(file);
return R.ok();
}
实现监听器
另一种注入dictMapper方式。
public class DictListener extends AnalysisEventListener<DictEeVo> {
private DictMapper dictMapper;
public DictListener(DictMapper dictMapper) {
this.dictMapper = dictMapper;
}
//一行一行读取
@Override
public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
//调用方法添加数据库
Dict dict = new Dict();
BeanUtils.copyProperties(dictEeVo,dict);
dictMapper.insert(dict);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}`
@Component //
public class DictListener extends AnalysisEventListener<DictEeVo> {
@Autowired
private DictMapper dictMapper;
@Override
public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
//转换类型
Dict dict = new Dict();
BeanUtils.copyProperties(dictEeVo,dict);
//设置默认值
dict.setIsDeleted(0);
dictMapper.insert(dict);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
service层
@Autowired
DictListener dictListener;
//导入字典数据
@Override
public void importData(MultipartFile file) {
try {
InputStream inputStream = file.getInputStream();
EasyExcel.read(inputStream,DictEeVo.class,dictListener)
.sheet()
.doRead();
} catch (IOException e) {
e.printStackTrace();
throw new YyghException(20001,"导入数据失败");
}
}
实体类修改
@Data
@ApiModel(description = "数据字典")
@TableName("dict")
public class Dict {
@ApiModelProperty(value = "id")
private Long id;
@ApiModelProperty(value = "上级id")
@TableField("parent_id")
private Long parentId;
@ApiModelProperty(value = "名称")
@TableField("name")
private String name;
@ApiModelProperty(value = "值")
@TableField("value")
private String value;
@ApiModelProperty(value = "编码")
@TableField("dict_code")
private String dictCode;
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField("create_time")
private Date createTime;
@ApiModelProperty(value = "更新时间")
@TableField("update_time")
private Date updateTime;
@ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")
@TableLogic
@TableField("is_deleted")
private Integer isDeleted;
@ApiModelProperty(value = "是否包含子节点")
@TableField(exist = false)
private boolean hasChildren;
@ApiModelProperty(value = "其他参数")
@TableField(exist = false)
private Map<String,Object> param = new HashMap<>();
}
给的建表sql文件is_deleted默认值为1;记得修改为0
思考,向数据库导入excel中的数据,如果不是覆盖,会越加越多,如何解决?若是可以覆盖,又会被随便修改数据。。
前端实现
添加页面元素
<div class="el-toolbar">
<div class="el-toolbar-body" style="justify-content: flex-start;">
<el-button type="text" @click="exportData"><i class="fa fa-plus" />
导出</el-button>
<el-button type="text" @click="importData"><i class="fa fa-plus" />
导入</el-button>
</div>
</div>
由于无法使用ajax传递文件,所以使用element的组件
上传组件及对话框dialog
<el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
<el-form label-position="right" label-width="170px">
<el-form-item label="文件">
<el-upload
<!--多文件支持-->
:multiple="false"
<!--上传成功调用方法-->
:on-success="onUploadSuccess"
:action="BASE_API" class="upload-demo">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传xls文件,且不超过500kb</div>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogImportVisible = false">取消</el-button>
</div>
</el-dialog>
页面js
data() {
return {
list: [], //字典数据集合
dialogImportVisible: false, //对话框,默认不显示;true时,弹出
BASE_API: `${process.env.VUE_APP_BASE_API}admin/cmn/dict/importData` //请求地址
}
},
created() {
this.getDate(1)
},
methods: {
....
//打开导入对话框
importData(){
this.dialogImportVisible = true
},
//上传成功后的操作
onUploadSuccess(){
//1.关闭对话框
this.dialogImportVisible = false
//2.上传成功提示
this.$message({
type: "success",
message: "上传成功!"
})
//3.刷新表格
this.getDate(1)
}
}
六.数据字典添加缓存
添加缓存时机:查询量大,且不经常改变,可以添加redis缓存,提高查询效率。
redis详细文章可以看这个
Spring Cache 是一个非常优秀的缓存组件。自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象,方便切换各种底层Cache(如:redis)
使用Spring Cache的好处:
1,提供基本的Cache抽象,方便切换各种底层Cache;
2,通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;
3,提供事务回滚时也自动回滚缓存;
4,支持比较复杂的缓存逻辑;
因为缓存也是公共使用,所有的service模块都有可能使用缓存,所以我们把依赖与部分配置加在service-util模块,这样其他service模块都可以使用了
1.整合redis
添加依赖在common_utils模块
<dependencies>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
添加redis配置类
使用redisTemplate,会出现序列化转换 观察不便的问题;其实也可以用StringRedisTemplate
@Configuration
@EnableCaching //开启缓存
public class RedisConfig {
/**
* 自定义key规则
* @return
*/
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* 设置RedisTemplate规则
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//序列号key value
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 设置CacheManager缓存规则
* @param factory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
说明:
@EnableCaching:标记注解 @EnableCaching,开启缓存,并配置Redis缓存管理器。@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。
配置文件
#redis
spring.redis.host=
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
2.同步缓存
写操作时,清空相关缓存。再次查询时同步
常用缓存标签
缓存@Cacheable
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
属性值如下:
value: 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames: 与 value 差不多,二选一即可
key: 可选属性,可以使用 SpEL 标签自定义缓存的key
缓存@CachePut 使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
属性值同上
缓存@CacheEvict 使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
allEntries 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
beforeInvocation 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存
添加缓存
/**
* 根据数据id查询子数据列表
* @param id
* @return
* keyGenerator 生成key的规则,在redisConfig写好过的
*/
@Cacheable(value = "dict", keyGenerator = "keyGenerator")
@Override
public List<Dict> findChildData(Long id)
/**
* 数据字典导入
* @param file
*/
@CacheEvict(value = "dict", allEntries = true)
@Override
public void importData(MultipartFile file)
mongodb
详细见这篇文章吧 链接
医院模拟系统接口开发
一.部署医院模拟系统
主要完成医院、科室、排版接口
父工程下创建 hospital-manage 模块
从资料里复制过来
二.上传(保存)医院信息接口
添加工具类
MD5
public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f' };
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
}
HttpRequestHelper
用于发送http请求给hosp模块
@Slf4j
public class HttpRequestHelper {
public static void main(String[] args) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("d", "4");
paramMap.put("b", "2");
paramMap.put("c", "3");
paramMap.put("a", "1");
paramMap.put("timestamp", getTimestamp());
log.info(getSign(paramMap, "111111111"));
}
/**
*
* @param paramMap
* @return
*/
public static Map<String, Object> switchMap(Map<String, String[]> paramMap) {
Map<String, Object> resultMap = new HashMap<>();
for (Map.Entry<String, String[]> param : paramMap.entrySet()) {
resultMap.put(param.getKey(), param.getValue()[0]);
}
return resultMap;
}
/**
* 请求数据获取签名
* @param paramMap
* @param signKey
* @return
*/
public static String getSign(Map<String, Object> paramMap, String signKey) {
if(paramMap.containsKey("sign")) {
paramMap.remove("sign");
}
String md5Str = MD5.encrypt(signKey);
// TreeMap<String, Object> sorted = new TreeMap<>(paramMap);
// StringBuilder str = new StringBuilder();
// for (Map.Entry<String, Object> param : sorted.entrySet()) {
// str.append(param.getValue()).append("|");
// }
// str.append(signKey);
// log.info("加密前:" + str.toString());
// String md5Str = MD5.encrypt(str.toString());
// log.info("加密后:" + md5Str);
return md5Str;
}
/**
* 签名校验
* @param paramMap
* @param signKey
* @return
*/
public static boolean isSignEquals(Map<String, Object> paramMap, String signKey) {
String sign = (String)paramMap.get("sign");
String md5Str = getSign(paramMap, signKey);
if(!sign.equals(md5Str)) {
return false;
}
return true;
}
/**
* 获取时间戳
* @return
*/
public static long getTimestamp() {
return new Date().getTime();
}
/**
* 封装同步请求
* @param paramMap
* @param url
* @return
*/
public static JSONObject sendRequest(Map<String, Object> paramMap, String url){
String result = "";
try {
//封装post参数
StringBuilder postdata = new StringBuilder();
for (Map.Entry<String, Object> param : paramMap.entrySet()) {
postdata.append(param.getKey()).append("=")
.append(param.getValue()).append("&");
}
log.info(String.format("--> 发送请求:post data %1s", postdata));
byte[] reqData = postdata.toString().getBytes("utf-8");
byte[] respdata = HttpUtil.doPost(url,reqData);
result = new String(respdata);
log.info(String.format("--> 应答结果:result data %1s", result));
} catch (Exception ex) {
ex.printStackTrace();
}
return JSONObject.parseObject(result);
}
}
mogdodbp配置
#mongo 配置
spring.data.mongodb.uri=mongodb://yourhost:27017/test
创建接口 repository
//实体类,主键类型
@Repository
public interface HospitalRepository extends MongoRepository<Hospital,String> {
}
ApiController
//工具自带跨域解决
@Api(tags = "医院管理API接口")
@RestController
@RequestMapping("/api/hosp") //请求地址参考文档;要与医院模拟系统那个一致
public class ApiController {
@Autowired
private HospitalService hospitalService;
}
上传医院功能实现
controller层
@ApiOperation(value = "上传医院")
@PostMapping("saveHospital")
public Result saveHospital(HttpServletRequest request){
//1.获取参数,转换类型
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, Object> paramMap = HttpRequestHelper.switchMap(parameterMap);
//2. TODO 校验签名
//3. 调用接口,数据入库
hospitalService.saveHospital(paramMap);
return Result.ok();
}
serviceImpl层
//上传医院
@Override
public void saveHospital(Map<String, Object> parameterMap) {
//1.转化参数类型paramMap => Hospital
//String paramJsonString = JSONObject.toJSONString(parameterMap);
//Hospital hospital = JSONObject.parseObject(paramJsonString, Hospital.class);
String paramJsonString = JSONObject.toJSONString(parameterMap);
Hospital hospital = JSONObject.parseObject(paramJsonString,Hospital.class);
//2.根据hoscode查询医院信息
Hospital targetHospital = hospitalRepository.getByHoscode(hospital.getHoscode());
//3.存在医院信息进行更新
if(targetHospital!=null){
hospital.setId(targetHospital.getId());
hospital.setCreateTime(targetHospital.getCreateTime());
hospital.setUpdateTime(new Date());
hospital.setStatus(targetHospital.getStatus());
hospital.setIsDeleted(0);
hospitalRepository.save(hospital);
}else {
//4.没有医院信息新增
hospital.setCreateTime(new Date());
hospital.setUpdateTime(new Date());
hospital.setStatus(0);
hospital.setIsDeleted(0);
hospitalRepository.save(hospital);
}
}
签名校验
确认签名 (尚医通、医院方),要求签名一致
修改医院系统签名加密方式为MD5
实现医院签名接口方法
三.查询医院
四.科室接口–上传科室
五.查询科室
六.删除科室
七.排班的上传和查询接口
八.排班的分页查询和删除
nextday
一.集成nacos
二.医院列表
三.字典查询
四.医院上线状态
后端接口
前端实现
五.医院详情查询
后端接口
前端实现
nextday
一.Gateway集成
(1)科室信息(大科室与小科室树形展示)
(2)根据医院、科室按排班日期统计号源信息、按日期分页、根据日期判断周几
(3)根据医院、科室、排班日期查询排班列表数据