尚医通项目实现01

1. service-hosp 医院模块开发

1.1 需求

  • 医院设置主要是用来保存开通医院的一些基本信息,每个医院一条信息,保存了医院编号(平台分配,全局唯一)和接口调用相关的签名key等信息,是整个流程的第一步,只有开通了医院设置信息,才可以上传医院相关信息。我们所开发的功能就是基于单表的一个CRUD、锁定/解锁和发送签名信息这些基本功能。

  • 表结构

	CREATE TABLE `hospital_set` (
	  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
	  `hosname` varchar(100) DEFAULT NULL COMMENT '医院名称',
	  `hoscode` varchar(30) DEFAULT NULL COMMENT '医院编号',
	  `api_url` varchar(100) DEFAULT NULL COMMENT 'api基础路径',
	  `sign_key` varchar(50) DEFAULT NULL COMMENT '签名秘钥',
	  `contacts_name` varchar(20) DEFAULT NULL COMMENT '联系人',
	  `contacts_phone` varchar(11) DEFAULT NULL COMMENT '联系人手机',
	  `status` tinyint(3) NOT NULL DEFAULT '0' COMMENT '状态',
	  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
	  `is_deleted` tinyint(3) NOT NULL DEFAULT '0' COMMENT '逻辑删除(1:已删除,0:未删除)',
	  PRIMARY KEY (`id`),
	  UNIQUE KEY `uk_hoscode` (`hoscode`)
	) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='医院设置表';
  • 后端实体类结构
@Data
public class BaseEntity implements Serializable {

    @ApiModelProperty(value = "id")
    @TableId(type = IdType.AUTO)        // 该注解用于将某个成员变量指定为数据表主键
    private Long id;

    @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             // @TableLogic 注解用于实现数据库数据逻辑删除
    @TableField("is_deleted")
    private Integer isDeleted;

    @ApiModelProperty(value = "其他参数")
    @TableField(exist = false)
    private Map<String,Object> param = new HashMap<>();
}

1.2 代码实现

1.2.1 使用mybatis plus 实现mapper接口

@Repository
@Mapper
public interface HospitalSetMapper extends BaseMapper<HospitalSet> {
    // 使用mybatis plus 需继承BaseMapper,
}
  • 项目可能涉及到复杂的数据库操作,如联表查询等,需要用自定义SQL语句操作
    • 在 Java 包下创建 xxxMapper.java 接口类,然后再 resources 资源包下创建对应的 xxxMapper.xml 文件;
    • 创建好 .java 和 .xml 文件后,在 Java 文件编写接口方法,然后再 xml 文件中编写对应方法的 SQL 语句;
    • 当调用接口中方法后,Mybatis 就会去 xml 文件中找到对应的 SQL。

1.2.2 Service接口

public interface HospitalSetService extends IService<HospitalSet> {
}

1.2.3 ServiceImpl

@Service
public class HospitalSetServiceImpl extends ServiceImpl<HospitalSetMapper, HospitalSet> implements HospitalSetService {
    @Autowired
    private HospitalSetMapper hospitalSetMapper;


}

1.2.4 Controller

@Api(tags = "医院设置管理")
@RestController         // 相当于@Controller 和 @ResponseBody两个注解合并,
@RequestMapping("/admin/hosp/hospitalSet")
public class HospitalSetController {
    // 注入service

    @Autowired
    private HospitalSetService hospitalSetService;

 // http://localhost:8201/admin/hosp/hospitalSet/
    // 3. 条件查询带分页
    @ApiOperation(value = "条件查询带分页")
    @PostMapping("findPageHospSet/{current}/{limit}")
    public Result findPageHospSet (@PathVariable long current,
                                   @PathVariable long limit,
                                   @RequestBody(required = false)HospitalSetQueryVo hospitalSetQueryVo){    // 此处使用VO 对象 代替 DTO 接收前端的查询值
        // 创建page对象,传递当前页,每页记录数
        Page<HospitalSet> page = new Page<>(current,limit);
        // 构建条件
        // QueryWrapper 是mybatis plus 实现查询的对象封装操作类
        QueryWrapper<HospitalSet> wrapper = new QueryWrapper<>();
        String hosname = hospitalSetQueryVo.getHosname();
        String hoscode = hospitalSetQueryVo.getHoscode();
        if(!StringUtils.isEmpty(hosname)){
            wrapper.like("hosname",hospitalSetQueryVo.getHosname());   // 此处第一个参数 column 需要为数据库中的真实字段
        }
        if(!StringUtils.isEmpty(hoscode)){
            wrapper.eq("hoscode",hospitalSetQueryVo.getHoscode());
        }
        // 调用方法实现分页查询
        IPage<HospitalSet> pageHospitalSet = hospitalSetService.page(page,wrapper);

        return Result.ok(pageHospitalSet);
    }

条件分页功能实现,需要在config中加载分页插件

@Configuration
@MapperScan("com.wxz.hospital.hosp.mapper")
public class HospConfig {
    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

2. 管理平台前端搭建

