目录
项目简介:
包含系统
项目架构
前端开发流程:
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要完成以下三部
-
@Api:修饰整个类,描述Controller的作用
-
@ApiOperation:描述一个类的一个方法,或者说一个接口
-
@ApiParam:单个参数描述
-
@ApiModel:用对象来接收参数
-
@ApiModelProperty:用对象接收参数时,描述对象的一个字段
-
@ApiImplicitParam:一个请求参数
-
@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 {
@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", "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();
}
@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获取用户信息)
<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){
//获取用户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<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");
//调用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模块
定义了所有共用的枚举类和实体类(表数据)封装了所有表连接查询类(将不同表中的部分数据封装在一起作为查询字段)。
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")
@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<String,Object> param = new HashMap<>();
}
service
包含以下api接口服务
service_cmn(数据字典接口)
数据字典中包全国省市区、医院等级、证件类型、民族、学历。
表中id与parent_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);
}
//导出数据字典接口
@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<Dict> dictList = baseMapper.selectList(null);
//将dict转换成dictEoVo
List<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;
}
//一行一行读取数据
@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 ,具体看前端传数据。
//根据数据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
*/
@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<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和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<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=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)
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(...)。
//医院查询(条件查询带分页)
@Override
public 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必不可少
@Autowired
private MongoTemplate mongoTemplate;
然后就是实现方法
//查询排班规则数据
@Override
public 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的关系entry
for (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=oss-cn-beijing.aliyuncs.com
aliyun.oss.accessKeyId=LTAI5tBC4S
aliyun.oss.secret=h9zyp1uRKpzyl9U0XFpiV8
aliyun.oss.bucket=yygh-linxi
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
<!--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 {
@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实现图表折线类统计图
采用服务调用(需要配置网关),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中的修改较复杂(太多了),最简单的方法就是修改本地时间,可以查出之前的排班数据,但是微信退款也会获取本地时间导致证书过期,我妥协了只好当要退款时又将时间修改回来即可
。