目录

项目简介:

包含系统

项目架构​

前端开发流程:​

common模块

swagger2

Result(全局统一返回结果)

YyghException(自定义全局异常)

GlobalExceptionHandler(全局异常处理器)

JwtHelper(生成Token、根据Token获取用户信息)

AuthContextHolder(获取用户信息)

HttpRequestHelper

MD5加密

HttpUtil

model模块

BaseEntity

BaseMongoEntity

service_cmn(数据字典接口)

easyexcel(导入导出字典)

listener

树形列表

spring Cache + redis 缓存数据

service_hosp(医院api接口)

MybatisPlus

Mongodb

部门查询

nacos

JWT

登录功能

手机号登录

微信登录

微信支付

退款

阿里OSS

RabbitMQ

定时任务

ECharts统计

Bug


项目简介:

包含系统

预约挂号后台管理系统

前台用户系统就是114挂号网站

114网上预约挂号 - 北京市预约挂号统一平台

医院接口系统:

项目架构

前端开发流程:

约定 > 配置 > 编码,项目父工程中规定所有共用依赖的版本。

common模块

将全局要使用的实体类和工具放到此模块中,避免代码冗余

swagger2

swagger通过注解表明该接口会生成文档,包括接口名、请求方法、参数、返回信息的等等。

使用swagger要完成以下三部

  1. @Api:修饰整个类,描述Controller的作用

  2. @ApiOperation:描述一个类的一个方法,或者说一个接口

  3. @ApiParam:单个参数描述

  4. @ApiModel:用对象来接收参数

  5. @ApiModelProperty:用对象接收参数时,描述对象的一个字段

  6. @ApiImplicitParam:一个请求参数

  7. @ApiImplicitParams:多个请求参数

1、导入pom依赖

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>

2、配置拦截路径

/*** Swagger2配置信息*/
@Configuration
@EnableSwagger2
public class Swagger2Config {@Beanpublic Docket webApiConfig(){return new Docket(DocumentationType.SWAGGER_2).groupName("webApi").apiInfo(webApiInfo()).select()//只显示api路径下的页面.paths(Predicates.and(PathSelectors.regex("/api/.*"))).build();}@Beanpublic 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("linxi", "http://linxi.com", "2738328047@qq.com")).build();}private ApiInfo adminApiInfo(){return new ApiInfoBuilder().title("后台管理系统-API文档").description("本文档描述了后台管理系统微服务接口定义").version("1.0").contact(new Contact("linxi", "http://linxi.com", "2738328047@qq.com")).build();}
}

3、在主启动上添加注解

//扫描swagger的包
@ComponentScan(basePackages = "com.linxi")

Result(全局统一返回结果)

将所有请求映射返回的信息封装在Result中,泛型为任意类型。 Result.ok()返回前端code为200,Result.fail()返回前端code为201,当然这里面可以添数据,Result.ok(map)返回一个 map集合,配合枚举类使用更方便。

Result类

/*** 全局统一返回结果类*/
@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;}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);}public Result<T> message(String msg){this.setMessage(msg);return this;}public Result<T> code(Integer code){this.setCode(code);return this;}public boolean isOk() {if(this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue()) {return true;}return false;}
}

枚举类

/*** 统一返回结果状态信息类*/
@Getter
public enum ResultCodeEnum {SUCCESS(200,"成功"),FAIL(201, "失败"),PARAM_ERROR( 202, "参数不正确"),SERVICE_ERROR(203, "服务异常"),DATA_ERROR(204, "数据异常"),DATA_UPDATE_ERROR(205, "数据版本异常"),LOGIN_AUTH(208, "未登陆"),PERMISSION(209, "没有权限"),CODE_ERROR(210, "验证码错误"),
//    LOGIN_MOBLE_ERROR(211, "账号不正确"),LOGIN_DISABLED_ERROR(212, "该用户已被禁用"),REGISTER_MOBLE_ERROR(213, "手机号已被使用"),LOGIN_AURH(214, "需要登录"),LOGIN_ACL(215, "没有权限"),URL_ENCODE_ERROR( 216, "URL编码失败"),ILLEGAL_CALLBACK_REQUEST_ERROR( 217, "非法回调请求"),FETCH_ACCESSTOKEN_FAILD( 218, "获取accessToken失败"),FETCH_USERINFO_ERROR( 219, "获取用户信息失败"),DEPARTMENT_DELETE_FAIL(221,"科室不存在"),//LOGIN_ERROR( 23005, "登录失败"),PAY_RUN(220, "支付中"),CANCEL_ORDER_FAIL(225, "取消订单失败"),CANCEL_ORDER_NO(225, "不能取消预约"),HOSCODE_EXIST(230, "医院编号已经存在"),NUMBER_NO(240, "可预约号不足"),TIME_NO(250, "当前时间不可以预约"),SIGN_ERROR(300, "签名错误"),HOSPITAL_OPEN(310, "医院未开通,暂时不能访问"),HOSPITAL_LOCK(320, "医院被锁定,暂时不能访问"),;private Integer code;private String message;private ResultCodeEnum(Integer code, String message) {this.code = code;this.message = message;}
}