  • git 上下载 vue-admin-template-master,
npm install --global windows-build-tools --save
npm install node-sass@4.12.0 --save 
npm rebuild node-sass						# 一般不需这一步
npm run dev
  • ./build/dev.env.js
module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
 // BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
  BASE_API: '"http://localhost:8201"',	// 此处设置连接的后端项目地址
})

image-20220814160010402

image-20220814160059985

2.1 医院管理功能

2.1.1 添加路由

  • src/router/index.js 添加路由
export const constantRouterMap = [
{
	path: '/hospSet',
	component: Layout,
	redirect: '/hospSet/hospital/list',
	name: 'hospital',
	meta: { title: '医院管理', icon: 'example' },
	children: [
	    {
	path: 'hospitalSet/list',
	name: '医院设置查看',
	component: () =>import('@/views/hosp/hospitalSet/list'),  // 设置要跳转的路径
	meta: { title: '查看',icon: 'table' }
	    },
	    {
	path: 'hospitalSet/add',
	name: '医院设置添加',
	component: () =>import('@/views/hosp/hospitalSet/form'),
	meta: { title: '添加',icon: 'table' }
	},
	 {
	path: 'hospitalSet/edit/:id',
	name: '医院设置修改',
	component: () =>import('@/views/hosp/hospitalSet/form'),
	meta: { title: '编辑',noCache: true },
	hidden: true
	},
	 {
	  path: 'hosp/list',
	  name: '医院列表',
	  component: () =>import('@/views/hosp/hospital/list'),
	  meta: { title: '医院列表', icon: 'table' }
	 },
	 {
	  path: 'hospital/show/:id',
	  name: '查看',
	  component: () => import('@/views/hosp/hospital/show'),
	  meta: { title: '查看', noCache: true },
	  hidden: true
	},
	{
	  path: 'hospital/schedule/:hoscode',
	  name: '排班',
	  component: () => import('@/views/hosp/hospital/schedule'),
	  meta: { title: '排班', noCache: true },
	  hidden: true
	}
	  ]
  }
]
export default new Router({
  // mode: 'history', //后端支持可开
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRouterMap
})

2.1.2 定义接口路径

  • src/api/hosp/hospitalSet.js 中定义接口路径
import request from '@/utils/request'  //可以理解为导入的公共包

const api_name = '/admin/hosp/hospitalSet' //从java对应的controller复制过来!

export default {
  
getPageList(current, limit, searchObj) {
return request({
url: `${api_name}/findPageHospSet/${current}/${limit}`,//这里前边还会拼上dev.env.js文件中的BASE_API
method: 'post',
data: searchObj   //使用json传递参数 用data 其他用params
    })
  },
  
 deleteHospSet(id) {
  return request ({
    url: `${api_name}/${id}`,
    method: 'delete'
  })
},

removeRows(idList) {
return request({
url: `${api_name}/batchRemove`,
method: 'delete',
data: idList
  })
},

//锁定和取消锁定
lockHospSet(id,status) {
  return request ({
    url: `${api_name}/lockHospitalSet/${id}/${status}`,
    method: 'put'
  })
},


 //添加医院设置
  saveHospSet(hospitalSet) {
    return request ({
      url: `${api_name}/saveHospitalSet`,
      method: 'post',
      data: hospitalSet
    })
 },
  //医院院设置  根据id查询
   getHospSet(id) {
    return request ({
      url: `${api_name}/getHospSet/${id}`,
      method: 'get'
    })
  },
  
   //修改医院设置
  updateHospSet(hospitalSet) {
    return request ({
      url: `${api_name}/updateHospitalSet`,
      method: 'post',
      data: hospitalSet
    })
  }
}

2.1.3 定义页面组件脚本

  • src/views/hosp/hospitalSet/list.vue
<el-button type="primary" icon="el-icon-search" @click="getList()">查询</el-button>
<el-button type="danger" size="mini" @click="removeRows()">批量删除</el-button>
	  <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="primary" size="mini" 
         icon="el-icon-delete" @click="lockHostSet(scope.row.id,0)">锁定</el-button>
      <el-button v-if="scope.row.status==0" type="danger" size="mini" 
         icon="el-icon-delete" @click="lockHostSet(scope.row.id,1)">取消锁定</el-button>
	   <router-link :to="'/hospSet/hospitalSet/edit/'+scope.row.id">
	   <el-button type="primary" size="mini" icon="el-icon-edit">编辑</el-button>
	   </router-link>
	   
<script>
import hospitalSetApi from '@/api/hosp/hospitalSet' //即前面定义的那个api下的js

