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

目录

项目简介:

包含系统

项目架构​

前端开发流程:​

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


项目简介:

包含系统

预约挂号后台管理系统

尚医通开发笔记(结尾含部分bug修复方法)_第1张图片

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

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

尚医通开发笔记(结尾含部分bug修复方法)_第2张图片

医院接口系统:

尚医通开发笔记(结尾含部分bug修复方法)_第3张图片

项目架构尚医通开发笔记(结尾含部分bug修复方法)_第4张图片

前端开发流程:尚医通开发笔记(结尾含部分bug修复方法)_第5张图片

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

common模块

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

swagger2

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

使用swagger要完成以下三部

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

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

  3. @ApiParam:单个参数描述

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

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

  6. @ApiImplicitParam:一个请求参数

  7. @ApiImplicitParams:多个请求参数

1、导入pom依赖


io.springfox
springfox-swagger2


io.springfox
springfox-swagger-ui

2、配置拦截路径

/**
 * Swagger2配置信息
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                //只显示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("linxi", "http://linxi.com", "[email protected]"))
                .build();
    }

    private ApiInfo adminApiInfo(){
        return new ApiInfoBuilder()
                .title("后台管理系统-API文档")
                .description("本文档描述了后台管理系统微服务接口定义")
                .version("1.0")
                .contact(new Contact("linxi", "http://linxi.com", "[email protected]"))
                .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 {

    @ApiModelProperty(value = "返回码")
    private Integer code;

    @ApiModelProperty(value = "返回消息")
    private String message;

    @ApiModelProperty(value = "返回数据")
    private T data;

    public Result(){}

    protected static  Result build(T data) {
        Result result = new Result();
        if (data != null)
            result.setData(data);
        return result;
    }

    public static  Result build(T body, ResultCodeEnum resultCodeEnum) {
        Result result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }

    public static  Result build(Integer code, String message) {
        Result result = build(null);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static Result ok(){
        return Result.ok(null);
    }

    /**
     * 操作成功
     * @param data
     * @param 
     * @return
     */
    public static Result ok(T data){
        Result result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }

    public static Result fail(){
        return Result.fail(null);
    }

    /**
     * 操作失败
     * @param data
     * @param 
     * @return
     */
    public static Result fail(T data){
        Result result = build(data);
        return build(data, ResultCodeEnum.FAIL);
    }

    public Result message(String msg){
        this.setMessage(msg);
        return this;
    }

    public Result 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();
    }

    @Override
    public String toString() {
        return "YyghException{" +
                "code=" + code +
                ", message=" + this.getMessage() +
                '}';
    }
}

GlobalExceptionHandler(全局异常处理器)

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

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

        
            io.jsonwebtoken
            jjwt
        
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 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 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){
        //获取用户token
        String token = request.getHeader("token");

        //jwt从token中获取userId
        Long userId = JwtHelper.getUserId(token);
        return userId;
    }

    /**
     * 获取用户名称
     */
    public static String getUserName(HttpServletRequest request){
        //获取用户token
        String token = request.getHeader("token");

        //jwt从token中获取userId
        String userName = JwtHelper.getUserName(token);
        return userName;
    }

}

HttpRequestHelper

@Slf4j
public class HttpRequestHelper {

    public static void main(String[] args) {
        Map 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 switchMap(Map paramMap) {
        Map resultMap = new HashMap<>();
        for (Map.Entry param : paramMap.entrySet()) {
            resultMap.put(param.getKey(), param.getValue()[0]);
        }
        return resultMap;
    }

    /**
     * 请求数据获取签名
     * @param paramMap
     * @param signKey
     * @return
     */
    public static String getSign(Map paramMap, String signKey) {
        if(paramMap.containsKey("sign")) {
            paramMap.remove("sign");
        }
        TreeMap sorted = new TreeMap<>(paramMap);
        StringBuilder str = new StringBuilder();

//        for (Map.Entry 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 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 paramMap, String url){
        String result = "";
        try {
            //封装post参数
            StringBuilder postdata = new StringBuilder();
            for (Map.Entry 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");
            //调用HttpUtil
            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);
    }
}

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;// ms
	static 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模块

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

尚医通开发笔记(结尾含部分bug修复方法)_第6张图片

 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 param = new HashMap<>();
}

BaseMongoEntity

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

@Data
public class BaseMongoEntity implements Serializable {

    @ApiModelProperty(value = "id")
    @Id
    private 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 param = new HashMap<>();
}

service

包含以下api接口服务

尚医通开发笔记(结尾含部分bug修复方法)_第7张图片

service_cmn(数据字典接口)

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

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

尚医通开发笔记(结尾含部分bug修复方法)_第8张图片

easyexcel(导入导出字典)

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

pom

        
            com.alibaba
            easyexcel
            2.2.10
        
    //导入数据字典接口
    @PostMapping("importData")
    public Result importDict(MultipartFile file){
        dictService.importDictData(file);
        return Result.ok();
    }