YyghException(自定义全局异常)

@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();}@Overridepublic String toString() {return "YyghException{" +"code=" + code +", message=" + this.getMessage() +'}';}
}

GlobalExceptionHandler(全局异常处理器)

@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)@ResponseBodypublic Result error(Exception e) {e.printStackTrace();return Result.fail();}@ExceptionHandler(YyghException.class)@ResponseBodypublic Result error(YyghException e) {e.printStackTrace();return Result.fail();}
}

JwtHelper(生成Token、根据Token获取用户信息)

        <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency>
public class JwtHelper {//Token过期时间(ms)private static long tokenExpiration = 24*60*60*1000;//Token签名密钥private static String tokenSignKey = "linxi";/***根据参数生成Token*/public static String createToken(Long userId, String userName) {String token = Jwts.builder().setSubject("YYGH-USER").setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)).claim("userId", userId).claim("userName", userName).signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();return token;}/***根据Token得到用户id*/public static Long getUserId(String token) {if(StringUtils.isEmpty(token)) return null;Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);Claims claims = claimsJws.getBody();Integer userId = (Integer)claims.get("userId");return userId.longValue();}/***根据Token得到用户名称*/public static String getUserName(String token) {if(StringUtils.isEmpty(token)) return "";Jws<Claims> claimsJws= Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);Claims claims = claimsJws.getBody();return (String)claims.get("userName");}//测试方法public static void main(String[] args) {String token = JwtHelper.createToken(1L, "linxi");System.out.println(token);System.out.println(JwtHelper.getUserId(token));System.out.println(JwtHelper.getUserName(token));}
}

AuthContextHolder(获取用户信息)

AuthContextHolder类封装了JwtHelper中的方法,使得业务分离。全局根据Token获取信息调用这个的方法,而生成Token使用JwtHelper中的方法createToken。

/*** 获取当前用户信息的工具类*/
public class AuthContextHolder {/***     获取用户id*/public static Long getUserId(HttpServletRequest request){//获取用户tokenString token = request.getHeader("token");//jwt从token中获取userIdLong userId = JwtHelper.getUserId(token);return userId;}/*** 获取用户名称*/public static String getUserName(HttpServletRequest request){//获取用户tokenString token = request.getHeader("token");//jwt从token中获取userIdString userName = JwtHelper.getUserName(token);return userName;}}

HttpRequestHelper

@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");}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");//调用HttpUtilbyte[] 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);}
}

MD5加密