export default {  
/*
下面的代码是有结构的 
data(){ return{};},
created(){},
methods:{}
*/
// 定义变量和初始值
data() {
  return {     //(page, limit, searchObj)参考这里传递的参数进行定义变量

  current:1,  //当前页
  limit:3,  //每页显示记录数
  searchObj:{ hosname:'',hoscode:''},  //条件封装对象
  list:[], //这个是用来接受返回的列表的
  total:0,
  multipleSelection:[]
  }
  },

// 页面渲染之前执行 : 一般用来调用methods定义的方法,得到数据
  created() {  //只能调用当前vue的方法,不能直接调用到  hospitalSetApi.getPageList的
  this.getList()
  },

methods: {   //定义方法,进行请求接口调用
     getList(page=1){ //不传默认第1页   //前面已经 import hospitalSetApi from '@/api/hosp/hospitalSet'
       
       this.current = page
       
       //使用hospitalSetApi调用方法即可  ,  这个方法是暴露出来的
       hospitalSetApi.getPageList(this.current,this.limit,this.searchObj)
       .then(response=>{
          this.list = response.data.records
          this.total = response.data.total
       })
       .catch(error=>{
          console.log(error)
       })
     },
     //删除医院设置的方法
     removeDataById(id){
     	this.$confirm('此操作将永久删除医院是设置信息,是否继续?','提示',{
     	   confirmButtonText:'确实',
     	   cancelButtonText:'取消',
     	   type:'warning'
     	}).then(()=>{  //确定执行then方法
     		 //调用删除接口
     		hospitalSetApi.deleteHospSet(id)
     		.then(response=>{
     			this.$message({
     				type:'success',
     				message:'删除成功!'
     			})
     			  //刷新页面
     			this.getList(1)
     		})
     	})
     },
     
     handleSelectionChange(selection) {
     this.multipleSelection = selection
     },
     
     //批量删除 
   removeRows(){
   this.$confirm('此操作将永久删除医院是设置信息, 是否继续?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
   }).then(() => { //确定执行then方法
      var idList = []
      //遍历数组得到每个id值,设置到idList里面
      for(var i=0;i<this.multipleSelection.length;i++) {
         var obj = this.multipleSelection[i]
         var id = obj.id
         idList.push(id)
      }
      //调用接口
      hospitalSetApi.removeRows(idList)
         .then(response => {
            //提示
            this.$message({
               type: 'success',
               message: '删除成功!'
            })
            //刷新页面
            this.getList(1)
         })
      })
    },
    
   lockHostSet(id,status) {
   hospitalSetApi.lockHospSet(id,status)
      .then(response => {
         //刷新
         this.getList(this.current)
      })
   }     
  }
}
</script>
  • 此时点击医院列表http://localhost:9528/#/hospSet/hosp/list, 请求地址为http://localhost:8201/admin/hosp/hospital/list/1/10 , 地址正确,但无法得到正确值, 此时就遇到了跨域的问题

2.1.4 跨域处理

跨域:浏览器对于javascript的同源策略的限制 。

以下情况都属于跨域:

跨域原因说明 实例
域名不同 www.jd.com 与 www.taobao.com
域名相同,端口不同 www.jd.com:8080 与 www.jd.com:8081
二级域名不同 item.jd.com 与 miaosha.jd.com

如果域名和端口都相同,但是请求路径不同,不属于跨域,而我们刚才是从localhost:9528去访问localhost:8201,这属于端口不同,跨域了。

解决跨域: 在controller 上添加注解 @CrossOrigin

2.2 数据管理功能

何为数据字典?数据字典就是管理系统常用的分类数据或者一些固定数据,例如:省市区三级联动数据、民族数据、行业数据、学历数据等,由于该系统大量使用这种数据,所以我们要做一个数据管理方便管理系统数据,一般系统基本都会做数据管理。

2.2.1 数据字典表结构