    //导出数据字典接口
    @GetMapping("exportData")
    public void exportDict(HttpServletResponse response){
        dictService.exportDictData(response);
    }
    //导出数据字典接口
    @Override
    public 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 dictList = baseMapper.selectList(null);

        //将dict转换成dictEoVo
        List 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 {
    private DictMapper dictMapper;
    
    public DictDataListener(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) {
    }
}

树形列表

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

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

尚医通开发笔记(结尾含部分bug修复方法)_第9张图片

尚医通开发笔记(结尾含部分bug修复方法)_第10张图片

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

spring Cache + redis 缓存数据

xml



org.springframework.boot
spring-boot-starter-data-redis




org.apache.commons
commons-pool2
2.6.0

增加redis配置类

@EnableCaching 开启缓存

@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 redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate 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 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=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

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

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

service_hosp(医院api接口)

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

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

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

尚医通开发笔记(结尾含部分bug修复方法)_第11张图片

MybatisPlus

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

@Mapper
public interface HospitalSetMapper extends BaseMapper {
}
public interface HospitalSetService extends IService {

}
@Service
public class HospitalSetServiceImpl extends ServiceImpl 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 page = new Page<>(current, limit);
        //构造条件
        QueryWrapper 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 queryPage = hospitalSetService.page(page, queryWrapper);
        return Result.ok(queryPage);
    }

Mongodb

mongodb使用分两种 MongoTemplate 、MongoRepository


    org.springframework.boot
    spring-boot-starter-data-mongodb

下面使用MongoRepository

@Repository
public interface HospitalRepository extends MongoRepository {
    //判断是否存在数据
    Hospital getHospitalByHoscode(String hoscode);

    //根据医院名称查询
    List findHospitalByHosnameLike(String hosname);
}

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

尚医通开发笔记(结尾含部分bug修复方法)_第12张图片尚医通开发笔记(结尾含部分bug修复方法)_第13张图片

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

findScheduleByHoscodeAndDepcodeAndWorkDate(...)。
    //医院查询(条件查询带分页)
    @Override
    public Page 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 example = Example.of(hospital, matcher);

        Pageable pageable = PageRequest.of(page - 1, limit);
        Page 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必不可少

    @Autowired
    private MongoTemplate mongoTemplate;

然后就是实现方法

    //查询排班规则数据
    @Override
    public Map 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 aggResults =
                mongoTemplate.aggregate(agg, Schedule.class, BookingScheduleRuleVo.class);
        List bookingScheduleRuleVoList = aggResults.getMappedResults();

        //分组查询总记录数
        Aggregation totalAgg = Aggregation.newAggregation(
                Aggregation.match(criteria),
                Aggregation.group("workDate") //通过工作日期进行分组
        );
        //调用方法查询
        AggregationResults 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 resultMap = new HashMap<>();
        resultMap.put("bookingScheduleRuleList",bookingScheduleRuleVoList);
        resultMap.put("total",total);

        //获取医院名称
        String hosName = hospitalService.getHosName(hoscode);

        Map baseMap = new HashMap<>();
        baseMap.put("hosname",hosName);
        resultMap.put("baseMap",baseMap);

        return resultMap;
    }

部门查询

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

尚医通开发笔记(结尾含部分bug修复方法)_第14张图片

尚医通开发笔记(结尾含部分bug修复方法)_第15张图片

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

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

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

      //遍历map集合:通过key和value的关系entry
        for (Map.Entry> entry : departmentMap.entrySet()){
            //大科室编号
            String bigcode = entry.getKey();
            //大科室编号对应的全部数据
            List departments = entry.getValue();
            /*
            封装大科室
             */
            DepartmentVo departmentVo = new DepartmentVo();
            departmentVo.setDepcode(bigcode); //设置大科室编号
            departmentVo.setDepname(departments.get(0).getBigname());//设置大科室名称

            /*
            封装小科室
             */
            List 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>>

 
  

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

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

nacos

JWT

手机号登录

微信登录

微信支付

尚医通开发笔记(结尾含部分bug修复方法)_第16张图片

退款

阿里OSS

pom

        
            com.aliyun.oss
            aliyun-sdk-oss
        
        
        
            joda-time
            joda-time
        

配置文件

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.jpg
        String 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

        
        
            org.springframework.boot
            spring-boot-starter-actuator
        
        
            org.springframework.cloud
            spring-cloud-starter-bus-amqp
        
        
            com.alibaba
            fastjson
        

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

尚医通开发笔记(结尾含部分bug修复方法)_第17张图片

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

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

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

@Service
public class RabbitService {
    @Autowired
    private 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 {
    @Autowired
    private 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实现图表折线类统计图

尚医通开发笔记(结尾含部分bug修复方法)_第18张图片

 尚医通开发笔记(结尾含部分bug修复方法)_第19张图片

采用服务调用(需要配置网关),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类等公共类,最后再打包业务类

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    execute
                
            
        
    

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

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

你可能感兴趣的:(实战项目,spring,java,maven,java-ee,tomcat)