/*** 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);}}
}

HttpUtil

@Slf4j
public final class HttpUtil {static final String POST = "POST";static final String GET = "GET";static final int CONN_TIMEOUT = 30000;// msstatic final int READ_TIMEOUT = 30000;// ms/*** post 方式发送http请求.* * @param strUrl* @param reqData* @return*/public static byte[] doPost(String strUrl, byte[] reqData) {return send(strUrl, POST, reqData);}/*** get方式发送http请求.* * @param strUrl* @return*/public static byte[] doGet(String strUrl) {return send(strUrl, GET, null);}/*** @param strUrl* @param reqmethod* @param reqData* @return*/public static byte[] send(String strUrl, String reqmethod, byte[] reqData) {try {URL url = new URL(strUrl);HttpURLConnection httpcon = (HttpURLConnection) url.openConnection();httpcon.setDoOutput(true);httpcon.setDoInput(true);httpcon.setUseCaches(false);httpcon.setInstanceFollowRedirects(true);httpcon.setConnectTimeout(CONN_TIMEOUT);httpcon.setReadTimeout(READ_TIMEOUT);httpcon.setRequestMethod(reqmethod);httpcon.connect();if (reqmethod.equalsIgnoreCase(POST)) {OutputStream os = httpcon.getOutputStream();os.write(reqData);os.flush();os.close();}BufferedReader in = new BufferedReader(new InputStreamReader(httpcon.getInputStream(),"utf-8"));String inputLine;StringBuilder bankXmlBuffer = new StringBuilder();while ((inputLine = in.readLine()) != null) {  bankXmlBuffer.append(inputLine);  }  in.close();  httpcon.disconnect();return bankXmlBuffer.toString().getBytes();} catch (Exception ex) {log.error(ex.toString(), ex);return null;}}/*** 从输入流中读取数据* * @param inStream* @return* @throws Exception*/public static byte[] readInputStream(InputStream inStream) throws Exception {ByteArrayOutputStream outStream = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int len = 0;while ((len = inStream.read(buffer)) != -1) {outStream.write(buffer, 0, len);}byte[] data = outStream.toByteArray();// 网页的二进制数据outStream.close();inStream.close();return data;}
}

model模块

定义了所有共用的枚举类和实体类(表数据)封装了所有表连接查询类(将不同表中的部分数据封装在一起作为查询字段)。

BaseEntity

所有关于mysql表的实体类继承 BaseEntity ,他们都有这些共同的字段。最后的map集合是封装其它数据返回给前端的,数据库中不存在该字段,因此@TableField(exist = false)。

@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@TableField("is_deleted")private Integer isDeleted;@ApiModelProperty(value = "其他参数")@TableField(exist = false)private Map<String,Object> param = new HashMap<>();
}

BaseMongoEntity

与BaseEntity同功能,但针对于mongodb, @Transien表示不录入到数据库中。

@Data
public class BaseMongoEntity implements Serializable {@ApiModelProperty(value = "id")@Idprivate String id;@ApiModelProperty(value = "创建时间")private Date createTime;@ApiModelProperty(value = "更新时间")private Date updateTime;@ApiModelProperty(value = "逻辑删除(1:已删除,0:未删除)")private Integer isDeleted;@ApiModelProperty(value = "其他参数")@Transient //被该注解标注的,将不会被录入到数据库中。只作为普通的javaBean属性private Map<String,Object> param = new HashMap<>();
}

service

包含以下api接口服务

service_cmn(数据字典接口)

数据字典中包全国省市区、医院等级、证件类型、民族、学历。

表中idparent_id相对应,dict_code和id建立联系,value表示数据对应的值或者说用该值代表数据,所有数据存在一张表中,避免连表查询(笛卡尔积)

easyexcel(导入导出字典)

导入导出数据字典理应excel文件,需要引入依赖

pom

        <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.10</version></dependency>
    //导入数据字典接口@PostMapping("importData")public Result importDict(MultipartFile file){dictService.importDictData(file);return Result.ok();}//导出数据字典接口@GetMapping("exportData")public void exportDict(HttpServletResponse response){dictService.exportDictData(response);}
    //导出数据字典接口@Overridepublic void exportDictData(HttpServletResponse response) {response.setContentType("application/vnd.ms-excel");response.setCharacterEncoding("utf-8");//这里的 URLEncoder.encode 可以防止中文乱码,与easyExcel无关系String fileName = "dict";//已下载形式response.setHeader("Content-disposition", "attachment;fileName" + fileName + ".xlsx");//查询寻数据库List<Dict> dictList = baseMapper.selectList(null);//将dict转换成dictEoVoList<DictEeVo> dictVoList = new ArrayList<>();for (Dict dict : dictList) {DictEeVo dictEeVo = new DictEeVo();BeanUtils.copyProperties(dict, dictEeVo);dictVoList.add(dictEeVo);}//调用方法实现写操作try {EasyExcel.write(response.getOutputStream(), DictEeVo.class).sheet("dict").doWrite(dictVoList);} catch (IOException e) {e.printStackTrace();}}//导入数据字典@Override@CacheEvict(value = "dict", allEntries = true)//清空所有缓存public void importDictData(MultipartFile file) {try {EasyExcel.read(file.getInputStream(), DictEeVo.class, new DictDataListener(baseMapper)).sheet().doRead();} catch (IOException e) {e.printStackTrace();}}