CREATE TABLE `dict` (
  `id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'id',
  `parent_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '上级id',
  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '名称',
  `value` bigint(20) DEFAULT NULL COMMENT '值',
  `dict_code` varchar(20) DEFAULT NULL COMMENT '编码',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_deleted` tinyint(3) NOT NULL DEFAULT '1' COMMENT '删除标记(0:不可用 1:可用)',
  PRIMARY KEY (`id`),
  KEY `idx_dict_code` (`dict_code`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='组织架构表';

parent_id:上级id,通过id与parent_id构建上下级关系,例如:我们要获取所有行业数据,那么只需要查询parent_id=20000的数据

name:名称,例如:填写用户信息,我们要select标签选择民族,“汉族”就是数据字典的名称

value:值,例如:填写用户信息,我们要select标签选择民族,“1”(汉族的标识)就是数据字典的值

dict_code:编码,编码是我们自定义的,全局唯一,例如:我们要获取行业数据,我们可以通过parent_id获取,但是parent_id是不确定的,所以我们可以根据编码来获取行业数据

2.2.2 实体类Dict结构

@Data
@ApiModel(description = "数据字典")
@TableName("dict")
public class Dict {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    private Long id;

    @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 Map<String,Object> param = new HashMap<>();

    @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 = "是否包含子节点")
    @TableField(exist = false)	// mysql 表中没有此字段 为了树形目录显示创建
    private boolean hasChildren;
}
  • Spring Cache 缓存注解

@Cacheable: 根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存( value 缓存名) 中。一般用在查询方法上。

@CacheEvict

使用该注解标志的方法,会清空指定( value 缓存名) 的缓存。一般用在更新或者删除方法上

2.2.2 service-cmn 模块

后端搭建service-cmn模块,用于实现数据字典功能.

  1. 结合mybatis plus 添加service-cmn模块的mapper, service, serviceImpl, controller
  • 实现数据字典列表功能

  • 实现数据字典导入导出功能

  • 数据字典不需要什么改动, 将其添加入缓存中, 可大幅提升访问速度

数据字典中的数据一般不需要进行改动, 选择从本地的excel 表格中导入导出, 作为保存方法. 操作excel表格选择EasyExcel插件

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
</dependency>
  • controller
@Api(tags = "数据字典接口")
@RestController
@RequestMapping("/admin/cmn/dict")
@CrossOrigin
public class DictController {
    @Autowired
    private DictService dictService;

    @ApiOperation(value = "根据id获取查询子数据列表")
    @GetMapping("findChildData/{id}")
    public Result findChildData(@PathVariable Long id){
        List<Dict> childData = dictService.findChildData(id);

        return Result.ok(childData);
    }

    @ApiOperation(value = "导入数据字典")
    @PostMapping("importData")
    public Result importData(MultipartFile file){
        dictService.importDictData(file);
        return Result.ok();
    }

    @ApiOperation(value = "导出数据字典")
    @GetMapping("exportData")
    public void exportData(HttpServletResponse response){
        dictService.exportDictData(response);
    }
}
  • serviceImpl
// 2. 导出数据字典接口
    @Override
    public void exportDictData(HttpServletResponse response) {
        // 设置下载信息
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        String fileName = "dict";
        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("dict").doWrite(dictEeVoList);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    // 3. 导入数据字典
    @Override
    public void importDictData(MultipartFile file) {
        try {
            EasyExcel.read(file.getInputStream(),DictEeVo.class, new DictListener(baseMapper)).sheet().doRead();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.2.3 前端模块

  • 数据字典列表 (树形显示层级关系)

load绑定点击箭头后的函数 tree-pros 是否显示箭头 在目录树中点击当前节点, 调用 getChildren函数, 获取其子节点并显示

<el-table :data="list" style="width: 100%" row-key="id" border lazy :load="getChildren"
            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
  • 数据导入导出

点击导出, 浏览器会在一个新打开未命名的窗口载入目标文档 target="_blank"

<a href="http://localhost:8202/admin/cmn/dict/exportData" target="_blank">
     <el-button type="text"><i class="fa fa-plus" /> 导出</el-button>
</a>

点击导入, 执行点击事件importData, 该事件函数将dialogImportVisible 置为true, 出现文件选择对话框, 点击确定后, 发送给 http://localhost:8202/admin/cmn/dict/importData

<el-button type="text" @click="importData"><i class="fa fa-plus" /> 导入</el-button>
<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="'http://localhost:8202/admin/cmn/dict/importData'" 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>
<script>
    export default {
    data() {
        return {
            list: [], //数据字典列表数组
            dialogImportVisible: false //设置弹框是否弹出
        }
    },
    created() {
        this.getDictList(1)     // 1 是sql中的全部分裂id
    },
    methods: {
        importData() {
            this.dialogImportVisible = true
        },
        //上传成功调用方法,
        onUploadSuccess(response, file) {
            this.$message.info('上传成功')
            //关闭弹框
            this.dialogImportVisible = false
            //刷新页面
            this.getDictList(1)
        }
    }
</script>

2.2.4 引入nginx

目前我们共有两个项目, 地址分别为 http://localhost:8201http://localhost:8202, 在前端难以实现通过同一个BASE_API 来访问两个域, 此时通过nginx 作为 反向代理服务器来实现统一api接口

反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址

  • nginx.conf 中添加以下配置
server {
        listen       9001;
        server_name  localhost;

	location ~ /hosp/ {           
	    proxy_pass http://localhost:8201;
	}
	location ~ /cmn/ {           
	    proxy_pass http://localhost:8202;
	}
}

posted @ 2022-08-15 22:30  Firewooood  阅读(570)  评论(0)    收藏  举报