尚医通项目实现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"', // 此处设置连接的后端项目地址
})
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模块,用于实现数据字典功能.
- 结合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:8201 和 http://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;
}
}
-
调整 前端 /config/dev.env.js中的BASE_API
BASE_API: 'http://localhost:9001'
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异