listener

在读取excel表格数据时需要监听器来封装读取操作。

public class DictDataListener extends AnalysisEventListener<DictEeVo> {private DictMapper dictMapper;public DictDataListener(DictMapper dictMapper) {this.dictMapper = dictMapper;}//一行一行读取数据@Overridepublic void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {Dict dict = new Dict();//数据转换BeanUtils.copyProperties(dictEeVo,dict);dictMapper.insert(dict);}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}
}

树形列表

当我们点击任意节点,会判断是否存在子节点,有则会显示。

根据上表的数据库字段,可以先根据 dict_code 查出 id,再通过 id 和 parent_id 的关系依次查出,也可以直接使用 id ,具体看前端传数据。

    //根据数据id查询子数据列表@Override@Cacheable(value = "dict", keyGenerator = "keyGenerator")//第一次查询后将数据放入缓存中public List<Dict> findChildData(Long id) {QueryWrapper<Dict> queryWrapper = new QueryWrapper<>();queryWrapper.eq("parent_id", id);List<Dict> dictList = baseMapper.selectList(queryWrapper);//循环得到每个对象for (Dict dict : dictList) {Long dictId = dict.getId();//根据id判断下面是否有子节点boolean haschild = baseMapper.selectCount(new QueryWrapper<Dict>().eq("parent_id", dictId)) > 0;dict.setHasChildren(haschild);}return dictList;}

spring Cache + redis 缓存数据

xml

<!-- 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>

增加redis配置类

@EnableCaching 开启缓存

@Configuration
@EnableCaching
public class RedisConfig {/*** 自定义key规则* @return*/@Beanpublic KeyGenerator keyGenerator() {return new KeyGenerator() {@Overridepublic 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*/@Beanpublic 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和publicom.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);//序列号key valueredisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}/*** 设置CacheManager缓存规则* @param factory* @return*/@Beanpublic 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;}
}
#redis
spring.redis.host=192.168.*.*
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000spring.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

配置完成后在查询方法上增加注解就行,这个方法在上面的树形列表中

@Cacheable(value = "dict", keyGenerator = "keyGenerator")//第一次查询后将数据放入缓存中

service_hosp(医院api接口)

结构:api包下接口提供给前台用户系统使用,其余接口提供给后台管理系统使用。

医院的基础信息设置是在mysql中(由后台管理系统crud)

医院的详细信息在Mongodb中(由医院接口系统crud)

MybatisPlus

项目中所有的MybatisPlus的使用类都继承于 IService 、ServiceImpl。就可以直接使用它们的方法,如下面的分页查询(记得配置分页插件)。

@Mapper
public interface HospitalSetMapper extends BaseMapper<HospitalSet> {
}
public interface HospitalSetService extends IService<HospitalSet> {}
@Service
public class HospitalSetServiceImpl extends ServiceImpl<HospitalSetMapper, HospitalSet> implements HospitalSetService {}

这里面调用的page方法就是Iservice里面的,并没有在hospitalSetService定义(注意Page导的时mybatisPlus的包),当然还有其他的方法:list、save、update、getById、updateById、removeByIds

