软件架构
健康管理平台的总目标是:在计算机网络,数据库和先进的开发平台上,利用现有的软件,配置一定的硬件,开发一个具有开放体系结构的、易扩充的、易维护的、具有良好人机交互界面的大学生健康管理平台。
具体的要求如下:
- 大学生可以方便的使用五大基本功能
- 独立于具体的数据库平台。对于与数据库交互的部分充分考虑兼容性
- 容易在原先基础上进行二次开发
- 中间基础平台的无型性。尽量采用开放的标准技术,达到跨平台运行的效果
为了达到以上的开发目的,健康管理平台应遵循一定的设计和开发与原则:
- 模块接口定义清晰
- 基础类库和接口设计合理,尽量建立叮扩展的接口和抽象类
- 数据库操作使用标准SQL语句,如果一定要使用有差别的SQL语句,则尽量集中在一起,以减少将来维护和移植的难度
- 采用开放的标准和工具等
2.2 系统的功能要求
2.2.1 前台功能
在前台的功能实现上,可以分为以下几个部分:
- 大学生健康阅读:每日可以在首页进行健康资料阅读
- 大学生健康日志:用户可以记录自己健康日记,记录每一天的睡眠,饮酒,吸烟,血压,体重等情况
- 大学生体检管理:大学生可通过该平台查询每次的体检结果,显示部分项目随时间变化的曲线
- 疾病预测评估:针对糖尿病和高血压两种疾病,利用数学线性回归的方法,模拟计算出大学生患病概率
2.2.2 后台功能
后台主要有四个功能:
- 管理员用户登录功能:通过编号登录系统
- 管理员管理大学生功能:可以增删改查大学生基本信息
- 管理员管理体检结果:可以增删改查大学生体检记录
- 管理员对体检表,健康文档,体检数据图标展示等进行管理,以及权限管理:指定不同科室医生进行不同的操作
2.3 系统的性能需求
健康管理平台的特性要求网站系统需要具备以下几个主要特性:
- 多样性:健康管理平台在设计时必须遵循的一个重要原则,就是以用户为系统设计的中心。网络的发展大大降低了信息咨询的成本,用户面临更多的选择,所以,健康管理平台除了降低成本以外,还必须在外观,功能的多样性上下工夫,才能够得到用户的认可。具体来说,应用系统应能实现用户特定的需求,操作界面友好、方便,功能设置符合用户的习惯和要求
- 易维护:管理员能够简单方便地管理系统,定义新的应用,并对系统进行日常维护;随着健康管理平台信息的变更或增加、数据规模的增大,系统升级、维护应易于操作;各子系统、功能模块的设计应具有良好的灵活性,保证在健康管理平台系统建设过程中后期系统功能的实现不影响前期已投入使用的系统功能
- 安全性:系统应充分考虑信息、设备、运行和管理的安全性,建立完善的多层次的安全保障体系,保证用户的个人信息等资料的数据安全
- 可扩展:健康管理平台的用户从事着各种行业,他们的需求各不相同,系统在设计时应充分考虑到用户需求不断变化的需要,降低系统在扩展新的业务功能时的复杂度[5]
2.4 系统的数据要求
2.4.1 数据的性质
数据录入和处理的准确性和实时性
数据的输入是否准确是数据处理的前提,错误的输入会导致系统输出的不正确和不可用,从而使系统的工作失去意义。数据的输入来源是用户的手工输入。手工输入要通过系统界面上的安排系统具有容错性,并且对用户操作进行实时的跟踪和错误提示。
在系统中,数据的输入往往是大量的,因此系统要有一定的处理能力,以保证迅速的处理数据。
数据的一致性与完整性
由于网站的数据是共享的,所以如何保证这些数据的一致性,是网站必须解决的问题。要解决这一问题,要有一定的人员维护数据的一致性,在数据录入处控制数据的去向,并且要求对数据库的数据完整性进行严格的约束。
对于输入的数据,要为其定义完整性规则,如果不能符合完整性约束,网站应该拒绝该数据。
数据的共享与独立性
整个健康管理平台的数据是共享的。然而,从网站开发的角度上看,共享会给设计和调试带来困难。因此,应该提供灵活的配置,使各个分系统能够独立运行,而通过人工干预的手段进行系统数据的交换。这样,也能提供系统的强壮性。
2.4.2 数据字典
健康管理平台主要涉及到真实姓名数据字典,如表2.1所示:
真实姓名数据字典
系统信息表
报告信息表
权限表
权限资源表
资源表
健康阅读表
检查报告表
2.4.3 实体联系图
3.项目实现
3.1 项目说明
项目是采用SpringBoot + Mybatis + Shiro 的搭建的单体后台服务。同时通过引入通用mapper减少代码量,已达到重点关注业务的开发。
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>${mapper-starter-version}</version><exclusions><exclusion><groupId>javax.persistence</groupId><artifactId>persistence-api</artifactId></exclusion></exclusions></dependency>
3.1.1 实体
在创建实体时,首先定义了一个BaseEntity的类,所有的实体均集成该类,同时该类需要加上@MappedSuperclass注解。BaseEntity里面包含了所有实体的公共字段,如:id、createTime、updateTime。这样做是为了减少代码的冗余。
实体上配置了@Table注解,以及实体字段配置了 @Column注解是为了搭配jpa的自动建表,已达到添加字段时,重新运行项目,数据库表字段也会自动添加。
但是存在缺点: 更改字段类型或删除字段,数据库表不能自动修改或删除字段,需要手动去变更。
3.1.2 Dao层
再创建相关的dao层接口时,需要接口继承自定义的BaseDao接口,该接口集成了通用mapper相关的接口和注解,便于我们自定义一些通用接口。
3.1.3 Service层
所有的service接口需要继承自定义的BaseService接口,该接口定义了一些常用到的接口,例如增、删、改、查等等。
publicinterfaceBaseService<T>{List<T>selectAll();TselectByKey(Serializablekey);intinsert(Tentity);intdeleteByKey(Serializablekey);intdeletes(List<String>keys);intdelete(Tentity);intupdate(Tentity);List<T>selectByExample(Exampleexample);List<T>select(Conditioncondition);PageInfo<T>selectPage(Conditioncondition);}
同时创建接口实现类时需要继承自定义的BaseServiceImpl类,该类实现了BaseService接口的所有方法,这样可以达到不用书写这些接口,就可在需要的时候调用,如果需要自定义某个方法时,只需要实通过@Override重写某个方法即可。
@Slf4j@Transactional(propagation=Propagation.SUPPORTS,readOnly=true,rollbackFor=Exception.class)publicabstractclassBaseServiceImpl<T,MextendsBaseDao<T>>implementsBaseService<T>{privateClass<?>clazz=(Class<?>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];@AutowiredprotectedMentityDao;@OverridepublicList<T>selectAll(){returnentityDao.selectAll();}@OverridepublicTselectByKey(Serializablekey){log.info("执行筛选==>参数为【"+key+"】");returnentityDao.selectByPrimaryKey(key);}@Override@Transactional(rollbackFor=Exception.class)publicintinsert(Tentity){log.info("执行插入==>参数为【"+entity+"】");returnentityDao.insertSelective(entity);}@Override@Transactional(rollbackFor=Exception.class)publicintdeleteByKey(Serializablekey){log.info("执行删除==>参数为【"+key+"】");returnentityDao.deleteByPrimaryKey(key);}@Override@Transactional(rollbackFor=Exception.class)publicintdeletes(List<String>keys){log.info("执行批量删除==>参数为【"+keys+"】");StringkeyName=null;Field[]fields=clazz.getDeclaredFields();for(Fieldfield:fields){if(field.isAnnotationPresent(Id.class)){keyName=field.getName();break;}}Exampleexample=newExample(clazz);example.createCriteria().andIn(keyName,keys);returnentityDao.deleteByExample(example);}@Override@Transactional(rollbackFor=Exception.class)publicintdelete(Tentity){log.info("执行删除==>参数为【"+entity+"】");if(entity!=null){returnentityDao.delete(entity);}return0;}@Override@Transactional(rollbackFor=Exception.class)publicintupdate(Tentity){log.info("执行修改==>参数为【"+entity+"】");returnentityDao.updateByPrimaryKeySelective(entity);}@OverridepublicList<T>selectByExample(Exampleexample){returnentityDao.selectByExample(example);}@OverridepublicList<T>select(Conditioncondition){log.info("执行条件查询===>参数为【"+JSONObject.toJSONString(condition)+"】");Map<String,EntityColumn>propertyMap=EntityHelper.getEntityTable(clazz).getPropertyMap();Exampleexample=newExample(clazz);List<Clause>clauses=condition.getClauses();if(!CollectionUtils.isEmpty(clauses)){Example.Criteriacriteria=example.createCriteria();clauses.forEach(clause->{if(!StringUtils.isEmpty(clause.getColumn())){if("isNull".equalsIgnoreCase(clause.getOperation())){criteria.andIsNull(clause.getColumn());}if("isNotNull".equalsIgnoreCase(clause.getOperation())){criteria.andIsNotNull(clause.getColumn());}else{if(!StringUtils.isEmpty(clause.getValue())){if("between".equalsIgnoreCase(clause.getOperation())){ArrayList<?>list=(ArrayList<?>)clause.getValue();criteria.andBetween(clause.getColumn(),list.get(0),list.get(1));}if("like".equalsIgnoreCase(clause.getOperation())){criteria.andLike(clause.getColumn(),"%"+clause.getValue()+"%");}if("=".equalsIgnoreCase(clause.getOperation())){criteria.andCondition(propertyMap.get(clause.getColumn()).getColumn()+clause.getOperation(),clause.getValue());}}}}});}Map<String,Object>sortMap=condition.getSortMap();if(sortMap.size()>0){StringBuffersb=newStringBuffer();sortMap.forEach((k,v)->{if(!StringUtils.isEmpty(k)&&!StringUtils.isEmpty(v)){sb.append(propertyMap.get(k).getColumn());sb.append(" ");sb.append(v);sb.append(",");}});if(sb.toString().endsWith(",")){sb.deleteCharAt(sb.length()-1);}example.setOrderByClause(sb.toString());}returnselectByExample(example);}@OverridepublicPageInfo<T>selectPage(Conditioncondition){PageHelper.startPage(condition.getPageNum(),condition.getPageSize());List<T>list=select(condition);returnnewPageInfo<>(list);}}
3.1.4 Web层
在web层,项目定义了一个BaseController的抽象类,里面实体了基本的增删改查的接口定义。再我们创建某个模块的Controller时只需要继承该类就 可达到不用在创建的类下面定义接口,也会自动拥有相关接口,如果需要对某个接口方法做定制时,只需要重新某个方法就可以了。
@Slf4jpublicabstractclassBaseController<SextendsBaseService<T>,T,IDextendsSerializable>{@AutowiredprotectedSservice;@ApiOperation(value="基础接口: 新增操作")@PostMapping(value="add")publicResponseEntity<T>save(@RequestBodyTentity){try{if(service.insert(entity)<1){thrownewMyException(ExceptionEnums.ADD_ERROR);}}catch(Exceptione){log.error(e.getMessage());thrownewMyException(ExceptionEnums.ADD_ERROR);}returnResponseEntity.ok(entity);}@ApiOperation(value="基础接口: 返回指定ID的数据")@GetMapping(value="get/{id}")publicResponseEntity<T>get(@PathVariable("id")IDid){Tt=null;try{t=service.selectByKey(id);}catch(Exceptione){log.error(e.getMessage());thrownewMyException(ExceptionEnums.GET_ITEM);}returnResponseEntity.ok(t);}@ApiOperation(value="基础接口: 返回所有数据")@GetMapping(value="all")publicResponseEntity<List<T>>all(){List<T>list;try{list=service.selectAll();}catch(Exceptione){log.error(e.getMessage());thrownewMyException(ExceptionEnums.GET_LIST_ERROR);}returnResponseEntity.ok(list);}@ApiOperation(value="基础接口: 分页返回数据")@PostMapping(value="page")publicResponseEntity<PageInfo<T>>page(@RequestBodyConditioncondition){PageInfo<T>page;try{page=service.selectPage(condition);}catch(Exceptione){log.error(e.getMessage());thrownewMyException(ExceptionEnums.GET_LIST_ERROR);}returnResponseEntity.ok(page);}@ApiOperation(value="基础接口: 修改数据")@PostMapping(value="update")publicResponseEntity<T>update(@RequestBodyTentity){try{if(service.update(entity)<1){thrownewMyException(ExceptionEnums.UPDATE_ERROR);}}catch(Exceptione){log.error(e.getMessage());thrownewMyException(ExceptionEnums.UPDATE_ERROR);}returnResponseEntity.ok(entity);}@ApiOperation(value="基础接口: 删除指定ID的数据")@GetMapping(value="delete/{id}")publicResponseEntity<String>delete(@PathVariable("id")IDid){try{if(service.deleteByKey(id)<1){thrownewMyException(ExceptionEnums.DELETE_ERROR);}}catch(Exceptione){log.error(e.getMessage());thrownewMyException(ExceptionEnums.DELETE_ERROR);}returnResponseEntity.ok("删除成功");}}
3.2 权限相关
权限主要采用apache shiro,需要引入相关依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.3.2</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.3.2</version></dependency>
在使用shiro时,需要创建一个realm进行进行权限的授权和认证,同时需要创建一个自定义的凭证验证器来校验密码是否一致。配置好这些后需要定义一个shiro的配置类ShiroConfiguration来配置shiro的权限过滤工厂beanShiroFilterFactoryBean,在里面引入自定义的AuthRealmbean以达到权限的校验。
因为项目采用的是前后端分离,会产生跨域请求等问题,所以需要开放所有来源对该服务的请求,所以需要配置相关过滤器CorsFilter。
publicclassCorsFilterimplementsFilter{@Overridepublicvoidinit(FilterConfigfilterConfig)throwsServletException{}@OverridepublicvoiddoFilter(ServletRequestservletRequest,ServletResponseservletResponse,FilterChainfilterChain)throwsIOException,ServletException{servletRequest.setCharacterEncoding("utf-8");HttpServletResponsehttpServletResponse=(HttpServletResponse)servletResponse;HttpServletRequesthttpServletRequest=(HttpServletRequest)servletRequest;//是否支持cookie跨域 httpServletResponse.setHeader("Access-Control-Allow-Credentials","true");//指定允许其他域名访问 httpServletResponse.setHeader("Access-Control-Allow-Origin",httpServletRequest.getHeader("Origin"));//响应头设置 httpServletResponse.setHeader("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept,client_id, uuid, Authorization,user-agent,X-XSRF-TOKEN");// 设置过期时间 httpServletResponse.setHeader("Access-Control-Max-Age","3600");//响应类型 httpServletResponse.setHeader("Access-Control-Allow-Methods","POST, GET, PUT, OPTIONS, DELETE");// 支持HTTP1.1. httpServletResponse.setHeader("Cache-Control","no-cache, no-store, must-revalidate");// 支持HTTP 1.0. httpServletResponse.setHeader("Pragma","no-cache");httpServletResponse.setHeader("Allow","GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH");if("OPTIONS".equals(httpServletRequest.getMethod())){httpServletResponse.setStatus(204);}filterChain.doFilter(servletRequest,servletResponse);}@Overridepublicvoiddestroy(){}}
使用shiro权限验证时,第一次请求为option方式,所以要配置shiro过滤掉option方式,不对此方式做权限拦截。故配置ShiroFilter
@Slf4jpublicclassShiroFiterextendsFormAuthenticationFilter{@OverrideprotectedbooleanisAccessAllowed(ServletRequestrequest,ServletResponseresponse,ObjectmappedValue){booleanallowed=super.isAccessAllowed(request,response,mappedValue);if(!allowed){// 判断请求是否是options请求 Stringmethod=WebUtils.toHttp(request).getMethod();if(StringUtils.equalsIgnoreCase("OPTIONS",method)){returntrue;}}returnallowed;}@OverrideprotectedbooleanonAccessDenied(ServletRequestrequest,ServletResponseresponse)throwsException{returnsuper.onAccessDenied(request,response);}}
3.3 其他
项目中定义了全局异常捕获,再项目中需要抛出异常时只需要抛出自定义的异常MyException,就可对该异常进行捕获,当用户请求某个接口如果出现异常时,后台服务就会返回自定义的错误代码,不会出现乱码的情况,以便于前端对返回结果做统一处理或错误拦截。同时我们也可预先自定义需要抛出的错误,在前段可对错误code进行区别,采用不同的样式提示。
@Getter@NoArgsConstructor@AllArgsConstructorpublicenumExceptionEnums{/** * 错误返回信息 */NO_CHECK_INFO(400,"暂无健康档案表,无法进行数据分析"),CHOOSE_FILE(400,"文件未上传"),IS_NOT_LOGIN(400,"用户未登录"),UPLOAD_FAIL(400,"上传失败"),RESOURCE_FOUNT_FAIL(400,"获取菜单权限失败!"),NOT_AUTHORIZED(403,"您暂无权限访问该资源!"),ACCOUNT_IS_EXIT(400,"用户名已存在"),PASSWORD_WRONG(400,"密码错误"),ACCOUNT_IS_NOT_EXIT(400,"用户名不存在"),UPDATE_ERROR(400,"更新失败"),ADD_ERROR(400,"新增失败"),DELETE_ERROR(400,"删除失败"),GET_LIST_ERROR(400,"获取列表失败"),GET_ITEM(400,"获取对象失败"),NO_WEIGHT_HEIGHT(400,"当前档案未完善身高体重信息");/** * 代码值 */privateintcode;/** * 消息 */privateStringmsg;}