    //条件查询带分页@ApiOperation(value = "条件查询带分页")@PostMapping("findPageHospSet/{current}/{limit}")public Result findPageHospSet(@PathVariable("current") Long current,@PathVariable("limit") Long limit,//通过json传入数据,可以为空@RequestBody(required = false) HospitalSetQueryVo hospitalSetQueryVo) {//当前页、每页记录数Page<HospitalSet> page = new Page<>(current, limit);//构造条件QueryWrapper<HospitalSet> queryWrapper = new QueryWrapper<>();String hosname = hospitalSetQueryVo.getHosname();String hoscode = hospitalSetQueryVo.getHoscode();if (!StringUtils.isEmpty(hosname)) {//医院名称模糊查询queryWrapper.like("hosname", hosname);}if (!StringUtils.isEmpty(hoscode)) {//匹配医院编号queryWrapper.eq("hoscode", hoscode);}Page<HospitalSet> queryPage = hospitalSetService.page(page, queryWrapper);return Result.ok(queryPage);}

Mongodb

mongodb使用分两种 MongoTemplate 、MongoRepository

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

下面使用MongoRepository:

@Repository
public interface HospitalRepository extends MongoRepository<Hospital,String> {//判断是否存在数据Hospital getHospitalByHoscode(String hoscode);//根据医院名称查询List<Hospital> findHospitalByHosnameLike(String hosname);
}

有意思的是:只需要在继承了MongoRepository中书写方法体 MongoRepository就会帮我们自动实现这个方法,非常的简便。Spring Data 提供了对mongodb数据访问我们只需要继承MongoRepository类,按照Spring Data规范就可以。

在使用时先将 定义HospitalRepository 注入,然后调用MongoRepository方法即可,或者根据业务需要按照springData规范自定义方法列如:getHospitalByHoscode(hoscode)、

findScheduleByHoscodeAndDepcodeAndWorkDate(...)。
    //医院查询(条件查询带分页)@Overridepublic Page<Hospital> selectHospPage(int page, int limit, HospitalQueryVo hospitalQueryVo) {Hospital hospital = new Hospital();BeanUtils.copyProperties(hospitalQueryVo, hospital);ExampleMatcher matcher = ExampleMatcher.matching().withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //模糊查询.withIgnoreCase(true);//忽略大小写Example<Hospital> example = Example.of(hospital, matcher);Pageable pageable = PageRequest.of(page - 1, limit);Page<Hospital> pages = hospitalRepository.findAll(example, pageable);//获取查询list集合,遍历进行医院等级封装pages.getContent().stream().forEach(item -> {this.setHospitalHosType(item);});return pages;}//获取查询list集合,遍历进行医院等级封装private Hospital setHospitalHosType(Hospital hospital) {//更具dictCode和value获取医院名称String hostypeString = dictFeignClient.getName("hostype", hospital.getHostype());//查询省市区String provinceString = dictFeignClient.getName(hospital.getProvinceCode());String cityString = dictFeignClient.getName(hospital.getCityCode());String districtString = dictFeignClient.getName(hospital.getDistrictCode());hospital.getParam().put("fullAddress", provinceString + cityString + districtString);hospital.getParam().put("hostypeString", hostypeString);return hospital;}

下面使用mongoTemplate 进行所有的排班查询

注入bean必不可少

    @Autowiredprivate MongoTemplate mongoTemplate;

然后就是实现方法

    //查询排班规则数据@Overridepublic Map<String, Object> getScheduleRule(int page, int limit, String hoscode, String depcode) {//1、根据医院编号和科室编号进行查询Criteria criteria = Criteria.where("hoscode").is(hoscode).and("depcode").is(depcode);//2、根据工作日期workDate进行分组Aggregation agg = Aggregation.newAggregation(Aggregation.match(criteria),//匹配条件Aggregation.group("workDate")//分组字段.first("workDate").as("workDate")//3、统计号源数量(求和).count().as("docCount").sum("reservedNumber").as("reservedNumber").sum("availableNumber").as("availableNumber"),//排序Aggregation.sort(Sort.Direction.DESC, "workDate"),//4、实现分页Aggregation.skip((page - 1) * limit),Aggregation.limit(limit));//调用方法,最后执行AggregationResults<BookingScheduleRuleVo> aggResults =mongoTemplate.aggregate(agg, Schedule.class, BookingScheduleRuleVo.class);List<BookingScheduleRuleVo> bookingScheduleRuleVoList = aggResults.getMappedResults();//分组查询总记录数Aggregation totalAgg = Aggregation.newAggregation(Aggregation.match(criteria),Aggregation.group("workDate") //通过工作日期进行分组);//调用方法查询AggregationResults<BookingScheduleRuleVo> totalAggResult =mongoTemplate.aggregate(totalAgg, Schedule.class, BookingScheduleRuleVo.class);int total = totalAggResult.getMappedResults().size(); //某天的总记录数//根据日期获取星期for (BookingScheduleRuleVo bookingScheduleRuleVo : bookingScheduleRuleVoList) {//获取日期Date workDate = bookingScheduleRuleVo.getWorkDate();//getDayOfWeek 自定义的方法,利用String dayOfWeek = this.getDayOfWeek(new DateTime(workDate));bookingScheduleRuleVo.setDayOfWeek(dayOfWeek);}//设置最终数据进行返回Map<String, Object> resultMap = new HashMap<>();resultMap.put("bookingScheduleRuleList",bookingScheduleRuleVoList);resultMap.put("total",total);//获取医院名称String hosName = hospitalService.getHosName(hoscode);Map<String, Object> baseMap = new HashMap<>();baseMap.put("hosname",hosName);resultMap.put("baseMap",baseMap);return resultMap;}

部门查询

做成树性列表,大科室包含很多小科室。配合上面查出的排版信息可做成下列画面

这个功能最难的代码是:查询出的所有部门信息是一个list集合,如何将它们进行分组,代码如下

        //根据大科室 bigcode分组,获取每个大科室的所有子科室Map<String, List<Department>> departmentMap =departmentList.stream().collect(Collectors.groupingBy(Department::getBigcode));

分组后要封装所有大小科室的信息,

      //遍历map集合:通过key和value的关系entryfor (Map.Entry<String, List<Department>> entry : departmentMap.entrySet()){//大科室编号String bigcode = entry.getKey();//大科室编号对应的全部数据List<Department> departments = entry.getValue();/*封装大科室*/DepartmentVo departmentVo = new DepartmentVo();departmentVo.setDepcode(bigcode); //设置大科室编号departmentVo.setDepname(departments.get(0).getBigname());//设置大科室名称/*封装小科室*/List<DepartmentVo> children = new ArrayList<>();//遍历得到每个小科室for (Department department : departments){DepartmentVo departmentVo1 = new DepartmentVo();departmentVo1.setDepcode(department.getDepcode());//设置小科室编号departmentVo1.setDepname(department.getDepname());//设置小科室名称children.add(departmentVo1);}//把小科室放到对应大科室的children去departmentVo.setChildren(children);//最终放到result去返回result.add(departmentVo);}

最终返回list集合给前端。它是这样的结构:List<Map<String, List<Department>>>

前端传递的json数据经过HttpRequestHelper处理后是map的json串,将他装成对象使JSONObject

    public void save(Map<String, Object> paraMap) {//将json转换成对象String s = JSONObject.toJSONString(paraMap);Department department = JSONObject.parseObject(s, Department.class);}

nacos

JWT

手机号登录

微信登录

微信支付

退款

阿里OSS

pom

        <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId></dependency><!-- 日期工具栏依赖 --><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId></dependency>

配置文件

aliyun.oss.endpoint=***********************
aliyun.oss.accessKeyId=**********
aliyun.oss.secret=**********************
aliyun.oss.bucket=***************

controller的方法参数使用的是MultipartFile,post请求,

具体实现方法:

  public String upload(MultipartFile file) {String endpoint = ConstantOssPropertiesUtils.ENDPOINT;String accessKeyId = ConstantOssPropertiesUtils.ACCESS_KEY_ID;String accessKeySecret = ConstantOssPropertiesUtils.SECRECT;String bucketName = ConstantOssPropertiesUtils.BUCKET;//保证文件名唯一String fileName = UUID.randomUUID().toString().substring(0,16).replaceAll("-","")+file.getOriginalFilename();//按照当前日期创建文件夹,放入当日上传的文件(便于查改)//  /2022/02/28/ xxx.jpgString timeUrl = new DateTime().toString("yyyy/MM/dd");fileName = timeUrl + "/" + fileName;try {// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);//获取文件流InputStream inputStream = file.getInputStream();//调用方法实现上传ossClient.putObject(bucketName, fileName, inputStream);//关闭实例if (ossClient != null) {ossClient.shutdown();}//返回文件路径String url = "https://"+bucketName+"."+endpoint+"/"+fileName;return url;} catch (Exception e) {e.printStackTrace();return null;}}

RabbitMQ

pom

        <!--rabbitmq消息队列--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bus-amqp</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency>

将RabbitMQ放到公共类中,便于后面多个模块使用。这里定义了项目所需的队列交换机和路由。

在配置类中配置消息转换器

/*** mq消息转换器* 默认是字符串转换器*/
@Configuration
public class MQConfig {@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}
}

编写sendMessage方法,便于后面所有模块的调用。

@Service
public class RabbitService {@Autowiredprivate RabbitTemplate rabbitTemplate;/***  发送消息* @param exchange 交换机* @param routingKey 路由键* @param message 消息*/public boolean sendMessage(String exchange, String routingKey, Object message) {rabbitTemplate.convertAndSend(exchange, routingKey, message);return true;}
}

定时任务

其实就是利用了两个注解,cron 表达式 定时发送信息(task)给信息队列,另一服务端监听到(task)并实写提醒方法,筛选提醒人群,发送消息(msm短信)传递参数到rabbit,由msm模块监听(msm)后调用业务类实现提醒短信发送。

@Component
@EnableScheduling
public class ScheduledTask {@Autowiredprivate RabbitService rabbitService;//每天8点执行提醒//cron 表达式,设置时间间隔(0 0 8 * * ?)@Scheduled(cron = "0/30 * * * * ?") //为了测试实际使用public void task1() {rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_TASK, MqConst.ROUTING_TASK_8, "");}
}

之后的方法就不一 一阐述了。

ECharts统计

选用ECharts实现图表折线类统计图

采用服务调用(需要配置网关),Statistics 调用 order ,具体方法实现在order,

Bug

1、mongodb 8小时时间差问题:在有关时间的字段上添加注解:

@JsonFormat(pattern = "yyyy-MM-dd", timezone="GMT+8")

或者在配置文件中添加:

#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

2、在远程服务调用时,不加“{ }”总是注入不了bean,莫名出错,可能是版本问题

@EnableFeignClients(basePackages = {"com.linxi"})

3、前端myheader中微信登陆回调openid判断由 "" 改成!=null,后端传过来的' '将无法被识别导致你每次微信扫码登录都需要手机号注册

4、将OrderInfo实体类中scheduleId的@TableField的参数改成与数据库一致的hos_schedule_id,

5、将getSign加密方法后面的for循环加密参数注掉,否者签名容易为null,manage和order都要注掉

6、修改ApiServiceImpl类中saveHospital方法paramMap.put("sign",MD5.encrypt(this.getSignKey()));加密方式为MD5加密,保证mysql和mongodb的签名一致

7、微信退款请求微信api报SSL协议错误 ,因为:微信服务端更新取消TLSv1协议。修改工具类HttpClient的execute方法,使用

 SSLConnectionSocketFactory sslsf =new SSLConnectionSocketFactory(sslContext,new DefaultHostnameVerifier());

8、项目无法打包,无法找到该包,但是在业务类中导包和使用都是正确的,解决方案:

添加xml在pom中,先将父工程打包,再打包common类、model类等公共类,最后再打包业务类

    <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><classifier>execute</classifier></configuration></plugin></plugins></build>

9、部分业务类无法启动,报无法连接redis,但是pom和配置文件中并没有有关redis的引入与设置,也没有使用redis缓存,于是我配置了本地虚拟机的redis,但是他尝试连接的IP和我输入的IP不一致,很神奇的bug,好像我是重启idea再将项目重上到下打包好几遍最终它又消失了

10、微信退款证书过失,因为老师给的mysql数据和mongodb数据都是写的之前的时间,而我们在查询排班时有需要获取本地时间(本地时间排班无数据),mysql中的所有时间数据修改较容易但是mongodb中的修改较复杂(太多了),最简单的方法就是修改本地时间,可以查出之前的排班数据,但是微信退款也会获取本地时间导致证书过期,我妥协了只好当要退款时又将时间修改回来即可

尚医通开发笔记(结尾含部分bug修复方法)相关推荐

  1. 尚硅谷 尚医通学习笔记

    尚医通简介 尚医通可以网上预约挂号. 核心技术 SpringBoot : 简化新 Spring 应用的初始搭建以及开发过程 SpringCloud : 基于 Springboot 实现的云原生的开发工 ...

  2. 尚医通项目笔记--包括每个接口对应页面的图片

    废话不多说,直接上图 博客目录结构 工程目录结构 yygh-parent根目录common公共模块父节点common-util公共工具类rabbit-util业务封装RabbitMQservice-u ...

  3. 尚医通项目学习笔记Part1

    尚医通项目学习笔记 前言 一.目前学习进度 二.学习记录 1.项目简介 1.1 项目所会用到的技术栈 1.2 业务流程 2.项目学习笔记 2.1MyBatis-Plus相关 2.2搭建项目框架 2.3 ...

  4. 尚医通-OAuth2-微信登录接口开发(三十一)

    目录: (1)微信登录-OAuth2介绍 (2)前台用户系统-微信登录-准备工作 (3)微信登录-生成微信二维码-接口开发 (4)微信登录-生成验证码-前端整合 (5)微信登录-获取扫码人信息-实现分 ...

  5. 尚医通【预约挂号系统】总结

    这里写目录标题 1.项目介绍 2.技术点 3.业务流程 4.项目架构 5.项目源码(包含sql) 6.启动步骤 7.项目模块说明 8.项目功能总结 9.效果图 后台管理端 前端展示端 数据库 1.项目 ...

  6. 尚医通 (三十六) --------- 微信支付

    目录 一.微信支付介绍 二.微信支付开发 1. api 接口 2. 前端 3. 处理支付结果 三.取消预约 1. 需求描述 2. 开发微信退款接口 3. 前端 一.微信支付介绍 A.微信扫码支付申请 ...

  7. 尚医通 (一)项目介绍

    目录 一.功能简介 二.技术点 三.业务流程 四.系统架构 一.功能简介 尚医通即为网上预约挂号系统,网上预约挂号是近年来开展的一项便民就医服务,旨在缓解看病难.挂号难的就医难题,许多患者为看一次病要 ...

  8. 尚医通项目:项目总结

    课程链接:尚医通项目 开始时间:2022-07-09 项目总结 尚医通业务流程 项目功能总结(后台管理系统) 本地访问地址 http://localhost:9528/ 医院设置管理 (1)医院设置列 ...

  9. Day228229.上传查询医院接口、上传查询删除科室接口、上传查询删除排班接口 -尚医通

    尚医通 一.上传医院接口 参考<尚医通API接口文档.docx>业务接口4.1上传医院 参考<医院接口模拟系统.docx>进行接口测试与数据上传 这里要整合hospital-m ...

最新文章

  1. 无监督域对抗算法:ICCV2019论文解析
  2. 不知道什么时间收集的code
  3. 2020 我的C++的学习之路
  4. jdk 11 模块系统_JDK 9:模块系统状态的重点
  5. 移动应用开发——作业3
  6. cad加载dll_你的CAD为什么安装不上?进来看看你就知道了
  7. PowerDesigner设置线风格(直线,折线。。。)
  8. 架构师软技能之协商(上)
  9. Arturia Analog Lab for Mac - 原始混合合成器
  10. 配置JDK环境变量(详细图文教程)
  11. python爬去淘宝客订单_Python 应用淘宝客API接口简单获取优惠券的实现
  12. pr导入无法打开磁盘上的文件_为什么用pr导入视频显示文件导入失败?
  13. 第九届蓝桥杯大赛软件类国赛
  14. Facebook投资者Peter Thiel—一个不折不扣的“魔戒”迷
  15. 黑马头条项目-Vue-day9-文章详情模块、关注与取消关注,点赞和喜欢功能
  16. python爬虫音乐犯法么_Python爬虫案例:爬取网易云音乐
  17. 结对编程——经验教训总结
  18. 图书馆管理信息系统的用例图和类图
  19. Forge 发布倒计时三天:陈天写下他加入 ArcBlock 一周年的感慨 | ArcBlock 博客
  20. DevOps 面试小宝典:总有一个你会用到

热门文章

  1. 重谈Zigbee的绑定问题
  2. Android炫酷的Toolbar+Bottom+Fab悬浮按钮显示、隐藏、渐变的各种实现姿势
  3. “华为杯”研究生数学建模竞赛2020年-【华为杯】A题:基于通信仿真的载波恢复算法设计与 ASIC 实现(附获奖论文及matlab代码实现)
  4. Linux-LAMP搭建论坛站
  5. 泡妞新招三十六计 (一计在手,百发百中。如有落空,挥剑自宫)
  6. 【Linux 内核】进程管理 - 进程优先级 ① ( 限期进程 | 实时进程 | 普通进程 | 进程优先级相关字段 )
  7. idea中你必须学会的一些基础操作
  8. CV中的transformer模型创新思路总结
  9. Gym - 101911B Glider(前缀和+二分)
  10. 获取东方财富ETF日线历史数据python